@@ -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