package itest import ( "bytes" "fmt" "testing" "time" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/stretchr/testify/require" ) // testRevokedCloseRetribution tests that Carol is able carry out // retribution in the event that she fails immediately after detecting Bob's // breach txn in the mempool. func testRevokedCloseRetribution(ht *lntest.HarnessTest) { const ( chanAmt = funding.MaxBtcFundingAmount paymentAmt = 10000 numInvoices = 6 ) // Carol will be the breached party. We set --nolisten to ensure Bob // won't be able to connect to her and trigger the channel data // protection logic automatically. We also can't have Carol // automatically re-connect too early, otherwise DLP would be initiated // instead of the breach we want to provoke. carol := ht.NewNode( "Carol", []string{"--hodl.exit-settle", "--nolisten", "--minbackoff=1h"}, ) // We must let Bob communicate with Carol before they are able to open // channel, so we connect Bob and Carol, bob := ht.Bob ht.ConnectNodes(carol, bob) // Before we make a channel, we'll load up Carol with some coins sent // directly from the miner. ht.FundCoins(btcutil.SatoshiPerBitcoin, carol) // In order to test Carol's response to an uncooperative channel // closure by Bob, we'll first open up a channel between them with a // 0.5 BTC value. chanPoint := ht.OpenChannel( carol, bob, lntest.OpenChannelParams{Amt: chanAmt}, ) // With the channel open, we'll create a few invoices for Bob that // Carol will pay to in order to advance the state of the channel. bobPayReqs, _, _ := ht.CreatePayReqs(bob, paymentAmt, numInvoices) // Send payments from Carol to Bob using 3 of Bob's payment hashes // generated above. ht.CompletePaymentRequests(carol, bobPayReqs[:numInvoices/2]) // Next query for Bob's channel state, as we sent 3 payments of 10k // satoshis each, Bob should now see his balance as being 30k satoshis. bobChan := ht.AssertChannelLocalBalance(bob, chanPoint, 30_000) // Grab Bob's current commitment height (update number), we'll later // revert him to this state after additional updates to force him to // broadcast this soon to be revoked state. bobStateNumPreCopy := bobChan.NumUpdates // With the temporary file created, copy Bob's current state into the // temporary file we created above. Later after more updates, we'll // restore this state. ht.BackupDB(bob) // Reconnect the peers after the restart that was needed for the db // backup. ht.EnsureConnected(carol, bob) // Because Bob has been restarted, we need to make sure Carol has // remarked the channel as active after that. ht.AssertChannelExists(carol, chanPoint) // Finally, send payments from Carol to Bob, consuming Bob's remaining // payment hashes. ht.CompletePaymentRequests(carol, bobPayReqs[numInvoices/2:]) // Now we shutdown Bob, copying over the his temporary database state // which has the *prior* channel state over his current most up to date // state. With this, we essentially force Bob to travel back in time // within the channel's history. ht.RestartNodeAndRestoreDB(bob) // Now query for Bob's channel state, it should show that he's at a // state number in the past, not the *latest* state. ht.AssertChannelCommitHeight(bob, chanPoint, int(bobStateNumPreCopy)) // Now force Bob to execute a *force* channel closure by unilaterally // broadcasting his current channel state. This is actually the // commitment transaction of a prior *revoked* state, so he'll soon // feel the wrath of Carol's retribution. _, breachTXID := ht.CloseChannelAssertPending(bob, chanPoint, true) // Here, Carol sees Bob's breach transaction in the mempool, but is // waiting for it to confirm before continuing her retribution. We // restart Carol to ensure that she is persisting her retribution state // and continues watching for the breach transaction to confirm even // after her node restarts. ht.RestartNode(carol) // Finally, generate a single block, wait for the final close status // update, then ensure that the closing transaction was included in the // block. block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] ht.Miner.AssertTxInBlock(block, breachTXID) // Query the mempool for Carol's justice transaction, this should be // broadcast as Bob's contract breaching transaction gets confirmed // above. justiceTXID := ht.Miner.AssertNumTxsInMempool(1)[0] // Query for the mempool transaction found above. Then assert that all // the inputs of this transaction are spending outputs generated by // Bob's breach transaction above. justiceTx := ht.Miner.GetRawTransaction(justiceTXID) for _, txIn := range justiceTx.MsgTx().TxIn { require.Equal(ht, breachTXID[:], txIn.PreviousOutPoint.Hash[:], "justice tx not spending commitment utxo") } // We restart Carol here to ensure that she persists her retribution // state and successfully continues exacting retribution after // restarting. At this point, Carol has broadcast the justice // transaction, but it hasn't been confirmed yet; when Carol restarts, // she should start waiting for the justice transaction to confirm // again. ht.RestartNode(carol) // Now mine a block, this transaction should include Carol's justice // transaction which was just accepted into the mempool. block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] // The block should have exactly *two* transactions, one of which is // the justice transaction. require.Len(ht, block.Transactions, 2, "transaction wasn't mined") justiceSha := block.Transactions[1].TxHash() require.Equal(ht, justiceTx.Hash()[:], justiceSha[:], "justice tx wasn't mined") ht.AssertNodeNumChannels(carol, 0) // Mine enough blocks for Bob's channel arbitrator to wrap up the // references to the breached channel. The chanarb waits for commitment // tx's confHeight+CSV-1 blocks and since we've already mined one that // included the justice tx we only need to mine extra DefaultCSV-2 // blocks to unlock it. ht.MineBlocks(defaultCSV - 2) ht.AssertNumPendingForceClose(bob, 0) } // testRevokedCloseRetributionZeroValueRemoteOutput tests that Dave is able // carry out retribution in the event that he fails in state where the remote // commitment output has zero-value. func testRevokedCloseRetributionZeroValueRemoteOutput(ht *lntest.HarnessTest) { const ( chanAmt = funding.MaxBtcFundingAmount paymentAmt = 10000 numInvoices = 6 ) // Since we'd like to test some multi-hop failure scenarios, we'll // introduce another node into our test network: Carol. carol := ht.NewNode("Carol", []string{"--hodl.exit-settle"}) // Dave will be the breached party. We set --nolisten to ensure Carol // won't be able to connect to him and trigger the channel data // protection logic automatically. We also can't have Dave automatically // re-connect too early, otherwise DLP would be initiated instead of the // breach we want to provoke. dave := ht.NewNode( "Dave", []string{"--hodl.exit-settle", "--nolisten", "--minbackoff=1h"}, ) // We must let Dave have an open channel before he can send a node // announcement, so we open a channel with Carol, ht.ConnectNodes(dave, carol) // Before we make a channel, we'll load up Dave with some coins sent // directly from the miner. ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) // In order to test Dave's response to an uncooperative channel // closure by Carol, we'll first open up a channel between them with a // 0.5 BTC value. chanPoint := ht.OpenChannel( dave, carol, lntest.OpenChannelParams{Amt: chanAmt}, ) // With the channel open, we'll create a few invoices for Carol that // Dave will pay to in order to advance the state of the channel. carolPayReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, numInvoices) // Next query for Carol's channel state, as we sent 0 payments, Carol // should now see her balance as being 0 satoshis. carolChan := ht.AssertChannelLocalBalance(carol, chanPoint, 0) // Grab Carol's current commitment height (update number), we'll later // revert her to this state after additional updates to force her to // broadcast this soon to be revoked state. carolStateNumPreCopy := int(carolChan.NumUpdates) // With the temporary file created, copy Carol's current state into the // temporary file we created above. Later after more updates, we'll // restore this state. ht.BackupDB(carol) // Reconnect the peers after the restart that was needed for the db // backup. ht.EnsureConnected(dave, carol) // Once connected, give Dave some time to enable the channel again. ht.AssertTopologyChannelOpen(dave, chanPoint) // Finally, send payments from Dave to Carol, consuming Carol's // remaining payment hashes. ht.CompletePaymentRequestsNoWait(dave, carolPayReqs, chanPoint) // Now we shutdown Carol, copying over the her temporary database state // which has the *prior* channel state over her current most up to date // state. With this, we essentially force Carol to travel back in time // within the channel's history. ht.RestartNodeAndRestoreDB(carol) // Now query for Carol's channel state, it should show that she's at a // state number in the past, not the *latest* state. ht.AssertChannelCommitHeight(carol, chanPoint, carolStateNumPreCopy) // Now force Carol to execute a *force* channel closure by unilaterally // broadcasting her current channel state. This is actually the // commitment transaction of a prior *revoked* state, so she'll soon // feel the wrath of Dave's retribution. stream, closeTxID := ht.CloseChannelAssertPending( carol, chanPoint, true, ) // Finally, generate a single block, wait for the final close status // update, then ensure that the closing transaction was included in the // block. ht.MineBlocksAndAssertNumTxes(1, 1) // Here, Dave receives a confirmation of Carol's breach transaction. // We restart Dave to ensure that he is persisting his retribution // state and continues exacting justice after his node restarts. ht.RestartNode(dave) // The breachTXID should match the above closeTxID. breachTXID := ht.WaitForChannelCloseEvent(stream) require.EqualValues(ht, breachTXID, closeTxID) // Query the mempool for Dave's justice transaction, this should be // broadcast as Carol's contract breaching transaction gets confirmed // above. justiceTXID := ht.Miner.AssertNumTxsInMempool(1)[0] // Query for the mempool transaction found above. Then assert that all // the inputs of this transaction are spending outputs generated by // Carol's breach transaction above. justiceTx := ht.Miner.GetRawTransaction(justiceTXID) for _, txIn := range justiceTx.MsgTx().TxIn { require.Equal(ht, breachTXID[:], txIn.PreviousOutPoint.Hash[:], "justice tx not spending commitment utxo ") } // We restart Dave here to ensure that he persists his retribution state // and successfully continues exacting retribution after restarting. At // this point, Dave has broadcast the justice transaction, but it hasn't // been confirmed yet; when Dave restarts, he should start waiting for // the justice transaction to confirm again. ht.RestartNode(dave) // Now mine a block, this transaction should include Dave's justice // transaction which was just accepted into the mempool. block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] // The block should have exactly *two* transactions, one of which is // the justice transaction. require.Len(ht, block.Transactions, 2, "transaction wasn't mined") justiceSha := block.Transactions[1].TxHash() require.Equal(ht, justiceTx.Hash()[:], justiceSha[:], "justice tx wasn't mined") ht.AssertNodeNumChannels(dave, 0) } // testRevokedCloseRetributionRemoteHodl tests that Dave properly responds to a // channel breach made by the remote party, specifically in the case that the // remote party breaches before settling extended HTLCs. func testRevokedCloseRetributionRemoteHodl(ht *lntest.HarnessTest) { const ( chanAmt = funding.MaxBtcFundingAmount pushAmt = 200000 paymentAmt = 10000 numInvoices = 6 ) // Since this test will result in the counterparty being left in a // weird state, we will introduce another node into our test network: // Carol. carol := ht.NewNode("Carol", []string{"--hodl.exit-settle"}) // We'll also create a new node Dave, who will have a channel with // Carol, and also use similar settings so we can broadcast a commit // with active HTLCs. Dave will be the breached party. We set // --nolisten to ensure Carol won't be able to connect to him and // trigger the channel data protection logic automatically. dave := ht.NewNode( "Dave", []string{"--hodl.exit-settle", "--nolisten"}, ) // We must let Dave communicate with Carol before they are able to open // channel, so we connect Dave and Carol, ht.ConnectNodes(dave, carol) // Before we make a channel, we'll load up Dave with some coins sent // directly from the miner. ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) // In order to test Dave's response to an uncooperative channel closure // by Carol, we'll first open up a channel between them with a // funding.MaxBtcFundingAmount (2^24) satoshis value. chanPoint := ht.OpenChannel( dave, carol, lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: pushAmt, }, ) // With the channel open, we'll create a few invoices for Carol that // Dave will pay to in order to advance the state of the channel. carolPayReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, numInvoices) // We'll introduce an closure to validate that Carol's current // number of updates is at least as large as the provided minimum // number. checkCarolNumUpdatesAtLeast := func(carolChan *lnrpc.Channel, minimum int) { require.GreaterOrEqual(ht, int(carolChan.NumUpdates), minimum, "carol's numupdates is incorrect") } // Ensure that carol's balance starts with the amount we pushed to her. ht.AssertChannelLocalBalance(carol, chanPoint, pushAmt) // Send payments from Dave to Carol using 3 of Carol's payment hashes // generated above. ht.CompletePaymentRequestsNoWait( dave, carolPayReqs[:numInvoices/2], chanPoint, ) // At this point, we'll also send over a set of HTLC's from Carol to // Dave. This ensures that the final revoked transaction has HTLC's in // both directions. davePayReqs, _, _ := ht.CreatePayReqs(dave, paymentAmt, numInvoices) // Send payments from Carol to Dave using 3 of Dave's payment hashes // generated above. ht.CompletePaymentRequestsNoWait( carol, davePayReqs[:numInvoices/2], chanPoint, ) // Next query for Carol's channel state, as we sent 3 payments of 10k // satoshis each, however Carol should now see her balance as being // equal to the push amount in satoshis since she has not settled. // Ensure that carol's balance still reflects the original amount we // pushed to her, minus the HTLCs she just sent to Dave. carolChan := ht.AssertChannelLocalBalance( carol, chanPoint, pushAmt-3*paymentAmt, ) // Grab Carol's current commitment height (update number), we'll later // revert her to this state after additional updates to force her to // broadcast this soon to be revoked state. carolStateNumPreCopy := int(carolChan.NumUpdates) // Since Carol has not settled, she should only see at least one update // to her channel. checkCarolNumUpdatesAtLeast(carolChan, 1) // With the temporary file created, copy Carol's current state into the // temporary file we created above. Later after more updates, we'll // restore this state. ht.BackupDB(carol) // Reconnect the peers after the restart that was needed for the db // backup. ht.EnsureConnected(dave, carol) // Once connected, give Dave some time to enable the channel again. ht.AssertTopologyChannelOpen(dave, chanPoint) // Finally, send payments from Dave to Carol, consuming Carol's // remaining payment hashes. ht.CompletePaymentRequestsNoWait( dave, carolPayReqs[numInvoices/2:], chanPoint, ) // Ensure that carol's balance still shows the amount we originally // pushed to her (minus the HTLCs she sent to Bob), and that at least // one more update has occurred. carolChan = ht.AssertChannelLocalBalance( carol, chanPoint, pushAmt-3*paymentAmt, ) checkCarolNumUpdatesAtLeast(carolChan, carolStateNumPreCopy+1) // Suspend Dave, such that Carol won't reconnect at startup, triggering // the data loss protection. restartDave := ht.SuspendNode(dave) // Now we shutdown Carol, copying over the her temporary database state // which has the *prior* channel state over her current most up to date // state. With this, we essentially force Carol to travel back in time // within the channel's history. ht.RestartNodeAndRestoreDB(carol) // Ensure that Carol's view of the channel is consistent with the state // of the channel just before it was snapshotted. carolChan = ht.AssertChannelLocalBalance( carol, chanPoint, pushAmt-3*paymentAmt, ) checkCarolNumUpdatesAtLeast(carolChan, 1) // Now query for Carol's channel state, it should show that she's at a // state number in the past, *not* the latest state. ht.AssertChannelCommitHeight(carol, chanPoint, carolStateNumPreCopy) // Now force Carol to execute a *force* channel closure by unilaterally // broadcasting her current channel state. This is actually the // commitment transaction of a prior *revoked* state, so she'll soon // feel the wrath of Dave's retribution. closeUpdates, closeTxID := ht.CloseChannelAssertPending( carol, chanPoint, true, ) // Generate a single block to mine the breach transaction. block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] // We resurrect Dave to ensure he will be exacting justice after his // node restarts. require.NoError(ht, restartDave(), "unable to restart Dave's node") // Finally, wait for the final close status update, then ensure that // the closing transaction was included in the block. breachTXID := ht.WaitForChannelCloseEvent(closeUpdates) require.Equal(ht, closeTxID[:], breachTXID[:], "expected breach ID to be equal to close ID") ht.Miner.AssertTxInBlock(block, breachTXID) // Query the mempool for Dave's justice transaction, this should be // broadcast as Carol's contract breaching transaction gets confirmed // above. Since Carol might have had the time to take some of the HTLC // outputs to the second level before Dave broadcasts his justice tx, // we'll search through the mempool for a tx that matches the number of // expected inputs in the justice tx. var justiceTxid *chainhash.Hash errNotFound := errors.New("justice tx not found") findJusticeTx := func() (*chainhash.Hash, error) { mempool := ht.Miner.GetRawMempool() for _, txid := range mempool { // Check that the justice tx has the appropriate number // of inputs. // // NOTE: We don't use `ht.Miner.GetRawTransaction` // which asserts a txid must be found as the HTLC // spending txes might be aggregated. tx, err := ht.Miner.Client.GetRawTransaction(txid) if err != nil { return nil, err } exNumInputs := 2 + numInvoices if len(tx.MsgTx().TxIn) == exNumInputs { return txid, nil } } return nil, errNotFound } err := wait.NoError(func() error { txid, err := findJusticeTx() if err != nil { return err } justiceTxid = txid return nil }, defaultTimeout) if err != nil && errors.Is(err, errNotFound) { // If Dave is unable to broadcast his justice tx on first // attempt because of the second layer transactions, he will // wait until the next block epoch before trying again. Because // of this, we'll mine a block if we cannot find the justice tx // immediately. Since we cannot tell for sure how many // transactions will be in the mempool at this point, we pass 0 // as the last argument, indicating we don't care what's in the // mempool. ht.MineBlocks(1) err = wait.NoError(func() error { txid, err := findJusticeTx() if err != nil { return err } justiceTxid = txid return nil }, defaultTimeout) } require.NoError(ht, err, "timeout finding justice tx") justiceTx := ht.Miner.GetRawTransaction(justiceTxid) // isSecondLevelSpend checks that the passed secondLevelTxid is a // potentitial second level spend spending from the commit tx. isSecondLevelSpend := func(commitTxid, secondLevelTxid *chainhash.Hash) bool { secondLevel := ht.Miner.GetRawTransaction(secondLevelTxid) // A second level spend should have only one input, and one // output. if len(secondLevel.MsgTx().TxIn) != 1 { return false } if len(secondLevel.MsgTx().TxOut) != 1 { return false } // The sole input should be spending from the commit tx. txIn := secondLevel.MsgTx().TxIn[0] return bytes.Equal(txIn.PreviousOutPoint.Hash[:], commitTxid[:]) } // Check that all the inputs of this transaction are spending outputs // generated by Carol's breach transaction above. for _, txIn := range justiceTx.MsgTx().TxIn { if bytes.Equal(txIn.PreviousOutPoint.Hash[:], breachTXID[:]) { continue } // If the justice tx is spending from an output that was not on // the breach tx, Carol might have had the time to take an // output to the second level. In that case, check that the // justice tx is spending this second level output. if isSecondLevelSpend(breachTXID, &txIn.PreviousOutPoint.Hash) { continue } require.Fail(ht, "justice tx not spending commitment utxo "+ "instead is: %v", txIn.PreviousOutPoint) } // We restart Dave here to ensure that he persists he retribution state // and successfully continues exacting retribution after restarting. At // this point, Dave has broadcast the justice transaction, but it // hasn't been confirmed yet; when Dave restarts, he should start // waiting for the justice transaction to confirm again. ht.RestartNode(dave) // Now mine a block, this transaction should include Dave's justice // transaction which was just accepted into the mempool. ht.MineBlocksAndAssertNumTxes(1, 1) // Dave should have no open channels. ht.AssertNodeNumChannels(dave, 0) } // testRevokedCloseRetributionAltruistWatchtower establishes a channel between // Carol and Dave, where Carol is using a third node Willy as her watchtower. // After sending some payments, Dave reverts his state and force closes to // trigger a breach. Carol is kept offline throughout the process and the test // asserts that Willy responds by broadcasting the justice transaction on // Carol's behalf sweeping her funds without a reward. func testRevokedCloseRetributionAltruistWatchtower(ht *lntest.HarnessTest) { testCases := []struct { name string anchors bool }{{ name: "anchors", anchors: true, }, { name: "legacy", anchors: false, }} for _, tc := range testCases { tc := tc testFunc := func(ht *lntest.HarnessTest) { testRevokedCloseRetributionAltruistWatchtowerCase( ht, tc.anchors, ) } success := ht.Run(tc.name, func(tt *testing.T) { st := ht.Subtest(tt) st.RunTestCase(&lntest.TestCase{ Name: tc.name, TestFunc: testFunc, }) }) if !success { // Log failure time to help relate the lnd logs to the // failure. ht.Logf("Failure time: %v", time.Now().Format( "2006-01-02 15:04:05.000", )) break } } } func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest, anchors bool) { const ( chanAmt = funding.MaxBtcFundingAmount paymentAmt = 10000 numInvoices = 6 externalIP = "1.2.3.4" ) // Since we'd like to test some multi-hop failure scenarios, we'll // introduce another node into our test network: Carol. carolArgs := []string{"--hodl.exit-settle"} if anchors { carolArgs = append(carolArgs, "--protocol.anchors") } carol := ht.NewNode("Carol", carolArgs) // Willy the watchtower will protect Dave from Carol's breach. He will // remain online in order to punish Carol on Dave's behalf, since the // breach will happen while Dave is offline. willy := ht.NewNode( "Willy", []string{"--watchtower.active", "--watchtower.externalip=" + externalIP}, ) willyInfo := willy.RPC.GetInfoWatchtower() // Assert that Willy has one listener and it is 0.0.0.0:9911 or // [::]:9911. Since no listener is explicitly specified, one of these // should be the default depending on whether the host supports IPv6 or // not. require.Len(ht, willyInfo.Listeners, 1, "Willy should have 1 listener") listener := willyInfo.Listeners[0] if listener != "0.0.0.0:9911" && listener != "[::]:9911" { ht.Fatalf("expected listener on 0.0.0.0:9911 or [::]:9911, "+ "got %v", listener) } // Assert the Willy's URIs properly display the chosen external IP. require.Len(ht, willyInfo.Uris, 1, "Willy should have 1 uri") require.Contains(ht, willyInfo.Uris[0], externalIP) // Dave will be the breached party. We set --nolisten to ensure Carol // won't be able to connect to him and trigger the channel data // protection logic automatically. daveArgs := []string{ "--nolisten", "--wtclient.active", } if anchors { daveArgs = append(daveArgs, "--protocol.anchors") } dave := ht.NewNode("Dave", daveArgs) addTowerReq := &wtclientrpc.AddTowerRequest{ Pubkey: willyInfo.Pubkey, Address: listener, } dave.RPC.AddTower(addTowerReq) // We must let Dave have an open channel before she can send a node // announcement, so we open a channel with Carol, ht.ConnectNodes(dave, carol) // Before we make a channel, we'll load up Dave with some coins sent // directly from the miner. ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) // Send one more UTXOs if this is a neutrino backend. if ht.IsNeutrinoBackend() { ht.FundCoins(btcutil.SatoshiPerBitcoin, dave) } // In order to test Dave's response to an uncooperative channel // closure by Carol, we'll first open up a channel between them with a // 0.5 BTC value. params := lntest.OpenChannelParams{ Amt: 3 * (chanAmt / 4), PushAmt: chanAmt / 4, } chanPoint := ht.OpenChannel(dave, carol, params) // With the channel open, we'll create a few invoices for Carol that // Dave will pay to in order to advance the state of the channel. carolPayReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, numInvoices) // Next query for Carol's channel state, as we sent 0 payments, Carol // should still see her balance as the push amount, which is 1/4 of the // capacity. carolChan := ht.AssertChannelLocalBalance( carol, chanPoint, int64(chanAmt/4), ) // Grab Carol's current commitment height (update number), we'll later // revert her to this state after additional updates to force him to // broadcast this soon to be revoked state. carolStateNumPreCopy := int(carolChan.NumUpdates) // With the temporary file created, copy Carol's current state into the // temporary file we created above. Later after more updates, we'll // restore this state. ht.BackupDB(carol) // Reconnect the peers after the restart that was needed for the db // backup. ht.EnsureConnected(dave, carol) // Once connected, give Dave some time to enable the channel again. ht.AssertTopologyChannelOpen(dave, chanPoint) // Finally, send payments from Dave to Carol, consuming Carol's // remaining payment hashes. ht.CompletePaymentRequestsNoWait(dave, carolPayReqs, chanPoint) daveBalResp := dave.RPC.WalletBalance() davePreSweepBalance := daveBalResp.ConfirmedBalance // Wait until the backup has been accepted by the watchtower before // shutting down Dave. err := wait.NoError(func() error { bkpStats := dave.RPC.WatchtowerStats() if bkpStats == nil { return errors.New("no active backup sessions") } if bkpStats.NumBackups == 0 { return errors.New("no backups accepted") } return nil }, defaultTimeout) require.NoError(ht, err, "unable to verify backup task completed") // Shutdown Dave to simulate going offline for an extended period of // time. Once he's not watching, Carol will try to breach the channel. restart := ht.SuspendNode(dave) // Now we shutdown Carol, copying over the his temporary database state // which has the *prior* channel state over his current most up to date // state. With this, we essentially force Carol to travel back in time // within the channel's history. ht.RestartNodeAndRestoreDB(carol) // Now query for Carol's channel state, it should show that he's at a // state number in the past, not the *latest* state. ht.AssertChannelCommitHeight(carol, chanPoint, carolStateNumPreCopy) // Now force Carol to execute a *force* channel closure by unilaterally // broadcasting his current channel state. This is actually the // commitment transaction of a prior *revoked* state, so he'll soon // feel the wrath of Dave's retribution. closeUpdates, closeTxID := ht.CloseChannelAssertPending( carol, chanPoint, true, ) // Finally, generate a single block, wait for the final close status // update, then ensure that the closing transaction was included in the // block. block := ht.MineBlocksAndAssertNumTxes(1, 1)[0] breachTXID := ht.WaitForChannelCloseEvent(closeUpdates) ht.Miner.AssertTxInBlock(block, breachTXID) // The breachTXID should match the above closeTxID. require.EqualValues(ht, breachTXID, closeTxID) // Query the mempool for Dave's justice transaction, this should be // broadcast as Carol's contract breaching transaction gets confirmed // above. justiceTXID := ht.Miner.AssertNumTxsInMempool(1)[0] // Query for the mempool transaction found above. Then assert that all // the inputs of this transaction are spending outputs generated by // Carol's breach transaction above. justiceTx := ht.Miner.GetRawTransaction(justiceTXID) for _, txIn := range justiceTx.MsgTx().TxIn { require.Equal(ht, breachTXID[:], txIn.PreviousOutPoint.Hash[:], "justice tx not spending commitment utxo") } willyBalResp := willy.RPC.WalletBalance() require.Zero(ht, willyBalResp.ConfirmedBalance, "willy should have 0 balance before mining justice transaction") // Now mine a block, this transaction should include Dave's justice // transaction which was just accepted into the mempool. block = ht.MineBlocksAndAssertNumTxes(1, 1)[0] // The block should have exactly *two* transactions, one of which is // the justice transaction. require.Len(ht, block.Transactions, 2, "transaction wasn't mined") justiceSha := block.Transactions[1].TxHash() require.Equal(ht, justiceTx.Hash()[:], justiceSha[:], "justice tx wasn't mined") // Ensure that Willy doesn't get any funds, as he is acting as an // altruist watchtower. err = wait.NoError(func() error { willyBalResp := willy.RPC.WalletBalance() if willyBalResp.ConfirmedBalance != 0 { return fmt.Errorf("Expected Willy to have no funds "+ "after justice transaction was mined, found %v", willyBalResp) } return nil }, time.Second*5) require.NoError(ht, err, "timeout checking willy's balance") // Before restarting Dave, shutdown Carol so Dave won't sync with her. // Otherwise, during the restart, Dave will realize Carol is falling // behind and return `ErrCommitSyncRemoteDataLoss`, thus force closing // the channel. Although this force close tx will be later replaced by // the breach tx, it will create two anchor sweeping txes for neutrino // backend, causing the confirmed wallet balance to be zero later on // because the utxos are used in sweeping. ht.Shutdown(carol) // Restart Dave, who will still think his channel with Carol is open. // We should him to detect the breach, but realize that the funds have // then been swept to his wallet by Willy. require.NoError(ht, restart(), "unable to restart dave") err = wait.NoError(func() error { daveBalResp := dave.RPC.ChannelBalance() if daveBalResp.LocalBalance.Sat != 0 { return fmt.Errorf("Dave should end up with zero "+ "channel balance, instead has %d", daveBalResp.LocalBalance.Sat) } return nil }, defaultTimeout) require.NoError(ht, err, "timeout checking dave's channel balance") ht.AssertNumPendingForceClose(dave, 0) // If this is an anchor channel, Dave would sweep the anchor. if anchors { ht.MineBlocksAndAssertNumTxes(1, 1) } // Check that Dave's wallet balance is increased. err = wait.NoError(func() error { daveBalResp := dave.RPC.WalletBalance() if daveBalResp.ConfirmedBalance <= davePreSweepBalance { return fmt.Errorf("Dave should have more than %d "+ "after sweep, instead has %d", davePreSweepBalance, daveBalResp.ConfirmedBalance) } return nil }, defaultTimeout) require.NoError(ht, err, "timeout checking dave's wallet balance") // Dave should have no open channels. ht.AssertNodeNumChannels(dave, 0) }