mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
653a8ac55e
This commit replaces `AssertTopologyChannelOpen` with `AssertChannelInGraph`, which asserts a given channel edge is found. `AssertTopologyChannelOpen` only asserts a given edge has been received via the topology subscription, while we need to make sure the channel is in the graph before continuing our tests.
1956 lines
61 KiB
Go
1956 lines
61 KiB
Go
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,
|
|
},
|
|
)
|
|
|
|
// If this is a taproot channel, then we'll decode the PSBT to assert
|
|
// that an internal key is included.
|
|
if commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT {
|
|
decodedPSBT, err := psbt.NewFromRawBytes(
|
|
bytes.NewReader(tempPsbt), false,
|
|
)
|
|
require.NoError(ht, err)
|
|
|
|
require.Len(ht, decodedPSBT.Outputs[0].TaprootInternalKey, 32)
|
|
}
|
|
|
|
// 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.AssertChannelInGraph(carol, chanPoint)
|
|
ht.AssertChannelInGraph(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.AssertChannelInGraph(carol, chanPoint)
|
|
ht.AssertChannelInGraph(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.AssertChannelInGraph(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)
|
|
}
|
|
|
|
// 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)
|
|
}
|