From 6643a0b9bfcafe8b66f30bd5e8c8d104602ae04e Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 31 Jul 2025 15:48:18 +0200 Subject: [PATCH 1/5] itest+make: add lnd remote signing mode This commit adds a new flag to the integration test suite that starts the single lnd node (alice) either in normal or in remote-signing mode. In remote-signing mode, alice is actually a watch-only node that is connected to a seconardy signer node over RPC. Co-authored-by: George Tsagkarelis --- itest/test_harness.go | 70 +++++++++++++++++++++++++++++++++++++++---- make/testing_flags.mk | 5 ++++ 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/itest/test_harness.go b/itest/test_harness.go index cf5b9c83ca..0e79219b82 100644 --- a/itest/test_harness.go +++ b/itest/test_harness.go @@ -15,6 +15,8 @@ import ( "github.com/lightninglabs/taproot-assets/taprpc" unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc" "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/miner" "github.com/lightningnetwork/lnd/lntest/node" @@ -38,13 +40,24 @@ var ( // noDelete is a command line flag for disabling deleting the tapd // data directories. - noDelete = flag.Bool("nodelete", false, "Set to true to keep all "+ - "tapd data directories after completing the tests") + noDelete = flag.Bool( + "nodelete", false, "Set to true to keep all tapd data "+ + "directories after completing the tests", + ) // logLevel is a command line flag for setting the log level of the // integration test output. - logLevel = flag.String("loglevel", "info", "Set the log level of the "+ - "integration test output") + logLevel = flag.String( + "loglevel", "info", "Set the log level of the integration "+ + "test output", + ) + + // lndRemoteSigner is a command line flag that indicates whether the + // lnd instances should be set up to use a remote signer. + lndRemoteSigner = flag.Bool( + "lndremotesigner", false, "if true, the lnd instances will "+ + "be set up to use a remote signer", + ) ) const ( @@ -294,7 +307,54 @@ func setupHarnesses(t *testing.T, ht *harnessTest, proofCourier = universeServer } - alice := lndHarness.NewNodeWithCoins("Alice", nil) + var alice *node.HarnessNode + if *lndRemoteSigner { + signer := lndHarness.NewNode("Signer", nil) + + // AccountsToWatchOnly only exports accounts that already exist + // on the signer. Tapd's own key family + // (asset.TaprootAssetsKeyFamily) is not pre-created on a fresh + // lnd node and is therefore not in this snapshot. Itests work + // today because lnd's watch-only path tolerates derivations in + // families that weren't in the snapshot for this scope; if + // that ever changes, the signer node would need an explicit + // DeriveNextKey call for that family before this snapshot is + // taken. + rpcAccts := signer.RPC.ListAccounts( + &walletrpc.ListAccountsRequest{}, + ) + + watchOnlyAccounts, err := walletrpc.AccountsToWatchOnly( + rpcAccts.Accounts, + ) + require.NoError(t, err) + alice = lndHarness.NewNodeRemoteSigner( + "WatchOnly", []string{ + "--remotesigner.enable", + fmt.Sprintf( + "--remotesigner.rpchost=localhost:%d", + signer.Cfg.RPCPort, + ), + fmt.Sprintf( + "--remotesigner.tlscertpath=%s", + signer.Cfg.TLSCertPath, + ), + fmt.Sprintf( + "--remotesigner.macaroonpath=%s", + signer.Cfg.AdminMacPath, + ), + }, + []byte("itestpassword"), &lnrpc.WatchOnly{ + MasterKeyBirthdayTimestamp: 0, + MasterKeyFingerprint: nil, + Accounts: watchOnlyAccounts, + }, + ) + + lndHarness.FundNumCoins(alice, 5) + } else { + alice = lndHarness.NewNodeWithCoins("Alice", nil) + } // Create a tapd that uses Alice and connect it to the universe server. tapdHarness := setupTapdHarness( diff --git a/make/testing_flags.mk b/make/testing_flags.mk index b34a50f982..a9b3b16845 100644 --- a/make/testing_flags.mk +++ b/make/testing_flags.mk @@ -87,6 +87,11 @@ ifneq ($(nodelete),) ITEST_FLAGS += -nodelete endif +# Run the lnd node in remote signing mode. +ifneq ($(remotesigning),) +ITEST_FLAGS += -lndremotesigner +endif + # Run the optional tests. ifneq ($(optional),) ITEST_FLAGS += -optional -postgrestimeout=240m From 24fcb0a75391413abc0e63e28cec7989174c4d05 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 31 Jul 2025 15:53:11 +0200 Subject: [PATCH 2/5] GitHub: run itests in lnd remote signing mode To make sure `tapd` works when the connected `lnd` node is running in remote-signing mode, we add a new CI target that runs all integration tests in that mode. Co-authored-by: George Tsagkarelis --- .github/workflows/main.yaml | 47 ++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 3bf36c1951..05bc726c00 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -461,7 +461,52 @@ jobs: format: 'golang' parallel: true - # Integration tests on Postgres backend. + ######################## + # run integration tests with lnd remote-signer + ######################## + integration-test-lnd-remote-signer: + name: run itests with lnd remote-signer + runs-on: ubuntu-latest + steps: + - name: cleanup space + run: rm -rf /opt/hostedtoolcache + + - name: git checkout + uses: actions/checkout@v5 + + - name: Setup go ${{ env.GO_VERSION }} + uses: actions/setup-go@v5 + with: + go-version: '${{ env.GO_VERSION }}' + + - name: run itest + run: make itest icase='(mint_assets|basic_send_unidirectional)' remotesigning=1 + + - name: Zip log files on failure + if: ${{ failure() }} + run: 7z a logs-itest-lnd-remotesigner.zip itest/regtest/logs-tranche*/*.log + + - name: Upload log files on failure + uses: actions/upload-artifact@v7 + if: ${{ failure() }} + with: + name: logs-itest-lnd-remotesigner + path: logs-itest-lnd-remotesigner.zip + retention-days: 5 + + - name: Send coverage + uses: coverallsapp/github-action@v2 + if: ${{ success() }} + continue-on-error: true + with: + file: itest/coverage.txt + flag-name: 'itest-remotesigner' + format: 'golang' + parallel: true + + ######################## + # run integration tests with Postgres backend + ######################## integration-test-postgres: name: run itests postgres runs-on: ubuntu-latest From 901f40c95235014348b3601f65b8ef1b139b1a4f Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 4 Aug 2025 14:10:36 +0200 Subject: [PATCH 3/5] itest: fix itests in remote-signing mode Co-authored-by: George Tsagkarelis --- itest/mint_fund_seal_test.go | 12 ++++++------ itest/multisig.go | 9 +++++++++ itest/psbt_test.go | 4 ++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/itest/mint_fund_seal_test.go b/itest/mint_fund_seal_test.go index c87a789210..a3ab07bf88 100644 --- a/itest/mint_fund_seal_test.go +++ b/itest/mint_fund_seal_test.go @@ -599,18 +599,18 @@ func testMintExternalGroupKeyChantools(t *harnessTest) { func deriveRandomKey(t *testing.T, ctxt context.Context, keyRing *lndservices.LndRpcKeyRing) keychain.KeyDescriptor { + // The random key family is capped to MaxInt8 to stay inside the range + // of accounts known to the watch-only signer set up by the remote + // signing itest mode. The cap also keeps the family safely below + // asset.TaprootAssetsKeyFamily, so no explicit collision check is + // needed. var ( - randFam = test.RandInt31n(math.MaxInt32) + randFam = test.RandInt31n(math.MaxInt8) randInd = test.RandInt31n(255) desc keychain.KeyDescriptor err error ) - // Ensure that we use a different key family from tapd. - for randFam == asset.TaprootAssetsKeyFamily { - randFam = test.RandInt31n(math.MaxInt32) - } - desc, err = keyRing.DeriveNextKey( ctxt, keychain.KeyFamily(randFam), ) diff --git a/itest/multisig.go b/itest/multisig.go index c8be94fec1..67dbb23ae0 100644 --- a/itest/multisig.go +++ b/itest/multisig.go @@ -482,6 +482,15 @@ func FinalizePacket(t *testing.T, lnd *rpc.HarnessRPC, return signedPacket } +// FinalizeFullySigned is a helper function that finalizes a PSBT packet +// that is already fully signed. It will return the finalized packet. +func FinalizeFullySigned(t *testing.T, pkt *psbt.Packet) *psbt.Packet { + err := psbt.MaybeFinalizeAll(pkt) + require.NoError(t, err) + + return pkt +} + // PublishAndLogTransferOption defines a functional option for // PublishAndLogTransfer. type PublishAndLogTransferOption func(*publishAndLogTransferOptions) diff --git a/itest/psbt_test.go b/itest/psbt_test.go index 74f426c406..7b74b9295e 100644 --- a/itest/psbt_test.go +++ b/itest/psbt_test.go @@ -1254,7 +1254,7 @@ func testPsbtInteractiveAltLeafAnchoring(t *harnessTest) { require.NoError(t.t, err) commitPacket = signPacket(t.t, senderLnd, commitPacket) - commitPacket = FinalizePacket(t.t, senderLnd.RPC, commitPacket) + commitPacket = FinalizeFullySigned(t.t, commitPacket) publishResp := PublishAndLogTransfer( t.t, sender, commitPacket, []*tappsbt.VPacket{activePacket}, []*tappsbt.VPacket{passivePacket}, commitResp, @@ -2866,7 +2866,7 @@ func testPsbtExternalCommit(t *harnessTest) { t.Logf("Committed transaction: %v", toJSON(t.t, commitResp)) btcPacket = signPacket(t.t, aliceLnd, btcPacket) - btcPacket = FinalizePacket(t.t, aliceLnd.RPC, btcPacket) + btcPacket = FinalizeFullySigned(t.t, btcPacket) transferLabel := "itest-psbt-external-commit" From d5d75f187dd4ac978ba2784ab219acd9ddb12a5b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 7 Aug 2025 09:29:52 +0200 Subject: [PATCH 4/5] docs: add release notes Co-authored-by: George Tsagkarelis --- docs/release-notes/release-notes-0.8.0.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/release-notes/release-notes-0.8.0.md b/docs/release-notes/release-notes-0.8.0.md index d36c7328b0..4ce32a6bc4 100644 --- a/docs/release-notes/release-notes-0.8.0.md +++ b/docs/release-notes/release-notes-0.8.0.md @@ -484,6 +484,13 @@ Add sell-side constraint rejection unit tests for TestResolveRequest. +- [PR#1694](https://github.com/lightninglabs/taproot-assets/pull/1694) + Add a CI sanity job that runs a small subset of itests (mint + + send/receive) against an `lnd` backend in remote-signing (watch-only) + mode, paired with a separate signer `lnd` node. Guards against + regressions in tapd's support for watch-only `lnd` deployments while + keeping CI cost low. + ## Database - [forwards table](https://github.com/lightninglabs/taproot-assets/pull/1921): From 5823c4c6e4195b24d44c7e26d6188a66dcd12bff Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Fri, 22 May 2026 23:56:49 +0000 Subject: [PATCH 5/5] itest: fix remaining FinalizePacket sites for remote-signing The first iteration of this PR converted two FinalizePacket call sites to a new FinalizeFullySigned helper to work around the fact that lnd's FinalizePsbt RPC fails on p2tr inputs in watch-only mode ("is not a p2wkh or np2wkh address"). Six more call sites following the same signPacket -> FinalizePacket pattern have been added to main since then and were broken under -lndremotesigner; convert them as well. Two remaining FinalizePacket call sites (in MultiSigTest and in the mint+fund+seal test) cannot use FinalizeFullySigned because they need lnd to sign the BTC fee/change inputs that CommitVirtualPsbts added. For those, FinalizePacket itself is rewritten to do SignPsbt + a local MaybeFinalizeAll instead of calling FinalizePsbt RPC. SignPsbt handles p2tr inputs correctly in both regular and watch-only modes, and the local finalize sidesteps the FinalizePsbt bug entirely. --- itest/multi_send_test.go | 2 +- itest/multisig.go | 12 ++++++++++-- itest/psbt_test.go | 32 ++++++-------------------------- 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/itest/multi_send_test.go b/itest/multi_send_test.go index 1dc2f95be0..de58c76859 100644 --- a/itest/multi_send_test.go +++ b/itest/multi_send_test.go @@ -371,7 +371,7 @@ func testAnchorMultipleVirtualTransactions(t *harnessTest) { ) btcPacket = signPacket(t.t, aliceLnd, btcPacket) - btcPacket = FinalizePacket(t.t, aliceLnd.RPC, btcPacket) + btcPacket = FinalizeFullySigned(t.t, btcPacket) sendResp = PublishAndLogTransfer( t.t, aliceTapd, btcPacket, activeAssets, passiveAssets, commitResp, diff --git a/itest/multisig.go b/itest/multisig.go index 67dbb23ae0..b276ec32e4 100644 --- a/itest/multisig.go +++ b/itest/multisig.go @@ -466,19 +466,27 @@ func CommitVirtualPsbts(t *testing.T, funder commands.RpcClientsBundle, func FinalizePacket(t *testing.T, lnd *rpc.HarnessRPC, pkt *psbt.Packet) *psbt.Packet { + // We sign and locally finalize instead of calling lnd's FinalizePsbt + // RPC because the latter is broken for p2tr inputs when lnd runs in + // watch-only / remote-signing mode (it fails with "is not a p2wkh or + // np2wkh address" while trying to compute the input script). SignPsbt + // + local MaybeFinalizeAll is equivalent and works in both modes. var buf bytes.Buffer err := pkt.Serialize(&buf) require.NoError(t, err) - finalizeResp := lnd.FinalizePsbt(&walletrpc.FinalizePsbtRequest{ + signResp := lnd.SignPsbt(&walletrpc.SignPsbtRequest{ FundedPsbt: buf.Bytes(), }) signedPacket, err := psbt.NewFromRawBytes( - bytes.NewReader(finalizeResp.SignedPsbt), false, + bytes.NewReader(signResp.SignedPsbt), false, ) require.NoError(t, err) + err = psbt.MaybeFinalizeAll(signedPacket) + require.NoError(t, err) + return signedPacket } diff --git a/itest/psbt_test.go b/itest/psbt_test.go index 7b74b9295e..097194838a 100644 --- a/itest/psbt_test.go +++ b/itest/psbt_test.go @@ -839,7 +839,7 @@ func runPsbtInteractiveMarkerV0MixedVersions(ctxt context.Context, require.NoError(t.t, err) btcPkt = signPacket(t.t, sender.cfg.LndNode, btcPkt) - btcPkt = FinalizePacket(t.t, sender.cfg.LndNode.RPC, btcPkt) + btcPkt = FinalizeFullySigned(t.t, btcPkt) PublishAndLogTransfer( t.t, sender, btcPkt, activePkt, passivePkt, commitResp, withExpectedErr("error validating input assets: mixed virtual "+ @@ -2526,7 +2526,7 @@ func testPsbtTrustlessSwap(t *harnessTest) { require.Equal(t.t, bobInputIdx, signResp.SignedInputs[0]) require.NoError(t.t, finalPsbt.SanityCheck()) - signedPkt := finalizePacket(t.t, lndBob, finalPsbt) + signedPkt := FinalizeFullySigned(t.t, finalPsbt) require.True(t.t, signedPkt.IsComplete()) logResp := PublishAndLogTransfer( @@ -3119,7 +3119,7 @@ func testPsbtLockTimeSend(t *harnessTest) { t.t, bob, btcPacket, vPackets, nil, -1, ) btcPacket = signPacket(t.t, bobLnd, btcPacket) - btcPacket = FinalizePacket(t.t, bobLnd.RPC, btcPacket) + btcPacket = FinalizeFullySigned(t.t, btcPacket) spendTx, err := psbt.Extract(btcPacket) require.NoError(t.t, err) @@ -3332,7 +3332,7 @@ func testPsbtRelativeLockTimeSend(t *harnessTest) { t.t, bob, btcPacket, vPackets, nil, -1, ) btcPacket = signPacket(t.t, lndBob, btcPacket) - btcPacket = FinalizePacket(t.t, lndBob.RPC, btcPacket) + btcPacket = FinalizeFullySigned(t.t, btcPacket) spendTx, err := psbt.Extract(btcPacket) require.NoError(t.t, err) @@ -3547,9 +3547,7 @@ func testPsbtRelativeLockTimeSendProofFail(t *harnessTest) { t.t, bob, btcPacket, vPackets, nil, -1, ) btcPacketTimeLocked := signPacket(t.t, lndBob, btcPacket) - btcPacketTimeLocked = FinalizePacket( - t.t, lndBob.RPC, btcPacketTimeLocked, - ) + btcPacketTimeLocked = FinalizeFullySigned(t.t, btcPacketTimeLocked) spendTxTimeLocked, err := psbt.Extract(btcPacketTimeLocked) require.NoError(t.t, err) @@ -3573,7 +3571,7 @@ func testPsbtRelativeLockTimeSendProofFail(t *harnessTest) { } btcPacket = signPacket(t.t, lndBob, btcPacket) - btcPacket = FinalizePacket(t.t, lndBob.RPC, btcPacket) + btcPacket = FinalizeFullySigned(t.t, btcPacket) spendTxTimeLocked, err = psbt.Extract(btcPacket) require.NoError(t.t, err) @@ -3742,24 +3740,6 @@ func signPacket(t *testing.T, lnd *node.HarnessNode, return signedPacket } -func finalizePacket(t *testing.T, lnd *node.HarnessNode, - pkt *psbt.Packet) *psbt.Packet { - - pktBytes, err := fn.Serialize(pkt) - require.NoError(t, err) - - finalizeResp := lnd.RPC.FinalizePsbt(&walletrpc.FinalizePsbtRequest{ - FundedPsbt: pktBytes, - }) - - signedPacket, err := psbt.NewFromRawBytes( - bytes.NewReader(finalizeResp.SignedPsbt), false, - ) - require.NoError(t, err) - - return signedPacket -} - // getAddressBip32Derivation returns the PSBT BIP-0032 derivation info of an // address. func getAddressBip32Derivation(t testing.TB, addr string,