test: add integration test to excerise uncooperative channel breaches
This commit adds a new rather extensive integration tests to excerise uncooperative channel breaches triggered by a counter-party broadcasting a previously revoked commitment state. In order to programmatically script such logic using the integration testing framework, the test manually manipulates the database files of one of the nodes within the test network in order to force Bob to travel back in time to a revoked commitment state. With this manipulation, we then force Bob to broadcast the revoked state, triggering Alice’s retribution logic which sweeps ALL the funds within the channel.
This commit is contained in:
parent
5e5bc3884a
commit
6ff357686f
250
lnd_test.go
250
lnd_test.go
@ -3,6 +3,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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 {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
test func(net *networkHarness, t *harnessTest)
|
test func(net *networkHarness, t *harnessTest)
|
||||||
@ -946,6 +1190,12 @@ var testsCases = []*testCase{
|
|||||||
name: "invoice update subscription",
|
name: "invoice update subscription",
|
||||||
test: testInvoiceSubscriptions,
|
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
|
// TestLightningNetworkDaemon performs a series of integration tests amongst a
|
||||||
|
Loading…
Reference in New Issue
Block a user