itest: refactor testDataLossProtection

This commit is contained in:
yyforyongyu 2022-07-28 18:25:06 +08:00
parent e8dc15dae4
commit 36c84bbd43
No known key found for this signature in database
GPG key ID: 9BCD95C4FF296868
4 changed files with 50 additions and 371 deletions

View file

@ -1101,198 +1101,6 @@ func assertNumPendingChannels(t *harnessTest, node *lntest.HarnessNode,
require.NoErrorf(t.t, err, "got err: %v", predErr)
}
// assertDLPExecuted asserts that Dave is a node that has recovered their state
// form scratch. Carol should then force close on chain, with Dave sweeping his
// funds immediately, and Carol sweeping her fund after her CSV delay is up. If
// the blankSlate value is true, then this means that Dave won't need to sweep
// on chain as he has no funds in the channel.
func assertDLPExecutedOld(net *lntest.NetworkHarness, t *harnessTest,
carol *lntest.HarnessNode, carolStartingBalance int64,
dave *lntest.HarnessNode, daveStartingBalance int64,
commitType lnrpc.CommitmentType) {
// Increase the fee estimate so that the following force close tx will
// be cpfp'ed.
net.SetFeeEstimate(30000)
// We disabled auto-reconnect for some tests to avoid timing issues.
// To make sure the nodes are initiating DLP now, we have to manually
// re-connect them.
ctxb := context.Background()
net.EnsureConnected(t.t, carol, dave)
// Upon reconnection, the nodes should detect that Dave is out of sync.
// Carol should force close the channel using her latest commitment.
expectedTxes := 1
if commitTypeHasAnchors(commitType) {
expectedTxes = 2
}
_, err := waitForNTxsInMempool(
net.Miner.Client, expectedTxes, minerMempoolTimeout,
)
require.NoError(
t.t, err,
"unable to find Carol's force close tx in mempool",
)
// 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.
require.NoError(t.t, net.RestartNode(dave, nil), "Node restart failed")
// Generate a single block, which should confirm the closing tx.
_ = mineBlocks(t, net, 1, expectedTxes)[0]
// 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 we must wait
// before she can sweep her outputs.
assertNumPendingChannels(t, carol, 0, 1)
if commitType == lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE {
// Dave should sweep his anchor only, since he still has the
// lease CLTV constraint on his commitment output.
_, err = waitForNTxsInMempool(
net.Miner.Client, 1, minerMempoolTimeout,
)
require.NoError(t.t, err, "unable to find Dave's anchor sweep "+
"tx in mempool")
// Mine Dave's anchor sweep tx.
_ = mineBlocks(t, net, 1, 1)[0]
// After Carol's output matures, she should also reclaim her
// funds.
//
// The commit sweep resolver publishes the sweep tx at
// defaultCSV-1 and we already mined one block after the
// commitmment was published, so take that into account.
mineBlocks(t, net, defaultCSV-1-1, 0)
carolSweep, err := waitForTxInMempool(
net.Miner.Client, minerMempoolTimeout,
)
require.NoError(t.t, err, "unable to find Carol's sweep tx in "+
"mempool")
block := mineBlocks(t, net, 1, 1)[0]
assertTxInBlock(t, block, carolSweep)
// Now the channel should be fully closed also from Carol's POV.
assertNumPendingChannels(t, carol, 0, 0)
// We'll now mine the remaining blocks to prompt Dave to sweep
// his CLTV-constrained output.
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
resp, err := dave.PendingChannels(
ctxt, &lnrpc.PendingChannelsRequest{},
)
require.NoError(t.t, err)
blocksTilMaturity :=
resp.PendingForceClosingChannels[0].BlocksTilMaturity
require.Positive(t.t, blocksTilMaturity)
mineBlocks(t, net, uint32(blocksTilMaturity), 0)
daveSweep, err := waitForTxInMempool(
net.Miner.Client, minerMempoolTimeout,
)
require.NoError(t.t, err, "unable to find Dave's sweep tx in "+
"mempool")
block = mineBlocks(t, net, 1, 1)[0]
assertTxInBlock(t, block, daveSweep)
// Now Dave should consider the channel fully closed.
assertNumPendingChannels(t, dave, 0, 0)
} else {
// Dave should sweep his funds immediately, as they are not
// timelocked. We also expect Dave to sweep his anchor, if
// present.
_, err = waitForNTxsInMempool(
net.Miner.Client, expectedTxes, minerMempoolTimeout,
)
require.NoError(t.t, err, "unable to find Dave's sweep tx in "+
"mempool")
// Mine the sweep tx.
_ = mineBlocks(t, net, 1, expectedTxes)[0]
// Now Dave should consider the channel fully closed.
assertNumPendingChannels(t, dave, 0, 0)
// After Carol's output matures, she should also reclaim her
// funds.
//
// The commit sweep resolver publishes the sweep tx at
// defaultCSV-1 and we already mined one block after the
// commitmment was published, so take that into account.
mineBlocks(t, net, defaultCSV-1-1, 0)
carolSweep, err := waitForTxInMempool(
net.Miner.Client, minerMempoolTimeout,
)
require.NoError(t.t, err, "unable to find Carol's sweep tx in "+
"mempool")
block := mineBlocks(t, net, 1, 1)[0]
assertTxInBlock(t, block, carolSweep)
// Now the channel should be fully closed also from Carol's POV.
assertNumPendingChannels(t, carol, 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.
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
balReq := &lnrpc.WalletBalanceRequest{}
daveBalResp, err := dave.WalletBalance(ctxt, balReq)
require.NoError(t.t, err, "unable to get dave's balance")
daveBalance := daveBalResp.ConfirmedBalance
require.Greater(
t.t, daveBalance, daveStartingBalance, "balance not increased",
)
// Make sure Carol got her balance back.
err = wait.NoError(func() error {
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
carolBalResp, err := carol.WalletBalance(ctxt, balReq)
if err != nil {
return fmt.Errorf("unable to get carol's balance: %v", err)
}
carolBalance := carolBalResp.ConfirmedBalance
// With Neutrino we don't get a backend error when trying to
// publish an orphan TX (which is what the sweep for the remote
// anchor is since the remote commitment TX was not broadcast).
// That's why the wallet still sees that as unconfirmed and we
// need to count the total balance instead of the confirmed.
if net.BackendCfg.Name() == lntest.NeutrinoBackendName {
carolBalance = carolBalResp.TotalBalance
}
if carolBalance <= carolStartingBalance {
return fmt.Errorf("expected carol to have balance "+
"above %d, instead had %v", carolStartingBalance,
carolBalance)
}
return nil
}, defaultTimeout)
require.NoError(t.t, err)
assertNodeNumChannels(t, dave, 0)
assertNodeNumChannels(t, carol, 0)
}
// verifyCloseUpdate is used to verify that a closed channel update is of the
// expected type.
func verifyCloseUpdate(chanUpdate *lnrpc.ChannelEventUpdate,

View file

@ -23,4 +23,8 @@ var allTestCasesTemp = []*lntemp.TestCase{
Name: "channel backup restore",
TestFunc: testChannelBackupRestore,
},
{
Name: "data loss protection",
TestFunc: testDataLossProtection,
},
}

View file

@ -1167,8 +1167,7 @@ func testChanRestoreScenario(ht *lntemp.HarnessTest,
// 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
// opportunity to sweep its output.
func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) {
ctxb := context.Background()
func testDataLossProtection(ht *lntemp.HarnessTest) {
const (
chanAmt = funding.MaxBtcFundingAmount
paymentAmt = 10000
@ -1180,36 +1179,31 @@ func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) {
// protection logic automatically. We also can't have Carol
// automatically re-connect too early, otherwise DLP would be initiated
// at the wrong moment.
carol := net.NewNode(
t.t, "Carol", []string{"--nolisten", "--minbackoff=1h"},
)
defer shutdownAndAssert(net, t, carol)
carol := ht.NewNode("Carol", []string{"--nolisten", "--minbackoff=1h"})
// Dave will be the party losing his state.
dave := net.NewNode(t.t, "Dave", nil)
defer shutdownAndAssert(net, t, dave)
dave := ht.NewNode("Dave", nil)
// Before we make a channel, we'll load up Carol with some coins sent
// directly from the miner.
net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, carol)
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
// timeTravel is a method that will make Carol open a channel to the
// passed node, settle a series of payments, then reset the node back
// to the state before the payments happened. When this method returns
// the node will be unaware of the new state updates. The returned
// function can be used to restart the node in this state.
timeTravel := func(node *lntest.HarnessNode) (func() error,
*lnrpc.ChannelPoint, int64, error) {
timeTravel := func(node *node.HarnessNode) (func() error,
*lnrpc.ChannelPoint, int64) {
// We must let the node communicate with Carol before they are
// able to open channel, so we connect them.
net.EnsureConnected(t.t, carol, node)
ht.EnsureConnected(carol, node)
// We'll first open up a channel between them with a 0.5 BTC
// value.
chanPoint := openChannelAndAssert(
t, net, carol, node,
lntest.OpenChannelParams{
chanPoint := ht.OpenChannel(
carol, node, lntemp.OpenChannelParams{
Amt: chanAmt,
},
)
@ -1219,54 +1213,18 @@ func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) {
// the channel.
// TODO(halseth): have dangling HTLCs on the commitment, able to
// retrieve funds?
payReqs, _, _, err := createPayReqs(
node, paymentAmt, numInvoices,
)
if err != nil {
t.Fatalf("unable to create pay reqs: %v", err)
}
// Wait for Carol to receive the channel edge from the funding
// manager.
err = carol.WaitForNetworkChannelOpen(chanPoint)
if err != nil {
t.Fatalf("carol didn't see the carol->%s channel "+
"before timeout: %v", node.Name(), err)
}
payReqs, _, _ := ht.CreatePayReqs(node, paymentAmt, numInvoices)
// Send payments from Carol using 3 of the payment hashes
// generated above.
err = completePaymentRequests(
carol, carol.RouterClient,
payReqs[:numInvoices/2], true,
)
if err != nil {
t.Fatalf("unable to send payments: %v", err)
}
ht.CompletePaymentRequests(carol, payReqs[:numInvoices/2])
// Next query for the node's channel state, as we sent 3
// payments of 10k satoshis each, it should now see his balance
// as being 30k satoshis.
var nodeChan *lnrpc.Channel
var predErr error
err = wait.Predicate(func() bool {
bChan, err := getChanInfo(node)
if err != nil {
t.Fatalf("unable to get channel info: %v", err)
}
if bChan.LocalBalance != 30000 {
predErr = fmt.Errorf("balance is incorrect, "+
"got %v, expected %v",
bChan.LocalBalance, 30000)
return false
}
nodeChan = bChan
return true
}, defaultTimeout)
if err != nil {
t.Fatalf("%v", predErr)
}
nodeChan := ht.AssertChannelLocalBalance(
node, chanPoint, 30_000,
)
// Grab the current commitment height (update number), we'll
// later revert him to this state after additional updates to
@ -1276,94 +1234,53 @@ func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) {
// With the temporary file created, copy the current state into
// the temporary file we created above. Later after more
// updates, we'll restore this state.
if err := net.BackupDb(node); err != nil {
t.Fatalf("unable to copy database files: %v", err)
}
ht.BackupDB(node)
// Reconnect the peers after the restart that was needed for the db
// backup.
net.EnsureConnected(t.t, carol, node)
// Reconnect the peers after the restart that was needed for
// the db backup.
ht.EnsureConnected(carol, node)
// Finally, send more payments from , using the remaining
// payment hashes.
err = completePaymentRequests(
carol, carol.RouterClient, payReqs[numInvoices/2:], true,
)
if err != nil {
t.Fatalf("unable to send payments: %v", err)
}
nodeChan, err = getChanInfo(node)
if err != nil {
t.Fatalf("unable to get dave chan info: %v", err)
}
ht.CompletePaymentRequests(carol, payReqs[numInvoices/2:])
// Now we shutdown the node, copying over the its temporary
// database state which has the *prior* channel state over his
// current most up to date state. With this, we essentially
// force the node to travel back in time within the channel's
// history.
if err = net.RestartNode(node, func() error {
return net.RestoreDb(node)
}); err != nil {
t.Fatalf("unable to restart node: %v", err)
}
ht.RestartNodeAndRestoreDB(node)
// Make sure the channel is still there from the PoV of the
// node.
assertNodeNumChannels(t, node, 1)
ht.AssertNodeNumChannels(node, 1)
// Now query for the channel state, it should show that it's at
// a state number in the past, not the *latest* state.
nodeChan, err = getChanInfo(node)
if err != nil {
t.Fatalf("unable to get dave chan info: %v", err)
}
if nodeChan.NumUpdates != stateNumPreCopy {
t.Fatalf("db copy failed: %v", nodeChan.NumUpdates)
}
ht.AssertChannelNumUpdates(node, stateNumPreCopy, chanPoint)
balReq := &lnrpc.WalletBalanceRequest{}
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
balResp, err := node.WalletBalance(ctxt, balReq)
if err != nil {
t.Fatalf("unable to get dave's balance: %v", err)
}
balResp := node.RPC.WalletBalance()
restart := ht.SuspendNode(node)
restart, err := net.SuspendNode(node)
if err != nil {
t.Fatalf("unable to suspend node: %v", err)
}
return restart, chanPoint, balResp.ConfirmedBalance, nil
return restart, chanPoint, balResp.ConfirmedBalance
}
// Reset Dave to a state where he has an outdated channel state.
restartDave, _, daveStartingBalance, err := timeTravel(dave)
if err != nil {
t.Fatalf("unable to time travel dave: %v", err)
}
restartDave, _, daveStartingBalance := timeTravel(dave)
// We 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{}
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
carolBalResp, err := carol.WalletBalance(ctxt, balReq)
if err != nil {
t.Fatalf("unable to get carol's balance: %v", err)
}
carolBalResp := carol.RPC.WalletBalance()
carolStartingBalance := carolBalResp.ConfirmedBalance
// Restart Dave to trigger a channel resync.
if err := restartDave(); err != nil {
t.Fatalf("unable to restart dave: %v", err)
}
require.NoError(ht, restartDave(), "unable to restart dave")
// Assert that once Dave comes up, they reconnect, Carol force closes
// on chain, and both of them properly carry out the DLP protocol.
assertDLPExecutedOld(
net, t, carol, carolStartingBalance, dave, daveStartingBalance,
lnrpc.CommitmentType_STATIC_REMOTE_KEY,
assertDLPExecuted(
ht, carol, carolStartingBalance, dave,
daveStartingBalance, lnrpc.CommitmentType_STATIC_REMOTE_KEY,
)
// As a second part of this test, we will test the scenario where a
@ -1373,93 +1290,47 @@ func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) {
// closed channel, such that Dave can retrieve his funds.
//
// We start by letting Dave time travel back to an outdated state.
restartDave, chanPoint2, daveStartingBalance, err := timeTravel(dave)
if err != nil {
t.Fatalf("unable to time travel eve: %v", err)
}
restartDave, chanPoint2, daveStartingBalance := timeTravel(dave)
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
carolBalResp, err = carol.WalletBalance(ctxt, balReq)
if err != nil {
t.Fatalf("unable to get carol's balance: %v", err)
}
carolBalResp = carol.RPC.WalletBalance()
carolStartingBalance = carolBalResp.ConfirmedBalance
// Now let Carol force close the channel while Dave is offline.
closeChannelAndAssert(t, net, carol, chanPoint2, true)
// Wait for the channel to be marked pending force close.
err = waitForChannelPendingForceClose(carol, chanPoint2)
if err != nil {
t.Fatalf("channel not pending force close: %v", err)
}
// Mine enough blocks for Carol to sweep her funds.
mineBlocks(t, net, defaultCSV-1, 0)
carolSweep, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
if err != nil {
t.Fatalf("unable to find Carol's sweep tx in mempool: %v", err)
}
block := mineBlocks(t, net, 1, 1)[0]
assertTxInBlock(t, block, carolSweep)
// Now the channel should be fully closed also from Carol's POV.
assertNumPendingChannels(t, carol, 0, 0)
ht.ForceCloseChannel(carol, chanPoint2)
// Make sure Carol got her balance back.
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
carolBalResp, err = carol.WalletBalance(ctxt, balReq)
if err != nil {
t.Fatalf("unable to get carol's balance: %v", err)
}
carolBalResp = carol.RPC.WalletBalance()
carolBalance := carolBalResp.ConfirmedBalance
if carolBalance <= carolStartingBalance {
t.Fatalf("expected carol to have balance above %d, "+
"instead had %v", carolStartingBalance,
carolBalance)
}
require.Greater(ht, carolBalance, carolStartingBalance,
"expected carol to have balance increased")
assertNodeNumChannels(t, carol, 0)
ht.AssertNodeNumChannels(carol, 0)
// When Dave comes online, he will reconnect to Carol, try to resync
// the channel, but it will already be closed. Carol should resend the
// information Dave needs to sweep his funds.
if err := restartDave(); err != nil {
t.Fatalf("unable to restart Eve: %v", err)
}
require.NoError(ht, restartDave(), "unable to restart Eve")
// Dave should sweep his funds.
_, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
if err != nil {
t.Fatalf("unable to find Dave's sweep tx in mempool: %v", err)
}
ht.Miner.AssertNumTxsInMempool(1)
// Mine a block to confirm the sweep, and make sure Dave got his
// balance back.
mineBlocks(t, net, 1, 1)
assertNodeNumChannels(t, dave, 0)
err = wait.NoError(func() error {
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
daveBalResp, err := dave.WalletBalance(ctxt, balReq)
if err != nil {
return fmt.Errorf("unable to get dave's balance: %v",
err)
}
ht.Miner.MineBlocksAndAssertNumTxes(1, 1)
ht.AssertNodeNumChannels(dave, 0)
err := wait.NoError(func() error {
daveBalResp := dave.RPC.WalletBalance()
daveBalance := daveBalResp.ConfirmedBalance
if daveBalance <= daveStartingBalance {
return fmt.Errorf("expected dave to have balance "+
"above %d, instead had %v", daveStartingBalance,
"above %d, intead had %v", daveStartingBalance,
daveBalance)
}
return nil
}, defaultTimeout)
if err != nil {
t.Fatalf("%v", err)
}
require.NoError(ht, err, "timeout while checking dave's balance")
}
// createLegacyRevocationChannel creates a single channel using the legacy

View file

@ -223,10 +223,6 @@ var allTestCases = []*testCase{
name: "revoked uncooperative close retribution altruist watchtower",
test: testRevokedCloseRetributionAltruistWatchtower,
},
{
name: "data loss protection",
test: testDataLossProtection,
},
{
name: "query routes",
test: testQueryRoutes,