diff --git a/lntest/itest/lnd_signer_test.go b/lntest/itest/lnd_signer_test.go index ce933ff8e..0d4b78185 100644 --- a/lntest/itest/lnd_signer_test.go +++ b/lntest/itest/lnd_signer_test.go @@ -1,12 +1,18 @@ package itest import ( + "bytes" "context" "fmt" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" "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/stretchr/testify/require" ) @@ -190,6 +196,201 @@ func runDeriveSharedKey(t *harnessTest, alice *lntest.HarnessNode) { assertErrorMatch("use either raw_key_bytes or key_index", req) } +// testSignOutputRaw makes sure that the SignOutputRaw RPC can be used with all +// custom ways of specifying the signing key in the key descriptor/locator. +func testSignOutputRaw(net *lntest.NetworkHarness, t *harnessTest) { + runSignOutputRaw(t, net, net.Alice) +} + +// runSignOutputRaw makes sure that the SignOutputRaw RPC can be used with all +// custom ways of specifying the signing key in the key descriptor/locator. +func runSignOutputRaw(t *harnessTest, net *lntest.NetworkHarness, + alice *lntest.HarnessNode) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + // For the next step, we need a public key. Let's use a special family + // for this. We want this to be an index of zero. + const testCustomKeyFamily = 44 + keyDesc, err := alice.WalletKitClient.DeriveNextKey( + ctxt, &walletrpc.KeyReq{ + KeyFamily: testCustomKeyFamily, + }, + ) + require.NoError(t.t, err) + require.Equal(t.t, int32(0), keyDesc.KeyLoc.KeyIndex) + + targetPubKey, err := btcec.ParsePubKey(keyDesc.RawKeyBytes) + require.NoError(t.t, err) + + // First, try with a key descriptor that only sets the public key. + assertSignOutputRaw( + t, net, alice, targetPubKey, &signrpc.KeyDescriptor{ + RawKeyBytes: keyDesc.RawKeyBytes, + }, + ) + + // Now try again, this time only with the (0 index!) key locator. + assertSignOutputRaw( + t, net, alice, targetPubKey, &signrpc.KeyDescriptor{ + KeyLoc: &signrpc.KeyLocator{ + KeyFamily: keyDesc.KeyLoc.KeyFamily, + KeyIndex: keyDesc.KeyLoc.KeyIndex, + }, + }, + ) + + // And now test everything again with a new key where we know the index + // is not 0. + ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + keyDesc, err = alice.WalletKitClient.DeriveNextKey( + ctxt, &walletrpc.KeyReq{ + KeyFamily: testCustomKeyFamily, + }, + ) + require.NoError(t.t, err) + require.Equal(t.t, int32(1), keyDesc.KeyLoc.KeyIndex) + + targetPubKey, err = btcec.ParsePubKey(keyDesc.RawKeyBytes) + require.NoError(t.t, err) + + // First, try with a key descriptor that only sets the public key. + assertSignOutputRaw( + t, net, alice, targetPubKey, &signrpc.KeyDescriptor{ + RawKeyBytes: keyDesc.RawKeyBytes, + }, + ) + + // Now try again, this time only with the key locator. + assertSignOutputRaw( + t, net, alice, targetPubKey, &signrpc.KeyDescriptor{ + KeyLoc: &signrpc.KeyLocator{ + KeyFamily: keyDesc.KeyLoc.KeyFamily, + KeyIndex: keyDesc.KeyLoc.KeyIndex, + }, + }, + ) +} + +// assertSignOutputRaw sends coins to a p2wkh address derived from the given +// target public key and then tries to spend that output again by invoking the +// SignOutputRaw RPC with the key descriptor provided. +func assertSignOutputRaw(t *harnessTest, net *lntest.NetworkHarness, + alice *lntest.HarnessNode, targetPubKey *btcec.PublicKey, + keyDesc *signrpc.KeyDescriptor) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + pubKeyHash := btcutil.Hash160(targetPubKey.SerializeCompressed()) + targetAddr, err := btcutil.NewAddressWitnessPubKeyHash( + pubKeyHash, harnessNetParams, + ) + require.NoError(t.t, err) + targetScript, err := txscript.PayToAddrScript(targetAddr) + require.NoError(t.t, err) + + // Send some coins to the generated p2wpkh address. + _, err = alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{ + Addr: targetAddr.String(), + Amount: 800_000, + }) + require.NoError(t.t, err) + + // Wait until the TX is found in the mempool. + txid, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) + require.NoError(t.t, err) + + targetOutputIndex := getOutputIndex( + t, net.Miner, txid, targetAddr.String(), + ) + + // Clear the mempool. + mineBlocks(t, net, 1, 1) + + // Try to spend the output now to a new p2wkh address. + p2wkhResp, err := alice.NewAddress(ctxt, &lnrpc.NewAddressRequest{ + Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH, + }) + require.NoError(t.t, err) + + p2wkhAdrr, err := btcutil.DecodeAddress( + p2wkhResp.Address, harnessNetParams, + ) + require.NoError(t.t, err) + + p2wkhPkScript, err := txscript.PayToAddrScript(p2wkhAdrr) + require.NoError(t.t, err) + + tx := wire.NewMsgTx(2) + tx.TxIn = []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: *txid, + Index: uint32(targetOutputIndex), + }, + }} + value := int64(800_000 - 200) + tx.TxOut = []*wire.TxOut{{ + PkScript: p2wkhPkScript, + Value: value, + }} + + var buf bytes.Buffer + require.NoError(t.t, tx.Serialize(&buf)) + + signResp, err := alice.SignerClient.SignOutputRaw( + ctxt, &signrpc.SignReq{ + RawTxBytes: buf.Bytes(), + SignDescs: []*signrpc.SignDescriptor{{ + Output: &signrpc.TxOut{ + PkScript: targetScript, + Value: 800_000, + }, + InputIndex: 0, + KeyDesc: keyDesc, + Sighash: uint32(txscript.SigHashAll), + WitnessScript: targetScript, + }}, + }, + ) + require.NoError(t.t, err) + + tx.TxIn[0].Witness = wire.TxWitness{ + append(signResp.RawSigs[0], byte(txscript.SigHashAll)), + targetPubKey.SerializeCompressed(), + } + + buf.Reset() + require.NoError(t.t, tx.Serialize(&buf)) + + _, err = alice.WalletKitClient.PublishTransaction( + ctxt, &walletrpc.Transaction{ + TxHex: buf.Bytes(), + }, + ) + require.NoError(t.t, err) + + // Wait until the spending tx is found. + txid, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout) + require.NoError(t.t, err) + p2wkhOutputIndex := getOutputIndex( + t, net.Miner, txid, p2wkhAdrr.String(), + ) + op := &lnrpc.OutPoint{ + TxidBytes: txid[:], + OutputIndex: uint32(p2wkhOutputIndex), + } + assertWalletUnspent(t, alice, op) + + // Mine another block to clean up the mempool and to make sure the spend + // tx is actually included in a block. + mineBlocks(t, net, 1, 1) +} + // deriveCustomizedKey uses the family and index to derive a public key from // the node's walletkit client. func deriveCustomizedKey(ctx context.Context, node *lntest.HarnessNode, diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index 24b6cd0d7..72c2ed0d0 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -157,6 +157,10 @@ var allTestCases = []*testCase{ name: "derive shared key", test: testDeriveSharedKey, }, + { + name: "sign output raw", + test: testSignOutputRaw, + }, { name: "async payments benchmark", test: testAsyncPayments, diff --git a/lntest/itest/utils.go b/lntest/itest/utils.go index 5c11f9b9b..fdc611eb2 100644 --- a/lntest/itest/utils.go +++ b/lntest/itest/utils.go @@ -8,7 +8,9 @@ import ( "time" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/input" @@ -463,3 +465,31 @@ func findTxAtHeight(t *harnessTest, height int32, return nil } + +// getOutputIndex returns the output index of the given address in the given +// transaction. +func getOutputIndex(t *harnessTest, miner *lntest.HarnessMiner, + txid *chainhash.Hash, addr string) int { + + t.t.Helper() + + // We'll then extract the raw transaction from the mempool in order to + // determine the index of the p2tr output. + tx, err := miner.Client.GetRawTransaction(txid) + require.NoError(t.t, err) + + p2trOutputIndex := -1 + for i, txOut := range tx.MsgTx().TxOut { + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + txOut.PkScript, miner.ActiveNet, + ) + require.NoError(t.t, err) + + if addrs[0].String() == addr { + p2trOutputIndex = i + } + } + require.Greater(t.t, p2trOutputIndex, -1) + + return p2trOutputIndex +}