Skip to content

Commit

Permalink
Merge bf5d4b2 into 4e48a8c
Browse files Browse the repository at this point in the history
  • Loading branch information
cfromknecht committed Aug 24, 2017
2 parents 4e48a8c + bf5d4b2 commit 441ef53
Show file tree
Hide file tree
Showing 13 changed files with 2,232 additions and 273 deletions.
706 changes: 617 additions & 89 deletions breacharbiter.go

Large diffs are not rendered by default.

808 changes: 808 additions & 0 deletions breacharbiter_test.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion chainntnfs/btcdnotify/btcd.go
Expand Up @@ -567,7 +567,7 @@ func (b *BtcdNotifier) checkConfirmationTrigger(txSha *chainhash.Hash,
if confClients, ok := b.confNotifications[*txSha]; ok {
// Either all of the registered confirmations will be
// dispatched due to a single confirmation, or added to the
// conf head. Therefor we unconditionally delete the registered
// conf head. Therefore we unconditionally delete the registered
// confirmations from the staging zone.
defer func() {
delete(b.confNotifications, *txSha)
Expand Down
341 changes: 324 additions & 17 deletions lnd_test.go
Expand Up @@ -1720,9 +1720,40 @@ func copyFile(dest, src string) error {
}

return d.Close()
}

func waitForTxInMempool(miner *btcrpcclient.Client,
timeout time.Duration) (*chainhash.Hash, error) {

var txid *chainhash.Hash
breakTimeout := time.After(timeout)
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
poll:
for {
select {
case <-breakTimeout:
return nil, errors.New("no tx found in mempool")
case <-ticker.C:
mempool, err := miner.GetRawMempool()
if err != nil {
return nil, err
}

if len(mempool) == 0 {
continue
}

txid = mempool[0]
break 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 (
Expand Down Expand Up @@ -1883,35 +1914,298 @@ func testRevokedCloseRetribution(net *networkHarness, t *harnessTest) {
// 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.
breachTXID := closeChannelAndAssert(ctxb, t, net, net.Bob, chanPoint,
true)
force := true
closeUpdates, _, err := net.CloseChannel(ctxb, net.Bob, chanPoint, force)
if err != nil {
t.Fatalf("unable to close channel: %v", err)
}

// Wait for Bob's breach transaction to show up in the mempool to ensure
// that Alice's node has started waiting for confirmations.
_, err = waitForTxInMempool(net.Miner.Node, 5*time.Second)
if err != nil {
t.Fatalf("unable to find Bob's breach tx in mempool: %v", err)
}

// 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
// ensure that she is persisting her retribution state and continues
// watching for the breach transaction to confirm even after her node
// restarts.
if err := net.RestartNode(net.Alice, nil); err != nil {
t.Fatalf("unable to restart Alice's node: %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]

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.
var justiceTXID *chainhash.Hash
breakTimeout := time.After(time.Second * 5)
poll:
for {
select {
case <-breakTimeout:
t.Fatalf("justice tx not found in mempool")
default:
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)
}

mempool, err := net.Miner.Node.GetRawMempool()
// 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))
}
}

// 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 get mempool: %v", err)
t.Fatalf("unable to add invoice: %v", err)
}

if len(mempool) == 0 {
continue
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))
}

justiceTXID = mempool[0]
break poll
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.
Expand All @@ -1926,9 +2220,18 @@ poll:
}
}

// 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]
block = mineBlocks(t, net, 1)[0]

// The block should have exactly *two* transactions, one of which is
// the justice transaction.
Expand Down Expand Up @@ -3139,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
Expand Down

0 comments on commit 441ef53

Please sign in to comment.