diff --git a/funding/manager_test.go b/funding/manager_test.go index db61c30006..69e23eeb83 100644 --- a/funding/manager_test.go +++ b/funding/manager_test.go @@ -556,6 +556,11 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey, return nil, nil }, AliasManager: aliasMgr, + // For unit tests we default to false meaning that no funds + // originated from the sweeper. + IsSweeperOutpoint: func(wire.OutPoint) bool { + return false + }, } for _, op := range options { diff --git a/itest/list_on_test.go b/itest/list_on_test.go index b4f586fa21..4696fc80ec 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -117,6 +117,14 @@ var allTestCases = []*lntest.TestCase{ Name: "batch channel funding", TestFunc: testBatchChanFunding, }, + { + Name: "open channel with unstable utxos", + TestFunc: testChannelFundingWithUnstableUtxos, + }, + { + Name: "open psbt channel with unstable utxos", + TestFunc: testPsbtChanFundingWithUnstableUtxos, + }, { Name: "update channel policy", TestFunc: testUpdateChannelPolicy, diff --git a/itest/lnd_channel_funding_utxo_selection_test.go b/itest/lnd_channel_funding_utxo_selection_test.go index 4ba4a557bd..8504a17cb9 100644 --- a/itest/lnd_channel_funding_utxo_selection_test.go +++ b/itest/lnd_channel_funding_utxo_selection_test.go @@ -330,7 +330,8 @@ func runUtxoSelectionTestCase(ht *lntest.HarnessTest, alice, // When re-selecting a spent output for funding another channel we // expect the respective error message. if tc.reuseUtxo { - expectedErrStr := fmt.Sprintf("outpoint already spent: %s:%d", + expectedErrStr := fmt.Sprintf("outpoint already spent or "+ + "locked by another subsystem: %s:%d", selectedOutpoints[0].TxidStr, selectedOutpoints[0].OutputIndex) expectedErr := fmt.Errorf(expectedErrStr) diff --git a/itest/lnd_funding_test.go b/itest/lnd_funding_test.go index 97613429eb..38ca71d92f 100644 --- a/itest/lnd_funding_test.go +++ b/itest/lnd_funding_test.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainreg" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/labels" @@ -1250,3 +1251,211 @@ func deriveFundingShim(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, return fundingShim, chanPoint } + +// testChannelFundingWithUnstableUtxos tests channel openings with restricted +// utxo selection. Internal wallet utxos might be restricted due to another +// subsystems still using it therefore it would be unsecure to use them for +// channel openings. This test focuses on unconfirmed utxos which are still +// being used by the sweeper subsystem hence should only be used when confirmed. +func testChannelFundingWithUnstableUtxos(ht *lntest.HarnessTest) { + // Select funding amt below wumbo size because we later use fundMax to + // open a channel with the total balance. + fundingAmt := btcutil.Amount(3_000_000) + + // We use STATIC_REMOTE_KEY channels because anchor sweeps would + // interfere and create additional utxos. + // Although its the current default we explicitly signal it. + cType := lnrpc.CommitmentType_STATIC_REMOTE_KEY + + // First, we'll create two new nodes that we'll use to open channel + // between for this test. + carol := ht.NewNode("carol", nil) + // We'll attempt at max 2 pending channels, so Dave will need to accept + // two pending ones. + dave := ht.NewNode("dave", []string{ + "--maxpendingchannels=2", + }) + ht.EnsureConnected(carol, dave) + + // Fund Carol's wallet with a confirmed utxo. + ht.FundCoins(fundingAmt, carol) + + // 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) + + // We will attempt to open 2 channels at a time. + chanSize := btcutil.Amount(walletUtxo.AmountSat / 3) + + // Open a channel to dave with an unconfirmed utxo. Although this utxo + // is unconfirmed it can be used to open a channel because it did not + // originated from the sweeper subsystem. + update := ht.OpenChannelAssertPending(carol, dave, + lntest.OpenChannelParams{ + Amt: chanSize, + SpendUnconfirmed: true, + CommitmentType: cType, + }) + chanPoint1 := lntest.ChanPointFromPendingUpdate(update) + + // Verify that both nodes know about the channel. + ht.AssertNumPendingOpenChannels(carol, 1) + ht.AssertNumPendingOpenChannels(dave, 1) + + // We open another channel on the fly, funds are unconfirmed but because + // the tx was not created by the sweeper we can use it and open another + // channel. This is a common use case when opening zeroconf channels, + // so unconfirmed utxos originated from prior channel opening are safe + // to use because channel opening should not be RBFed, at least not for + // now. + update = ht.OpenChannelAssertPending(carol, dave, + lntest.OpenChannelParams{ + Amt: chanSize, + SpendUnconfirmed: true, + CommitmentType: cType, + }) + + chanPoint2 := lntest.ChanPointFromPendingUpdate(update) + + ht.AssertNumPendingOpenChannels(carol, 2) + ht.AssertNumPendingOpenChannels(dave, 2) + + // We expect the initial funding tx to confirm and also the two + // unconfirmed channel openings. + ht.MineBlocksAndAssertNumTxes(1, 3) + + // Now we create an unconfirmed utxo which originated from the sweeper + // subsystem and hence is not safe to use for channel openings. We do + // that by dave force-closing the channel. Which let's carol sweep its + // to_remote output which is not encumbered by any relative locktime. + ht.CloseChannelAssertPending(dave, chanPoint2, true) + // Mine the force close commitment transaction. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Mine one block to trigger the sweep transaction. + ht.MineEmptyBlocks(1) + + // We need to wait for carol initiating the sweep of the to_remote + // output of chanPoint2. + utxos := ht.AssertNumUTXOsUnconfirmed(carol, 1) + + // We filter for the unconfirmed utxo and try to open a channel with + // that utxo. + utxoOpt := fn.Find(func(u *lnrpc.Utxo) bool { + return u.Confirmations == 0 + }, utxos) + fundingUtxo := utxoOpt.UnwrapOrFail(ht.T) + + // Now try to open the channel with this utxo and expect an error. + expectedErr := fmt.Errorf("outpoint already spent or "+ + "locked by another subsystem: %s:%d", + fundingUtxo.Outpoint.TxidStr, + fundingUtxo.Outpoint.OutputIndex) + + ht.OpenChannelAssertErr(carol, dave, + lntest.OpenChannelParams{ + FundMax: true, + SpendUnconfirmed: true, + Outpoints: []*lnrpc.OutPoint{ + fundingUtxo.Outpoint, + }, + }, expectedErr) + + // The channel opening failed because the utxo was unconfirmed and + // originated from the sweeper subsystem. Now we confirm the + // to_remote sweep and expect the channel opening to work. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Try opening the channel with the same utxo (now confirmed) again. + update = ht.OpenChannelAssertPending(carol, dave, + lntest.OpenChannelParams{ + FundMax: true, + SpendUnconfirmed: true, + Outpoints: []*lnrpc.OutPoint{ + fundingUtxo.Outpoint, + }, + }) + + chanPoint3 := lntest.ChanPointFromPendingUpdate(update) + ht.AssertNumPendingOpenChannels(carol, 1) + ht.AssertNumPendingOpenChannels(dave, 1) + + // We expect chanPoint3 to confirm. + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Force Close the channel and test the opening flow without preselected + // utxos. + // Before we tested the channel funding with a selected coin, now we + // want to make sure that our internal coin selection also adheres to + // the restictions of unstable utxos. + // We create the unconfirmed sweeper originating utxo just like before + // by force-closing a channel from dave's side. + ht.CloseChannelAssertPending(dave, chanPoint3, true) + ht.MineBlocksAndAssertNumTxes(1, 1) + + // Mine one block to trigger the sweep transaction. + ht.MineEmptyBlocks(1) + + // Wait for the to_remote sweep tx to show up in carol's wallet. + ht.AssertNumUTXOsUnconfirmed(carol, 1) + + // Calculate the maximum amount our wallet has for the channel funding + // so that we will use all utxos. + carolBalance := carol.RPC.WalletBalance() + + // Now calculate the fee for the channel opening transaction. We don't + // have to keep a channel reserve because we are using STATIC_REMOTE_KEY + // channels. + // NOTE: The TotalBalance includes the unconfirmed balance as well. + chanSize = btcutil.Amount(carolBalance.TotalBalance) - + fundingFee(2, false) + + // We are trying to open a channel with the maximum amount and expect it + // to fail because one of the utxos cannot be used because it is + // unstable. + expectedErr = fmt.Errorf("not enough witness outputs to create " + + "funding transaction") + + ht.OpenChannelAssertErr(carol, dave, + lntest.OpenChannelParams{ + Amt: chanSize, + SpendUnconfirmed: true, + CommitmentType: cType, + }, expectedErr) + + // Confirm the to_remote sweep utxo. + ht.MineBlocksAndAssertNumTxes(1, 1) + + ht.AssertNumUTXOsConfirmed(carol, 2) + + // Now after the sweep utxo is confirmed it is stable and can be used + // for channel openings again. + update = ht.OpenChannelAssertPending(carol, dave, + lntest.OpenChannelParams{ + Amt: chanSize, + SpendUnconfirmed: true, + CommitmentType: cType, + }) + chanPoint4 := lntest.ChanPointFromPendingUpdate(update) + + // Verify that both nodes know about the channel. + ht.AssertNumPendingOpenChannels(carol, 1) + ht.AssertNumPendingOpenChannels(dave, 1) + + ht.MineBlocksAndAssertNumTxes(1, 1) + + ht.CloseChannel(carol, chanPoint1) + ht.CloseChannel(carol, chanPoint4) +} diff --git a/itest/lnd_psbt_test.go b/itest/lnd_psbt_test.go index a3b5f757b9..a6f15fd357 100644 --- a/itest/lnd_psbt_test.go +++ b/itest/lnd_psbt_test.go @@ -1592,3 +1592,321 @@ func testPsbtChanFundingFailFlow(ht *lntest.HarnessTest) { // 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) + + // 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.Miner.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) + + // 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) + channelPoint3 := &lnrpc.ChannelPoint{ + FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{ + FundingTxidBytes: upd.ChanPending.Txid, + }, + OutputIndex: upd.ChanPending.OutputIndex, + } + + err = finalTx.Deserialize(bytes.NewReader(finalizeRes.RawFinalTx)) + require.NoError(ht, err) + + txHash = finalTx.TxHash() + block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] + ht.Miner.AssertTxInBlock(block, &txHash) + + ht.CloseChannel(carol, channelPoint3) +} diff --git a/lntest/harness.go b/lntest/harness.go index c291e7cf39..5c2bef4889 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -1205,6 +1205,7 @@ func (h *HarnessTest) OpenChannelAssertErr(srcNode, destNode *node.HarnessNode, // Receive an error to be sent from the stream. _, err := h.receiveOpenChannelUpdate(respStream) + require.NotNil(h, err, "expected channel opening to fail") // Use string comparison here as we haven't codified all the RPC errors // yet. diff --git a/lntest/rpc/wallet_kit.go b/lntest/rpc/wallet_kit.go index 79ccdcbdf4..967679d664 100644 --- a/lntest/rpc/wallet_kit.go +++ b/lntest/rpc/wallet_kit.go @@ -68,6 +68,16 @@ func (h *HarnessRPC) FundPsbt( return resp } +// FundPsbtAssertErr makes a RPC call to the node's FundPsbt and asserts an +// error is returned. +func (h *HarnessRPC) FundPsbtAssertErr(req *walletrpc.FundPsbtRequest) { + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + _, err := h.WalletKit.FundPsbt(ctxt, req) + require.Error(h, err, "expected error returned") +} + // FinalizePsbt makes a RPC call to node's FinalizePsbt and asserts. func (h *HarnessRPC) FinalizePsbt( req *walletrpc.FinalizePsbtRequest) *walletrpc.FinalizePsbtResponse { diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go index 12b3aafc96..dd1db5f20c 100644 --- a/lnwallet/test/test_interface.go +++ b/lnwallet/test/test_interface.go @@ -2947,6 +2947,11 @@ func waitForWalletSync(r *rpctest.Harness, w *lnwallet.LightningWallet) error { func testSingleFunderExternalFundingTx(miner *rpctest.Harness, alice, bob *lnwallet.LightningWallet, t *testing.T) { + // Define a filter function without any restrictions. + allowUtxo := func(lnwallet.Utxo) bool { + return true + } + // First, we'll obtain multi-sig keys from both Alice and Bob which // simulates them exchanging keys on a higher level. aliceFundingKey, err := alice.DeriveNextKey(keychain.KeyFamilyMultiSig) @@ -2964,7 +2969,7 @@ func testSingleFunderExternalFundingTx(miner *rpctest.Harness, aliceChanFunder := chanfunding.NewWalletAssembler( chanfunding.WalletConfig{ CoinSource: lnwallet.NewCoinSource( - alice, nil, + alice, allowUtxo, ), CoinSelectLocker: alice, CoinLeaser: alice,