walletrpc+itest: add SignPsbt RPC
This commit is contained in:
11 files changed
+844
-249
No files matched your search
+195
-38
@@ -6,12 +6,15 @@ import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcutil/psbt"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lntest"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -40,6 +43,12 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol,
|
||||
dave *lntest.HarnessNode) {
|
||||
|
||||
ctxb := context.Background()
|
||||
|
||||
// Everything we do here should be done within a second or two, so we
|
||||
// can just keep a single timeout context around for all calls.
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
const chanSize = funding.MaxBtcFundingAmount
|
||||
net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, dave)
|
||||
|
||||
@@ -63,8 +72,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol,
|
||||
// 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.
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
chanUpdates, tempPsbt, err := openChannelPsbt(
|
||||
ctxt, carol, dave, lntest.OpenChannelParams{
|
||||
Amt: chanSize,
|
||||
@@ -83,8 +90,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol,
|
||||
// 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.
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
chanUpdates2, psbtBytes2, err := openChannelPsbt(
|
||||
ctxt, carol, net.Alice, lntest.OpenChannelParams{
|
||||
Amt: chanSize,
|
||||
@@ -104,8 +109,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol,
|
||||
// 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.
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
fundReq := &walletrpc.FundPsbtRequest{
|
||||
Template: &walletrpc.FundPsbtRequest_Psbt{
|
||||
Psbt: psbtBytes2,
|
||||
@@ -140,8 +143,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol,
|
||||
|
||||
// Now we'll ask Dave's wallet to sign the PSBT so we can finish the
|
||||
// funding flow.
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
finalizeReq := &walletrpc.FinalizePsbtRequest{
|
||||
FundedPsbt: fundResp.FundedPsbt,
|
||||
}
|
||||
@@ -161,8 +162,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol,
|
||||
|
||||
// Consume the "channel pending" update. This waits until the funding
|
||||
// transaction was fully compiled.
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
updateResp, err := receiveChanUpdate(ctxt, chanUpdates)
|
||||
require.NoError(t.t, err)
|
||||
upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending)
|
||||
@@ -195,8 +194,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol,
|
||||
// Consume the "channel pending" update for the second channel. This
|
||||
// waits until the funding transaction was fully compiled and in this
|
||||
// case published.
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
updateResp2, err := receiveChanUpdate(ctxt, chanUpdates2)
|
||||
require.NoError(t.t, err)
|
||||
upd2, ok := updateResp2.Update.(*lnrpc.OpenStatusUpdate_ChanPending)
|
||||
@@ -225,8 +222,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol,
|
||||
// With the channel open, ensure that it is counted towards Carol's
|
||||
// total channel balance.
|
||||
balReq := &lnrpc.ChannelBalanceRequest{}
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
balRes, err := carol.ChannelBalance(ctxt, balReq)
|
||||
require.NoError(t.t, err)
|
||||
require.NotEqual(t.t, int64(0), balRes.LocalBalance.Sat)
|
||||
@@ -238,7 +233,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol,
|
||||
Memo: "new chans",
|
||||
Value: int64(payAmt),
|
||||
}
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
resp, err := dave.AddInvoice(ctxt, invoice)
|
||||
require.NoError(t.t, err)
|
||||
err = completePaymentRequests(
|
||||
@@ -261,6 +255,11 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
ctxb := context.Background()
|
||||
const chanSize = funding.MaxBtcFundingAmount
|
||||
|
||||
// Everything we do here should be done within a second or two, so we
|
||||
// can just keep a single timeout context around for all calls.
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// First, we'll create two new nodes that we'll use to open channels
|
||||
// between for this test. Both these nodes have an empty wallet as Alice
|
||||
// will be funding the channel.
|
||||
@@ -290,8 +289,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
// 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.
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
chanUpdates, tempPsbt, err := openChannelPsbt(
|
||||
ctxt, carol, dave, lntest.OpenChannelParams{
|
||||
Amt: chanSize,
|
||||
@@ -310,8 +307,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
// 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.
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
chanUpdates2, psbtBytes2, err := openChannelPsbt(
|
||||
ctxt, carol, net.Alice, lntest.OpenChannelParams{
|
||||
Amt: chanSize,
|
||||
@@ -331,8 +326,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
// 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.
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
fundReq := &walletrpc.FundPsbtRequest{
|
||||
Template: &walletrpc.FundPsbtRequest_Psbt{
|
||||
Psbt: psbtBytes2,
|
||||
@@ -375,8 +368,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
|
||||
// Consume the "channel pending" update. This waits until the funding
|
||||
// transaction was fully compiled for both channels.
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
updateResp, err := receiveChanUpdate(ctxt, chanUpdates)
|
||||
require.NoError(t.t, err)
|
||||
upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending)
|
||||
@@ -403,8 +394,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
|
||||
// Now we'll ask Alice's wallet to sign the PSBT so we can finish the
|
||||
// funding flow.
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
finalizeReq := &walletrpc.FinalizePsbtRequest{
|
||||
FundedPsbt: fundResp.FundedPsbt,
|
||||
}
|
||||
@@ -439,8 +428,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
// With the channel open, ensure that it is counted towards Carol's
|
||||
// total channel balance.
|
||||
balReq := &lnrpc.ChannelBalanceRequest{}
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
balRes, err := carol.ChannelBalance(ctxt, balReq)
|
||||
require.NoError(t.t, err)
|
||||
require.NotEqual(t.t, int64(0), balRes.LocalBalance.Sat)
|
||||
@@ -452,7 +439,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
Memo: "new chans",
|
||||
Value: int64(payAmt),
|
||||
}
|
||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||
resp, err := dave.AddInvoice(ctxt, invoice)
|
||||
require.NoError(t.t, err)
|
||||
err = completePaymentRequests(
|
||||
@@ -476,6 +462,11 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
ctxb := context.Background()
|
||||
const chanSize = funding.MaxBtcFundingAmount
|
||||
|
||||
// Everything we do here should be done within a second or two, so we
|
||||
// can just keep a single timeout context around for all calls.
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
args := nodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS)
|
||||
|
||||
// First, we'll create two new nodes that we'll use to open channels
|
||||
@@ -513,8 +504,6 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
|
||||
// Now that we have the pending channel ID, Carol will open the channel
|
||||
// by specifying a PSBT shim.
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
chanUpdates, tempPsbt, err := openChannelPsbt(
|
||||
ctxt, carol, dave, lntest.OpenChannelParams{
|
||||
Amt: chanSize,
|
||||
@@ -547,8 +536,6 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
err = decodedPsbt.Serialize(&psbtBytes)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
fundReq := &walletrpc.FundPsbtRequest{
|
||||
Template: &walletrpc.FundPsbtRequest_Psbt{
|
||||
Psbt: psbtBytes.Bytes(),
|
||||
@@ -583,8 +570,6 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
|
||||
// Now we'll ask Alice's wallet to sign the PSBT so we can finish the
|
||||
// funding flow.
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
finalizeReq := &walletrpc.FinalizePsbtRequest{
|
||||
FundedPsbt: fundResp.FundedPsbt,
|
||||
}
|
||||
@@ -604,8 +589,6 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
|
||||
// Consume the "channel pending" update. This waits until the funding
|
||||
// transaction was fully compiled.
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
updateResp, err := receiveChanUpdate(ctxt, chanUpdates)
|
||||
require.NoError(t.t, err)
|
||||
upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending)
|
||||
@@ -634,8 +617,6 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
Memo: "new chans",
|
||||
Value: int64(payAmt),
|
||||
}
|
||||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
resp, err := dave.AddInvoice(ctxt, invoice)
|
||||
require.NoError(t.t, err)
|
||||
err = completePaymentRequests(
|
||||
@@ -651,6 +632,182 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
closeChannelAndAssert(t, net, carol, chanPoint, false)
|
||||
}
|
||||
|
||||
// runSignPsbt tests that the SignPsbt RPC works correctly.
|
||||
func testSignPsbt(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
runSignPsbt(t, net, net.Alice)
|
||||
}
|
||||
|
||||
// runSignPsbt tests that the SignPsbt RPC works correctly.
|
||||
func runSignPsbt(t *harnessTest, net *lntest.NetworkHarness,
|
||||
alice *lntest.HarnessNode) {
|
||||
|
||||
ctxb := context.Background()
|
||||
|
||||
// Everything we do here should be done within a second or two, so we
|
||||
// can just keep a single timeout context around for all calls.
|
||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// 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, err := alice.WalletKitClient.ListAccounts(
|
||||
ctxt, &walletrpc.ListAccountsRequest{},
|
||||
)
|
||||
require.NoError(t.t, err)
|
||||
require.NotEmpty(t.t, 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(t.t, err)
|
||||
|
||||
account := parsedAccounts[0]
|
||||
xpub, err := hdkeychain.NewKeyFromString(account.Xpub)
|
||||
require.NoError(t.t, 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) // nolint:staticcheck
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// At an index that we are certainly not watching in the wallet.
|
||||
addrKey, err := change.DeriveNonStandard(addrIndex) // nolint:staticcheck
|
||||
require.NoError(t.t, err)
|
||||
|
||||
addrPubKey, err := addrKey.ECPubKey()
|
||||
require.NoError(t.t, err)
|
||||
pubKeyHash := btcutil.Hash160(addrPubKey.SerializeCompressed())
|
||||
witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash(
|
||||
pubKeyHash, harnessNetParams,
|
||||
)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
pkScript, err := txscript.PayToAddrScript(witnessAddr)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// Let's send some coins to that address now.
|
||||
utxo := &wire.TxOut{
|
||||
Value: 600_000,
|
||||
PkScript: pkScript,
|
||||
}
|
||||
resp, err := alice.WalletKitClient.SendOutputs(
|
||||
ctxt, &walletrpc.SendOutputsRequest{
|
||||
Outputs: []*signrpc.TxOut{{
|
||||
Value: utxo.Value,
|
||||
PkScript: utxo.PkScript,
|
||||
}},
|
||||
MinConfs: 0,
|
||||
SpendUnconfirmed: true,
|
||||
SatPerKw: 2500,
|
||||
},
|
||||
)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
prevTx := wire.NewMsgTx(2)
|
||||
err = prevTx.Deserialize(bytes.NewReader(resp.RawTx))
|
||||
require.NoError(t.t, err)
|
||||
|
||||
prevOut := -1
|
||||
for idx, txOut := range prevTx.TxOut {
|
||||
if bytes.Equal(txOut.PkScript, pkScript) {
|
||||
prevOut = idx
|
||||
}
|
||||
}
|
||||
require.Greater(t.t, 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(t.t, err)
|
||||
|
||||
// Now let's add the meta information that we need for signing.
|
||||
packet.Inputs[0].Bip32Derivation = []*psbt.Bip32Derivation{{
|
||||
PubKey: addrPubKey.SerializeCompressed(),
|
||||
Bip32Path: fullDerivationPath,
|
||||
}}
|
||||
packet.Inputs[0].WitnessUtxo = utxo
|
||||
packet.Inputs[0].NonWitnessUtxo = prevTx
|
||||
packet.Inputs[0].SighashType = txscript.SigHashAll
|
||||
|
||||
// That's it, we should be able to sign the PSBT now.
|
||||
var buf bytes.Buffer
|
||||
err = packet.Serialize(&buf)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
signResp, err := alice.WalletKitClient.SignPsbt(
|
||||
ctxt, &walletrpc.SignPsbtRequest{
|
||||
FundedPsbt: buf.Bytes(),
|
||||
},
|
||||
)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// Let's make sure we have a partial signature.
|
||||
signedPacket, err := psbt.NewFromRawBytes(
|
||||
bytes.NewReader(signResp.SignedPsbt), false,
|
||||
)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
require.Len(t.t, signedPacket.Inputs, 1)
|
||||
require.Len(t.t, signedPacket.Inputs[0].PartialSigs, 1)
|
||||
|
||||
partialSig := signedPacket.Inputs[0].PartialSigs[0]
|
||||
require.Equal(t.t, partialSig.PubKey, addrPubKey.SerializeCompressed())
|
||||
require.Greater(t.t, len(partialSig.Signature), btcec.MinSigLen)
|
||||
|
||||
// We should be able to finalize the PSBT and extract the final TX now.
|
||||
err = psbt.MaybeFinalizeAll(signedPacket)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
finalTx, err := psbt.Extract(signedPacket)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
buf.Reset()
|
||||
err = finalTx.Serialize(&buf)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// Publish the second transaction and then mine both of them.
|
||||
_, err = alice.WalletKitClient.PublishTransaction(
|
||||
ctxt, &walletrpc.Transaction{
|
||||
TxHex: buf.Bytes(),
|
||||
},
|
||||
)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
// Mine one block which should contain two transactions.
|
||||
block := mineBlocks(t, net, 1, 2)[0]
|
||||
firstTxHash := prevTx.TxHash()
|
||||
secondTxHash := finalTx.TxHash()
|
||||
assertTxInBlock(t, block, &firstTxHash)
|
||||
assertTxInBlock(t, block, &secondTxHash)
|
||||
}
|
||||
|
||||
// openChannelPsbt attempts to open a channel between srcNode and destNode with
|
||||
// the passed channel funding parameters. If the passed context has a timeout,
|
||||
// then if the timeout is reached before the channel pending notification is
|
||||
|
||||
@@ -166,6 +166,7 @@ func testRemoteSigner(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
name: "psbt",
|
||||
fn: func(tt *harnessTest, wo, carol *lntest.HarnessNode) {
|
||||
runPsbtChanFunding(net, tt, carol, wo)
|
||||
runSignPsbt(tt, net, wo)
|
||||
},
|
||||
}}
|
||||
|
||||
|
||||
@@ -283,6 +283,10 @@ var allTestCases = []*testCase{
|
||||
name: "psbt channel funding external",
|
||||
test: testPsbtChanFundingExternal,
|
||||
},
|
||||
{
|
||||
name: "sign psbt",
|
||||
test: testSignPsbt,
|
||||
},
|
||||
{
|
||||
name: "batch channel funding",
|
||||
test: testBatchChanFunding,
|
||||
|
||||
Reference in new issue
Block a user