From c45d0f6b075b3517550695d34f4340466be072cb Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 29 Jul 2022 18:20:10 +0200 Subject: [PATCH] itest: add integration tests for tapscript import We add basic integration test coverage for importing Tapscript addresses. Note that FundPsbt is not supported with those types of imported keys, so we need to manually fill in all information in the PSBT to be able to sweep the funds again. --- lntest/itest/lnd_onchain_test.go | 2 +- lntest/itest/lnd_signer_test.go | 2 +- lntest/itest/lnd_taproot_test.go | 391 ++++++++++++++++++++++++++++++- lntest/itest/test_harness.go | 19 +- 4 files changed, 402 insertions(+), 12 deletions(-) diff --git a/lntest/itest/lnd_onchain_test.go b/lntest/itest/lnd_onchain_test.go index bed4173c8..add35d8a0 100644 --- a/lntest/itest/lnd_onchain_test.go +++ b/lntest/itest/lnd_onchain_test.go @@ -99,7 +99,7 @@ func runCPFP(net *lntest.NetworkHarness, t *harnessTest, TxidBytes: txid[:], OutputIndex: uint32(bobOutputIdx), } - assertWalletUnspent(t, bob, op) + assertWalletUnspent(t, bob, op, "") // We'll attempt to bump the fee of this transaction by performing a // CPFP from Alice's point of view. diff --git a/lntest/itest/lnd_signer_test.go b/lntest/itest/lnd_signer_test.go index ea7d147cd..6cee27b82 100644 --- a/lntest/itest/lnd_signer_test.go +++ b/lntest/itest/lnd_signer_test.go @@ -390,7 +390,7 @@ func assertSignOutputRaw(t *harnessTest, net *lntest.NetworkHarness, TxidBytes: txid[:], OutputIndex: uint32(p2wkhOutputIndex), } - assertWalletUnspent(t, alice, op) + 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. diff --git a/lntest/itest/lnd_taproot_test.go b/lntest/itest/lnd_taproot_test.go index 348eb640c..f953bf7a2 100644 --- a/lntest/itest/lnd_taproot_test.go +++ b/lntest/itest/lnd_taproot_test.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" @@ -57,6 +58,11 @@ func testTaproot(net *lntest.NetworkHarness, t *harnessTest) { testTaprootMuSig2KeySpendRootHash(ctxt, t, net.Alice, net) testTaprootMuSig2ScriptSpend(ctxt, t, net.Alice, net) testTaprootMuSig2CombinedLeafKeySpend(ctxt, t, net.Alice, net) + + testTaprootImportTapscriptFullTree(ctxt, t, net.Alice, net) + testTaprootImportTapscriptPartialReveal(ctxt, t, net.Alice, net) + testTaprootImportTapscriptRootHashOnly(ctxt, t, net.Alice, net) + testTaprootImportTapscriptFullKey(ctxt, t, net.Alice, net) } // testTaprootSendCoinsKeySpendBip86 tests sending to and spending from @@ -97,7 +103,7 @@ func testTaprootSendCoinsKeySpendBip86(ctxt context.Context, TxidBytes: txid[:], OutputIndex: uint32(p2trOutputIndex), } - assertWalletUnspent(t, alice, op) + assertWalletUnspent(t, alice, op, "") // Mine a block to clean up the mempool. mineBlocks(t, net, 1, 1) @@ -152,7 +158,7 @@ func testTaprootComputeInputScriptKeySpendBip86(ctxt context.Context, TxidBytes: txid[:], OutputIndex: uint32(p2trOutputIndex), } - assertWalletUnspent(t, alice, op) + assertWalletUnspent(t, alice, op, "") p2trOutpoint := wire.OutPoint{ Hash: *txid, @@ -1117,6 +1123,354 @@ func testTaprootMuSig2CombinedLeafKeySpend(ctxt context.Context, t *harnessTest, ) } +// testTaprootImportTapscriptScriptSpend tests importing p2tr script addresses +// using the script path with the full tree known. +func testTaprootImportTapscriptFullTree(ctxt context.Context, t *harnessTest, + alice *lntest.HarnessNode, net *lntest.NetworkHarness) { + + // For the next step, we need a public key. Let's use a special family + // for this. + _, internalKey, derivationPath := deriveInternalKey(ctxt, t, alice) + + // Let's create a taproot script output now. This is a hash lock with a + // simple preimage of "foobar". + leaf1 := testScriptHashLock(t.t, []byte("foobar")) + + // Let's add a second script output as well to test the partial reveal. + leaf2 := testScriptSchnorrSig(t.t, internalKey) + + tapscript := input.TapscriptFullTree(internalKey, leaf1, leaf2) + tree := txscript.AssembleTaprootScriptTree(leaf1, leaf2) + rootHash := tree.RootNode.TapHash() + taprootKey, err := tapscript.TaprootKey() + require.NoError(t.t, err) + + // Import the scripts and make sure we get the same address back as we + // calculated ourselves. + importResp, err := alice.WalletKitClient.ImportTapscript( + ctxt, &walletrpc.ImportTapscriptRequest{ + InternalPublicKey: schnorr.SerializePubKey(internalKey), + Script: &walletrpc.ImportTapscriptRequest_FullTree{ + FullTree: &walletrpc.TapscriptFullTree{ + AllLeaves: []*walletrpc.TapLeaf{{ + LeafVersion: uint32( + leaf1.LeafVersion, + ), + Script: leaf1.Script, + }, { + LeafVersion: uint32( + leaf2.LeafVersion, + ), + Script: leaf2.Script, + }}, + }, + }, + }, + ) + require.NoError(t.t, err) + + calculatedAddr, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(taprootKey), harnessNetParams, + ) + require.NoError(t.t, err) + require.Equal(t.t, calculatedAddr.String(), importResp.P2TrAddress) + + // Send some coins to the generated tapscript address. + p2trOutpoint, p2trPkScript := sendToTaprootOutput( + ctxt, t, net, alice, taprootKey, testAmount, + ) + + p2trOutputRPC := &lnrpc.OutPoint{ + TxidBytes: p2trOutpoint.Hash[:], + OutputIndex: p2trOutpoint.Index, + } + assertWalletUnspent(t, alice, p2trOutputRPC, "imported") + assertAccountBalance(t.t, alice, "imported", testAmount, 0) + + // Funding a PSBT from an imported script is not yet possible. So we + // basically need to add all information manually for the wallet to be + // able to sign for it. + utxo := &wire.TxOut{ + Value: testAmount, + PkScript: p2trPkScript, + } + clearWalletImportedTapscriptBalance( + ctxt, t, alice, utxo, p2trOutpoint, internalKey, derivationPath, + rootHash[:], + ) +} + +// testTaprootImportTapscriptPartialReveal tests importing p2tr script addresses +// for which we only know part of the tree. +func testTaprootImportTapscriptPartialReveal(ctxt context.Context, + t *harnessTest, alice *lntest.HarnessNode, net *lntest.NetworkHarness) { + + // For the next step, we need a public key. Let's use a special family + // for this. + _, internalKey, derivationPath := deriveInternalKey(ctxt, t, alice) + + // Let's create a taproot script output now. This is a hash lock with a + // simple preimage of "foobar". + leaf1 := testScriptHashLock(t.t, []byte("foobar")) + + // Let's add a second script output as well to test the partial reveal. + leaf2 := testScriptSchnorrSig(t.t, internalKey) + leaf2Hash := leaf2.TapHash() + + tapscript := input.TapscriptPartialReveal( + internalKey, leaf1, leaf2Hash[:], + ) + rootHash := tapscript.ControlBlock.RootHash(leaf1.Script) + taprootKey, err := tapscript.TaprootKey() + require.NoError(t.t, err) + + // Import the scripts and make sure we get the same address back as we + // calculated ourselves. + importResp, err := alice.WalletKitClient.ImportTapscript( + ctxt, &walletrpc.ImportTapscriptRequest{ + InternalPublicKey: schnorr.SerializePubKey(internalKey), + Script: &walletrpc.ImportTapscriptRequest_PartialReveal{ + PartialReveal: &walletrpc.TapscriptPartialReveal{ + RevealedLeaf: &walletrpc.TapLeaf{ + LeafVersion: uint32(leaf1.LeafVersion), + Script: leaf1.Script, + }, + FullInclusionProof: leaf2Hash[:], + }, + }, + }, + ) + require.NoError(t.t, err) + + calculatedAddr, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(taprootKey), harnessNetParams, + ) + require.NoError(t.t, err) + require.Equal(t.t, calculatedAddr.String(), importResp.P2TrAddress) + + // Send some coins to the generated tapscript address. + p2trOutpoint, p2trPkScript := sendToTaprootOutput( + ctxt, t, net, alice, taprootKey, testAmount, + ) + + p2trOutputRPC := &lnrpc.OutPoint{ + TxidBytes: p2trOutpoint.Hash[:], + OutputIndex: p2trOutpoint.Index, + } + assertWalletUnspent(t, alice, p2trOutputRPC, "imported") + assertAccountBalance(t.t, alice, "imported", testAmount, 0) + + // Funding a PSBT from an imported script is not yet possible. So we + // basically need to add all information manually for the wallet to be + // able to sign for it. + utxo := &wire.TxOut{ + Value: testAmount, + PkScript: p2trPkScript, + } + clearWalletImportedTapscriptBalance( + ctxt, t, alice, utxo, p2trOutpoint, internalKey, derivationPath, + rootHash, + ) +} + +// testTaprootImportTapscriptRootHashOnly tests importing p2tr script addresses +// for which we only know the root hash. +func testTaprootImportTapscriptRootHashOnly(ctxt context.Context, + t *harnessTest, alice *lntest.HarnessNode, net *lntest.NetworkHarness) { + + // For the next step, we need a public key. Let's use a special family + // for this. + _, internalKey, derivationPath := deriveInternalKey(ctxt, t, alice) + + // Let's create a taproot script output now. This is a hash lock with a + // simple preimage of "foobar". + leaf1 := testScriptHashLock(t.t, []byte("foobar")) + rootHash := leaf1.TapHash() + + tapscript := input.TapscriptRootHashOnly(internalKey, rootHash[:]) + taprootKey, err := tapscript.TaprootKey() + require.NoError(t.t, err) + + // Import the scripts and make sure we get the same address back as we + // calculated ourselves. + importResp, err := alice.WalletKitClient.ImportTapscript( + ctxt, &walletrpc.ImportTapscriptRequest{ + InternalPublicKey: schnorr.SerializePubKey(internalKey), + Script: &walletrpc.ImportTapscriptRequest_RootHashOnly{ + RootHashOnly: rootHash[:], + }, + }, + ) + require.NoError(t.t, err) + + calculatedAddr, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(taprootKey), harnessNetParams, + ) + require.NoError(t.t, err) + require.Equal(t.t, calculatedAddr.String(), importResp.P2TrAddress) + + // Send some coins to the generated tapscript address. + p2trOutpoint, p2trPkScript := sendToTaprootOutput( + ctxt, t, net, alice, taprootKey, testAmount, + ) + + p2trOutputRPC := &lnrpc.OutPoint{ + TxidBytes: p2trOutpoint.Hash[:], + OutputIndex: p2trOutpoint.Index, + } + assertWalletUnspent(t, alice, p2trOutputRPC, "imported") + assertAccountBalance(t.t, alice, "imported", testAmount, 0) + + // Funding a PSBT from an imported script is not yet possible. So we + // basically need to add all information manually for the wallet to be + // able to sign for it. + utxo := &wire.TxOut{ + Value: testAmount, + PkScript: p2trPkScript, + } + clearWalletImportedTapscriptBalance( + ctxt, t, alice, utxo, p2trOutpoint, internalKey, derivationPath, + rootHash[:], + ) +} + +// testTaprootImportTapscriptFullKey tests importing p2tr script addresses for +// which we only know the full Taproot key. +func testTaprootImportTapscriptFullKey(ctxt context.Context, t *harnessTest, + alice *lntest.HarnessNode, net *lntest.NetworkHarness) { + + // For the next step, we need a public key. Let's use a special family + // for this. + _, internalKey, derivationPath := deriveInternalKey(ctxt, t, alice) + + // Let's create a taproot script output now. This is a hash lock with a + // simple preimage of "foobar". + leaf1 := testScriptHashLock(t.t, []byte("foobar")) + + tapscript := input.TapscriptFullTree(internalKey, leaf1) + rootHash := leaf1.TapHash() + taprootKey, err := tapscript.TaprootKey() + require.NoError(t.t, err) + + // Import the scripts and make sure we get the same address back as we + // calculated ourselves. + importResp, err := alice.WalletKitClient.ImportTapscript( + ctxt, &walletrpc.ImportTapscriptRequest{ + InternalPublicKey: schnorr.SerializePubKey(taprootKey), + Script: &walletrpc.ImportTapscriptRequest_FullKeyOnly{ + FullKeyOnly: true, + }, + }, + ) + require.NoError(t.t, err) + + calculatedAddr, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(taprootKey), harnessNetParams, + ) + require.NoError(t.t, err) + require.Equal(t.t, calculatedAddr.String(), importResp.P2TrAddress) + + // Send some coins to the generated tapscript address. + p2trOutpoint, p2trPkScript := sendToTaprootOutput( + ctxt, t, net, alice, taprootKey, testAmount, + ) + + p2trOutputRPC := &lnrpc.OutPoint{ + TxidBytes: p2trOutpoint.Hash[:], + OutputIndex: p2trOutpoint.Index, + } + assertWalletUnspent(t, alice, p2trOutputRPC, "imported") + assertAccountBalance(t.t, alice, "imported", testAmount, 0) + + // Funding a PSBT from an imported script is not yet possible. So we + // basically need to add all information manually for the wallet to be + // able to sign for it. + utxo := &wire.TxOut{ + Value: testAmount, + PkScript: p2trPkScript, + } + clearWalletImportedTapscriptBalance( + ctxt, t, alice, utxo, p2trOutpoint, internalKey, derivationPath, + rootHash[:], + ) +} + +// clearWalletImportedTapscriptBalance manually assembles and then attempts to +// sign a TX to sweep funds from an imported tapscript address. +func clearWalletImportedTapscriptBalance(ctx context.Context, t *harnessTest, + node *lntest.HarnessNode, utxo *wire.TxOut, outPoint wire.OutPoint, + internalKey *btcec.PublicKey, derivationPath []uint32, + rootHash []byte) { + + _, sweepPkScript := newAddrWithScript( + ctx, t.t, node, lnrpc.AddressType_WITNESS_PUBKEY_HASH, + ) + + output := &wire.TxOut{ + PkScript: sweepPkScript, + Value: utxo.Value - 1000, + } + packet, err := psbt.New( + []*wire.OutPoint{&outPoint}, []*wire.TxOut{output}, 2, 0, + []uint32{0}, + ) + require.NoError(t.t, err) + + // We have everything we need to know to sign the PSBT. + in := &packet.Inputs[0] + in.Bip32Derivation = []*psbt.Bip32Derivation{{ + PubKey: internalKey.SerializeCompressed(), + Bip32Path: derivationPath, + }} + in.TaprootBip32Derivation = []*psbt.TaprootBip32Derivation{{ + XOnlyPubKey: schnorr.SerializePubKey(internalKey), + Bip32Path: derivationPath, + }} + in.SighashType = txscript.SigHashDefault + in.TaprootMerkleRoot = rootHash + in.WitnessUtxo = utxo + + var buf bytes.Buffer + require.NoError(t.t, packet.Serialize(&buf)) + + // Sign the manually funded PSBT now. + signResp, err := node.WalletKitClient.SignPsbt( + ctx, &walletrpc.SignPsbtRequest{ + FundedPsbt: buf.Bytes(), + }, + ) + require.NoError(t.t, err) + + signedPacket, err := psbt.NewFromRawBytes( + bytes.NewReader(signResp.SignedPsbt), false, + ) + require.NoError(t.t, err) + + // We should be able to finalize the PSBT and extract the sweep TX now. + err = psbt.MaybeFinalizeAll(signedPacket) + require.NoError(t.t, err) + + sweepTx, err := psbt.Extract(signedPacket) + require.NoError(t.t, err) + + buf.Reset() + err = sweepTx.Serialize(&buf) + require.NoError(t.t, err) + + // Publish the sweep transaction and then mine it as well. + _, err = node.WalletKitClient.PublishTransaction( + ctx, &walletrpc.Transaction{ + TxHex: buf.Bytes(), + }, + ) + require.NoError(t.t, err) + + // Mine one block which should contain the sweep transaction. + block := mineBlocks(t, t.lndHarness, 1, 1)[0] + sweepTxHash := sweepTx.TxHash() + assertTxInBlock(t, block, &sweepTxHash) +} + // testScriptHashLock returns a simple bitcoin script that locks the funds to // a hash lock of the given preimage. func testScriptHashLock(t *testing.T, preimage []byte) txscript.TapLeaf { @@ -1196,6 +1550,37 @@ func sendToTaprootOutput(ctx context.Context, t *harnessTest, Index: uint32(p2trOutputIndex), } + // Make sure the transaction is recognized by our wallet and has the + // correct output type. + var outputDetail *lnrpc.OutputDetail + walletTxns, err := node.GetTransactions( + ctx, &lnrpc.GetTransactionsRequest{ + StartHeight: 0, + EndHeight: -1, + }, + ) + require.NoError(t.t, err) + require.NotEmpty(t.t, walletTxns.Transactions) + for _, tx := range walletTxns.Transactions { + if tx.TxHash != txid.String() { + continue + } + + for outputIdx, out := range tx.OutputDetails { + if out.Address != tapScriptAddr.String() { + continue + } + + outputDetail = tx.OutputDetails[outputIdx] + break + } + } + require.NotNil(t.t, outputDetail, "transaction not found in wallet") + require.Equal( + t.t, lnrpc.OutputScriptType_SCRIPT_TYPE_WITNESS_V1_TAPROOT, + outputDetail.OutputType, + ) + // Clear the mempool. mineBlocks(t, net, 1, 1) @@ -1288,7 +1673,7 @@ func confirmAddress(ctx context.Context, t *harnessTest, TxidBytes: txid[:], OutputIndex: uint32(addrOutputIndex), } - assertWalletUnspent(t, node, op) + assertWalletUnspent(t, node, op, "") // Before we confirm the transaction, let's register a confirmation // listener for it, which we expect to fire after mining a block. diff --git a/lntest/itest/test_harness.go b/lntest/itest/test_harness.go index 1aff55b0a..a5450026f 100644 --- a/lntest/itest/test_harness.go +++ b/lntest/itest/test_harness.go @@ -18,6 +18,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/stretchr/testify/require" @@ -312,13 +313,20 @@ func assertTxInBlock(t *harnessTest, block *wire.MsgBlock, txid *chainhash.Hash) t.Fatalf("tx was not included in block") } -func assertWalletUnspent(t *harnessTest, node *lntest.HarnessNode, out *lnrpc.OutPoint) { +func assertWalletUnspent(t *harnessTest, node *lntest.HarnessNode, + out *lnrpc.OutPoint, account string) { + t.t.Helper() + ctxb := context.Background() err := wait.NoError(func() error { - ctxt, cancel := context.WithTimeout(context.Background(), defaultTimeout) + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) defer cancel() - unspent, err := node.ListUnspent(ctxt, &lnrpc.ListUnspentRequest{}) + unspent, err := node.WalletKitClient.ListUnspent( + ctxt, &walletrpc.ListUnspentRequest{ + Account: account, + }, + ) if err != nil { return err } @@ -339,8 +347,5 @@ func assertWalletUnspent(t *harnessTest, node *lntest.HarnessNode, out *lnrpc.Ou return err }, defaultTimeout) - if err != nil { - t.Fatalf("outpoint %s not unspent by %s's wallet: %v", out, - node.Name(), err) - } + require.NoError(t.t, err) }