lnd_test: add testDataLossProtection
This commit adds the integration test testDataLossProtection, that ensures that when a node loses state, the channel counterparty will force close the channel, and they both can recover their funds.
This commit is contained in:
parent
00154bda24
commit
afccca59c4
332
lnd_test.go
332
lnd_test.go
@ -6046,19 +6046,343 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness,
|
|||||||
assertNodeNumChannels(t, ctxb, dave, 0)
|
assertNodeNumChannels(t, ctxb, dave, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assertNumPendingChannels checks that a PendingChannels response from the
|
||||||
|
// node reports the expected number of pending channels.
|
||||||
|
func assertNumPendingChannels(t *harnessTest, node *lntest.HarnessNode,
|
||||||
|
expWaitingClose, expPendingForceClose int) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
var predErr error
|
||||||
|
err := lntest.WaitPredicate(func() bool {
|
||||||
|
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
|
||||||
|
pendingChanResp, err := node.PendingChannels(ctxb,
|
||||||
|
pendingChansRequest)
|
||||||
|
if err != nil {
|
||||||
|
predErr = fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n := len(pendingChanResp.WaitingCloseChannels)
|
||||||
|
if n != expWaitingClose {
|
||||||
|
predErr = fmt.Errorf("Expected to find %d channels "+
|
||||||
|
"waiting close, found %d", expWaitingClose, n)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n = len(pendingChanResp.PendingForceClosingChannels)
|
||||||
|
if n != expPendingForceClose {
|
||||||
|
predErr = fmt.Errorf("expected to find %d channel "+
|
||||||
|
"pending force close, found %d", expPendingForceClose, n)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, time.Second*15)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", predErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testDataLossProtection tests that if one of the nodes in a channel
|
||||||
|
// relationship lost state, they will detect this during channel sync, and the
|
||||||
|
// up-to-date party will force close the channel, giving the outdated party the
|
||||||
|
// oppurtunity to sweep its output.
|
||||||
|
func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
const (
|
||||||
|
timeout = time.Duration(time.Second * 10)
|
||||||
|
chanAmt = maxBtcFundingAmount
|
||||||
|
paymentAmt = 10000
|
||||||
|
numInvoices = 6
|
||||||
|
defaultCSV = uint32(4)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Carol will be the up-to-date party. We set --nolisten to ensure Dave
|
||||||
|
// won't be able to connect to her and trigger the channel data
|
||||||
|
// protection logic automatically.
|
||||||
|
carol, err := net.NewNode("Carol", []string{"--nolisten"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create new carol node: %v", err)
|
||||||
|
}
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
// Dave will be the party losing his state.
|
||||||
|
dave, err := net.NewNode("Dave", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create new node: %v", err)
|
||||||
|
}
|
||||||
|
defer shutdownAndAssert(net, t, dave)
|
||||||
|
|
||||||
|
// We must let Dave communicate with Carol before they are able to open
|
||||||
|
// channel, so we connect them.
|
||||||
|
if err := net.ConnectNodes(ctxb, carol, dave); err != nil {
|
||||||
|
t.Fatalf("unable to connect dave to carol: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before we make a channel, we'll load up Carol with some coins sent
|
||||||
|
// directly from the miner.
|
||||||
|
err = net.SendCoins(ctxb, btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send coins to carol: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, carol, dave, chanAmt, 0, false,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We a´make a note of the nodes' current on-chain balances, to make
|
||||||
|
// sure they are able to retrieve the channel funds eventually,
|
||||||
|
balReq := &lnrpc.WalletBalanceRequest{}
|
||||||
|
carolBalResp, err := carol.WalletBalance(ctxb, balReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get carol's balance: %v", err)
|
||||||
|
}
|
||||||
|
carolStartingBalance := carolBalResp.ConfirmedBalance
|
||||||
|
|
||||||
|
daveBalResp, err := dave.WalletBalance(ctxb, balReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get dave's balance: %v", err)
|
||||||
|
}
|
||||||
|
daveStartingBalance := daveBalResp.ConfirmedBalance
|
||||||
|
|
||||||
|
// With the channel open, we'll create a few invoices for Dave that
|
||||||
|
// Carol will pay to in order to advance the state of the channel.
|
||||||
|
// TODO(halseth): have dangling HTLCs on the commitment, able to
|
||||||
|
// retrive funds?
|
||||||
|
davePayReqs := make([]string, numInvoices)
|
||||||
|
for i := 0; i < numInvoices; i++ {
|
||||||
|
preimage := bytes.Repeat([]byte{byte(17 - i)}, 32)
|
||||||
|
invoice := &lnrpc.Invoice{
|
||||||
|
Memo: "testing",
|
||||||
|
RPreimage: preimage,
|
||||||
|
Value: paymentAmt,
|
||||||
|
}
|
||||||
|
resp, err := dave.AddInvoice(ctxb, invoice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to add invoice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
davePayReqs[i] = resp.PaymentRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// As we'll be querying the state of Dave's channels frequently we'll
|
||||||
|
// create a closure helper function for the purpose.
|
||||||
|
getDaveChanInfo := func() (*lnrpc.Channel, error) {
|
||||||
|
req := &lnrpc.ListChannelsRequest{}
|
||||||
|
daveChannelInfo, err := dave.ListChannels(ctxb, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(daveChannelInfo.Channels) != 1 {
|
||||||
|
t.Fatalf("dave should only have a single channel, "+
|
||||||
|
"instead he has %v",
|
||||||
|
len(daveChannelInfo.Channels))
|
||||||
|
}
|
||||||
|
|
||||||
|
return daveChannelInfo.Channels[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for Carol to receive the channel edge from the funding manager.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("carol didn't see the carol->dave channel before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send payments from Carol to Dave using 3 of Dave's payment hashes
|
||||||
|
// generated above.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
err = completePaymentRequests(ctxt, carol, davePayReqs[:numInvoices/2],
|
||||||
|
true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payments: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next query for Dave's channel state, as we sent 3 payments of 10k
|
||||||
|
// satoshis each, Dave should now see his balance as being 30k satoshis.
|
||||||
|
var daveChan *lnrpc.Channel
|
||||||
|
var predErr error
|
||||||
|
err = lntest.WaitPredicate(func() bool {
|
||||||
|
bChan, err := getDaveChanInfo()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get dave's channel info: %v", err)
|
||||||
|
}
|
||||||
|
if bChan.LocalBalance != 30000 {
|
||||||
|
predErr = fmt.Errorf("dave's balance is incorrect, "+
|
||||||
|
"got %v, expected %v", bChan.LocalBalance,
|
||||||
|
30000)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
daveChan = bChan
|
||||||
|
return true
|
||||||
|
}, time.Second*15)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", predErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab Dave's current commitment height (update number), we'll later
|
||||||
|
// revert him to this state after additional updates to revoke this
|
||||||
|
// state.
|
||||||
|
daveStateNumPreCopy := daveChan.NumUpdates
|
||||||
|
|
||||||
|
// Create a temporary file to house Dave's database state at this
|
||||||
|
// particular point in history.
|
||||||
|
daveTempDbPath, err := ioutil.TempDir("", "dave-past-state")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create temp db folder: %v", err)
|
||||||
|
}
|
||||||
|
daveTempDbFile := filepath.Join(daveTempDbPath, "channel.db")
|
||||||
|
defer os.Remove(daveTempDbPath)
|
||||||
|
|
||||||
|
// With the temporary file created, copy Dave's current state into the
|
||||||
|
// temporary file we created above. Later after more updates, we'll
|
||||||
|
// restore this state.
|
||||||
|
if err := copyFile(daveTempDbFile, dave.DBPath()); err != nil {
|
||||||
|
t.Fatalf("unable to copy database files: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, send payments from Carol to Dave, consuming Dave's remaining
|
||||||
|
// payment hashes.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
||||||
|
err = completePaymentRequests(ctxt, carol, davePayReqs[numInvoices/2:],
|
||||||
|
true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payments: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
daveChan, err = getDaveChanInfo()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get dave chan info: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we shutdown Dave, 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 Dave to travel back in time
|
||||||
|
// within the channel's history.
|
||||||
|
if err = net.RestartNode(dave, func() error {
|
||||||
|
return os.Rename(daveTempDbFile, dave.DBPath())
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("unable to restart node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now query for Dave's channel state, it should show that he's at a
|
||||||
|
// state number in the past, not the *latest* state.
|
||||||
|
daveChan, err = getDaveChanInfo()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get dave chan info: %v", err)
|
||||||
|
}
|
||||||
|
if daveChan.NumUpdates != daveStateNumPreCopy {
|
||||||
|
t.Fatalf("db copy failed: %v", daveChan.NumUpdates)
|
||||||
|
}
|
||||||
|
assertNodeNumChannels(t, ctxb, dave, 1)
|
||||||
|
|
||||||
|
// Upon reconnection, the nodes should detect that Dave is out of sync.
|
||||||
|
if err := net.ConnectNodes(ctxb, carol, dave); err != nil {
|
||||||
|
t.Fatalf("unable to connect dave to carol: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carol should force close the channel using her latest commitment.
|
||||||
|
forceClose, err := waitForTxInMempool(net.Miner.Node, 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to find Carol's force close tx in mempool: %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel should be in the state "waiting close" for Carol since she
|
||||||
|
// broadcasted the force close tx.
|
||||||
|
assertNumPendingChannels(t, carol, 1, 0)
|
||||||
|
|
||||||
|
// Dave should also consider the channel "waiting close", as he noticed
|
||||||
|
// the channel was out of sync, and is now waiting for a force close to
|
||||||
|
// hit the chain.
|
||||||
|
assertNumPendingChannels(t, dave, 1, 0)
|
||||||
|
|
||||||
|
// Restart Dave to make sure he is able to sweep the funds after
|
||||||
|
// shutdown.
|
||||||
|
if err := net.RestartNode(dave, nil); err != nil {
|
||||||
|
t.Fatalf("Node restart failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a single block, which should confirm the closing tx.
|
||||||
|
block := mineBlocks(t, net, 1)[0]
|
||||||
|
assertTxInBlock(t, block, forceClose)
|
||||||
|
|
||||||
|
// Dave should sweep his funds immediately, as they are not timelocked.
|
||||||
|
daveSweep, err := waitForTxInMempool(net.Miner.Node, 15*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to find Dave's sweep tx in mempool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dave should consider the channel pending force close (since he is
|
||||||
|
// waiting for his sweep to confirm).
|
||||||
|
assertNumPendingChannels(t, dave, 0, 1)
|
||||||
|
|
||||||
|
// Carol is considering it "pending force close", as whe must wait
|
||||||
|
// before she can sweep her outputs.
|
||||||
|
assertNumPendingChannels(t, carol, 0, 1)
|
||||||
|
|
||||||
|
block = mineBlocks(t, net, 1)[0]
|
||||||
|
assertTxInBlock(t, block, daveSweep)
|
||||||
|
|
||||||
|
// Now Dave should consider the channel fully closed.
|
||||||
|
assertNumPendingChannels(t, dave, 0, 0)
|
||||||
|
|
||||||
|
// We query Dave's balance to make sure it increased after the channel
|
||||||
|
// closed. This checks that he was able to sweep the funds he had in
|
||||||
|
// the channel.
|
||||||
|
daveBalResp, err = dave.WalletBalance(ctxb, balReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get dave's balance: %v", err)
|
||||||
|
}
|
||||||
|
daveBalance := daveBalResp.ConfirmedBalance
|
||||||
|
if daveBalance <= daveStartingBalance {
|
||||||
|
t.Fatalf("expected dave to have balance above %d, intead had %v",
|
||||||
|
daveStartingBalance, daveBalance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// After the Carol's output matures, she should also reclaim her funds.
|
||||||
|
mineBlocks(t, net, defaultCSV-1)
|
||||||
|
carolSweep, err := waitForTxInMempool(net.Miner.Node, 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to find Carol's sweep tx in mempool: %v", err)
|
||||||
|
}
|
||||||
|
block = mineBlocks(t, net, 1)[0]
|
||||||
|
assertTxInBlock(t, block, carolSweep)
|
||||||
|
|
||||||
|
// Now the channel should be fully closed also from Carol's POV.
|
||||||
|
assertNumPendingChannels(t, carol, 0, 0)
|
||||||
|
|
||||||
|
// Make sure Carol got her balance back.
|
||||||
|
carolBalResp, err = carol.WalletBalance(ctxb, balReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get carol's balance: %v", err)
|
||||||
|
}
|
||||||
|
carolBalance := carolBalResp.ConfirmedBalance
|
||||||
|
if carolBalance <= carolStartingBalance {
|
||||||
|
t.Fatalf("expected carol to have balance above %d, "+
|
||||||
|
"instead had %v", carolStartingBalance,
|
||||||
|
carolBalance)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNodeNumChannels(t, ctxb, dave, 0)
|
||||||
|
assertNodeNumChannels(t, ctxb, carol, 0)
|
||||||
|
}
|
||||||
|
|
||||||
// assertNodeNumChannels polls the provided node's list channels rpc until it
|
// assertNodeNumChannels polls the provided node's list channels rpc until it
|
||||||
// reaches the desired number of total channels.
|
// reaches the desired number of total channels.
|
||||||
func assertNodeNumChannels(t *harnessTest, ctxb context.Context,
|
func assertNodeNumChannels(t *harnessTest, ctxb context.Context,
|
||||||
node *lntest.HarnessNode, numChannels int) {
|
node *lntest.HarnessNode, numChannels int) {
|
||||||
|
|
||||||
// Poll alice for her list of channels.
|
// Poll node for its list of channels.
|
||||||
req := &lnrpc.ListChannelsRequest{}
|
req := &lnrpc.ListChannelsRequest{}
|
||||||
|
|
||||||
var predErr error
|
var predErr error
|
||||||
pred := func() bool {
|
pred := func() bool {
|
||||||
chanInfo, err := node.ListChannels(ctxb, req)
|
chanInfo, err := node.ListChannels(ctxb, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
predErr = fmt.Errorf("unable to query for alice's "+
|
predErr = fmt.Errorf("unable to query for node's "+
|
||||||
"channels: %v", err)
|
"channels: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -10814,6 +11138,10 @@ var testsCases = []*testCase{
|
|||||||
name: "revoked uncooperative close retribution remote hodl",
|
name: "revoked uncooperative close retribution remote hodl",
|
||||||
test: testRevokedCloseRetributionRemoteHodl,
|
test: testRevokedCloseRetributionRemoteHodl,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "data loss protection",
|
||||||
|
test: testDataLossProtection,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "query routes",
|
name: "query routes",
|
||||||
test: testQueryRoutes,
|
test: testQueryRoutes,
|
||||||
|
Loading…
Reference in New Issue
Block a user