Skip to content

Commit 21c7e50

Browse files
committed
itest: add test for FundPsbt with taproot script path inputs
In this commit, we add an integration test that verifies FundPsbt can correctly estimate fees for PSBTs containing taproot script path inputs. The test creates a simple OP_CHECKSIG tapscript, sends coins to the resulting taproot address, then constructs a PSBT with the script path input properly populated (TaprootLeafScript, TaprootBip32Derivation). It calls FundPsbt and verifies that the fee is calculated correctly for a script path witness, which is approximately twice the size of a key path witness (~136 WU vs 67 WU).
1 parent c247848 commit 21c7e50

File tree

2 files changed

+146
-0
lines changed

2 files changed

+146
-0
lines changed

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,10 @@ var allTestCases = []*lntest.TestCase{
302302
Name: "fund psbt custom lock",
303303
TestFunc: testFundPsbtCustomLock,
304304
},
305+
{
306+
Name: "fund psbt taproot script path",
307+
TestFunc: testFundPsbtTaprootScriptPath,
308+
},
305309
{
306310
Name: "resolution handoff",
307311
TestFunc: testResHandoff,

itest/lnd_psbt_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,3 +2038,145 @@ func testFundPsbtCustomLock(ht *lntest.HarnessTest) {
20382038
leasesRespAfter := alice.RPC.ListLeases()
20392039
require.Empty(ht, leasesRespAfter.LockedUtxos)
20402040
}
2041+
2042+
// testFundPsbtTaprootScriptPath tests that FundPsbt can correctly estimate fees
2043+
// when the PSBT contains inputs that spend via a taproot script path.
2044+
// Previously, FundPsbt would return an error for script path spends because
2045+
// the weight estimation didn't support them. This test verifies the fix.
2046+
func testFundPsbtTaprootScriptPath(ht *lntest.HarnessTest) {
2047+
alice := ht.NewNodeWithCoins("Alice", nil)
2048+
2049+
// Derive the signing key and its derivation path.
2050+
keyDesc, leafSigningKey, derivationPath := deriveInternalKey(ht, alice)
2051+
2052+
// Create a simple OP_CHECKSIG tapscript leaf.
2053+
leaf := testScriptSchnorrSig(ht.T, leafSigningKey)
2054+
2055+
// Create a tapscript with a single leaf (no sibling for simplicity).
2056+
// We use a dummy internal key since we're testing script path spend.
2057+
tapscript := input.TapscriptFullTree(dummyInternalKey, leaf)
2058+
taprootKey, err := tapscript.TaprootKey()
2059+
require.NoError(ht, err)
2060+
2061+
// Send some coins to the generated tapscript address.
2062+
// Note: sendToTaprootOutput already mines a block to confirm the tx.
2063+
p2trOutpoint, p2trPkScript := sendToTaprootOutput(ht, alice, taprootKey)
2064+
2065+
// Create the sweep destination address.
2066+
sweepAddr, sweepPkScript := newAddrWithScript(
2067+
ht, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
2068+
)
2069+
2070+
// Create a PSBT with the tapscript input.
2071+
tx := wire.NewMsgTx(2)
2072+
tx.TxIn = []*wire.TxIn{{
2073+
PreviousOutPoint: p2trOutpoint,
2074+
}}
2075+
// Output value is a placeholder - FundPsbt will adjust for fees.
2076+
tx.TxOut = []*wire.TxOut{{
2077+
PkScript: sweepPkScript,
2078+
Value: 1,
2079+
}}
2080+
2081+
packet, err := psbt.New(
2082+
[]*wire.OutPoint{&p2trOutpoint}, []*wire.TxOut{tx.TxOut[0]},
2083+
2, 0, []uint32{0},
2084+
)
2085+
require.NoError(ht, err)
2086+
2087+
// Populate the PSBT input with tapscript information so that
2088+
// FundPsbt can determine it's a script path spend and estimate
2089+
// the witness size correctly.
2090+
controlBlockBytes, err := tapscript.ControlBlock.ToBytes()
2091+
require.NoError(ht, err)
2092+
2093+
leafHash := leaf.TapHash()
2094+
in := &packet.Inputs[0]
2095+
in.WitnessUtxo = &wire.TxOut{
2096+
PkScript: p2trPkScript,
2097+
Value: testAmount,
2098+
}
2099+
in.TaprootLeafScript = []*psbt.TaprootTapLeafScript{{
2100+
ControlBlock: controlBlockBytes,
2101+
Script: leaf.Script,
2102+
LeafVersion: leaf.LeafVersion,
2103+
}}
2104+
in.TaprootBip32Derivation = []*psbt.TaprootBip32Derivation{{
2105+
XOnlyPubKey: schnorr.SerializePubKey(leafSigningKey),
2106+
LeafHashes: [][]byte{leafHash[:]},
2107+
Bip32Path: derivationPath,
2108+
}}
2109+
in.SighashType = txscript.SigHashDefault
2110+
2111+
_ = keyDesc // Suppress unused warning
2112+
2113+
var buf bytes.Buffer
2114+
require.NoError(ht, packet.Serialize(&buf))
2115+
2116+
// Call FundPsbt with the script path input.
2117+
// This previously would fail with "cannot estimate witness size for
2118+
// script spend". Now it should succeed.
2119+
change := &walletrpc.PsbtCoinSelect_ExistingOutputIndex{
2120+
ExistingOutputIndex: 0,
2121+
}
2122+
fundResp := alice.RPC.FundPsbt(&walletrpc.FundPsbtRequest{
2123+
Template: &walletrpc.FundPsbtRequest_CoinSelect{
2124+
CoinSelect: &walletrpc.PsbtCoinSelect{
2125+
Psbt: buf.Bytes(),
2126+
ChangeOutput: change,
2127+
},
2128+
},
2129+
Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{
2130+
SatPerVbyte: 10,
2131+
},
2132+
})
2133+
2134+
// Parse the funded PSBT.
2135+
fundedPacket, err := psbt.NewFromRawBytes(
2136+
bytes.NewReader(fundResp.FundedPsbt), false,
2137+
)
2138+
require.NoError(ht, err)
2139+
2140+
// Verify that the fee was calculated correctly for a script path spend.
2141+
// Script path witness is ~136 WU vs 67 WU for key path, so the fee
2142+
// should be noticeably higher than a key path estimate.
2143+
//
2144+
// Calculate expected weight for script path:
2145+
// - Base input: 41 bytes * 4 = 164 WU
2146+
// - Witness: sig (65) + script length (1) + script (~35) + control
2147+
// block length (1) + control block (33) + element count (1) = ~136 WU
2148+
// - Output: ~31 bytes * 4 = 124 WU
2149+
// - Base tx: 8 bytes * 4 = 32 WU
2150+
// Total: ~456 WU = ~114 vbytes
2151+
//
2152+
// With 10 sat/vbyte, fee should be around 1140 sats for the input.
2153+
fee, err := fundedPacket.GetTxFee()
2154+
require.NoError(ht, err)
2155+
2156+
// The fee should be at least higher than what a key path would cost.
2157+
// Key path: ~67 WU witness, so ~80 vbytes total = 800 sats at 10 sat/vb.
2158+
// Script path should be at least 900+ sats.
2159+
require.Greater(ht, int64(fee), int64(900),
2160+
"fee %d should be higher than key path estimate (900 sats)", fee)
2161+
2162+
ht.Logf("FundPsbt with script path input succeeded! Fee: %d sats "+
2163+
"(~%d vbytes at 10 sat/vbyte)", fee, fee/10)
2164+
2165+
// Calculate expected weight for script path using the same estimator
2166+
// that lnd uses internally.
2167+
estimator := input.TxWeightEstimator{}
2168+
estimator.AddTapscriptInput(
2169+
input.TaprootSignatureWitnessSize, tapscript,
2170+
)
2171+
estimator.AddP2WKHOutput()
2172+
expectedVSize := estimator.VSize()
2173+
expectedFee := int64(10 * expectedVSize)
2174+
2175+
// Verify the fee is close to our expected calculation (within 20%).
2176+
require.InDelta(ht, expectedFee, int64(fee), float64(expectedFee)*0.2,
2177+
"fee should be close to script-path estimate")
2178+
2179+
ht.Logf("Script path FundPsbt test completed! Expected vsize=%d, "+
2180+
"expected fee=%d, actual fee=%d, sweep destination=%s",
2181+
expectedVSize, expectedFee, fee, sweepAddr)
2182+
}

0 commit comments

Comments
 (0)