walletrpc+itest: add SignPsbt RPC

This commit is contained in:
Oliver Gugger committed 2022-01-06 14:35:07 +01:00
1 parent d135b638f6
commit 8994d86a1d
11 files changed
+844 -249

No files matched your search

+195 -38
View File
@@ -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
+1
View File
@@ -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)
},
}}
+4
View File
@@ -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,