diff --git a/lnd_test.go b/lnd_test.go index d443e6037..e19874c31 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -3,6 +3,10 @@ package main import ( "bytes" "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" "sync" "testing" "time" @@ -908,6 +912,246 @@ func testMaxPendingChannels(net *networkHarness, t *harnessTest) { } } +func copyFile(dest, src string) error { + s, err := os.Open(src) + if err != nil { + return err + } + defer s.Close() + + d, err := os.Create(dest) + if err != nil { + return err + } + + if _, err := io.Copy(d, s); err != nil { + d.Close() + return err + } + + return d.Close() + +} + +func testRevokedCloseRetribution(net *networkHarness, t *harnessTest) { + ctxb := context.Background() + const ( + timeout = time.Duration(time.Second * 5) + chanAmt = btcutil.Amount(btcutil.SatoshiPerBitcoin / 2) + 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(t, net, ctxt, net.Alice, net.Bob, chanAmt) + + // 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(i * 10)}, 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 + } + + // 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 _, err := alicePayStream.Recv(); err != nil { + return err + } + } + 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. + 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/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("copy failed: %v", bobChan.NumUpdates) + } + + if err = net.ConnectNodes(ctxb, net.Alice, net.Bob); err != nil { + t.Fatalf("unable to connect bob and alice: %v", err) + } + + // 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. + time.Sleep(time.Second * 2) + breachTXID := closeChannelAndAssert(t, net, ctxb, net.Bob, chanPoint, + true) + + // Query the mempool for Alice's justice transaction, this should be + // broadcast as Bob's contract breaching transaction gets confirmed + // above. + var justiceTXID *wire.ShaHash +poll: + for { + select { + case <-time.After(time.Second * 5): + t.Fatalf("justice tx not found in mempool") + default: + mempool, err := net.Miner.Node.GetRawMempool() + if err != nil { + t.Fatalf("unable to get mempool: %v", err) + } + + if len(mempool) == 0 { + continue + } + + justiceTXID = mempool[0] + break poll + } + } + + // 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("not sweeping output") + } + } + + // 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("transation wasn't mined") + } + if !bytes.Equal(justiceTx.Sha()[:], block.Transactions()[1].Sha()[:]) { + t.Fatalf("justice tx wasn't mined") + } + + // Finally, obtain Alie'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 deleted channel: %v", + spew.Sdump(aliceChanInfo.Channels)) + } +} + type testCase struct { name string test func(net *networkHarness, t *harnessTest) @@ -946,6 +1190,12 @@ var testsCases = []*testCase{ name: "invoice update subscription", test: testInvoiceSubscriptions, }, + { + // TODO(roasbeef): test always needs to be last as Bob's state + // is borked since we trick him into attempting to cheat Alice? + name: "revoked uncooperative close retribution", + test: testRevokedCloseRetribution, + }, } // TestLightningNetworkDaemon performs a series of integration tests amongst a