mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-09-08 14:57:38 +02:00
watchtower: account for rogue updates
In this commit, we introduce the concept of a rogue update. An update is rogue if we need to ACK it but we have already deleted all the data for the associated channel due to the channel being closed. In this case, we now no longer error out and instead keep count of how many rogue updates a session has backed-up.
This commit is contained in:
@@ -50,6 +50,7 @@ var (
|
||||
// => cSessionDBID -> db-assigned-id
|
||||
// => cSessionCommits => seqnum -> encoded CommittedUpdate
|
||||
// => cSessionAckRangeIndex => db-chan-id => start -> end
|
||||
// => cSessionRogueUpdateCount -> count
|
||||
cSessionBkt = []byte("client-session-bucket")
|
||||
|
||||
// cSessionDBID is a key used in the cSessionBkt to store the
|
||||
@@ -68,6 +69,12 @@ var (
|
||||
// chan-id => start -> end
|
||||
cSessionAckRangeIndex = []byte("client-session-ack-range-index")
|
||||
|
||||
// cSessionRogueUpdateCount is a key in the cSessionBkt bucket storing
|
||||
// the number of rogue updates that were backed up using the session.
|
||||
// Rogue updates are updates for channels that have been closed already
|
||||
// at the time of the back-up.
|
||||
cSessionRogueUpdateCount = []byte("client-session-rogue-update-count")
|
||||
|
||||
// cChanIDIndexBkt is a top-level bucket storing:
|
||||
// db-assigned-id -> channel-ID
|
||||
cChanIDIndexBkt = []byte("client-channel-id-index")
|
||||
@@ -1242,10 +1249,23 @@ func (c *ClientDB) NumAckedUpdates(id *SessionID) (uint64, error) {
|
||||
}
|
||||
|
||||
sessionBkt := sessions.NestedReadBucket(id[:])
|
||||
if sessionsBkt == nil {
|
||||
if sessionBkt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// First, account for any rogue updates.
|
||||
rogueCountBytes := sessionBkt.Get(cSessionRogueUpdateCount)
|
||||
if len(rogueCountBytes) != 0 {
|
||||
rogueCount, err := readBigSize(rogueCountBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
numAcked += rogueCount
|
||||
}
|
||||
|
||||
// Then, check if the session-ack-ranges contains any entries
|
||||
// to account for.
|
||||
sessionAckRanges := sessionBkt.NestedReadBucket(
|
||||
cSessionAckRangeIndex,
|
||||
)
|
||||
@@ -1525,14 +1545,37 @@ func (c *ClientDB) DeleteSession(id SessionID) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the acked updates range index for the session. This is
|
||||
// used to get the list of channels that the session has updates
|
||||
// for.
|
||||
ackRanges := sessionBkt.NestedReadBucket(cSessionAckRangeIndex)
|
||||
|
||||
// There is a small chance that the session only contains rogue
|
||||
// updates. In that case, there will be no ack-ranges index but
|
||||
// the rogue update count will be equal the MaxUpdates.
|
||||
rogueCountBytes := sessionBkt.Get(cSessionRogueUpdateCount)
|
||||
if len(rogueCountBytes) != 0 {
|
||||
rogueCount, err := readBigSize(rogueCountBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maxUpdates := sess.ClientSessionBody.Policy.MaxUpdates
|
||||
if rogueCount == uint64(maxUpdates) {
|
||||
// Do a sanity check to ensure that the acked
|
||||
// ranges bucket does not exist in this case.
|
||||
if ackRanges != nil {
|
||||
return fmt.Errorf("acked updates "+
|
||||
"exist for session with a "+
|
||||
"max-updates(%d) rogue count",
|
||||
rogueCount)
|
||||
}
|
||||
|
||||
return sessionsBkt.DeleteNestedBucket(id[:])
|
||||
}
|
||||
}
|
||||
|
||||
// A session would only be considered closable if it was
|
||||
// exhausted. Meaning that it should not be the case that it has
|
||||
// no acked-updates.
|
||||
if ackRanges == nil {
|
||||
// A session would only be considered closable if it
|
||||
// was exhausted. Meaning that it should not be the
|
||||
// case that it has no acked-updates.
|
||||
return fmt.Errorf("cannot delete session %s since it "+
|
||||
"is not yet exhausted", id)
|
||||
}
|
||||
@@ -1763,6 +1806,22 @@ func isSessionClosable(sessionsBkt, chanDetailsBkt, chanIDIndexBkt kvdb.RBucket,
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Either the acked-update bucket should exist _or_ the rogue update
|
||||
// count must be equal to the session's MaxUpdates value, otherwise
|
||||
// something is wrong because the above check ensures that the session
|
||||
// has been exhausted.
|
||||
rogueCountBytes := sessBkt.Get(cSessionRogueUpdateCount)
|
||||
if len(rogueCountBytes) != 0 {
|
||||
rogueCount, err := readBigSize(rogueCountBytes)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if rogueCount == uint64(session.Policy.MaxUpdates) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If the session has no acked-updates, then something is wrong since
|
||||
// the above check ensures that this session has been exhausted meaning
|
||||
// that it should have MaxUpdates acked updates.
|
||||
@@ -2005,12 +2064,83 @@ func (c *ClientDB) AckUpdate(id *SessionID, seqNum uint16,
|
||||
return err
|
||||
}
|
||||
|
||||
dbSessionID, dbSessIDBytes, err := getDBSessionID(sessions, *id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chanID := committedUpdate.BackupID.ChanID
|
||||
height := committedUpdate.BackupID.CommitHeight
|
||||
|
||||
// Get the DB representation of the channel-ID.
|
||||
// Get the DB representation of the channel-ID. There is a
|
||||
// chance that the channel corresponding to this update has been
|
||||
// closed and that the details for this channel no longer exist
|
||||
// in the tower client DB. In that case, we consider this a
|
||||
// rogue update and all we do is make sure to keep track of the
|
||||
// number of rogue updates for this session.
|
||||
_, dbChanIDBytes, err := getDBChanID(chanDetailsBkt, chanID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrChannelNotRegistered) {
|
||||
var (
|
||||
count uint64
|
||||
err error
|
||||
)
|
||||
|
||||
rogueCountBytes := sessionBkt.Get(
|
||||
cSessionRogueUpdateCount,
|
||||
)
|
||||
if len(rogueCountBytes) != 0 {
|
||||
count, err = readBigSize(rogueCountBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
rogueCount := count + 1
|
||||
countBytes, err := writeBigSize(rogueCount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = sessionBkt.Put(
|
||||
cSessionRogueUpdateCount, countBytes,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// In the rare chance that this session only has rogue
|
||||
// updates, we check here if the count is equal to the
|
||||
// MaxUpdate of the session. If it is, then we mark the
|
||||
// session as closable.
|
||||
if rogueCount != uint64(session.Policy.MaxUpdates) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Before we mark the session as closable, we do a
|
||||
// sanity check to ensure that this session has no
|
||||
// acked-update index.
|
||||
sessionAckRanges := sessionBkt.NestedReadBucket(
|
||||
cSessionAckRangeIndex,
|
||||
)
|
||||
if sessionAckRanges != nil {
|
||||
return fmt.Errorf("session(%s) has an "+
|
||||
"acked ranges index but has a rogue "+
|
||||
"count indicating saturation",
|
||||
session.ID)
|
||||
}
|
||||
|
||||
closableSessBkt := tx.ReadWriteBucket(
|
||||
cClosableSessionsBkt,
|
||||
)
|
||||
if closableSessBkt == nil {
|
||||
return ErrUninitializedDB
|
||||
}
|
||||
|
||||
var height [4]byte
|
||||
byteOrder.PutUint32(height[:], 0)
|
||||
|
||||
return closableSessBkt.Put(dbSessIDBytes, height[:])
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2024,11 +2154,6 @@ func (c *ClientDB) AckUpdate(id *SessionID, seqNum uint16,
|
||||
return err
|
||||
}
|
||||
|
||||
dbSessionID, _, err := getDBSessionID(sessions, *id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chanDetails := chanDetailsBkt.NestedReadWriteBucket(
|
||||
committedUpdate.BackupID.ChanID[:],
|
||||
)
|
||||
|
Reference in New Issue
Block a user