From 0c4693ca589af4b3348648a85fffc753d4b6ffab Mon Sep 17 00:00:00 2001 From: Oli Date: Thu, 6 Nov 2025 15:03:53 +0100 Subject: [PATCH] itest: add integration test for scbforceclose --- itest/README.md | 9 ++--- itest/cmd_scbforceclose_test.go | 43 +++++++++++++++++++++++ itest/docker/setup-test-network.sh | 23 +++++++----- itest/helpers.go | 56 ++++++++++++++++++++++++++++-- itest/integration_test.go | 4 +++ 5 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 itest/cmd_scbforceclose_test.go diff --git a/itest/README.md b/itest/README.md index 31fbe4c..c6cbf3f 100644 --- a/itest/README.md +++ b/itest/README.md @@ -11,10 +11,10 @@ around the network). The network is set up as follows: ``` -Alice ◄──► Bob ◄──► Charlie ◄──► Dave - └───────►└──► Rusty ◄──┘ - | └► Nifty - └► Snyke + Alice ◄──► Bob ◄──► Charlie ◄──► Dave + └───────►└──► Rusty ◄──┘ + | Nifty ◄──┘ | + └► Snyke ◄─────────────┘ ``` - Channel **Alice** - **Bob**: Remains open, used by `runZombieRecoveryLndLnd`. @@ -26,6 +26,7 @@ Alice ◄──► Bob ◄──► Charlie ◄──► Dave `runSweepRemoteClosedLnd`. - Channel **Charlie** - **Dave**: Remains open, used by `runTriggerForceCloseLnd`. +- Channel **Charlie** - **Snyke**: Remains open, used by `runSCBForceClose`. - Channel **Bob** - **Rusty**: Remains open, used by `runZombieRecoveryLndCln`. - Channel **Rusty** - **Charlie**: Remains open, used by `runZombieRecoveryClnLnd`. diff --git a/itest/cmd_scbforceclose_test.go b/itest/cmd_scbforceclose_test.go new file mode 100644 index 0000000..c75b229 --- /dev/null +++ b/itest/cmd_scbforceclose_test.go @@ -0,0 +1,43 @@ +package itest + +import ( + "fmt" + "testing" + + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/stretchr/testify/require" +) + +func runScbForceClose(t *testing.T) { + charlieChannels := readChannelsJSON(t, "charlie") + snykeIdentity := getNodeIdentityKeyCln(t, "snyke") + + var charlieSnykeChannel *lnrpc.Channel + for _, c := range charlieChannels { + if c.RemotePubkey == snykeIdentity { + charlieSnykeChannel = c + } + } + require.NotNil( + t, charlieSnykeChannel, "charlie-snyke channel not found", + ) + + scbFile := fmt.Sprintf(scbFilePattern, "charlie") + txHex, fullOutput := getScbForceClose( + t, "charlie", tempDir, scbFile, + charlieSnykeChannel.ChannelPoint, + ) + + // Outputs on a force-close transaction are always ordered by amount. + require.Contains( + t, fullOutput, "Possible anchor: idx=0 amount=330 sat", + ) + require.Contains( + t, fullOutput, "Possible anchor: idx=1 amount=330 sat", + ) + require.Contains(t, fullOutput, "Output to_remote: idx=2 amount=") + require.Contains(t, fullOutput, "Possible to_local/htlc: idx=3 amount=") + + backend := connectBitcoind(t) + publishTx(t, txHex, backend) +} diff --git a/itest/docker/setup-test-network.sh b/itest/docker/setup-test-network.sh index 7612617..f518a68 100755 --- a/itest/docker/setup-test-network.sh +++ b/itest/docker/setup-test-network.sh @@ -26,9 +26,7 @@ done # Spin up the network in detached mode. compose_up -# Set up the basic A ◄─► B ◄─► C ◄─► D network. -# └────►└► R ◄┘ -# └► N +# Set up the basic network. setup_bitcoin wait_for_nodes alice bob charlie dave rusty nifty snyke @@ -36,7 +34,7 @@ wait_for_nodes alice bob charlie dave rusty nifty snyke do_for fund_node alice bob charlie dave rusty nifty snyke # Alice, Bob and Charlie will open more than one channel each. -do_for fund_node alice alice bob charlie +do_for fund_node alice alice bob charlie charlie mine 6 @@ -51,6 +49,7 @@ connect_nodes bob rusty connect_nodes bob nifty connect_nodes charlie dave connect_nodes charlie rusty +connect_nodes charlie snyke connect_nodes rusty nifty open_channel alice bob @@ -59,17 +58,21 @@ open_channel bob charlie open_channel charlie dave open_channel bob rusty open_channel charlie rusty +open_channel charlie snyke open_channel rusty nifty open_channel alice snyke -echo "🔗 Set up network: Alice ◄─► Bob ◄─► Charlie ◄─► Dave network." -echo " └────────►└► Rusty ◄┘ " -echo " | └► Nifty" -echo " └► Snyke" +echo "🔗 Set up network:" +cat << EOF + Alice ◄──► Bob ◄──► Charlie ◄──► Dave + └───────►└──► Rusty ◄──┘ + | Nifty ◄──┘ | + └► Snyke ◄─────────────┘ +EOF mine 12 -num_channels=8 +num_channels=9 wait_graph_sync alice $num_channels wait_graph_sync bob $num_channels @@ -83,6 +86,7 @@ send_payment alice dave send_payment alice rusty send_payment dave rusty send_payment alice snyke +send_payment charlie snyke # Repeat the basic tests. send_payment bob dave @@ -91,6 +95,7 @@ send_payment alice dave send_payment alice rusty send_payment dave rusty send_payment alice snyke +send_payment charlie snyke # Store all the channel information in separate JSON files. alice listchannels > "$DIR/node-data/chantools/alice-channels.json" diff --git a/itest/helpers.go b/itest/helpers.go index f97123b..f5278c0 100644 --- a/itest/helpers.go +++ b/itest/helpers.go @@ -33,6 +33,8 @@ const ( channelsFilePattern = "docker/node-data/chantools/%s-channels.json" walletFilePattern = "docker/node-data/%s/data/chain/bitcoin/" + "regtest/wallet.db" + scbFilePattern = "docker/node-data/%s/data/chain/bitcoin/" + + "regtest/channel.backup" hsmSecretFilePattern = "docker/node-data/%s/regtest/hsm_secret" nodeIdentityFilePattern = "docker/node-data/chantools/identities.txt" nodeURIPattern = "%s@%s" @@ -48,9 +50,10 @@ const ( transactionHexIdent = "02000000" rowSign = "Press to continue and sign the " + "transaction or to abort:" - rowPublish = "Please publish this using any bitcoin node:" - rowTransaction = "Transaction:" - rowForceClose = "Found force close transaction" + rowPublish = "Please publish this using any bitcoin node:" + rowTransaction = "Transaction:" + rowForceClose = "Found force close transaction" + rowRawTransaction = "Raw transaction hex:" ) var ( @@ -321,6 +324,36 @@ func invokeCmdTriggerForceClose(t *testing.T, walletPassword *string, return output } +func invokeCmdScbForceClose(t *testing.T, walletPassword *string, + resultsDir string, args ...string) string { + + t.Helper() + + fullArgs := append([]string{ + "--regtest", "--resultsdir", resultsDir, "scbforceclose", + }, args...) + proc := StartChantools(t, fullArgs...) + defer proc.Wait(t) + + if walletPassword != nil { + pwPrompt := proc.ReadAvailableOutput(t, readTimeout) + + require.Contains(t, pwPrompt, "Input wallet password:") + proc.WriteInput(t, *walletPassword+"\n") + + proc.AssertNoStderr(t) + } + + warnPrompt := proc.ReadAvailableOutput(t, defaultTimeout) + require.Contains(t, warnPrompt, "Type YES to proceed: ") + proc.WriteInput(t, "YES\n") + + proc.AssertNoStderr(t) + + output := proc.ReadAvailableOutput(t, longTimeout) + return output +} + func getNodeIdentityKey(t *testing.T, node string) string { t.Helper() @@ -503,3 +536,20 @@ func getTriggerForceClose(t *testing.T, node, tempDir, apiURL, peerURI, return txid } + +func getScbForceClose(t *testing.T, node, tempDir, multiBackup, + channelPoint string) (string, string) { + + t.Helper() + + walletDbPath := fmt.Sprintf(walletFilePattern, node) + cmdOutput := invokeCmdScbForceClose( + t, &emptyPassword, tempDir, + "--channel_point", channelPoint, "--walletdb", walletDbPath, + "--multi_file", multiBackup, + ) + txHex := extractRowContent(cmdOutput, rowRawTransaction) + require.Contains(t, txHex, transactionHexIdent) + + return txHex, cmdOutput +} diff --git a/itest/integration_test.go b/itest/integration_test.go index a4940d2..aa24c4e 100644 --- a/itest/integration_test.go +++ b/itest/integration_test.go @@ -42,6 +42,10 @@ var testCases = []testCase{ name: "trigger force close cln", fn: runTriggerForceCloseCln, }, + { + name: "scb force close", + fn: runScbForceClose, + }, } // TestIntegration runs all integration test cases.