diff --git a/lnd_test.go b/lnd_test.go index 286c2f9e..88106dd8 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -1751,6 +1751,9 @@ poll: return txid, nil } +// testRevokedCloseRetributinPostBreachConf tests that Alice is able carry out +// retribution in the event that she fails immediately after detecting Bob's +// breach txn in the mempool. func testRevokedCloseRetribution(net *networkHarness, t *harnessTest) { ctxb := context.Background() const ( @@ -1923,7 +1926,6 @@ func testRevokedCloseRetribution(net *networkHarness, t *harnessTest) { if err != nil { t.Fatalf("unable to find Bob's breach tx in mempool: %v", err) } - time.Sleep(100 * time.Millisecond) // Here, Alice sees Bob's breach transaction in the mempool, but is waiting // for it to confirm before continuing her retribution. We restart Alice to @@ -2005,6 +2007,256 @@ func testRevokedCloseRetribution(net *networkHarness, t *harnessTest) { } } +// testRevokedCloseRetributinPostBreachConf tests that Alice is able carry out +// retribution in the event that she fails immediately after receiving a +// confirmation of Bob's breach txn. +func testRevokedCloseRetributionPostBreachConf( + net *networkHarness, + t *harnessTest) { + + ctxb := context.Background() + const ( + timeout = time.Duration(time.Second * 5) + chanAmt = maxFundingAmount + paymentAmt = 10000 + numInvoices = 6 + ) + + // In order to test Alice's response to an uncooperative channel + // closure by Bob, we'll first open up a channel between them with a + // 0.5 BTC value. + ctxt, _ := context.WithTimeout(ctxb, timeout) + chanPoint := openChannelAndAssert(ctxt, t, net, net.Alice, net.Bob, + chanAmt, 0) + + // With the channel open, we'll create a few invoices for Bob that + // Alice will pay to in order to advance the state of the channel. + bobPaymentHashes := make([][]byte, numInvoices) + for i := 0; i < numInvoices; i++ { + preimage := bytes.Repeat([]byte{byte(192 - i)}, 32) + invoice := &lnrpc.Invoice{ + Memo: "testing", + RPreimage: preimage, + Value: paymentAmt, + } + resp, err := net.Bob.AddInvoice(ctxb, invoice) + if err != nil { + t.Fatalf("unable to add invoice: %v", err) + } + + bobPaymentHashes[i] = resp.RHash + } + + // As we'll be querying the state of bob's channels frequently we'll + // create a closure helper function for the purpose. + getBobChanInfo := func() (*lnrpc.ActiveChannel, error) { + req := &lnrpc.ListChannelsRequest{} + bobChannelInfo, err := net.Bob.ListChannels(ctxb, req) + if err != nil { + return nil, err + } + if len(bobChannelInfo.Channels) != 1 { + t.Fatalf("bob should only have a single channel, instead he has %v", + len(bobChannelInfo.Channels)) + } + + return bobChannelInfo.Channels[0], nil + } + + // Wait for Alice to receive the channel edge from the funding manager. + ctxt, _ = context.WithTimeout(ctxb, timeout) + err := net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint) + if err != nil { + t.Fatalf("alice didn't see the alice->bob channel before "+ + "timeout: %v", err) + } + + // Open up a payment stream to Alice that we'll use to send payment to + // Bob. We also create a small helper function to send payments to Bob, + // consuming the payment hashes we generated above. + alicePayStream, err := net.Alice.SendPayment(ctxb) + if err != nil { + t.Fatalf("unable to create payment stream for alice: %v", err) + } + sendPayments := func(start, stop int) error { + for i := start; i < stop; i++ { + sendReq := &lnrpc.SendRequest{ + PaymentHash: bobPaymentHashes[i], + Dest: net.Bob.PubKey[:], + Amt: paymentAmt, + } + if err := alicePayStream.Send(sendReq); err != nil { + return err + } + if resp, err := alicePayStream.Recv(); err != nil { + t.Fatalf("payment stream has been closed: %v", err) + } else if resp.PaymentError != "" { + t.Fatalf("error when attempting recv: %v", + resp.PaymentError) + } + } + return nil + } + + // Send payments from Alice to Bob using 3 of Bob's payment hashes + // generated above. + if err := sendPayments(0, numInvoices/2); err != nil { + t.Fatalf("unable to send payment: %v", err) + } + + // 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. + time.Sleep(time.Millisecond * 200) + bobChan, err := getBobChanInfo() + if err != nil { + t.Fatalf("unable to get bob's channel info: %v", err) + } + if bobChan.LocalBalance != 30000 { + t.Fatalf("bob's balance is incorrect, got %v, expected %v", + bobChan.LocalBalance, 30000) + } + + // 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 + + // Create a temporary file to house Bob's database state at this + // particular point in history. + bobTempDbPath, err := ioutil.TempDir("", "bob-past-state") + if err != nil { + t.Fatalf("unable to create temp db folder: %v", err) + } + bobTempDbFile := filepath.Join(bobTempDbPath, "channel.db") + defer os.Remove(bobTempDbPath) + + // 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. + bobDbPath := filepath.Join(net.Bob.cfg.DataDir, "simnet/bitcoin/channel.db") + if err := copyFile(bobTempDbFile, bobDbPath); err != nil { + t.Fatalf("unable to copy database files: %v", err) + } + + // Finally, send payments from Alice to Bob, consuming Bob's remaining + // payment hashes. + if err := sendPayments(numInvoices/2, numInvoices); err != nil { + t.Fatalf("unable to send payment: %v", err) + } + + bobChan, err = getBobChanInfo() + if err != nil { + t.Fatalf("unable to get bob chan info: %v", err) + } + + // 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. + if err = net.RestartNode(net.Bob, func() error { + return os.Rename(bobTempDbFile, bobDbPath) + }); err != nil { + t.Fatalf("unable to restart node: %v", err) + } + + // Now query for Bob's channel state, it should show that he's at a + // state number in the past, not the *latest* state. + bobChan, err = getBobChanInfo() + if err != nil { + t.Fatalf("unable to get bob chan info: %v", err) + } + if bobChan.NumUpdates != bobStateNumPreCopy { + t.Fatalf("db copy failed: %v", bobChan.NumUpdates) + } + + // 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 Alice's retribution. + force := true + closeUpdates, _, err := net.CloseChannel(ctxb, net.Bob, chanPoint, force) + if err != nil { + t.Fatalf("unable to close channel: %v", err) + } + + // Finally, generate a single block, wait for the final close status + // update, then ensure that the closing transaction was included in the + // block. + block := mineBlocks(t, net, 1)[0] + + // Here, Alice receives a confirmation of Bob's breach transaction. We + // restart Alice to ensure that she is persisting her retribution state and + // continues exacting justice after her node restarts. + if err := net.RestartNode(net.Alice, nil); err != nil { + t.Fatalf("unable to stop Alice's node: %v", err) + } + + breachTXID, err := net.WaitForChannelClose(ctxb, closeUpdates) + if err != nil { + t.Fatalf("error while waiting for channel close: %v", err) + } + assertTxInBlock(t, block, breachTXID) + + // Query the mempool for Alice's justice transaction, this should be + // broadcast as Bob's contract breaching transaction gets confirmed + // above. + justiceTXID, err := waitForTxInMempool(net.Miner.Node, 5*time.Second) + if err != nil { + t.Fatalf("unable to find Alice's justice tx in mempool: %v", err) + } + time.Sleep(100 * time.Millisecond) + + // 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, err := net.Miner.Node.GetRawTransaction(justiceTXID) + if err != nil { + t.Fatalf("unable to query for justice tx: %v", err) + } + for _, txIn := range justiceTx.MsgTx().TxIn { + if !bytes.Equal(txIn.PreviousOutPoint.Hash[:], breachTXID[:]) { + t.Fatalf("justice tx not spending commitment utxo "+ + "instead is: %v", txIn.PreviousOutPoint) + } + } + + // We restart Alice here to ensure that she persists her retribution state + // and successfully continues exacting retribution after restarting. At + // this point, Alice has broadcast the justice transaction, but it hasn't + // been confirmed yet; when Alice restarts, she should start waiting for + // the justice transaction to confirm again. + if err := net.RestartNode(net.Alice, nil); err != nil { + t.Fatalf("unable to restart Alice's node: %v", err) + } + + // Now mine a block, this transaction should include Alice's justice + // transaction which was just accepted into the mempool. + block = mineBlocks(t, net, 1)[0] + + // The block should have exactly *two* transactions, one of which is + // the justice transaction. + if len(block.Transactions) != 2 { + t.Fatalf("transaction wasn't mined") + } + justiceSha := block.Transactions[1].TxHash() + if !bytes.Equal(justiceTx.Hash()[:], justiceSha[:]) { + t.Fatalf("justice tx wasn't mined") + } + + // Finally, obtain Alice's channel state, she shouldn't report any + // channel as she just successfully brought Bob to justice by sweeping + // all the channel funds. + req := &lnrpc.ListChannelsRequest{} + aliceChanInfo, err := net.Alice.ListChannels(ctxb, req) + if err != nil { + t.Fatalf("unable to query for alice's channels: %v", err) + } + if len(aliceChanInfo.Channels) != 0 { + t.Fatalf("alice shouldn't have a channel: %v", + spew.Sdump(aliceChanInfo.Channels)) + } +} + func testHtlcErrorPropagation(net *networkHarness, t *harnessTest) { // In this test we wish to exercise the daemon's correct parsing, // handling, and propagation of errors that occur while processing a @@ -3190,6 +3442,10 @@ var testsCases = []*testCase{ name: "revoked uncooperative close retribution", test: testRevokedCloseRetribution, }, + { + name: "revoked uncooperative close retribution post breach conf", + test: testRevokedCloseRetributionPostBreachConf, + }, } // TestLightningNetworkDaemon performs a series of integration tests amongst a