From 0ce5885002730aa8fcfdb3a10e9d350f166c5c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?= Date: Fri, 14 Aug 2026 07:38:20 +0000 Subject: [PATCH] fix(tbtcpg): price P2TR redeemer output scripts The redeemer-script switch in EstimateRedemptionFee has no P2TR arm, so a P2TR redeemer output script falls through to the non-standard default and returns an error. That aborts the fee estimate for the whole batch, so one P2TR request stalls every pending redemption for that wallet rather than only its own. The Bridge accepts P2TR redeemer addresses (Redemption.sol validates against a set that includes them), so this is reachable as soon as a redeemer supplies one. Price it with AddOutputScript rather than a dedicated helper: an output costs its script length plus a fixed header, and the real script is already in hand, so no canonical placeholder is needed. The default arm still rejects genuinely non-standard scripts and now names the type it rejected. --- pkg/tbtcpg/redemptions.go | 10 ++++- pkg/tbtcpg/redemptions_test.go | 72 ++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/pkg/tbtcpg/redemptions.go b/pkg/tbtcpg/redemptions.go index 7f2633681c..57d155a5e6 100644 --- a/pkg/tbtcpg/redemptions.go +++ b/pkg/tbtcpg/redemptions.go @@ -529,8 +529,16 @@ func EstimateRedemptionFee( sizeEstimator.AddScriptHashOutputs(1, false) case bitcoin.P2WSHScript: sizeEstimator.AddScriptHashOutputs(1, true) + case bitcoin.P2TRScript: + // AddOutputScript rather than a dedicated Add*Outputs helper: an + // output costs its script length plus a fixed header, and the real + // script is already in hand, so no canonical placeholder is needed. + sizeEstimator.AddOutputScript(script) default: - return 0, fmt.Errorf("non-standard redeemer output script type") + return 0, fmt.Errorf( + "non-standard redeemer output script type [%v]", + bitcoin.GetScriptType(script), + ) } } diff --git a/pkg/tbtcpg/redemptions_test.go b/pkg/tbtcpg/redemptions_test.go index 2aef02fa73..cfb407286e 100644 --- a/pkg/tbtcpg/redemptions_test.go +++ b/pkg/tbtcpg/redemptions_test.go @@ -95,6 +95,78 @@ func TestEstimateRedemptionFee(t *testing.T) { } } +// TestEstimateRedemptionFee_P2TRRedeemerScript asserts a redemption batch that +// contains a P2TR redeemer output script is priced rather than rejected. +// +// The Bridge accepts P2TR redeemer addresses, so a single such request in a batch +// used to abort the estimate for the whole batch and stall every pending +// redemption for that wallet, not just its own. +func TestEstimateRedemptionFee_P2TRRedeemerScript(t *testing.T) { + fromHex := func(hexString string) []byte { + bytes, err := hex.DecodeString(hexString) + if err != nil { + t.Fatal(err) + } + return bytes + } + + // OP_1 (0x51) OP_DATA_32 (0x20) followed by the 32-byte output key. + p2trScript := bitcoin.Script(fromHex( + "5120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c", + )) + p2wpkhScript := bitcoin.Script(fromHex( + "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", + )) + + estimate := func(scripts []bitcoin.Script) (int64, error) { + btcChain := tbtcpg.NewLocalBitcoinChain() + btcChain.SetEstimateSatPerVByteFee(1, 16) + + return tbtcpg.EstimateRedemptionFee(btcChain, scripts, 100000) + } + + withoutP2TR, err := estimate([]bitcoin.Script{p2wpkhScript}) + if err != nil { + t.Fatal(err) + } + + withP2TR, err := estimate([]bitcoin.Script{p2wpkhScript, p2trScript}) + if err != nil { + t.Fatalf("a P2TR redeemer script must be priced, not rejected: %v", err) + } + + // The P2TR request adds an output, so it must cost strictly more. An equal fee + // would mean the script was silently dropped from the size estimate and every + // redemption in the batch underpaid. + if withP2TR <= withoutP2TR { + t.Fatalf( + "adding a P2TR redeemer output must raise the fee: without [%d], with [%d]", + withoutP2TR, + withP2TR, + ) + } +} + +// TestEstimateRedemptionFee_NonStandardRedeemerScript asserts a genuinely +// non-standard redeemer script is still rejected, and that the error names the +// offending type so an operator can identify the request. +func TestEstimateRedemptionFee_NonStandardRedeemerScript(t *testing.T) { + btcChain := tbtcpg.NewLocalBitcoinChain() + btcChain.SetEstimateSatPerVByteFee(1, 16) + + _, err := tbtcpg.EstimateRedemptionFee( + btcChain, + []bitcoin.Script{bitcoin.Script{0x6a, 0x01, 0x00}}, // OP_RETURN + 100000, + ) + if err == nil { + t.Fatal("expected a non-standard redeemer output script to be rejected") + } + if !strings.Contains(err.Error(), "non-standard redeemer output script type") { + t.Fatalf("unexpected error: %v", err) + } +} + func TestRedemptionAction_FindPendingRedemptions(t *testing.T) { scenarios, err := test.LoadFindPendingRedemptionsTestScenario() if err != nil {