Merge pull request #9929 from yyforyongyu/fix-decalylog

htlcswitch: remove the bucket `batchReplayBucket`
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2025-06-20 14:58:45 -07:00
commit 1756f40b36
5 files changed
+72 -82

No files matched your search

@@ -51,6 +51,11 @@
## Performance Improvements ## Performance Improvements
- The replay protection is
[optimized](https://github.com/lightningnetwork/lnd/pull/9929) to use less disk
space such that the `sphinxreplay.db` or the `decayedlogdb_kv` table will grow
much more slowly.
## Deprecations ## Deprecations
# Technical and Architectural Updates # Technical and Architectural Updates
+3 -46
View File
@@ -1,7 +1,6 @@
package htlcswitch package htlcswitch
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
@@ -24,11 +23,6 @@ var (
// bytes of a received HTLC's hashed shared secret as the key and the HTLC's // bytes of a received HTLC's hashed shared secret as the key and the HTLC's
// CLTV expiry as the value. // CLTV expiry as the value.
sharedHashBucket = []byte("shared-hash") sharedHashBucket = []byte("shared-hash")
// batchReplayBucket is a bucket that maps batch identifiers to
// serialized ReplaySets. This is used to give idempotency in the event
// that a batch is processed more than once.
batchReplayBucket = []byte("batch-replay")
) )
var ( var (
@@ -138,11 +132,6 @@ func (d *DecayedLog) initBuckets() error {
return ErrDecayedLogInit return ErrDecayedLogInit
} }
_, err = tx.CreateTopLevelBucket(batchReplayBucket)
if err != nil {
return ErrDecayedLogInit
}
return nil return nil
}, func() {}) }, func() {})
} }
@@ -329,11 +318,8 @@ func (d *DecayedLog) Put(hash *sphinx.HashPrefix, cltv uint32) error {
// PutBatch accepts a pending batch of hashed secret entries to write to disk. // PutBatch accepts a pending batch of hashed secret entries to write to disk.
// Each hashed secret is inserted with a corresponding time value, dictating // Each hashed secret is inserted with a corresponding time value, dictating
// when the entry will be evicted from the log. // when the entry will be evicted from the log.
// NOTE: This method enforces idempotency by writing the replay set obtained //
// from the first attempt for a particular batch ID, and decoding the return // TODO(yy): remove this method and use `Put` instead.
// value to subsequent calls. For the indices of the replay set to be aligned
// properly, the batch MUST be constructed identically to the first attempt,
// pruning will cause the indices to become invalid.
func (d *DecayedLog) PutBatch(b *sphinx.Batch) (*sphinx.ReplaySet, error) { func (d *DecayedLog) PutBatch(b *sphinx.Batch) (*sphinx.ReplaySet, error) {
// Since batched boltdb txns may be executed multiple times before // Since batched boltdb txns may be executed multiple times before
// succeeding, we will create a new replay set for each invocation to // succeeding, we will create a new replay set for each invocation to
@@ -348,25 +334,6 @@ func (d *DecayedLog) PutBatch(b *sphinx.Batch) (*sphinx.ReplaySet, error) {
return ErrDecayedLogCorrupted return ErrDecayedLogCorrupted
} }
// Load the batch replay bucket, which will be used to either
// retrieve the result of previously processing this batch, or
// to write the result of this operation.
batchReplayBkt := tx.ReadWriteBucket(batchReplayBucket)
if batchReplayBkt == nil {
return ErrDecayedLogCorrupted
}
// Check for the existence of this batch's id in the replay
// bucket. If a non-nil value is found, this indicates that we
// have already processed this batch before. We deserialize the
// resulting and return it to ensure calls to put batch are
// idempotent.
replayBytes := batchReplayBkt.Get(b.ID)
if replayBytes != nil {
replays = sphinx.NewReplaySet()
return replays.Decode(bytes.NewReader(replayBytes))
}
// The CLTV will be stored into scratch and then stored into the // The CLTV will be stored into scratch and then stored into the
// sharedHashBucket. // sharedHashBucket.
var scratch [4]byte var scratch [4]byte
@@ -394,17 +361,7 @@ func (d *DecayedLog) PutBatch(b *sphinx.Batch) (*sphinx.ReplaySet, error) {
// batch's construction. // batch's construction.
replays.Merge(b.ReplaySet) replays.Merge(b.ReplaySet)
// Write the replay set under the batch identifier to the batch return nil
// replays bucket. This can be used during recovery to test (1)
// that a particular batch was successfully processed and (2)
// recover the indexes of the adds that were rejected as
// replays.
var replayBuf bytes.Buffer
if err := replays.Encode(&replayBuf); err != nil {
return err
}
return batchReplayBkt.Put(b.ID, replayBuf.Bytes())
}); err != nil { }); err != nil {
return nil, err return nil, err
} }
+10 -6
View File
@@ -745,7 +745,8 @@ func (r *DecodeHopIteratorResponse) Result() (Iterator, lnwire.FailCode) {
// the presented readers and rhashes *NEVER* deviate across invocations for the // the presented readers and rhashes *NEVER* deviate across invocations for the
// same id. // same id.
func (p *OnionProcessor) DecodeHopIterators(id []byte, func (p *OnionProcessor) DecodeHopIterators(id []byte,
reqs []DecodeHopIteratorRequest) ([]DecodeHopIteratorResponse, error) { reqs []DecodeHopIteratorRequest,
reforward bool) ([]DecodeHopIteratorResponse, error) {
var ( var (
batchSize = len(reqs) batchSize = len(reqs)
@@ -783,6 +784,8 @@ func (p *OnionProcessor) DecodeHopIterators(id []byte,
b.Val, b.Val,
)) ))
}) })
// TODO(yy): use `p.router.ProcessOnionPacket` instead.
err = tx.ProcessOnionPacket( err = tx.ProcessOnionPacket(
seqNum, onionPkt, req.RHash, req.IncomingCltv, opts..., seqNum, onionPkt, req.RHash, req.IncomingCltv, opts...,
) )
@@ -864,11 +867,12 @@ func (p *OnionProcessor) DecodeHopIterators(id []byte,
continue continue
} }
// If this index is contained in the replay set, mark it with a // If this index is contained in the replay set, and it is not a
// temporary channel failure error code. We infer that the // reforwarding on startup, mark it with a permanent channel
// offending error was due to a replayed packet because this // failure error code. We infer that the offending error was due
// index was found in the replay set. // to a replayed packet because this index was found in the
if replays.Contains(uint16(i)) { // replay set.
if !reforward && replays.Contains(uint16(i)) {
log.Errorf("unable to process onion packet: %v", log.Errorf("unable to process onion packet: %v",
sphinx.ErrReplayedPacket) sphinx.ErrReplayedPacket)
+53 -29
View File
@@ -108,8 +108,10 @@ type ChannelLinkConfig struct {
// blobs, which are then used to inform how to forward an HTLC. // blobs, which are then used to inform how to forward an HTLC.
// //
// NOTE: This function assumes the same set of readers and preimages // NOTE: This function assumes the same set of readers and preimages
// are always presented for the same identifier. // are always presented for the same identifier. The last boolean is
DecodeHopIterators func([]byte, []hop.DecodeHopIteratorRequest) ( // used to decide whether this is a reforwarding or not - when it's
// reforwarding, we skip the replay check enforced in our decay log.
DecodeHopIterators func([]byte, []hop.DecodeHopIteratorRequest, bool) (
[]hop.DecodeHopIteratorResponse, error) []hop.DecodeHopIteratorResponse, error)
// ExtractErrorEncrypter function is responsible for decoding HTLC // ExtractErrorEncrypter function is responsible for decoding HTLC
@@ -3630,6 +3632,13 @@ func (l *channelLink) processRemoteSettleFails(fwdPkg *channeldb.FwdPkg) {
return return
} }
// Exit early if the fwdPkg is already processed.
if fwdPkg.State == channeldb.FwdStateCompleted {
l.log.Debugf("skipped processing completed fwdPkg %v", fwdPkg)
return
}
l.log.Debugf("settle-fail-filter: %v", fwdPkg.SettleFailFilter) l.log.Debugf("settle-fail-filter: %v", fwdPkg.SettleFailFilter)
var switchPackets []*htlcPacket var switchPackets []*htlcPacket
@@ -3738,13 +3747,36 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) {
return return
} }
// Exit early if the fwdPkg is already processed.
if fwdPkg.State == channeldb.FwdStateCompleted {
l.log.Debugf("skipped processing completed fwdPkg %v", fwdPkg)
return
}
l.log.Tracef("processing %d remote adds for height %d", l.log.Tracef("processing %d remote adds for height %d",
len(fwdPkg.Adds), fwdPkg.Height) len(fwdPkg.Adds), fwdPkg.Height)
decodeReqs := make( // decodeReqs is a list of requests sent to the onion decoder. We expect
[]hop.DecodeHopIteratorRequest, 0, len(fwdPkg.Adds), // the same length of responses to be returned.
) decodeReqs := make([]hop.DecodeHopIteratorRequest, 0, len(fwdPkg.Adds))
for _, update := range fwdPkg.Adds {
// unackedAdds is a list of ADDs that's waiting for the remote's
// settle/fail update.
unackedAdds := make([]*lnwire.UpdateAddHTLC, 0, len(fwdPkg.Adds))
for i, update := range fwdPkg.Adds {
// If this index is already found in the ack filter, the
// response to this forwarding decision has already been
// committed by one of our commitment txns. ADDs in this state
// are waiting for the rest of the fwding package to get acked
// before being garbage collected.
if fwdPkg.State == channeldb.FwdStateProcessed &&
fwdPkg.AckFilter.Contains(uint16(i)) {
continue
}
if msg, ok := update.UpdateMsg.(*lnwire.UpdateAddHTLC); ok { if msg, ok := update.UpdateMsg.(*lnwire.UpdateAddHTLC); ok {
// Before adding the new htlc to the state machine, // Before adding the new htlc to the state machine,
// parse the onion object in order to obtain the // parse the onion object in order to obtain the
@@ -3761,15 +3793,20 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) {
} }
decodeReqs = append(decodeReqs, req) decodeReqs = append(decodeReqs, req)
unackedAdds = append(unackedAdds, msg)
} }
} }
// If the fwdPkg has already been processed, it means we are
// reforwarding the packets again, which happens only on a restart.
reforward := fwdPkg.State == channeldb.FwdStateProcessed
// Atomically decode the incoming htlcs, simultaneously checking for // Atomically decode the incoming htlcs, simultaneously checking for
// replay attempts. A particular index in the returned, spare list of // replay attempts. A particular index in the returned, spare list of
// channel iterators should only be used if the failure code at the // channel iterators should only be used if the failure code at the
// same index is lnwire.FailCodeNone. // same index is lnwire.FailCodeNone.
decodeResps, sphinxErr := l.cfg.DecodeHopIterators( decodeResps, sphinxErr := l.cfg.DecodeHopIterators(
fwdPkg.ID(), decodeReqs, fwdPkg.ID(), decodeReqs, reforward,
) )
if sphinxErr != nil { if sphinxErr != nil {
l.failf(LinkFailureError{code: ErrInternalError}, l.failf(LinkFailureError{code: ErrInternalError},
@@ -3779,23 +3816,10 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) {
var switchPackets []*htlcPacket var switchPackets []*htlcPacket
for i, update := range fwdPkg.Adds { for i, update := range unackedAdds {
idx := uint16(i) idx := uint16(i)
//nolint:forcetypeassert
add := *update.UpdateMsg.(*lnwire.UpdateAddHTLC)
sourceRef := fwdPkg.SourceRef(idx) sourceRef := fwdPkg.SourceRef(idx)
add := *update
if fwdPkg.State == channeldb.FwdStateProcessed &&
fwdPkg.AckFilter.Contains(idx) {
// If this index is already found in the ack filter,
// the response to this forwarding decision has already
// been committed by one of our commitment txns. ADDs
// in this state are waiting for the rest of the fwding
// package to get acked before being garbage collected.
continue
}
// An incoming HTLC add has been full-locked in. As a result we // An incoming HTLC add has been full-locked in. As a result we
// can now examine the forwarding details of the HTLC, and the // can now examine the forwarding details of the HTLC, and the
@@ -3815,8 +3839,10 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) {
add.ID, failureCode, add.OnionBlob, &sourceRef, add.ID, failureCode, add.OnionBlob, &sourceRef,
) )
l.log.Errorf("unable to decode onion hop "+ l.log.Errorf("unable to decode onion hop iterator "+
"iterator: %v", failureCode) "for htlc(id=%v, hash=%x): %v", add.ID,
add.PaymentHash, failureCode)
continue continue
} }
@@ -4120,17 +4146,15 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg) {
return return
} }
replay := fwdPkg.State != channeldb.FwdStateLockedIn l.log.Debugf("forwarding %d packets to switch: reforward=%v",
len(switchPackets), reforward)
l.log.Debugf("forwarding %d packets to switch: replay=%v",
len(switchPackets), replay)
// NOTE: This call is made synchronous so that we ensure all circuits // NOTE: This call is made synchronous so that we ensure all circuits
// are committed in the exact order that they are processed in the link. // are committed in the exact order that they are processed in the link.
// Failing to do this could cause reorderings/gaps in the range of // Failing to do this could cause reorderings/gaps in the range of
// opened circuits, which violates assumptions made by the circuit // opened circuits, which violates assumptions made by the circuit
// trimming. // trimming.
l.forwardBatch(replay, switchPackets...) l.forwardBatch(reforward, switchPackets...)
} }
// experimentalEndorsement returns the value to set for our outgoing // experimentalEndorsement returns the value to set for our outgoing
+1 -1
View File
@@ -522,7 +522,7 @@ func (p *mockIteratorDecoder) DecodeHopIterator(r io.Reader, rHash []byte,
} }
func (p *mockIteratorDecoder) DecodeHopIterators(id []byte, func (p *mockIteratorDecoder) DecodeHopIterators(id []byte,
reqs []hop.DecodeHopIteratorRequest) ( reqs []hop.DecodeHopIteratorRequest, _ bool) (
[]hop.DecodeHopIteratorResponse, error) { []hop.DecodeHopIteratorResponse, error) {
idHash := sha256.Sum256(id) idHash := sha256.Sum256(id)