From cce2df90182cc7337020e571b1ab780606e3c020 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 31 Jul 2025 15:03:50 +0200 Subject: [PATCH] itest: run FundPsbt test with imported account This demonstrates that the "imported" account behaves differently for a wallet that's watch-only vs. normal. The testTaprootImportTapscriptFullKeyFundPsbt test case succeeds in a normal run but fails in a remote-signing setup. For some reason, an imported tapscript address ends up in the "default" account when remote-signing but in the "imported" account for a normal wallet. --- itest/lnd_remote_signer_test.go | 22 ++++++ itest/lnd_taproot_test.go | 130 ++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/itest/lnd_remote_signer_test.go b/itest/lnd_remote_signer_test.go index 80f936dd0..eac22828b 100644 --- a/itest/lnd_remote_signer_test.go +++ b/itest/lnd_remote_signer_test.go @@ -27,6 +27,10 @@ var remoteSignerTestCases = []*lntest.TestCase{ Name: "account import", TestFunc: testRemoteSignerAccountImport, }, + { + Name: "tapscript import", + TestFunc: testRemoteSignerTapscriptImport, + }, { Name: "channel open", TestFunc: testRemoteSignerChannelOpen, @@ -228,6 +232,24 @@ func testRemoteSignerAccountImport(ht *lntest.HarnessTest) { tc.fn(ht, watchOnly, carol) } +func testRemoteSignerTapscriptImport(ht *lntest.HarnessTest) { + tc := remoteSignerTestCase{ + name: "tapscript import", + sendCoins: true, + fn: func(tt *lntest.HarnessTest, wo, carol *node.HarnessNode) { + testTaprootImportTapscriptFullTree(ht, wo) + testTaprootImportTapscriptPartialReveal(ht, wo) + testTaprootImportTapscriptRootHashOnly(ht, wo) + testTaprootImportTapscriptFullKey(ht, wo) + + testTaprootImportTapscriptFullKeyFundPsbt(ht, wo) + }, + } + + _, watchOnly, carol := prepareRemoteSignerTest(ht, tc) + tc.fn(ht, watchOnly, carol) +} + func testRemoteSignerChannelOpen(ht *lntest.HarnessTest) { tc := remoteSignerTestCase{ name: "basic channel open close", diff --git a/itest/lnd_taproot_test.go b/itest/lnd_taproot_test.go index ddbfb6983..842102965 100644 --- a/itest/lnd_taproot_test.go +++ b/itest/lnd_taproot_test.go @@ -90,6 +90,8 @@ func testTaprootImportScripts(ht *lntest.HarnessTest) { testTaprootImportTapscriptPartialReveal(ht, alice) testTaprootImportTapscriptRootHashOnly(ht, alice) testTaprootImportTapscriptFullKey(ht, alice) + + testTaprootImportTapscriptFullKeyFundPsbt(ht, alice) } // testTaprootSendCoinsKeySpendBip86 tests sending to and spending from @@ -1359,6 +1361,134 @@ func testTaprootImportTapscriptFullKey(ht *lntest.HarnessTest, ) } +// testTaprootImportTapscriptFullKeyFundPsbt tests importing p2tr script +// addresses for which we only know the full Taproot key. We also test that we +// can use such an imported script to fund a PSBT. +func testTaprootImportTapscriptFullKeyFundPsbt(ht *lntest.HarnessTest, + alice *node.HarnessNode) { + + // For the next step, we need a public key. Let's use a special family + // for this. + _, internalKey, derivationPath := 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")) + + tapscript := input.TapscriptFullTree(internalKey, leaf1) + rootHash := leaf1.TapHash() + taprootKey, err := tapscript.TaprootKey() + require.NoError(ht, err) + + // Import the scripts and make sure we get the same address back as we + // calculated ourselves. + req := &walletrpc.ImportTapscriptRequest{ + InternalPublicKey: schnorr.SerializePubKey(taprootKey), + Script: &walletrpc.ImportTapscriptRequest_FullKeyOnly{ + FullKeyOnly: true, + }, + } + importResp := alice.RPC.ImportTapscript(req) + + calculatedAddr, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(taprootKey), harnessNetParams, + ) + require.NoError(ht, err) + require.Equal(ht, calculatedAddr.String(), importResp.P2TrAddress) + + // Send some coins to the generated tapscript address. + p2trOutpoint, p2trPkScript := sendToTaprootOutput(ht, alice, taprootKey) + + p2trOutputRPC := &lnrpc.OutPoint{ + TxidBytes: p2trOutpoint.Hash[:], + OutputIndex: p2trOutpoint.Index, + } + ht.AssertUTXOInWallet(alice, p2trOutputRPC, "imported") + ht.AssertWalletAccountBalance(alice, "imported", testAmount, 0) + + // We now fund a PSBT that spends the imported tapscript address. + utxo := &wire.TxOut{ + Value: testAmount, + PkScript: p2trPkScript, + } + _, sweepPkScript := newAddrWithScript( + ht, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH, + ) + + output := &wire.TxOut{ + PkScript: sweepPkScript, + Value: 1, + } + packet, err := psbt.New( + []*wire.OutPoint{&p2trOutpoint}, []*wire.TxOut{output}, 2, 0, + []uint32{0}, + ) + require.NoError(ht, 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(ht, packet.Serialize(&buf)) + + change := &walletrpc.PsbtCoinSelect_ExistingOutputIndex{ + ExistingOutputIndex: 0, + } + fundResp := alice.RPC.FundPsbt(&walletrpc.FundPsbtRequest{ + Template: &walletrpc.FundPsbtRequest_CoinSelect{ + CoinSelect: &walletrpc.PsbtCoinSelect{ + Psbt: buf.Bytes(), + ChangeOutput: change, + }, + }, + Fees: &walletrpc.FundPsbtRequest_SatPerVbyte{ + SatPerVbyte: 1, + }, + }) + + // Sign the manually funded PSBT now. + signResp := alice.RPC.SignPsbt(&walletrpc.SignPsbtRequest{ + FundedPsbt: fundResp.FundedPsbt, + }) + + signedPacket, err := psbt.NewFromRawBytes( + bytes.NewReader(signResp.SignedPsbt), false, + ) + require.NoError(ht, err) + + // We should be able to finalize the PSBT and extract the sweep TX now. + err = psbt.MaybeFinalizeAll(signedPacket) + require.NoError(ht, err) + + sweepTx, err := psbt.Extract(signedPacket) + require.NoError(ht, err) + + buf.Reset() + err = sweepTx.Serialize(&buf) + require.NoError(ht, err) + + // Publish the sweep transaction and then mine it as well. + alice.RPC.PublishTransaction(&walletrpc.Transaction{ + TxHex: buf.Bytes(), + }) + + // Mine one block which should contain the sweep transaction. + block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] + sweepTxHash := sweepTx.TxHash() + ht.AssertTxInBlock(block, sweepTxHash) +} + // clearWalletImportedTapscriptBalance manually assembles and then attempts to // sign a TX to sweep funds from an imported tapscript address. func clearWalletImportedTapscriptBalance(ht *lntest.HarnessTest,