package itest import ( "bytes" "encoding/hex" "testing" "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/stretchr/testify/require" ) // testPsbtChanFunding makes sure a channel can be opened between carol and dave // by using a Partially Signed Bitcoin Transaction that funds the channel // multisig funding output. func testPsbtChanFunding(ht *lntest.HarnessTest) { const ( burnAddr = "bcrt1qxsnqpdc842lu8c0xlllgvejt6rhy49u6fmpgyz" ) testCases := []struct { name string commitmentType lnrpc.CommitmentType private bool }{ { name: "anchors", commitmentType: lnrpc.CommitmentType_ANCHORS, private: false, }, { name: "simple taproot", commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT, // Set this to true once simple taproot channels can be // announced to the network. private: true, }, } for _, tc := range testCases { tc := tc success := ht.T.Run(tc.name, func(tt *testing.T) { st := ht.Subtest(tt) args := lntest.NodeArgsForCommitType(tc.commitmentType) // First, we'll create two new nodes that we'll use to // open channels between for this test. Dave gets some // coins that will be used to fund the PSBT, just to // make sure that Carol has an empty wallet. carol := st.NewNode("carol", args) dave := st.NewNode("dave", args) // We just send enough funds to satisfy the anchor // channel reserve for 5 channels (50k sats). st.FundCoins(50_000, carol) st.FundCoins(50_000, dave) st.RunTestCase(&lntest.TestCase{ Name: tc.name, TestFunc: func(sst *lntest.HarnessTest) { runPsbtChanFunding( sst, carol, dave, tc.private, tc.commitmentType, ) }, }) // Empty out the wallets so there aren't any lingering // coins. sendAllCoinsConfirm(st, carol, burnAddr) sendAllCoinsConfirm(st, dave, burnAddr) // Now we test the second scenario. Again, we just send // enough funds to satisfy the anchor channel reserve // for 5 channels (50k sats). st.FundCoins(50_000, carol) st.FundCoins(50_000, dave) st.RunTestCase(&lntest.TestCase{ Name: tc.name, TestFunc: func(sst *lntest.HarnessTest) { runPsbtChanFundingExternal( sst, carol, dave, tc.private, tc.commitmentType, ) }, }) // Empty out the wallets a last time, so there aren't // any lingering coins. sendAllCoinsConfirm(st, carol, burnAddr) sendAllCoinsConfirm(st, dave, burnAddr) // The last test case tests the anchor channel reserve // itself, so we need empty wallets. st.RunTestCase(&lntest.TestCase{ Name: tc.name, TestFunc: func(sst *lntest.HarnessTest) { runPsbtChanFundingSingleStep( sst, carol, dave, tc.private, tc.commitmentType, ) }, }) }) if !success { // Log failure time to help relate the lnd logs to the // failure. ht.Logf("Failure time: %v", time.Now().Format( "2006-01-02 15:04:05.000", )) break } } } // runPsbtChanFunding makes sure a channel can be opened between carol and dave // by using a Partially Signed Bitcoin Transaction that funds the channel // multisig funding output. func runPsbtChanFunding(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, private bool, commitType lnrpc.CommitmentType) { const chanSize = funding.MaxBtcFundingAmount ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) // Before we start the test, we'll ensure both sides are connected so // the funding flow can be properly executed. alice := ht.Alice ht.EnsureConnected(carol, dave) ht.EnsureConnected(carol, alice) // At this point, we can begin our PSBT channel funding workflow. We'll // start by generating a pending channel ID externally that will be used // to track this new funding type. pendingChanID := ht.Random32Bytes() // We'll also test batch funding of two channels so we need another ID. pendingChanID2 := ht.Random32Bytes() // Now that we have the pending channel ID, Carol will open the channel // by specifying a PSBT shim. We use the NoPublish flag here to avoid // publishing the whole batch TX too early. chanUpdates, tempPsbt := ht.OpenChannelPsbt( carol, dave, lntest.OpenChannelParams{ Amt: chanSize, FundingShim: &lnrpc.FundingShim{ Shim: &lnrpc.FundingShim_PsbtShim{ PsbtShim: &lnrpc.PsbtShim{ PendingChanId: pendingChanID, NoPublish: true, }, }, }, Private: private, CommitmentType: commitType, }, ) // Let's add a second channel to the batch. This time between Carol and // Alice. We will publish the batch TX once this channel funding is // complete. chanUpdates2, psbtBytes2 := ht.OpenChannelPsbt( carol, alice, lntest.OpenChannelParams{ Amt: chanSize, FundingShim: &lnrpc.FundingShim{ Shim: &lnrpc.FundingShim_PsbtShim{ PsbtShim: &lnrpc.PsbtShim{ PendingChanId: pendingChanID2, NoPublish: false, BasePsbt: tempPsbt, }, }, }, // We haven't started Alice with the explicit params to // support the current commit type, so we'll just use // the default for this channel. That also allows us to // test batches of different channel types. }, ) // We'll now ask Dave's wallet to fund the PSBT for us. This will return // a packet with inputs and outputs set but without any witness data. // This is exactly what we need for the next step. fundReq := &walletrpc.FundPsbtRequest{ Template: &walletrpc.FundPsbtRequest_Psbt{ Psbt: psbtBytes2, }, Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{ SatPerVbyte: 2, }, } fundResp := dave.RPC.FundPsbt(fundReq) // We have a PSBT that has no witness data yet, which is exactly what we // need for the next step: Verify the PSBT with the funding intents. carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{ PsbtVerify: &lnrpc.FundingPsbtVerify{ PendingChanId: pendingChanID, FundedPsbt: fundResp.FundedPsbt, }, }, }) carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{ PsbtVerify: &lnrpc.FundingPsbtVerify{ PendingChanId: pendingChanID2, FundedPsbt: fundResp.FundedPsbt, }, }, }) // Now we'll ask Dave's wallet to sign the PSBT so we can finish the // funding flow. finalizeReq := &walletrpc.FinalizePsbtRequest{ FundedPsbt: fundResp.FundedPsbt, } finalizeRes := dave.RPC.FinalizePsbt(finalizeReq) // We've signed our PSBT now, let's pass it to the intent again. carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtFinalize{ PsbtFinalize: &lnrpc.FundingPsbtFinalize{ PendingChanId: pendingChanID, SignedPsbt: finalizeRes.SignedPsbt, }, }, }) // Consume the "channel pending" update. This waits until the funding // transaction was fully compiled. updateResp := ht.ReceiveOpenChannelUpdate(chanUpdates) upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending) require.True(ht, ok) chanPoint := &lnrpc.ChannelPoint{ FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ FundingTxidBytes: upd.ChanPending.Txid, }, OutputIndex: upd.ChanPending.OutputIndex, } // No transaction should have been published yet. ht.AssertNumTxsInMempool(0) // Let's progress the second channel now. This time we'll use the raw // wire format transaction directly. carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtFinalize{ PsbtFinalize: &lnrpc.FundingPsbtFinalize{ PendingChanId: pendingChanID2, FinalRawTx: finalizeRes.RawFinalTx, }, }, }) // Consume the "channel pending" update for the second channel. This // waits until the funding transaction was fully compiled and in this // case published. updateResp2 := ht.ReceiveOpenChannelUpdate(chanUpdates2) upd2, ok := updateResp2.Update.(*lnrpc.OpenStatusUpdate_ChanPending) require.True(ht, ok) chanPoint2 := &lnrpc.ChannelPoint{ FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ FundingTxidBytes: upd2.ChanPending.Txid, }, OutputIndex: upd2.ChanPending.OutputIndex, } // Great, now we can mine a block to get the transaction confirmed, then // wait for the new channel to be propagated through the network. var finalTx wire.MsgTx err := finalTx.Deserialize(bytes.NewReader(finalizeRes.RawFinalTx)) require.NoError(ht, err) txHash := finalTx.TxHash() block := ht.MineBlocksAndAssertNumTxes(6, 1)[0] ht.AssertTxInBlock(block, &txHash) ht.AssertTopologyChannelOpen(carol, chanPoint) ht.AssertTopologyChannelOpen(carol, chanPoint2) // With the channel open, ensure that it is counted towards Carol's // total channel balance. balRes := carol.RPC.ChannelBalance() require.NotZero(ht, balRes.LocalBalance.Sat) // Next, to make sure the channel functions as normal, we'll make some // payments within the channel. payAmt := btcutil.Amount(100000) invoice := &lnrpc.Invoice{ Memo: "new chans", Value: int64(payAmt), } resp := dave.RPC.AddInvoice(invoice) ht.CompletePaymentRequests(carol, []string{resp.PaymentRequest}) // To conclude, we'll close the newly created channel between Carol and // Dave. This function will also block until the channel is closed and // will additionally assert the relevant channel closing post // conditions. ht.CloseChannel(carol, chanPoint) ht.CloseChannel(carol, chanPoint2) } // runPsbtChanFundingExternal makes sure a channel can be opened between carol // and dave by using a Partially Signed Bitcoin Transaction that funds the // channel multisig funding output and is fully funded by an external third // party. func runPsbtChanFundingExternal(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, private bool, commitType lnrpc.CommitmentType) { const chanSize = funding.MaxBtcFundingAmount // Before we start the test, we'll ensure both sides are connected so // the funding flow can be properly executed. alice := ht.Alice ht.EnsureConnected(carol, dave) ht.EnsureConnected(carol, alice) // At this point, we can begin our PSBT channel funding workflow. We'll // start by generating a pending channel ID externally that will be used // to track this new funding type. pendingChanID := ht.Random32Bytes() // We'll also test batch funding of two channels so we need another ID. pendingChanID2 := ht.Random32Bytes() // Now that we have the pending channel ID, Carol will open the channel // by specifying a PSBT shim. We use the NoPublish flag here to avoid // publishing the whole batch TX too early. chanUpdates, tempPsbt := ht.OpenChannelPsbt( carol, dave, lntest.OpenChannelParams{ Amt: chanSize, FundingShim: &lnrpc.FundingShim{ Shim: &lnrpc.FundingShim_PsbtShim{ PsbtShim: &lnrpc.PsbtShim{ PendingChanId: pendingChanID, NoPublish: true, }, }, }, Private: private, CommitmentType: commitType, }, ) // Let's add a second channel to the batch. This time between Carol and // Alice. We will publish the batch TX once this channel funding is // complete. chanUpdates2, psbtBytes2 := ht.OpenChannelPsbt( carol, alice, lntest.OpenChannelParams{ Amt: chanSize, FundingShim: &lnrpc.FundingShim{ Shim: &lnrpc.FundingShim_PsbtShim{ PsbtShim: &lnrpc.PsbtShim{ PendingChanId: pendingChanID2, NoPublish: true, BasePsbt: tempPsbt, }, }, }, // We haven't started Alice with the explicit params to // support the current commit type, so we'll just use // the default for this channel. That also allows us to // test batches of different channel types. }, ) // We'll now ask Alice's wallet to fund the PSBT for us. This will // return a packet with inputs and outputs set but without any witness // data. This is exactly what we need for the next step. fundReq := &walletrpc.FundPsbtRequest{ Template: &walletrpc.FundPsbtRequest_Psbt{ Psbt: psbtBytes2, }, Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{ SatPerVbyte: 2, }, } fundResp := alice.RPC.FundPsbt(fundReq) // We have a PSBT that has no witness data yet, which is exactly what we // need for the next step: Verify the PSBT with the funding intents. // We tell the PSBT intent to skip the finalize step because we know the // final transaction will not be broadcast by Carol herself but by // Alice. And we assume that Alice is a third party that is not in // direct communication with Carol and won't send the signed TX to her // before broadcasting it. So we cannot call the finalize step but // instead just tell lnd to wait for a TX to be published/confirmed. carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{ PsbtVerify: &lnrpc.FundingPsbtVerify{ PendingChanId: pendingChanID, FundedPsbt: fundResp.FundedPsbt, SkipFinalize: true, }, }, }) carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{ PsbtVerify: &lnrpc.FundingPsbtVerify{ PendingChanId: pendingChanID2, FundedPsbt: fundResp.FundedPsbt, SkipFinalize: true, }, }, }) // Consume the "channel pending" update. This waits until the funding // transaction was fully compiled for both channels. updateResp := ht.ReceiveOpenChannelUpdate(chanUpdates) upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending) require.True(ht, ok) chanPoint := &lnrpc.ChannelPoint{ FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ FundingTxidBytes: upd.ChanPending.Txid, }, OutputIndex: upd.ChanPending.OutputIndex, } updateResp2 := ht.ReceiveOpenChannelUpdate(chanUpdates2) upd2, ok := updateResp2.Update.(*lnrpc.OpenStatusUpdate_ChanPending) require.True(ht, ok) chanPoint2 := &lnrpc.ChannelPoint{ FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ FundingTxidBytes: upd2.ChanPending.Txid, }, OutputIndex: upd2.ChanPending.OutputIndex, } ht.AssertNumPendingOpenChannels(carol, 2) // Now we'll ask Alice's wallet to sign the PSBT so we can finish the // funding flow. finalizeReq := &walletrpc.FinalizePsbtRequest{ FundedPsbt: fundResp.FundedPsbt, } finalizeRes := alice.RPC.FinalizePsbt(finalizeReq) // No transaction should have been published yet. ht.AssertNumTxsInMempool(0) // Great, now let's publish the final raw transaction. var finalTx wire.MsgTx err := finalTx.Deserialize(bytes.NewReader(finalizeRes.RawFinalTx)) require.NoError(ht, err) txHash := finalTx.TxHash() _, err = ht.SendRawTransaction(&finalTx, false) require.NoError(ht, err) // Now we can mine a block to get the transaction confirmed, then wait // for the new channel to be propagated through the network. block := ht.MineBlocksAndAssertNumTxes(6, 1)[0] ht.AssertTxInBlock(block, &txHash) ht.AssertTopologyChannelOpen(carol, chanPoint) ht.AssertTopologyChannelOpen(carol, chanPoint2) // With the channel open, ensure that it is counted towards Carol's // total channel balance. balRes := carol.RPC.ChannelBalance() require.NotZero(ht, balRes.LocalBalance.Sat) // Next, to make sure the channel functions as normal, we'll make some // payments within the channel. payAmt := btcutil.Amount(100000) invoice := &lnrpc.Invoice{ Memo: "new chans", Value: int64(payAmt), } resp := dave.RPC.AddInvoice(invoice) ht.CompletePaymentRequests(carol, []string{resp.PaymentRequest}) // To conclude, we'll close the newly created channel between Carol and // Dave. This function will also block until the channels are closed and // will additionally assert the relevant channel closing post // conditions. ht.CloseChannel(carol, chanPoint) ht.CloseChannel(carol, chanPoint2) } // runPsbtChanFundingSingleStep checks whether PSBT funding works also when // the wallet of both nodes are empty and one of them uses PSBT and an external // wallet to fund the channel while creating reserve output in the same // transaction. func runPsbtChanFundingSingleStep(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, private bool, commitType lnrpc.CommitmentType) { const chanSize = funding.MaxBtcFundingAmount alice := ht.Alice ht.FundCoins(btcutil.SatoshiPerBitcoin, alice) // Get new address for anchor reserve. req := &lnrpc.NewAddressRequest{ Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, } addrResp := carol.RPC.NewAddress(req) reserveAddr, err := btcutil.DecodeAddress( addrResp.Address, harnessNetParams, ) require.NoError(ht, err) reserveAddrScript, err := txscript.PayToAddrScript(reserveAddr) require.NoError(ht, err) // Before we start the test, we'll ensure both sides are connected so // the funding flow can be properly executed. ht.EnsureConnected(carol, dave) // At this point, we can begin our PSBT channel funding workflow. We'll // start by generating a pending channel ID externally that will be used // to track this new funding type. pendingChanID := ht.Random32Bytes() // Now that we have the pending channel ID, Carol will open the channel // by specifying a PSBT shim. chanUpdates, tempPsbt := ht.OpenChannelPsbt( carol, dave, lntest.OpenChannelParams{ Amt: chanSize, FundingShim: &lnrpc.FundingShim{ Shim: &lnrpc.FundingShim_PsbtShim{ PsbtShim: &lnrpc.PsbtShim{ PendingChanId: pendingChanID, NoPublish: false, }, }, }, Private: private, CommitmentType: commitType, }, ) decodedPsbt, err := psbt.NewFromRawBytes( bytes.NewReader(tempPsbt), false, ) require.NoError(ht, err) reserveTxOut := wire.TxOut{ Value: 10000, PkScript: reserveAddrScript, } decodedPsbt.UnsignedTx.TxOut = append( decodedPsbt.UnsignedTx.TxOut, &reserveTxOut, ) decodedPsbt.Outputs = append(decodedPsbt.Outputs, psbt.POutput{}) var psbtBytes bytes.Buffer err = decodedPsbt.Serialize(&psbtBytes) require.NoError(ht, err) fundReq := &walletrpc.FundPsbtRequest{ Template: &walletrpc.FundPsbtRequest_Psbt{ Psbt: psbtBytes.Bytes(), }, Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{ SatPerVbyte: 2, }, } fundResp := alice.RPC.FundPsbt(fundReq) // Make sure the wallets are actually empty ht.AssertNumUTXOsUnconfirmed(alice, 0) ht.AssertNumUTXOsUnconfirmed(dave, 0) // We have a PSBT that has no witness data yet, which is exactly what we // need for the next step: Verify the PSBT with the funding intents. carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{ PsbtVerify: &lnrpc.FundingPsbtVerify{ PendingChanId: pendingChanID, FundedPsbt: fundResp.FundedPsbt, }, }, }) // Now we'll ask Alice's wallet to sign the PSBT so we can finish the // funding flow. finalizeReq := &walletrpc.FinalizePsbtRequest{ FundedPsbt: fundResp.FundedPsbt, } finalizeRes := alice.RPC.FinalizePsbt(finalizeReq) // We've signed our PSBT now, let's pass it to the intent again. carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtFinalize{ PsbtFinalize: &lnrpc.FundingPsbtFinalize{ PendingChanId: pendingChanID, SignedPsbt: finalizeRes.SignedPsbt, }, }, }) // Consume the "channel pending" update. This waits until the funding // transaction was fully compiled. updateResp := ht.ReceiveOpenChannelUpdate(chanUpdates) upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending) require.True(ht, ok) chanPoint := &lnrpc.ChannelPoint{ FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ FundingTxidBytes: upd.ChanPending.Txid, }, OutputIndex: upd.ChanPending.OutputIndex, } var finalTx wire.MsgTx err = finalTx.Deserialize(bytes.NewReader(finalizeRes.RawFinalTx)) require.NoError(ht, err) txHash := finalTx.TxHash() block := ht.MineBlocksAndAssertNumTxes(6, 1)[0] ht.AssertTxInBlock(block, &txHash) ht.AssertTopologyChannelOpen(carol, chanPoint) // Next, to make sure the channel functions as normal, we'll make some // payments within the channel. payAmt := btcutil.Amount(100000) invoice := &lnrpc.Invoice{ Memo: "new chans", Value: int64(payAmt), } resp := dave.RPC.AddInvoice(invoice) ht.CompletePaymentRequests(carol, []string{resp.PaymentRequest}) // To conclude, we'll close the newly created channel between Carol and // Dave. This function will also block until the channel is closed and // will additionally assert the relevant channel closing post // conditions. ht.CloseChannel(carol, chanPoint) } // testSignPsbt tests that the SignPsbt RPC works correctly. func testSignPsbt(ht *lntest.HarnessTest) { psbtTestRunners := []struct { name string runner func(*lntest.HarnessTest, *node.HarnessNode) }{ { name: "sign psbt segwit v0 P2WPKH", runner: runSignPsbtSegWitV0P2WKH, }, { name: "sign psbt segwit v0 P2WSH", runner: runSignPsbtSegWitV0NP2WKH, }, { name: "sign psbt segwit v1 key spend bip86", runner: runSignPsbtSegWitV1KeySpendBip86, }, { name: "sign psbt segwit v1 key spend root hash", runner: runSignPsbtSegWitV1KeySpendRootHash, }, { name: "sign psbt segwit v1 script spend", runner: runSignPsbtSegWitV1ScriptSpend, }, { // The above tests all make sure we can sign for keys // that aren't in the wallet. But we also want to make // sure we can fund and then sign PSBTs from our // wallet. name: "fund and sign psbt", runner: runFundAndSignPsbt, }, } for _, tc := range psbtTestRunners { succeed := ht.Run(tc.name, func(t *testing.T) { st := ht.Subtest(t) tc.runner(st, st.Alice) }) // Abort the test if failed. if !succeed { return } } } // runSignPsbtSegWitV0P2WKH tests that the SignPsbt RPC works correctly for a // SegWit v0 p2wkh input. func runSignPsbtSegWitV0P2WKH(ht *lntest.HarnessTest, alice *node.HarnessNode) { // We test that we can sign a PSBT that spends funds from an input that // the wallet doesn't know about. To set up that test case, we first // derive an address manually that the wallet won't be watching on // chain. We can do that by exporting the account xpub of lnd's main // account. accounts := alice.RPC.ListAccounts(&walletrpc.ListAccountsRequest{}) require.NotEmpty(ht, accounts.Accounts) // We also need to parse the accounts, so we have easy access to the // parsed derivation paths. parsedAccounts, err := walletrpc.AccountsToWatchOnly(accounts.Accounts) require.NoError(ht, err) account := parsedAccounts[0] xpub, err := hdkeychain.NewKeyFromString(account.Xpub) require.NoError(ht, err) const ( changeIndex = 1 addrIndex = 1337 ) fullDerivationPath := []uint32{ hdkeychain.HardenedKeyStart + account.Purpose, hdkeychain.HardenedKeyStart + account.CoinType, hdkeychain.HardenedKeyStart + account.Account, changeIndex, addrIndex, } // Let's simulate a change address. change, err := xpub.DeriveNonStandard(changeIndex) require.NoError(ht, err) // At an index that we are certainly not watching in the wallet. addrKey, err := change.DeriveNonStandard(addrIndex) require.NoError(ht, err) addrPubKey, err := addrKey.ECPubKey() require.NoError(ht, err) pubKeyHash := btcutil.Hash160(addrPubKey.SerializeCompressed()) witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash( pubKeyHash, harnessNetParams, ) require.NoError(ht, err) pkScript, err := txscript.PayToAddrScript(witnessAddr) require.NoError(ht, err) // Send some funds to the output and then try to get a signature through // the SignPsbt RPC to spend that output again. assertPsbtSpend( ht, alice, pkScript, func(packet *psbt.Packet) { in := &packet.Inputs[0] in.Bip32Derivation = []*psbt.Bip32Derivation{{ PubKey: addrPubKey.SerializeCompressed(), Bip32Path: fullDerivationPath, }} in.SighashType = txscript.SigHashAll }, func(packet *psbt.Packet) { require.Len(ht, packet.Inputs, 1) require.Len(ht, packet.Inputs[0].PartialSigs, 1) partialSig := packet.Inputs[0].PartialSigs[0] require.Equal( ht, partialSig.PubKey, addrPubKey.SerializeCompressed(), ) require.Greater( ht, len(partialSig.Signature), ecdsa.MinSigLen, ) }, ) } // runSignPsbtSegWitV0NP2WKH tests that the SignPsbt RPC works correctly for a // SegWit v0 np2wkh input. func runSignPsbtSegWitV0NP2WKH(ht *lntest.HarnessTest, alice *node.HarnessNode) { // We test that we can sign a PSBT that spends funds from an input that // the wallet doesn't know about. To set up that test case, we first // derive an address manually that the wallet won't be watching on // chain. We can do that by exporting the account xpub of lnd's main // account. accounts := alice.RPC.ListAccounts(&walletrpc.ListAccountsRequest{}) require.NotEmpty(ht, accounts.Accounts) // We also need to parse the accounts, so we have easy access to the // parsed derivation paths. parsedAccounts, err := walletrpc.AccountsToWatchOnly(accounts.Accounts) require.NoError(ht, err) account := parsedAccounts[0] xpub, err := hdkeychain.NewKeyFromString(account.Xpub) require.NoError(ht, err) const ( changeIndex = 1 addrIndex = 1337 ) fullDerivationPath := []uint32{ hdkeychain.HardenedKeyStart + account.Purpose, hdkeychain.HardenedKeyStart + account.CoinType, hdkeychain.HardenedKeyStart + account.Account, changeIndex, addrIndex, } // Let's simulate a change address. change, err := xpub.DeriveNonStandard(changeIndex) require.NoError(ht, err) // At an index that we are certainly not watching in the wallet. addrKey, err := change.DeriveNonStandard(addrIndex) require.NoError(ht, err) addrPubKey, err := addrKey.ECPubKey() require.NoError(ht, err) pubKeyHash := btcutil.Hash160(addrPubKey.SerializeCompressed()) witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash( pubKeyHash, harnessNetParams, ) require.NoError(ht, err) witnessProgram, err := txscript.PayToAddrScript(witnessAddr) require.NoError(ht, err) np2wkhAddr, err := btcutil.NewAddressScriptHash( witnessProgram, harnessNetParams, ) require.NoError(ht, err) pkScript, err := txscript.PayToAddrScript(np2wkhAddr) require.NoError(ht, err) // Send some funds to the output and then try to get a signature through // the SignPsbt RPC to spend that output again. assertPsbtSpend( ht, alice, pkScript, func(packet *psbt.Packet) { in := &packet.Inputs[0] in.RedeemScript = witnessProgram in.Bip32Derivation = []*psbt.Bip32Derivation{{ PubKey: addrPubKey.SerializeCompressed(), Bip32Path: fullDerivationPath, }} in.SighashType = txscript.SigHashAll }, func(packet *psbt.Packet) { require.Len(ht, packet.Inputs, 1) require.Len(ht, packet.Inputs[0].PartialSigs, 1) partialSig := packet.Inputs[0].PartialSigs[0] require.Equal( ht, partialSig.PubKey, addrPubKey.SerializeCompressed(), ) require.Greater( ht, len(partialSig.Signature), ecdsa.MinSigLen, ) }, ) } // runSignPsbtSegWitV1KeySpendBip86 tests that the SignPsbt RPC works correctly // for a SegWit v1 p2tr key spend BIP-0086 input. func runSignPsbtSegWitV1KeySpendBip86(ht *lntest.HarnessTest, alice *node.HarnessNode) { // Derive a key we can use for signing. keyDesc, internalKey, fullDerivationPath := deriveInternalKey(ht, alice) // Our taproot key is a BIP0086 key spend only construction that just // commits to the internal key and no root hash. taprootKey := txscript.ComputeTaprootKeyNoScript(internalKey) tapScriptAddr, err := btcutil.NewAddressTaproot( schnorr.SerializePubKey(taprootKey), harnessNetParams, ) require.NoError(ht, err) p2trPkScript, err := txscript.PayToAddrScript(tapScriptAddr) require.NoError(ht, err) // Send some funds to the output and then try to get a signature through // the SignPsbt RPC to spend that output again. assertPsbtSpend( ht, alice, p2trPkScript, func(packet *psbt.Packet) { in := &packet.Inputs[0] in.Bip32Derivation = []*psbt.Bip32Derivation{{ PubKey: keyDesc.RawKeyBytes, Bip32Path: fullDerivationPath, }} p2trDerivation := []*psbt.TaprootBip32Derivation{{ XOnlyPubKey: keyDesc.RawKeyBytes[1:], Bip32Path: fullDerivationPath, }} in.TaprootBip32Derivation = p2trDerivation in.SighashType = txscript.SigHashDefault }, func(packet *psbt.Packet) { require.Len(ht, packet.Inputs, 1) require.Len( ht, packet.Inputs[0].TaprootKeySpendSig, 64, ) }, ) } // runSignPsbtSegWitV1KeySpendRootHash tests that the SignPsbt RPC works // correctly for a SegWit v1 p2tr key spend that also commits to a script tree // root hash. func runSignPsbtSegWitV1KeySpendRootHash(ht *lntest.HarnessTest, alice *node.HarnessNode) { // Derive a key we can use for signing. keyDesc, internalKey, fullDerivationPath := deriveInternalKey(ht, alice) // Let's create a taproot script output now. This is a hash lock with a // simple preimage of "foobar". leaf1 := testScriptHashLock(ht.T, []byte("foobar")) rootHash := leaf1.TapHash() taprootKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash[:]) tapScriptAddr, err := btcutil.NewAddressTaproot( schnorr.SerializePubKey(taprootKey), harnessNetParams, ) require.NoError(ht, err) p2trPkScript, err := txscript.PayToAddrScript(tapScriptAddr) require.NoError(ht, err) // Send some funds to the output and then try to get a signature through // the SignPsbt RPC to spend that output again. assertPsbtSpend( ht, alice, p2trPkScript, func(packet *psbt.Packet) { in := &packet.Inputs[0] in.Bip32Derivation = []*psbt.Bip32Derivation{{ PubKey: keyDesc.RawKeyBytes, Bip32Path: fullDerivationPath, }} p2trDerivation := []*psbt.TaprootBip32Derivation{{ XOnlyPubKey: keyDesc.RawKeyBytes[1:], Bip32Path: fullDerivationPath, }} in.TaprootBip32Derivation = p2trDerivation in.TaprootMerkleRoot = rootHash[:] in.SighashType = txscript.SigHashDefault }, func(packet *psbt.Packet) { require.Len(ht, packet.Inputs, 1) require.Len( ht, packet.Inputs[0].TaprootKeySpendSig, 64, ) }, ) } // runSignPsbtSegWitV1ScriptSpend tests that the SignPsbt RPC works correctly // for a SegWit v1 p2tr script spend. func runSignPsbtSegWitV1ScriptSpend(ht *lntest.HarnessTest, alice *node.HarnessNode) { // Derive a key we can use for signing. keyDesc, internalKey, fullDerivationPath := deriveInternalKey(ht, alice) // Let's create a taproot script output now. This is a hash lock with a // simple preimage of "foobar". leaf1 := testScriptSchnorrSig(ht.T, internalKey) rootHash := leaf1.TapHash() taprootKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash[:]) tapScriptAddr, err := btcutil.NewAddressTaproot( schnorr.SerializePubKey(taprootKey), harnessNetParams, ) require.NoError(ht, err) p2trPkScript, err := txscript.PayToAddrScript(tapScriptAddr) require.NoError(ht, err) // We need to assemble the control block to be able to spend through the // script path. tapscript := input.TapscriptPartialReveal(internalKey, leaf1, nil) controlBlockBytes, err := tapscript.ControlBlock.ToBytes() require.NoError(ht, err) // Send some funds to the output and then try to get a signature through // the SignPsbt RPC to spend that output again. assertPsbtSpend( ht, alice, p2trPkScript, func(packet *psbt.Packet) { in := &packet.Inputs[0] in.Bip32Derivation = []*psbt.Bip32Derivation{{ PubKey: keyDesc.RawKeyBytes, Bip32Path: fullDerivationPath, }} p2trDerivation := []*psbt.TaprootBip32Derivation{{ XOnlyPubKey: keyDesc.RawKeyBytes[1:], Bip32Path: fullDerivationPath, LeafHashes: [][]byte{rootHash[:]}, }} in.TaprootBip32Derivation = p2trDerivation in.SighashType = txscript.SigHashDefault in.TaprootLeafScript = []*psbt.TaprootTapLeafScript{{ ControlBlock: controlBlockBytes, Script: leaf1.Script, LeafVersion: leaf1.LeafVersion, }} }, func(packet *psbt.Packet) { require.Len(ht, packet.Inputs, 1) require.Len( ht, packet.Inputs[0].TaprootScriptSpendSig, 1, ) spendSig := packet.Inputs[0].TaprootScriptSpendSig[0] require.Len(ht, spendSig.Signature, 64) }, ) } // runFundAndSignPsbt makes sure we can sign PSBTs that were funded by our // internal wallet. func runFundAndSignPsbt(ht *lntest.HarnessTest, alice *node.HarnessNode) { alice.AddToLogf("================ runFundAndSignPsbt ===============") // We'll be using a "main" address where we send the funds to and from // several times. mainAddrResp := alice.RPC.NewAddress(&lnrpc.NewAddressRequest{ Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, }) fundOutputs := map[string]uint64{ mainAddrResp.Address: 999000, } spendAddrTypes := []lnrpc.AddressType{ lnrpc.AddressType_NESTED_PUBKEY_HASH, lnrpc.AddressType_WITNESS_PUBKEY_HASH, lnrpc.AddressType_TAPROOT_PUBKEY, } changeAddrTypes := []walletrpc.ChangeAddressType{ walletrpc.ChangeAddressType_CHANGE_ADDRESS_TYPE_UNSPECIFIED, walletrpc.ChangeAddressType_CHANGE_ADDRESS_TYPE_P2TR, } for _, addrType := range spendAddrTypes { for _, changeType := range changeAddrTypes { ht.Logf("testing with address type %s and "+ "change address type %s", addrType, changeType) // First, spend all the coins in the wallet to an // address of the given type so that UTXO will be picked // when funding a PSBT. sendAllCoinsToAddrType(ht, alice, addrType) // Let's fund a PSBT now where we want to send a few // sats to our main address. assertPsbtFundSignSpend( ht, alice, fundOutputs, changeType, false, ) // Send all coins back to a single address once again. sendAllCoinsToAddrType(ht, alice, addrType) // And now make sure the alternate way of signing a // PSBT, which is calling FinalizePsbt directly, also // works for this address type. assertPsbtFundSignSpend( ht, alice, fundOutputs, changeType, true, ) } } } // testFundPsbt tests the FundPsbt RPC use cases that aren't covered by the PSBT // channel funding tests above. These specifically are the use cases of funding // a PSBT that already specifies an input but where the user still wants the // wallet to perform coin selection. func testFundPsbt(ht *lntest.HarnessTest) { // We test a pay-join between Alice and Bob. Bob wants to send Alice // 5 million Satoshis in a non-obvious way. So Bob selects a UTXO that's // bigger than 5 million Satoshis and expects the change minus the send // amount back. Alice then funds the PSBT with coins of her own and // combines her change with the 5 million Satoshis from Bob. With this // Alice ends up paying the fees for a transfer to her. const sendAmount = 5_000_000 aliceAddr := ht.Alice.RPC.NewAddress(&lnrpc.NewAddressRequest{ Type: lnrpc.AddressType_TAPROOT_PUBKEY, }) bobAddr := ht.Bob.RPC.NewAddress(&lnrpc.NewAddressRequest{ Type: lnrpc.AddressType_TAPROOT_PUBKEY, }) ht.Alice.UpdateState() ht.Bob.UpdateState() aliceStartBalance := ht.Alice.State.Wallet.TotalBalance bobStartBalance := ht.Bob.State.Wallet.TotalBalance var bobUtxo *lnrpc.Utxo bobUnspent := ht.Bob.RPC.ListUnspent(&walletrpc.ListUnspentRequest{}) for _, utxo := range bobUnspent.Utxos { if utxo.AmountSat > sendAmount { bobUtxo = utxo break } } if bobUtxo == nil { ht.Fatalf("Bob doesn't have a UTXO of at least %d sats", sendAmount) } bobUtxoTxHash, err := chainhash.NewHash(bobUtxo.Outpoint.TxidBytes) require.NoError(ht, err) tx := wire.NewMsgTx(2) tx.TxIn = append(tx.TxIn, &wire.TxIn{ PreviousOutPoint: wire.OutPoint{ Hash: *bobUtxoTxHash, Index: bobUtxo.Outpoint.OutputIndex, }, }) tx.TxOut = append(tx.TxOut, &wire.TxOut{ // Change going back to Bob. PkScript: addressToPkScript(ht, bobAddr.Address), Value: bobUtxo.AmountSat - sendAmount, }, &wire.TxOut{ // Amount to be sent to Alice, but we'll also send her change // here. PkScript: addressToPkScript(ht, aliceAddr.Address), Value: sendAmount, }) packet, err := psbt.NewFromUnsignedTx(tx) require.NoError(ht, err) derivation, trDerivation := getAddressBip32Derivation( ht, bobUtxo.Address, ht.Bob, ) bobUtxoPkScript, _ := hex.DecodeString(bobUtxo.PkScript) firstInput := &packet.Inputs[0] firstInput.WitnessUtxo = &wire.TxOut{ PkScript: bobUtxoPkScript, Value: bobUtxo.AmountSat, } firstInput.Bip32Derivation = []*psbt.Bip32Derivation{derivation} firstInput.TaprootBip32Derivation = []*psbt.TaprootBip32Derivation{ trDerivation, } if txscript.IsPayToWitnessPubKeyHash(bobUtxoPkScript) { packet.Inputs[0].SighashType = txscript.SigHashAll } // We have the template now. Bob basically funds the 5 million Sats to // send to Alice and Alice now only needs to coin select to pay for the // fees. fundedPacket := fundPsbtCoinSelect(ht, ht.Alice, packet, 1) txFee, err := fundedPacket.GetTxFee() require.NoError(ht, err) // We now let Bob sign the transaction. signedPacket := signPacket(ht, ht.Bob, fundedPacket) // And then Alice, which should give us a fully signed TX. signedPacket = signPacket(ht, ht.Alice, signedPacket) // We should be able to finalize the PSBT and extract the final TX now. extractPublishAndMine(ht, ht.Alice, signedPacket) // Make sure the new wallet balances are reflected correctly. ht.AssertActiveNodesSynced() ht.Alice.UpdateState() ht.Bob.UpdateState() require.Equal( ht, aliceStartBalance+sendAmount-int64(txFee), ht.Alice.State.Wallet.TotalBalance, ) require.Equal( ht, bobStartBalance-sendAmount, ht.Bob.State.Wallet.TotalBalance, ) } // addressToPkScript parses the given address string and returns the pkScript // for the regtest environment. func addressToPkScript(t testing.TB, addr string) []byte { parsed, err := btcutil.DecodeAddress(addr, harnessNetParams) require.NoError(t, err) pkScript, err := txscript.PayToAddrScript(parsed) require.NoError(t, err) return pkScript } // getAddressBip32Derivation returns the PSBT BIP-0032 derivation info of an // address. func getAddressBip32Derivation(t testing.TB, addr string, node *node.HarnessNode) (*psbt.Bip32Derivation, *psbt.TaprootBip32Derivation) { // We can't query a single address directly, so we just query all wallet // addresses. addresses := node.RPC.ListAddresses( &walletrpc.ListAddressesRequest{}, ) var ( path []uint32 pubKeyBytes []byte err error ) for _, account := range addresses.AccountWithAddresses { for _, address := range account.Addresses { if address.Address == addr { path, err = lntest.ParseDerivationPath( address.DerivationPath, ) require.NoError(t, err) pubKeyBytes = address.PublicKey } } } if len(path) != 5 || len(pubKeyBytes) == 0 { t.Fatalf("Derivation path for address %s not found or invalid", addr) } // The actual derivation path in a PSBT needs to be using the hardened // uint32 notation for the first three elements. path[0] += hdkeychain.HardenedKeyStart path[1] += hdkeychain.HardenedKeyStart path[2] += hdkeychain.HardenedKeyStart return &psbt.Bip32Derivation{ PubKey: pubKeyBytes, Bip32Path: path, }, &psbt.TaprootBip32Derivation{ XOnlyPubKey: pubKeyBytes[1:], Bip32Path: path, } } // fundPsbtCoinSelect calls the FundPsbt RPC on the given node using the coin // selection with template PSBT mode. func fundPsbtCoinSelect(t testing.TB, node *node.HarnessNode, packet *psbt.Packet, changeIndex int32) *psbt.Packet { var buf bytes.Buffer err := packet.Serialize(&buf) require.NoError(t, err) cs := &walletrpc.PsbtCoinSelect{ Psbt: buf.Bytes(), } if changeIndex >= 0 { cs.ChangeOutput = &walletrpc.PsbtCoinSelect_ExistingOutputIndex{ ExistingOutputIndex: 1, } } else { cs.ChangeOutput = &walletrpc.PsbtCoinSelect_Add{ Add: true, } } fundResp := node.RPC.FundPsbt(&walletrpc.FundPsbtRequest{ Template: &walletrpc.FundPsbtRequest_CoinSelect{ CoinSelect: cs, }, Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{ SatPerVbyte: 50, }, }) fundedPacket, err := psbt.NewFromRawBytes( bytes.NewReader(fundResp.FundedPsbt), false, ) require.NoError(t, err) return fundedPacket } // signPacket calls the SignPsbt RPC on the given node. func signPacket(t testing.TB, node *node.HarnessNode, packet *psbt.Packet) *psbt.Packet { var buf bytes.Buffer err := packet.Serialize(&buf) require.NoError(t, err) signResp := node.RPC.SignPsbt(&walletrpc.SignPsbtRequest{ FundedPsbt: buf.Bytes(), }) // Let's make sure we have a partial signature. signedPacket, err := psbt.NewFromRawBytes( bytes.NewReader(signResp.SignedPsbt), false, ) require.NoError(t, err) return signedPacket } // extractAndPublish extracts the final transaction from the packet and // publishes it with the given node, mines a block and asserts the TX was mined // successfully. func extractPublishAndMine(ht *lntest.HarnessTest, node *node.HarnessNode, packet *psbt.Packet) *wire.MsgTx { err := psbt.MaybeFinalizeAll(packet) require.NoError(ht, err) finalTx, err := psbt.Extract(packet) require.NoError(ht, err) var buf bytes.Buffer err = finalTx.Serialize(&buf) require.NoError(ht, err) // Publish the second transaction and then mine both of them. node.RPC.PublishTransaction(&walletrpc.Transaction{TxHex: buf.Bytes()}) // Mine one block which should contain two transactions. block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] txHash := finalTx.TxHash() ht.AssertTxInBlock(block, &txHash) return finalTx } // assertPsbtSpend creates an output with the given pkScript on chain and then // attempts to create a sweep transaction that is signed using the SignPsbt RPC // that spends that output again. func assertPsbtSpend(ht *lntest.HarnessTest, alice *node.HarnessNode, pkScript []byte, decorateUnsigned func(*psbt.Packet), verifySigned func(*psbt.Packet)) { // Let's send some coins to that address now. utxo := &wire.TxOut{ Value: 600_000, PkScript: pkScript, } req := &walletrpc.SendOutputsRequest{ Outputs: []*signrpc.TxOut{{ Value: utxo.Value, PkScript: utxo.PkScript, }}, MinConfs: 0, SpendUnconfirmed: true, SatPerKw: 2500, } resp := alice.RPC.SendOutputs(req) prevTx := wire.NewMsgTx(2) err := prevTx.Deserialize(bytes.NewReader(resp.RawTx)) require.NoError(ht, err) prevOut := -1 for idx, txOut := range prevTx.TxOut { if bytes.Equal(txOut.PkScript, pkScript) { prevOut = idx } } require.Greater(ht, prevOut, -1) // Okay, we have everything we need to create a PSBT now. pendingTx := &wire.MsgTx{ Version: 2, TxIn: []*wire.TxIn{{ PreviousOutPoint: wire.OutPoint{ Hash: prevTx.TxHash(), Index: uint32(prevOut), }, }}, // We send to the same address again, but deduct some fees. TxOut: []*wire.TxOut{{ Value: utxo.Value - 600, PkScript: utxo.PkScript, }}, } packet, err := psbt.NewFromUnsignedTx(pendingTx) require.NoError(ht, err) // We first try to sign the psbt without the necessary input data // which should fail with the expected error. var buf bytes.Buffer err = packet.Serialize(&buf) require.NoError(ht, err) signReq := &walletrpc.SignPsbtRequest{FundedPsbt: buf.Bytes()} err = alice.RPC.SignPsbtErr(signReq) require.ErrorContains(ht, err, "input (index=0) doesn't specify "+ "any UTXO info", "error does not match") // Now let's add the meta information that we need for signing. packet.Inputs[0].WitnessUtxo = utxo packet.Inputs[0].NonWitnessUtxo = prevTx decorateUnsigned(packet) // That's it, we should be able to sign the PSBT now. signedPacket := signPacket(ht, alice, packet) // Allow the caller to also verify (and potentially move) some of the // returned fields. verifySigned(signedPacket) // We should be able to finalize the PSBT and extract the final TX now. err = psbt.MaybeFinalizeAll(signedPacket) require.NoError(ht, err) finalTx, err := psbt.Extract(signedPacket) require.NoError(ht, err) // Make sure we can also sign a second time. This makes sure any key // tweaking that happened for the signing didn't affect any keys in the // cache. signedPacket2 := signPacket(ht, alice, packet) verifySigned(signedPacket2) buf.Reset() err = finalTx.Serialize(&buf) require.NoError(ht, err) // Publish the second transaction and then mine both of them. txReq := &walletrpc.Transaction{TxHex: buf.Bytes()} alice.RPC.PublishTransaction(txReq) // Mine one block which should contain two transactions. block := ht.MineBlocksAndAssertNumTxes(1, 2)[0] firstTxHash := prevTx.TxHash() secondTxHash := finalTx.TxHash() ht.AssertTxInBlock(block, &firstTxHash) ht.AssertTxInBlock(block, &secondTxHash) } // assertPsbtFundSignSpend funds a PSBT from the internal wallet and then // attempts to sign it by using the SignPsbt or FinalizePsbt method. func assertPsbtFundSignSpend(ht *lntest.HarnessTest, alice *node.HarnessNode, fundOutputs map[string]uint64, changeType walletrpc.ChangeAddressType, useFinalize bool) { fundResp := alice.RPC.FundPsbt(&walletrpc.FundPsbtRequest{ Template: &walletrpc.FundPsbtRequest_Raw{ Raw: &walletrpc.TxTemplate{ Outputs: fundOutputs, }, }, Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{ SatPerVbyte: 2, }, MinConfs: 1, ChangeType: changeType, }, ) require.GreaterOrEqual(ht, fundResp.ChangeOutputIndex, int32(-1)) // Make sure our change output has all the meta information required for // signing. fundedPacket, err := psbt.NewFromRawBytes( bytes.NewReader(fundResp.FundedPsbt), false, ) require.NoError(ht, err) pOut := fundedPacket.Outputs[fundResp.ChangeOutputIndex] require.NotEmpty(ht, pOut.Bip32Derivation) derivation := pOut.Bip32Derivation[0] _, err = btcec.ParsePubKey(derivation.PubKey) require.NoError(ht, err) require.Len(ht, derivation.Bip32Path, 5) // Ensure we get the change output properly decorated with all the new // Taproot related fields, if it is a Taproot output. if changeType == walletrpc.ChangeAddressType_CHANGE_ADDRESS_TYPE_P2TR { require.NotEmpty(ht, pOut.TaprootBip32Derivation) require.NotEmpty(ht, pOut.TaprootInternalKey) trDerivation := pOut.TaprootBip32Derivation[0] require.Equal( ht, trDerivation.XOnlyPubKey, pOut.TaprootInternalKey, ) _, err := schnorr.ParsePubKey(pOut.TaprootInternalKey) require.NoError(ht, err) } var signedPsbt []byte if useFinalize { finalizeResp := alice.RPC.FinalizePsbt( &walletrpc.FinalizePsbtRequest{ FundedPsbt: fundResp.FundedPsbt, }, ) signedPsbt = finalizeResp.SignedPsbt } else { signResp := alice.RPC.SignPsbt( &walletrpc.SignPsbtRequest{ FundedPsbt: fundResp.FundedPsbt, }, ) signedPsbt = signResp.SignedPsbt } // Let's make sure we have a partial signature. signedPacket, err := psbt.NewFromRawBytes( bytes.NewReader(signedPsbt), false, ) require.NoError(ht, err) // We should be able to finalize the PSBT, extract and publish the final // TX now. finalTx := extractPublishAndMine(ht, alice, signedPacket) // Check type of the change script depending on the change address // type we provided in FundPsbt. changeScript := finalTx.TxOut[fundResp.ChangeOutputIndex].PkScript assertChangeScriptType(ht, changeScript, changeType) } // assertChangeScriptType checks if the given script has the right type given // the change address type we used in FundPsbt. By default, the script should // be a P2WPKH one. func assertChangeScriptType(ht *lntest.HarnessTest, script []byte, fundChangeType walletrpc.ChangeAddressType) { switch fundChangeType { case walletrpc.ChangeAddressType_CHANGE_ADDRESS_TYPE_P2TR: require.True(ht, txscript.IsPayToTaproot(script)) default: require.True(ht, txscript.IsPayToWitnessPubKeyHash(script)) } } // deriveInternalKey derives a signing key and returns its descriptor, full // derivation path and parsed public key. func deriveInternalKey(ht *lntest.HarnessTest, alice *node.HarnessNode) (*signrpc.KeyDescriptor, *btcec.PublicKey, []uint32) { // For the next step, we need a public key. Let's use a special family // for this. req := &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily} keyDesc := alice.RPC.DeriveNextKey(req) // The DeriveNextKey returns a key from the internal 1017 scope. fullDerivationPath := []uint32{ hdkeychain.HardenedKeyStart + keychain.BIP0043Purpose, hdkeychain.HardenedKeyStart + harnessNetParams.HDCoinType, hdkeychain.HardenedKeyStart + uint32(keyDesc.KeyLoc.KeyFamily), 0, uint32(keyDesc.KeyLoc.KeyIndex), } parsedPubKey, err := btcec.ParsePubKey(keyDesc.RawKeyBytes) require.NoError(ht, err) return keyDesc, parsedPubKey, fullDerivationPath } // sendAllCoinsToAddrType sweeps all coins from the wallet and sends them to a // new address of the given type. func sendAllCoinsToAddrType(ht *lntest.HarnessTest, hn *node.HarnessNode, addrType lnrpc.AddressType) { resp := hn.RPC.NewAddress(&lnrpc.NewAddressRequest{ Type: addrType, }) hn.RPC.SendCoins(&lnrpc.SendCoinsRequest{ Addr: resp.Address, SendAll: true, SpendUnconfirmed: true, TargetConf: 6, }) ht.MineBlocksAndAssertNumTxes(1, 1) ht.WaitForBlockchainSync(hn) } // testPsbtChanFundingFailFlow tests the failing of a funding flow by the // remote peer and that the initiator receives the expected error and aborts // the channel opening. The psbt funding flow is used to simulate this behavior // because we can easily let the remote peer run into the timeout. func testPsbtChanFundingFailFlow(ht *lntest.HarnessTest) { const chanSize = funding.MaxBtcFundingAmount // Decrease the timeout window for the remote peer to accelerate the // funding fail process. args := []string{ "--dev.reservationtimeout=1s", "--dev.zombiesweeperinterval=1s", } ht.RestartNodeWithExtraArgs(ht.Bob, args) // Before we start the test, we'll ensure both sides are connected so // the funding flow can be properly executed. alice := ht.Alice bob := ht.Bob ht.EnsureConnected(alice, bob) // At this point, we can begin our PSBT channel funding workflow. We'll // start by generating a pending channel ID externally that will be used // to track this new funding type. pendingChanID := ht.Random32Bytes() // Now that we have the pending channel ID, Alice will open the channel // by specifying a PSBT shim. chanUpdates, _ := ht.OpenChannelPsbt( alice, bob, lntest.OpenChannelParams{ Amt: chanSize, FundingShim: &lnrpc.FundingShim{ Shim: &lnrpc.FundingShim_PsbtShim{ PsbtShim: &lnrpc.PsbtShim{ PendingChanId: pendingChanID, }, }, }, }, ) // We received the AcceptChannel msg from our peer but we are not going // to fund this channel but instead wait for our peer to fail the // funding workflow with an internal error. ht.ReceiveOpenChannelError(chanUpdates, chanfunding.ErrRemoteCanceled) } // testPsbtChanFundingWithUnstableUtxos tests that channel openings with // unstable utxos, in this case in particular unconfirmed utxos still in use by // the sweeper subsystem, are not considered when opening a channel. They bear // the risk of being RBFed and are therefore not safe to open a channel with. func testPsbtChanFundingWithUnstableUtxos(ht *lntest.HarnessTest) { fundingAmt := btcutil.Amount(2_000_000) // First, we'll create two new nodes that we'll use to open channel // between for this test. carol := ht.NewNode("carol", nil) dave := ht.NewNode("dave", nil) ht.EnsureConnected(carol, dave) // Fund Carol's wallet with a confirmed utxo. ht.FundCoins(fundingAmt, carol) ht.AssertNumUTXOs(carol, 1) // Now spend the coins to create an unconfirmed transaction. This is // necessary to test also the neutrino behaviour. For neutrino nodes // only unconfirmed transactions originating from this node will be // recognized as unconfirmed. req := &lnrpc.NewAddressRequest{Type: AddrTypeTaprootPubkey} resp := carol.RPC.NewAddress(req) sendCoinsResp := carol.RPC.SendCoins(&lnrpc.SendCoinsRequest{ Addr: resp.Address, SendAll: true, SatPerVbyte: 1, }) walletUtxo := ht.AssertNumUTXOsUnconfirmed(carol, 1)[0] require.EqualValues(ht, sendCoinsResp.Txid, walletUtxo.Outpoint.TxidStr) chanSize := btcutil.Amount(walletUtxo.AmountSat / 2) // We use STATIC_REMOTE_KEY channels to easily generate sweeps without // anchor sweeps interfering. cType := lnrpc.CommitmentType_STATIC_REMOTE_KEY // We open a normal channel so that we can force-close it and produce // a sweeper originating utxo. update := ht.OpenChannelAssertPending(carol, dave, lntest.OpenChannelParams{ Amt: chanSize, SpendUnconfirmed: true, }) channelPoint := lntest.ChanPointFromPendingUpdate(update) ht.MineBlocksAndAssertNumTxes(1, 2) // Now force close the channel by dave to generate a utxo which is // swept by the sweeper. We have STATIC_REMOTE_KEY Channel Types. ht.CloseChannelAssertPending(dave, channelPoint, true) ht.MineBlocksAndAssertNumTxes(1, 1) // Make sure Carol sees her to_remote output from the force close tx. ht.AssertNumPendingSweeps(carol, 1) // Mine one block to trigger the sweep transaction. ht.MineEmptyBlocks(1) // We wait for the to_remote sweep tx. ht.AssertNumUTXOsUnconfirmed(carol, 1) // We need the maximum funding amount to ensure we are opening the next // channel with all available utxos. carolBalance := carol.RPC.WalletBalance() // The max chan size needs to account for the fee opening the channel // itself. // NOTE: We need to always account for a change here, because their is // an inaccurarcy in the backend code. chanSize = btcutil.Amount(carolBalance.TotalBalance) - fundingFee(2, true) // Now open a channel of this amount via a psbt workflow. // At this point, we can begin our PSBT channel funding workflow. We'll // start by generating a pending channel ID externally that will be used // to track this new funding type. pendingChanID := ht.Random32Bytes() // Now that we have the pending channel ID, Carol will open the channel // by specifying a PSBT shim. We expect it to fail because we try to // fund a channel with the maximum amount of our wallet, which also // includes an unstable utxo originating from the sweeper. chanUpdates, tempPsbt := ht.OpenChannelPsbt( carol, dave, lntest.OpenChannelParams{ Amt: chanSize, FundingShim: &lnrpc.FundingShim{ Shim: &lnrpc.FundingShim_PsbtShim{ PsbtShim: &lnrpc.PsbtShim{ PendingChanId: pendingChanID, }, }, }, CommitmentType: cType, SpendUnconfirmed: true, }, ) fundReq := &walletrpc.FundPsbtRequest{ Template: &walletrpc.FundPsbtRequest_Psbt{ Psbt: tempPsbt, }, Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{ SatPerVbyte: 50, }, MinConfs: 0, SpendUnconfirmed: true, } carol.RPC.FundPsbtAssertErr(fundReq) // We confirm the sweep transaction and make sure we see it as confirmed // from the perspective of the underlying wallet. ht.MineBlocksAndAssertNumTxes(1, 1) // We expect 2 confirmed utxos, the change of the prior successful // channel opening and the confirmed to_remote output. ht.AssertNumUTXOsConfirmed(carol, 2) // We fund the psbt request again and now all utxo are stable and can // finally be used to fund the channel. fundResp := carol.RPC.FundPsbt(fundReq) // We verify the psbt before finalizing it. carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{ PsbtVerify: &lnrpc.FundingPsbtVerify{ PendingChanId: pendingChanID, FundedPsbt: fundResp.FundedPsbt, }, }, }) // Now we'll ask Carol's wallet to sign the PSBT so we can finish the // funding flow. finalizeReq := &walletrpc.FinalizePsbtRequest{ FundedPsbt: fundResp.FundedPsbt, } finalizeRes := carol.RPC.FinalizePsbt(finalizeReq) // We've signed our PSBT now, let's pass it to the intent again. carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtFinalize{ PsbtFinalize: &lnrpc.FundingPsbtFinalize{ PendingChanId: pendingChanID, SignedPsbt: finalizeRes.SignedPsbt, }, }, }) // Consume the "channel pending" update. This waits until the funding // transaction was fully compiled. updateResp := ht.ReceiveOpenChannelUpdate(chanUpdates) upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending) require.True(ht, ok) channelPoint2 := &lnrpc.ChannelPoint{ FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ FundingTxidBytes: upd.ChanPending.Txid, }, OutputIndex: upd.ChanPending.OutputIndex, } var finalTx wire.MsgTx err := finalTx.Deserialize(bytes.NewReader(finalizeRes.RawFinalTx)) require.NoError(ht, err) txHash := finalTx.TxHash() block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] ht.AssertTxInBlock(block, &txHash) // Now we do the same but instead use preselected utxos to verify that // these utxos respects the utxo restrictions on sweeper unconfirmed // inputs as well. // Now force close the channel by dave to generate a utxo which is // swept by the sweeper. We have STATIC_REMOTE_KEY Channel Types. ht.CloseChannelAssertPending(dave, channelPoint2, true) ht.MineBlocksAndAssertNumTxes(1, 1) // Make sure Carol sees her to_remote output from the force close tx. ht.AssertNumPendingSweeps(carol, 1) // Mine one block to trigger the sweep transaction. ht.MineEmptyBlocks(1) // We wait for the to_remote sweep tx of channelPoint2. utxos := ht.AssertNumUTXOsUnconfirmed(carol, 1) // We need the maximum funding amount to ensure we are opening the next // channel with all available utxos. carolBalance = carol.RPC.WalletBalance() // The max chan size needs to account for the fee opening the channel // itself. // NOTE: We need to always account for a change here, because their is // an inaccurarcy in the backend code calculating the fee of a 1 input // one output transaction, it always account for a channge in that case // as well. chanSize = btcutil.Amount(carolBalance.TotalBalance) - fundingFee(2, true) // Now open a channel of this amount via a psbt workflow. // At this point, we can begin our PSBT channel funding workflow. We'll // start by generating a pending channel ID externally that will be used // to track this new funding type. pendingChanID = ht.Random32Bytes() // Now that we have the pending channel ID, Carol will open the channel // by specifying a PSBT shim. We expect it to fail because we try to // fund a channel with the maximum amount of our wallet, which also // includes an unstable utxo originating from the sweeper. chanUpdates, tempPsbt = ht.OpenChannelPsbt( carol, dave, lntest.OpenChannelParams{ Amt: chanSize, FundingShim: &lnrpc.FundingShim{ Shim: &lnrpc.FundingShim_PsbtShim{ PsbtShim: &lnrpc.PsbtShim{ PendingChanId: pendingChanID, }, }, }, CommitmentType: cType, SpendUnconfirmed: true, }, ) // Add selected utxos to the funding intent. decodedPsbt, err := psbt.NewFromRawBytes( bytes.NewReader(tempPsbt), false, ) require.NoError(ht, err) for _, input := range utxos { txHash, err := chainhash.NewHashFromStr(input.Outpoint.TxidStr) require.NoError(ht, err) decodedPsbt.UnsignedTx.TxIn = append( decodedPsbt.UnsignedTx.TxIn, &wire.TxIn{ PreviousOutPoint: wire.OutPoint{ Hash: *txHash, Index: input.Outpoint.OutputIndex, }, }) // The inputs we are using to fund the transaction are known to // the internal wallet that's why we just append an empty input // element so that the parsing of the psbt package succeeds. decodedPsbt.Inputs = append(decodedPsbt.Inputs, psbt.PInput{}) } var psbtBytes bytes.Buffer err = decodedPsbt.Serialize(&psbtBytes) require.NoError(ht, err) fundReq = &walletrpc.FundPsbtRequest{ Template: &walletrpc.FundPsbtRequest_Psbt{ Psbt: psbtBytes.Bytes(), }, Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{ SatPerVbyte: 50, }, MinConfs: 0, SpendUnconfirmed: true, } carol.RPC.FundPsbtAssertErr(fundReq) ht.MineBlocksAndAssertNumTxes(1, 1) // We expect 2 confirmed utxos, the change of the last successful // channel opening and the confirmed to_remote output of channelPoint2. ht.AssertNumUTXOsConfirmed(carol, 2) // After the confirmation of the sweep to_remote output the funding // will now proceed. fundResp = carol.RPC.FundPsbt(fundReq) // We verify the funded psbt. carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{ PsbtVerify: &lnrpc.FundingPsbtVerify{ PendingChanId: pendingChanID, FundedPsbt: fundResp.FundedPsbt, }, }, }) // Now we'll ask Carol's wallet to sign the PSBT so we can finish the // funding flow. finalizeReq = &walletrpc.FinalizePsbtRequest{ FundedPsbt: fundResp.FundedPsbt, } finalizeRes = carol.RPC.FinalizePsbt(finalizeReq) // We've signed our PSBT now, let's pass it to the intent again. carol.RPC.FundingStateStep(&lnrpc.FundingTransitionMsg{ Trigger: &lnrpc.FundingTransitionMsg_PsbtFinalize{ PsbtFinalize: &lnrpc.FundingPsbtFinalize{ PendingChanId: pendingChanID, SignedPsbt: finalizeRes.SignedPsbt, }, }, }) // Consume the "channel pending" update. This waits until the funding // transaction was fully compiled. updateResp = ht.ReceiveOpenChannelUpdate(chanUpdates) upd, ok = updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending) require.True(ht, ok) err = finalTx.Deserialize(bytes.NewReader(finalizeRes.RawFinalTx)) require.NoError(ht, err) txHash = finalTx.TxHash() block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] ht.AssertTxInBlock(block, &txHash) }