diff --git a/watchtower/wtclient/interface.go b/watchtower/wtclient/interface.go index 2ae0c108a..df5ee8257 100644 --- a/watchtower/wtclient/interface.go +++ b/watchtower/wtclient/interface.go @@ -140,6 +140,11 @@ type DB interface { // DeleteCommittedUpdates deletes all the committed updates belonging to // the given session from the db. DeleteCommittedUpdates(id *wtdb.SessionID) error + + // DeactivateTower sets the given tower's status to inactive. This means + // that this tower's sessions won't be loaded and used for backups. + // CreateTower can be used to reactivate the tower again. + DeactivateTower(pubKey *btcec.PublicKey) error } // AuthDialer connects to a remote node using an authenticated transport, such diff --git a/watchtower/wtdb/client_db.go b/watchtower/wtdb/client_db.go index e681e6c75..5ed986ff6 100644 --- a/watchtower/wtdb/client_db.go +++ b/watchtower/wtdb/client_db.go @@ -387,6 +387,9 @@ func (c *ClientDB) CreateTower(lnAddr *lnwire.NetAddress) (*Tower, error) { return err } + // Set its status to active. + tower.Status = TowerStatusActive + // Add the new address to the existing tower. If the // address is a duplicate, this will result in no // change. @@ -503,14 +506,14 @@ func (c *ClientDB) RemoveTower(pubKey *btcec.PublicKey, addr net.Addr) error { return nil } + tower, err := getTower(towers, towerIDBytes) + if err != nil { + return err + } + // If an address is provided, then we should _only_ remove the // address record from the database. if addr != nil { - tower, err := getTower(towers, towerIDBytes) - if err != nil { - return err - } - // Towers should always have at least one address saved. tower.RemoveAddress(addr) if len(tower.Addresses) == 0 { @@ -560,6 +563,13 @@ func (c *ClientDB) RemoveTower(pubKey *btcec.PublicKey, addr net.Addr) error { ) } + // Otherwise, we mark the tower as inactive. + tower.Status = TowerStatusInactive + err = putTower(towers, tower) + if err != nil { + return err + } + // We'll mark its sessions as inactive as long as they don't // have any pending updates to ensure we don't load them upon // restarts. @@ -579,6 +589,57 @@ func (c *ClientDB) RemoveTower(pubKey *btcec.PublicKey, addr net.Addr) error { }, func() {}) } +// DeactivateTower sets the given tower's status to inactive. This means that +// this tower's sessions won't be loaded and used for backups. CreateTower can +// be used to reactivate the tower again. +func (c *ClientDB) DeactivateTower(pubKey *btcec.PublicKey) error { + return kvdb.Update(c.db, func(tx kvdb.RwTx) error { + towers := tx.ReadWriteBucket(cTowerBkt) + if towers == nil { + return ErrUninitializedDB + } + + towerIndex := tx.ReadWriteBucket(cTowerIndexBkt) + if towerIndex == nil { + return ErrUninitializedDB + } + + towersToSessionsIndex := tx.ReadWriteBucket( + cTowerToSessionIndexBkt, + ) + if towersToSessionsIndex == nil { + return ErrUninitializedDB + } + + chanIDIndexBkt := tx.ReadBucket(cChanIDIndexBkt) + if chanIDIndexBkt == nil { + return ErrUninitializedDB + } + + pubKeyBytes := pubKey.SerializeCompressed() + towerIDBytes := towerIndex.Get(pubKeyBytes) + if towerIDBytes == nil { + return ErrTowerNotFound + } + + tower, err := getTower(towers, towerIDBytes) + if err != nil { + return err + } + + // If the tower already has the desired status, then we can exit + // here. + if tower.Status == TowerStatusInactive { + return nil + } + + // Otherwise, we update the status and re-store the tower. + tower.Status = TowerStatusInactive + + return putTower(towers, tower) + }, func() {}) +} + // LoadTowerByID retrieves a tower by its tower ID. func (c *ClientDB) LoadTowerByID(towerID TowerID) (*Tower, error) { var tower *Tower diff --git a/watchtower/wtdb/client_db_test.go b/watchtower/wtdb/client_db_test.go index 475b72837..464a6a7d9 100644 --- a/watchtower/wtdb/client_db_test.go +++ b/watchtower/wtdb/client_db_test.go @@ -89,6 +89,26 @@ func (h *clientDBHarness) createTower(lnAddr *lnwire.NetAddress, return tower } +func (h *clientDBHarness) deactivateTower(pubKey *btcec.PublicKey, + expErr error) { + + h.t.Helper() + + err := h.db.DeactivateTower(pubKey) + require.ErrorIs(h.t, err, expErr) +} + +func (h *clientDBHarness) listTowers(filterFn wtdb.TowerFilterFn, + expErr error) []*wtdb.Tower { + + h.t.Helper() + + towers, err := h.db.ListTowers(filterFn) + require.ErrorIs(h.t, err, expErr) + + return towers +} + func (h *clientDBHarness) removeTower(pubKey *btcec.PublicKey, addr net.Addr, hasSessions bool, expErr error) { @@ -547,6 +567,70 @@ func testRemoveTower(h *clientDBHarness) { }, nil) } +// testTowerStatusChange tests that the Tower status is updated accordingly +// given a variety of commands. +func testTowerStatusChange(h *clientDBHarness) { + // Create a new tower. + pk, err := randPubKey() + require.NoError(h.t, err) + + towerAddr := &lnwire.NetAddress{ + IdentityKey: pk, + Address: &net.TCPAddr{ + IP: []byte{0x01, 0x00, 0x00, 0x00}, Port: 9911, + }, + } + + tower := h.createTower(towerAddr, nil) + + // Add a new session. + session := h.randSession(h.t, tower.ID, 100) + h.insertSession(session, nil) + + // assertTowerStatus is a helper function that will assert that the + // tower's status is as expected. + assertTowerStatus := func(status wtdb.TowerStatus) { + activeFilter := func(tower *wtdb.Tower) bool { + return tower.Status == status + } + + towers := h.listTowers(activeFilter, nil) + require.Len(h.t, towers, 1) + require.EqualValues(h.t, towers[0].Status, status) + } + + // assertSessionStatus is a helper that will assert that the session's + // status is as expected + assertSessionStatus := func(status wtdb.CSessionStatus) { + sessions := h.listSessions(&tower.ID) + require.Len(h.t, sessions, 1) + for _, sess := range sessions { + require.EqualValues(h.t, sess.Status, status) + } + } + + // Initially, the tower and session should be active. + assertTowerStatus(wtdb.TowerStatusActive) + assertSessionStatus(wtdb.CSessionActive) + + // Removing the tower should change its status and its session status + // to inactive. + h.removeTower(tower.IdentityKey, nil, true, nil) + assertTowerStatus(wtdb.TowerStatusInactive) + assertSessionStatus(wtdb.CSessionInactive) + + // Re-adding the tower in some way should re-active it and its session. + h.createTower(towerAddr, nil) + assertTowerStatus(wtdb.TowerStatusActive) + assertSessionStatus(wtdb.CSessionActive) + + // Deactivating the tower should change its status but its session + // status should remain active. + h.deactivateTower(tower.IdentityKey, nil) + assertTowerStatus(wtdb.TowerStatusInactive) + assertSessionStatus(wtdb.CSessionActive) +} + // testChanSummaries tests the process of a registering a channel and its // associated sweep pkscript. func testChanSummaries(h *clientDBHarness) { @@ -1142,6 +1226,10 @@ func TestClientDB(t *testing.T) { name: "max commitment heights", run: testMaxCommitmentHeights, }, + { + name: "test tower status change", + run: testTowerStatusChange, + }, } for _, database := range dbs {