mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
653a8ac55e
This commit replaces `AssertTopologyChannelOpen` with `AssertChannelInGraph`, which asserts a given channel edge is found. `AssertTopologyChannelOpen` only asserts a given edge has been received via the topology subscription, while we need to make sure the channel is in the graph before continuing our tests.
713 lines
23 KiB
Go
713 lines
23 KiB
Go
package itest
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/go-errors/errors"
|
|
"github.com/lightningnetwork/lnd/funding"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
|
|
"github.com/lightningnetwork/lnd/lntest"
|
|
"github.com/lightningnetwork/lnd/lntest/node"
|
|
"github.com/lightningnetwork/lnd/lntest/port"
|
|
"github.com/lightningnetwork/lnd/lntest/rpc"
|
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// testWatchtower tests the behaviour of the watchtower client and server.
|
|
func testWatchtower(ht *lntest.HarnessTest) {
|
|
ht.Run("revocation", func(t *testing.T) {
|
|
tt := ht.Subtest(t)
|
|
testRevokedCloseRetributionAltruistWatchtower(tt)
|
|
})
|
|
|
|
ht.Run("session deletion", func(t *testing.T) {
|
|
tt := ht.Subtest(t)
|
|
testTowerClientSessionDeletion(tt)
|
|
})
|
|
|
|
ht.Run("tower and session activation", func(t *testing.T) {
|
|
tt := ht.Subtest(t)
|
|
testTowerClientTowerAndSessionManagement(tt)
|
|
})
|
|
}
|
|
|
|
// testTowerClientTowerAndSessionManagement tests the various control commands
|
|
// that a user has over the client's set of active towers and sessions.
|
|
func testTowerClientTowerAndSessionManagement(ht *lntest.HarnessTest) {
|
|
const (
|
|
chanAmt = funding.MaxBtcFundingAmount
|
|
externalIP = "1.2.3.4"
|
|
externalIP2 = "1.2.3.5"
|
|
sessionCloseRange = 1
|
|
)
|
|
|
|
// Set up Wallis the watchtower who will be used by Dave to watch over
|
|
// his channel commitment transactions.
|
|
wallisPk, wallisListener, _ := setUpNewTower(ht, "Wallis", externalIP)
|
|
|
|
// Dave will be the tower client.
|
|
daveArgs := []string{
|
|
"--wtclient.active",
|
|
fmt.Sprintf(
|
|
"--wtclient.session-close-range=%d", sessionCloseRange,
|
|
),
|
|
}
|
|
dave := ht.NewNode("Dave", daveArgs)
|
|
|
|
addWallisReq := &wtclientrpc.AddTowerRequest{
|
|
Pubkey: wallisPk,
|
|
Address: wallisListener,
|
|
}
|
|
dave.RPC.AddTower(addWallisReq)
|
|
|
|
assertNumSessions := func(towerPk []byte, expectedNum int,
|
|
mineOnFail bool) {
|
|
|
|
err := wait.NoError(func() error {
|
|
info := dave.RPC.GetTowerInfo(
|
|
&wtclientrpc.GetTowerInfoRequest{
|
|
Pubkey: towerPk,
|
|
IncludeSessions: true,
|
|
},
|
|
)
|
|
|
|
var numSessions uint32
|
|
for _, sessionType := range info.SessionInfo {
|
|
numSessions += sessionType.NumSessions
|
|
}
|
|
if numSessions == uint32(expectedNum) {
|
|
return nil
|
|
}
|
|
|
|
if mineOnFail {
|
|
ht.MineBlocks(1)
|
|
}
|
|
|
|
return fmt.Errorf("expected %d sessions, got %d",
|
|
expectedNum, numSessions)
|
|
}, defaultTimeout)
|
|
require.NoError(ht, err)
|
|
}
|
|
|
|
// Assert that there are a few sessions between Dave and Wallis. There
|
|
// should be one per client. There are currently 3 types of clients, so
|
|
// we expect 3 sessions.
|
|
assertNumSessions(wallisPk, 3, false)
|
|
|
|
// Before we make a channel, we'll load up Dave with some coins sent
|
|
// directly from the miner.
|
|
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
|
|
|
|
// Connect Dave and Alice.
|
|
ht.ConnectNodes(dave, ht.Alice)
|
|
|
|
// Open a channel between Dave and Alice.
|
|
params := lntest.OpenChannelParams{
|
|
Amt: chanAmt,
|
|
}
|
|
chanPoint := ht.OpenChannel(dave, ht.Alice, params)
|
|
|
|
// Show that the Wallis tower is currently seen as an active session
|
|
// candidate.
|
|
info := dave.RPC.GetTowerInfo(&wtclientrpc.GetTowerInfoRequest{
|
|
Pubkey: wallisPk,
|
|
})
|
|
require.GreaterOrEqual(ht, len(info.SessionInfo), 1)
|
|
require.True(ht, info.SessionInfo[0].ActiveSessionCandidate)
|
|
|
|
// Make some back-ups and assert that they are added to a session with
|
|
// the tower.
|
|
generateBackups(ht, dave, ht.Alice, 4)
|
|
|
|
// Assert that one of the sessions now has 4 backups.
|
|
assertNumBackups(ht, dave.RPC, wallisPk, 4, false)
|
|
|
|
// Now, deactivate the tower and show that it is no longer considered
|
|
// an active session candidate.
|
|
dave.RPC.DeactivateTower(&wtclientrpc.DeactivateTowerRequest{
|
|
Pubkey: wallisPk,
|
|
})
|
|
info = dave.RPC.GetTowerInfo(&wtclientrpc.GetTowerInfoRequest{
|
|
Pubkey: wallisPk,
|
|
})
|
|
require.GreaterOrEqual(ht, len(info.SessionInfo), 1)
|
|
require.False(ht, info.SessionInfo[0].ActiveSessionCandidate)
|
|
|
|
// Back up a few more states.
|
|
generateBackups(ht, dave, ht.Alice, 4)
|
|
|
|
// These should _not_ be on the tower. Therefore, the number of
|
|
// back-ups on the tower should be the same as before.
|
|
assertNumBackups(ht, dave.RPC, wallisPk, 4, false)
|
|
|
|
// Add new tower and connect Dave to it.
|
|
wilmaPk, wilmaListener, _ := setUpNewTower(ht, "Wilma", externalIP2)
|
|
dave.RPC.AddTower(&wtclientrpc.AddTowerRequest{
|
|
Pubkey: wilmaPk,
|
|
Address: wilmaListener,
|
|
})
|
|
assertNumSessions(wilmaPk, 3, false)
|
|
|
|
// The updates from before should now appear on the new watchtower.
|
|
assertNumBackups(ht, dave.RPC, wilmaPk, 4, false)
|
|
|
|
// Reactivate the Wallis tower and then deactivate the Wilma one.
|
|
dave.RPC.AddTower(addWallisReq)
|
|
dave.RPC.DeactivateTower(&wtclientrpc.DeactivateTowerRequest{
|
|
Pubkey: wilmaPk,
|
|
})
|
|
|
|
// Generate some more back-ups.
|
|
generateBackups(ht, dave, ht.Alice, 4)
|
|
|
|
// Assert that they get added to the first tower (Wallis) and that the
|
|
// number of sessions with Wallis has not changed - in other words, the
|
|
// previously used session was re-used.
|
|
assertNumBackups(ht, dave.RPC, wallisPk, 8, false)
|
|
assertNumSessions(wallisPk, 3, false)
|
|
|
|
findSession := func(towerPk []byte, numBackups uint32) []byte {
|
|
info := dave.RPC.GetTowerInfo(&wtclientrpc.GetTowerInfoRequest{
|
|
Pubkey: towerPk,
|
|
IncludeSessions: true,
|
|
})
|
|
|
|
for _, sessionType := range info.SessionInfo {
|
|
for _, session := range sessionType.Sessions {
|
|
if session.NumBackups == numBackups {
|
|
return session.Id
|
|
}
|
|
}
|
|
}
|
|
ht.Fatalf("session with %d backups not found", numBackups)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Now we will test the termination of a session.
|
|
// First, we need to figure out the ID of the session that has been used
|
|
// for back-ups.
|
|
sessionID := findSession(wallisPk, 8)
|
|
|
|
// Now, terminate the session.
|
|
dave.RPC.TerminateSession(&wtclientrpc.TerminateSessionRequest{
|
|
SessionId: sessionID,
|
|
})
|
|
|
|
// This should force the client to negotiate a new session. The old
|
|
// session still remains in our session list since the channel for which
|
|
// it has updates for is still open.
|
|
assertNumSessions(wallisPk, 4, false)
|
|
|
|
// Any new back-ups should now be backed up on a different session.
|
|
generateBackups(ht, dave, ht.Alice, 2)
|
|
assertNumBackups(ht, dave.RPC, wallisPk, 10, false)
|
|
findSession(wallisPk, 2)
|
|
|
|
// Close the channel.
|
|
ht.CloseChannelAssertPending(dave, chanPoint, false)
|
|
|
|
// Mine enough blocks to surpass the session close range buffer.
|
|
ht.MineBlocksAndAssertNumTxes(sessionCloseRange+6, 1)
|
|
|
|
// The session that was previously terminated now gets deleted since
|
|
// the channel for which it has updates has now been closed. All the
|
|
// remaining sessions are not yet closable since they are not yet
|
|
// exhausted and are all still active. We set mineOnFail to true here
|
|
// so that we can ensure that the closable session queue continues to
|
|
// be checked on each new block. It could have been the case that all
|
|
// checks with the above mined blocks were completed before the
|
|
// closable session was queued.
|
|
assertNumSessions(wallisPk, 3, true)
|
|
|
|
// For the sake of completion, we call RemoveTower here for both towers
|
|
// to show that this should never error.
|
|
dave.RPC.RemoveTower(&wtclientrpc.RemoveTowerRequest{
|
|
Pubkey: wallisPk,
|
|
})
|
|
dave.RPC.RemoveTower(&wtclientrpc.RemoveTowerRequest{
|
|
Pubkey: wilmaPk,
|
|
})
|
|
}
|
|
|
|
// testTowerClientSessionDeletion tests that sessions are correctly deleted
|
|
// when they are deemed closable.
|
|
func testTowerClientSessionDeletion(ht *lntest.HarnessTest) {
|
|
const (
|
|
chanAmt = funding.MaxBtcFundingAmount
|
|
numInvoices = 5
|
|
maxUpdates = numInvoices * 2
|
|
externalIP = "1.2.3.4"
|
|
sessionCloseRange = 1
|
|
)
|
|
|
|
// Set up Wallis the watchtower who will be used by Dave to watch over
|
|
// his channel commitment transactions.
|
|
wallisPk, listener, _ := setUpNewTower(ht, "Wallis", externalIP)
|
|
|
|
// Dave will be the tower client.
|
|
daveArgs := []string{
|
|
"--wtclient.active",
|
|
fmt.Sprintf("--wtclient.max-updates=%d", maxUpdates),
|
|
fmt.Sprintf(
|
|
"--wtclient.session-close-range=%d", sessionCloseRange,
|
|
),
|
|
}
|
|
dave := ht.NewNode("Dave", daveArgs)
|
|
|
|
addTowerReq := &wtclientrpc.AddTowerRequest{
|
|
Pubkey: wallisPk,
|
|
Address: listener,
|
|
}
|
|
dave.RPC.AddTower(addTowerReq)
|
|
|
|
// Assert that there exists a session between Dave and Wallis.
|
|
err := wait.NoError(func() error {
|
|
info := dave.RPC.GetTowerInfo(&wtclientrpc.GetTowerInfoRequest{
|
|
Pubkey: wallisPk,
|
|
IncludeSessions: true,
|
|
})
|
|
|
|
var numSessions uint32
|
|
for _, sessionType := range info.SessionInfo {
|
|
numSessions += sessionType.NumSessions
|
|
}
|
|
if numSessions > 0 {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("expected a non-zero number of sessions")
|
|
}, defaultTimeout)
|
|
require.NoError(ht, err)
|
|
|
|
// Before we make a channel, we'll load up Dave with some coins sent
|
|
// directly from the miner.
|
|
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
|
|
|
|
// Connect Dave and Alice.
|
|
ht.ConnectNodes(dave, ht.Alice)
|
|
|
|
// Open a channel between Dave and Alice.
|
|
params := lntest.OpenChannelParams{
|
|
Amt: chanAmt,
|
|
}
|
|
chanPoint := ht.OpenChannel(dave, ht.Alice, params)
|
|
|
|
// Since there are 2 updates made for every payment and the maximum
|
|
// number of updates per session has been set to 10, make 5 payments
|
|
// between the pair so that the session is exhausted.
|
|
generateBackups(ht, dave, ht.Alice, maxUpdates)
|
|
|
|
// Assert that one of the sessions now has 10 backups.
|
|
assertNumBackups(ht, dave.RPC, wallisPk, 10, false)
|
|
|
|
// Now close the channel and wait for the close transaction to appear
|
|
// in the mempool so that it is included in a block when we mine.
|
|
ht.CloseChannelAssertPending(dave, chanPoint, false)
|
|
|
|
// Mine enough blocks to surpass the session-close-range. This should
|
|
// trigger the session to be deleted.
|
|
ht.MineBlocksAndAssertNumTxes(sessionCloseRange+6, 1)
|
|
|
|
// Wait for the session to be deleted. We know it has been deleted once
|
|
// the number of backups is back to zero. We check for number of backups
|
|
// instead of number of sessions because it is expected that the client
|
|
// would immediately negotiate another session after deleting the
|
|
// exhausted one. This time we set the "mineOnFail" parameter to true to
|
|
// ensure that the session deleting logic is run.
|
|
assertNumBackups(ht, dave.RPC, wallisPk, 0, true)
|
|
}
|
|
|
|
// testRevokedCloseRetributionAltruistWatchtower establishes a channel between
|
|
// Carol and Dave, where Carol is using a third node Willy as her watchtower.
|
|
// After sending some payments, Dave reverts his state and force closes to
|
|
// trigger a breach. Carol is kept offline throughout the process and the test
|
|
// asserts that Willy responds by broadcasting the justice transaction on
|
|
// Carol's behalf sweeping her funds without a reward.
|
|
func testRevokedCloseRetributionAltruistWatchtower(ht *lntest.HarnessTest) {
|
|
for _, commitType := range []lnrpc.CommitmentType{
|
|
lnrpc.CommitmentType_LEGACY,
|
|
lnrpc.CommitmentType_ANCHORS,
|
|
lnrpc.CommitmentType_SIMPLE_TAPROOT,
|
|
} {
|
|
testName := fmt.Sprintf("%v", commitType.String())
|
|
ct := commitType
|
|
testFunc := func(ht *lntest.HarnessTest) {
|
|
testRevokedCloseRetributionAltruistWatchtowerCase(
|
|
ht, ct,
|
|
)
|
|
}
|
|
|
|
success := ht.Run(testName, func(tt *testing.T) {
|
|
st := ht.Subtest(tt)
|
|
|
|
st.RunTestCase(&lntest.TestCase{
|
|
Name: testName,
|
|
TestFunc: testFunc,
|
|
})
|
|
})
|
|
|
|
if !success {
|
|
// Log failure time to help relate the lnd logs to the
|
|
// failure.
|
|
ht.Logf("Failure time: %v", time.Now().Format(
|
|
"2006-01-02 15:04:05.000",
|
|
))
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest,
|
|
commitType lnrpc.CommitmentType) {
|
|
|
|
const (
|
|
chanAmt = funding.MaxBtcFundingAmount
|
|
paymentAmt = 10000
|
|
numInvoices = 6
|
|
externalIP = "1.2.3.4"
|
|
)
|
|
|
|
// Since we'd like to test some multi-hop failure scenarios, we'll
|
|
// introduce another node into our test network: Carol.
|
|
carolArgs := lntest.NodeArgsForCommitType(commitType)
|
|
carolArgs = append(carolArgs, "--hodl.exit-settle")
|
|
|
|
carol := ht.NewNode("Carol", carolArgs)
|
|
|
|
// Set up Willy the watchtower who will protect Dave from Carol's
|
|
// breach. He will remain online in order to punish Carol on Dave's
|
|
// behalf, since the breach will happen while Dave is offline.
|
|
willyInfoPk, listener, willy := setUpNewTower(ht, "Willy", externalIP)
|
|
|
|
// Dave will be the breached party. We set --nolisten to ensure Carol
|
|
// won't be able to connect to him and trigger the channel data
|
|
// protection logic automatically.
|
|
daveArgs := lntest.NodeArgsForCommitType(commitType)
|
|
daveArgs = append(daveArgs, "--nolisten", "--wtclient.active")
|
|
dave := ht.NewNode("Dave", daveArgs)
|
|
|
|
addTowerReq := &wtclientrpc.AddTowerRequest{
|
|
Pubkey: willyInfoPk,
|
|
Address: listener,
|
|
}
|
|
dave.RPC.AddTower(addTowerReq)
|
|
|
|
// We must let Dave have an open channel before she can send a node
|
|
// announcement, so we open a channel with Carol,
|
|
ht.ConnectNodes(dave, carol)
|
|
|
|
// Before we make a channel, we'll load up Dave with some coins sent
|
|
// directly from the miner.
|
|
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
|
|
|
|
// Send one more UTXOs if this is a neutrino backend.
|
|
if ht.IsNeutrinoBackend() {
|
|
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
|
|
}
|
|
|
|
// In order to test Dave's response to an uncooperative channel
|
|
// closure by Carol, we'll first open up a channel between them with a
|
|
// 0.5 BTC value.
|
|
params := lntest.OpenChannelParams{
|
|
Amt: 3 * (chanAmt / 4),
|
|
PushAmt: chanAmt / 4,
|
|
CommitmentType: commitType,
|
|
Private: true,
|
|
}
|
|
chanPoint := ht.OpenChannel(dave, carol, params)
|
|
|
|
// With the channel open, we'll create a few invoices for Carol that
|
|
// Dave will pay to in order to advance the state of the channel.
|
|
carolPayReqs, _, _ := ht.CreatePayReqs(carol, paymentAmt, numInvoices)
|
|
|
|
// Next query for Carol's channel state, as we sent 0 payments, Carol
|
|
// should still see her balance as the push amount, which is 1/4 of the
|
|
// capacity.
|
|
carolChan := ht.AssertChannelLocalBalance(
|
|
carol, chanPoint, int64(chanAmt/4),
|
|
)
|
|
|
|
// Grab Carol's current commitment height (update number), we'll later
|
|
// revert her to this state after additional updates to force him to
|
|
// broadcast this soon to be revoked state.
|
|
carolStateNumPreCopy := int(carolChan.NumUpdates)
|
|
|
|
// With the temporary file created, copy Carol's current state into the
|
|
// temporary file we created above. Later after more updates, we'll
|
|
// restore this state.
|
|
ht.BackupDB(carol)
|
|
|
|
// Reconnect the peers after the restart that was needed for the db
|
|
// backup.
|
|
ht.EnsureConnected(dave, carol)
|
|
|
|
// Once connected, give Dave some time to enable the channel again.
|
|
ht.AssertChannelInGraph(dave, chanPoint)
|
|
|
|
// Finally, send payments from Dave to Carol, consuming Carol's
|
|
// remaining payment hashes.
|
|
ht.CompletePaymentRequestsNoWait(dave, carolPayReqs, chanPoint)
|
|
|
|
daveBalResp := dave.RPC.WalletBalance()
|
|
davePreSweepBalance := daveBalResp.ConfirmedBalance
|
|
|
|
// Wait until the backup has been accepted by the watchtower before
|
|
// shutting down Dave.
|
|
err := wait.NoError(func() error {
|
|
bkpStats := dave.RPC.WatchtowerStats()
|
|
if bkpStats == nil {
|
|
return errors.New("no active backup sessions")
|
|
}
|
|
if bkpStats.NumBackups == 0 {
|
|
return errors.New("no backups accepted")
|
|
}
|
|
|
|
return nil
|
|
}, defaultTimeout)
|
|
require.NoError(ht, err, "unable to verify backup task completed")
|
|
|
|
// Shutdown Dave to simulate going offline for an extended period of
|
|
// time. Once he's not watching, Carol will try to breach the channel.
|
|
restart := ht.SuspendNode(dave)
|
|
|
|
// Now we shutdown Carol, 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 Carol to travel back in time
|
|
// within the channel's history.
|
|
ht.RestartNodeAndRestoreDB(carol)
|
|
|
|
// Now query for Carol's channel state, it should show that he's at a
|
|
// state number in the past, not the *latest* state.
|
|
ht.AssertChannelCommitHeight(carol, chanPoint, carolStateNumPreCopy)
|
|
|
|
// Now force Carol 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 Dave's retribution.
|
|
closeUpdates, closeTxID := ht.CloseChannelAssertPending(
|
|
carol, chanPoint, true,
|
|
)
|
|
|
|
// Finally, generate a single block, wait for the final close status
|
|
// update, then ensure that the closing transaction was included in the
|
|
// block.
|
|
block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
|
|
|
breachTXID := ht.WaitForChannelCloseEvent(closeUpdates)
|
|
ht.AssertTxInBlock(block, breachTXID)
|
|
|
|
// The breachTXID should match the above closeTxID.
|
|
require.EqualValues(ht, breachTXID, closeTxID)
|
|
|
|
// Query the mempool for Dave's justice transaction, this should be
|
|
// broadcast as Carol's contract breaching transaction gets confirmed
|
|
// above.
|
|
justiceTXID := ht.AssertNumTxsInMempool(1)[0]
|
|
|
|
// Query for the mempool transaction found above. Then assert that all
|
|
// the inputs of this transaction are spending outputs generated by
|
|
// Carol's breach transaction above.
|
|
justiceTx := ht.GetRawTransaction(justiceTXID)
|
|
for _, txIn := range justiceTx.MsgTx().TxIn {
|
|
require.Equal(ht, breachTXID[:], txIn.PreviousOutPoint.Hash[:],
|
|
"justice tx not spending commitment utxo")
|
|
}
|
|
|
|
willyBalResp := willy.WalletBalance()
|
|
require.Zero(ht, willyBalResp.ConfirmedBalance,
|
|
"willy should have 0 balance before mining justice transaction")
|
|
|
|
// Now mine a block, this transaction should include Dave's justice
|
|
// transaction which was just accepted into the mempool.
|
|
block = ht.MineBlocksAndAssertNumTxes(1, 1)[0]
|
|
|
|
// The block should have exactly *two* transactions, one of which is
|
|
// the justice transaction.
|
|
require.Len(ht, block.Transactions, 2, "transaction wasn't mined")
|
|
justiceSha := block.Transactions[1].TxHash()
|
|
require.Equal(ht, justiceTx.Hash()[:], justiceSha[:],
|
|
"justice tx wasn't mined")
|
|
|
|
// Ensure that Willy doesn't get any funds, as he is acting as an
|
|
// altruist watchtower.
|
|
err = wait.NoError(func() error {
|
|
willyBalResp := willy.WalletBalance()
|
|
|
|
if willyBalResp.ConfirmedBalance != 0 {
|
|
return fmt.Errorf("expected Willy to have no funds "+
|
|
"after justice transaction was mined, found %v",
|
|
willyBalResp)
|
|
}
|
|
|
|
return nil
|
|
}, time.Second*5)
|
|
require.NoError(ht, err, "timeout checking willy's balance")
|
|
|
|
// Before restarting Dave, shutdown Carol so Dave won't sync with her.
|
|
// Otherwise, during the restart, Dave will realize Carol is falling
|
|
// behind and return `ErrCommitSyncRemoteDataLoss`, thus force closing
|
|
// the channel. Although this force close tx will be later replaced by
|
|
// the breach tx, it will create two anchor sweeping txes for neutrino
|
|
// backend, causing the confirmed wallet balance to be zero later on
|
|
// because the utxos are used in sweeping.
|
|
ht.Shutdown(carol)
|
|
|
|
// Restart Dave, who will still think his channel with Carol is open.
|
|
// We should him to detect the breach, but realize that the funds have
|
|
// then been swept to his wallet by Willy.
|
|
require.NoError(ht, restart(), "unable to restart dave")
|
|
|
|
err = wait.NoError(func() error {
|
|
daveBalResp := dave.RPC.ChannelBalance()
|
|
if daveBalResp.LocalBalance.Sat != 0 {
|
|
return fmt.Errorf("Dave should end up with zero "+
|
|
"channel balance, instead has %d",
|
|
daveBalResp.LocalBalance.Sat)
|
|
}
|
|
|
|
return nil
|
|
}, defaultTimeout)
|
|
require.NoError(ht, err, "timeout checking dave's channel balance")
|
|
|
|
ht.AssertNumPendingForceClose(dave, 0)
|
|
|
|
// If this is an anchor channel, Dave would offer his sweeper the
|
|
// anchor. However, due to no time-sensitive outputs involved, the
|
|
// anchor sweeping won't happen as it's uneconomical.
|
|
if lntest.CommitTypeHasAnchors(commitType) {
|
|
ht.AssertNumPendingSweeps(dave, 1)
|
|
|
|
// Mine a block to trigger the sweep.
|
|
ht.MineEmptyBlocks(1)
|
|
}
|
|
|
|
// Check that Dave's wallet balance is increased.
|
|
err = wait.NoError(func() error {
|
|
daveBalResp := dave.RPC.WalletBalance()
|
|
|
|
if daveBalResp.ConfirmedBalance <= davePreSweepBalance {
|
|
return fmt.Errorf("Dave should have more than %d "+
|
|
"after sweep, instead has %d",
|
|
davePreSweepBalance,
|
|
daveBalResp.ConfirmedBalance)
|
|
}
|
|
|
|
return nil
|
|
}, defaultTimeout)
|
|
require.NoError(ht, err, "timeout checking dave's wallet balance")
|
|
|
|
// Dave should have no open channels.
|
|
ht.AssertNodeNumChannels(dave, 0)
|
|
}
|
|
|
|
func setUpNewTower(ht *lntest.HarnessTest, name, externalIP string) ([]byte,
|
|
string, *rpc.HarnessRPC) {
|
|
|
|
port := port.NextAvailablePort()
|
|
|
|
listenAddr := fmt.Sprintf("0.0.0.0:%d", port)
|
|
|
|
// Set up the new watchtower.
|
|
tower := ht.NewNode(name, []string{
|
|
"--watchtower.active",
|
|
"--watchtower.externalip=" + externalIP,
|
|
"--watchtower.listen=" + listenAddr,
|
|
})
|
|
|
|
towerInfo := tower.RPC.GetInfoWatchtower()
|
|
|
|
require.Len(ht, towerInfo.Listeners, 1)
|
|
listener := towerInfo.Listeners[0]
|
|
require.True(
|
|
ht, listener == listenAddr ||
|
|
listener == fmt.Sprintf("[::]:%d", port),
|
|
)
|
|
|
|
// Assert the Tower's URIs properly display the chosen external IP.
|
|
require.Len(ht, towerInfo.Uris, 1)
|
|
require.Contains(ht, towerInfo.Uris[0], externalIP)
|
|
|
|
return towerInfo.Pubkey, towerInfo.Listeners[0], tower.RPC
|
|
}
|
|
|
|
// generateBackups is a helper function that can be used to create a number of
|
|
// watchtower back-ups.
|
|
func generateBackups(ht *lntest.HarnessTest, srcNode,
|
|
dstNode *node.HarnessNode, numBackups int64) {
|
|
|
|
const paymentAmt = 10_000
|
|
|
|
require.EqualValuesf(ht, numBackups%2, 0, "the number of desired "+
|
|
"back-ups must be even")
|
|
|
|
// Two updates are made for every payment.
|
|
numPayments := int(numBackups / 2)
|
|
|
|
// Create the required number of invoices.
|
|
alicePayReqs, _, _ := ht.CreatePayReqs(
|
|
dstNode, paymentAmt, numPayments,
|
|
)
|
|
|
|
send := func(node *node.HarnessNode, payReq string) {
|
|
stream := node.RPC.SendPayment(
|
|
&routerrpc.SendPaymentRequest{
|
|
PaymentRequest: payReq,
|
|
TimeoutSeconds: 60,
|
|
FeeLimitMsat: noFeeLimitMsat,
|
|
},
|
|
)
|
|
|
|
ht.AssertPaymentStatusFromStream(
|
|
stream, lnrpc.Payment_SUCCEEDED,
|
|
)
|
|
}
|
|
|
|
// Pay each invoice.
|
|
for i := 0; i < numPayments; i++ {
|
|
send(srcNode, alicePayReqs[i])
|
|
}
|
|
}
|
|
|
|
// assertNumBackups is a helper that asserts that the given node has a certain
|
|
// number of backups backed up to the tower. If mineOnFail is true, then a block
|
|
// will be mined each time the assertion fails.
|
|
func assertNumBackups(ht *lntest.HarnessTest, node *rpc.HarnessRPC,
|
|
towerPk []byte, expectedNumBackups int, mineOnFail bool) {
|
|
|
|
err := wait.NoError(func() error {
|
|
info := node.GetTowerInfo(
|
|
&wtclientrpc.GetTowerInfoRequest{
|
|
Pubkey: towerPk,
|
|
IncludeSessions: true,
|
|
},
|
|
)
|
|
|
|
var numBackups uint32
|
|
for _, sessionType := range info.SessionInfo {
|
|
for _, session := range sessionType.Sessions {
|
|
numBackups += session.NumBackups
|
|
}
|
|
}
|
|
|
|
if numBackups == uint32(expectedNumBackups) {
|
|
return nil
|
|
}
|
|
|
|
if mineOnFail {
|
|
ht.MineBlocks(1)
|
|
}
|
|
|
|
return fmt.Errorf("expected %d backups, got %d",
|
|
expectedNumBackups, numBackups)
|
|
}, defaultTimeout)
|
|
require.NoError(ht, err)
|
|
}
|