package itest import ( "bytes" "fmt" "testing" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/stretchr/testify/require" ) func breachRetributionTestCase(ht *lntest.HarnessTest, commitType lnrpc.CommitmentType) { 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. nodeArgs := lntest.NodeArgsForCommitType(commitType) carol := ht.NewNode( "Carol", append( nodeArgs, []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.NewNode("Bob", nodeArgs) 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. privateChan := commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT chanPoint := ht.OpenChannel( carol, bob, lntest.OpenChannelParams{ CommitmentType: commitType, Amt: chanAmt, Private: privateChan, }, ) // 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.AssertTxInBlock(block, breachTXID) // Construct to_remote output which pays to Bob. Based on the output // ordering, the first output in this breach tx is the to_remote // output. toRemoteOp := wire.OutPoint{ Hash: *breachTXID, Index: 0, } // If this is an anchor-enabled channel, the first two outputs are // anchors, so the to_remote output is the third one. if lntest.CommitTypeHasAnchors(commitType) { toRemoteOp.Index = 2 } // Query the mempool for Carol's justice transaction, this should be // broadcast as Bob's contract breaching transaction gets confirmed // above. // // NOTE: For channels with anchors, we will also see the anchor // sweeping transactions in the mempool. Thus we directly assert that // the breach transaction's outpoint is seen in the mempool instead of // checking the number of transactions. justiceTx := ht.AssertOutpointInMempool(toRemoteOp) // Assert that all the inputs of this transaction are spending outputs // generated by Bob's breach transaction above. for _, txIn := range justiceTx.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) // For anchor channels, we'd offer the anchor output to the sweeper. // However, this anchor output won't be swept due to it being // uneconomical. if lntest.CommitTypeHasAnchors(commitType) { ht.AssertNumPendingSweeps(carol, 1) } // 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] justiceTxid := justiceTx.TxHash() ht.AssertTxInBlock(block, &justiceTxid) 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) } // 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) { for _, commitType := range []lnrpc.CommitmentType{ lnrpc.CommitmentType_SIMPLE_TAPROOT, } { testName := fmt.Sprintf("%v", commitType.String()) ht.Run(testName, func(t *testing.T) { st := ht.Subtest(t) breachRetributionTestCase(st, commitType) }) } } func revokedCloseRetributionZeroValueRemoteOutputCase(ht *lntest.HarnessTest, commitType lnrpc.CommitmentType) { 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. nodeArgs := lntest.NodeArgsForCommitType(commitType) carol := ht.NewNode( "Carol", append(nodeArgs, []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", append( nodeArgs, []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. privateChan := commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT chanPoint := ht.OpenChannel( dave, carol, lntest.OpenChannelParams{ CommitmentType: commitType, Amt: chanAmt, Private: privateChan, }, ) // 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) // Construct to_local output which pays to Dave. Based on the output // ordering, the first output in this breach tx is the to_local // output. toLocalOp := wire.OutPoint{ Hash: *breachTXID, Index: 0, } // If this is an anchor-enabled channel, we usaually have two anchors, // one for local and one for remote. However, since the to_remote // balance is zero, the remote anchor won't be created, thus the // to_local output is the second output. if lntest.CommitTypeHasAnchors(commitType) { toLocalOp.Index = 1 } // Query the mempool for Dave's justice transaction, this should be // broadcast as Carol's contract breaching transaction gets confirmed // above. // // NOTE: For channels with anchors, we will also see the anchor // sweeping transactions in the mempool. Thus we directly assert that // the breach transaction's outpoint is seen in the mempool instead of // checking the number of transactions. justiceTx := ht.AssertOutpointInMempool(toLocalOp) // Assert that all the inputs of this transaction are spending outputs // generated by Carol's breach transaction above. for _, txIn := range justiceTx.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) // For anchor channels, we'd also create the sweeping transaction. if lntest.CommitTypeHasAnchors(commitType) { ht.AssertNumPendingSweeps(dave, 1) } // 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] justiceTxid := justiceTx.TxHash() ht.AssertTxInBlock(block, &justiceTxid) // At this point, Dave should have no pending channels. ht.AssertNodeNumChannels(dave, 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) { for _, commitType := range []lnrpc.CommitmentType{ lnrpc.CommitmentType_SIMPLE_TAPROOT, } { testName := fmt.Sprintf("%v", commitType.String()) ht.Run(testName, func(t *testing.T) { st := ht.Subtest(t) revokedCloseRetributionZeroValueRemoteOutputCase( st, commitType, ) }) } } func revokedCloseRetributionRemoteHodlCase(ht *lntest.HarnessTest, commitType lnrpc.CommitmentType) { 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. nodeArgs := lntest.NodeArgsForCommitType(commitType) carol := ht.NewNode( "Carol", append(nodeArgs, []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", append( nodeArgs, []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. privateChan := commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT chanPoint := ht.OpenChannel( dave, carol, lntest.OpenChannelParams{ Amt: chanAmt, PushAmt: pushAmt, Private: privateChan, CommitmentType: commitType, }, ) // 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.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.GetRawMempool() for _, txid := range mempool { // Check that the justice tx has the appropriate number // of inputs. // // NOTE: We don't use `ht.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.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.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) // For anchor channels, we'd also create the sweeping transaction. if lntest.CommitTypeHasAnchors(commitType) { ht.AssertNumPendingSweeps(dave, 1) } // 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) } // 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) { for _, commitType := range []lnrpc.CommitmentType{ lnrpc.CommitmentType_SIMPLE_TAPROOT, } { testName := fmt.Sprintf("%v", commitType.String()) ht.Run(testName, func(t *testing.T) { st := ht.Subtest(t) revokedCloseRetributionRemoteHodlCase(st, commitType) }) } }