Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
vendor/
.idea
.aider*
CLAUDE.md
50 changes: 49 additions & 1 deletion replaylog.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,5 +184,53 @@ func (rl *MemoryReplayLog) PutBatch(batch *Batch) (*ReplaySet, error) {
return replays, nil
}

// A compile time asserting *MemoryReplayLog implements the RelayLog interface.
// A compile time asserting *MemoryReplayLog implements the ReplayLog interface.
Comment thread
gijswijs marked this conversation as resolved.
var _ ReplayLog = (*MemoryReplayLog)(nil)

// NoOpReplayLog is a ReplayLog implementation that performs no replay
// protection. This can be used when replay protection is handled externally
// or is not needed (e.g., for onion messaging where replay protection is
// not required).
type NoOpReplayLog struct{}

// NewNoOpReplayLog constructs a new NoOpReplayLog, which is an implementation
// of ReplayLog that performs no replay protection.
func NewNoOpReplayLog() *NoOpReplayLog {
return &NoOpReplayLog{}
}

// Start is a no-op.
Comment thread
gijswijs marked this conversation as resolved.
func (NoOpReplayLog) Start() error {
return nil
}

// Stop is a no-op.
Comment thread
gijswijs marked this conversation as resolved.
func (NoOpReplayLog) Stop() error {
return nil
}

// Get always returns ErrLogEntryNotFound since no entries are ever stored.
Comment thread
gijswijs marked this conversation as resolved.
func (NoOpReplayLog) Get(*HashPrefix) (uint32, error) {
return 0, ErrLogEntryNotFound
}

// Put is a no-op and always returns nil, allowing all packets through.
Comment thread
gijswijs marked this conversation as resolved.
func (NoOpReplayLog) Put(*HashPrefix, uint32) error {
return nil
}

// Delete is a no-op.
Comment thread
gijswijs marked this conversation as resolved.
func (NoOpReplayLog) Delete(*HashPrefix) error {
return nil
}

// PutBatch marks the batch as committed and returns an empty replay set,
// indicating no replays were detected.
Comment thread
gijswijs marked this conversation as resolved.
func (NoOpReplayLog) PutBatch(batch *Batch) (*ReplaySet, error) {
batch.IsCommitted = true
return NewReplaySet(), nil
}

// A compile time assertion that *NoOpReplayLog implements the ReplayLog
// interface.
var _ ReplayLog = (*NoOpReplayLog)(nil)
80 changes: 80 additions & 0 deletions replaylog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package sphinx

import (
"testing"

"github.com/stretchr/testify/require"
)

// TestMemoryReplayLogStorageAndRetrieval tests that the non-batch methods on
Expand Down Expand Up @@ -130,3 +132,81 @@ func TestMemoryReplayLogPutBatch(t *testing.T) {
t.Fatalf("Unexpected replay set after adding batch 2 to log: %v", err)
}
}

// TestNoOpReplayLog tests that NoOpReplayLog performs no replay protection,
// allowing all packets through without storing any state.
func TestNoOpReplayLog(t *testing.T) {
t.Parallel()

rl := NewNoOpReplayLog()

// Start and Stop should succeed without error.
require.NoError(t, rl.Start())
defer func() {
require.NoError(t, rl.Stop())
}()

var hashPrefix HashPrefix

hashPrefix[0] = 1

// Get should always return ErrLogEntryNotFound since nothing is stored.
_, err := rl.Get(&hashPrefix)
require.ErrorIs(t, err, ErrLogEntryNotFound)

// Put should always succeed.
require.NoError(t, rl.Put(&hashPrefix, 1))

// Put the same packet again - should still succeed (no replay
// detection).
require.NoError(t, rl.Put(&hashPrefix, 1))

// Get should still return ErrLogEntryNotFound (nothing is stored).
_, err = rl.Get(&hashPrefix)
require.ErrorIs(t, err, ErrLogEntryNotFound)

// Delete should succeed.
require.NoError(t, rl.Delete(&hashPrefix))
}

// TestNoOpReplayLogPutBatch tests that NoOpReplayLog's PutBatch marks batches
// as committed and never reports replays.
func TestNoOpReplayLogPutBatch(t *testing.T) {
t.Parallel()

rl := NewNoOpReplayLog()

var hashPrefix1, hashPrefix2 HashPrefix

hashPrefix1[0] = 1
hashPrefix2[0] = 2

// Create a batch with duplicate packets.
batch1 := NewBatch([]byte{1})
require.NoError(t, batch1.Put(1, &hashPrefix1, 1))
require.NoError(t, batch1.Put(2, &hashPrefix1, 1))

replays, err := rl.PutBatch(batch1)
require.NoError(t, err)
require.True(t, batch1.IsCommitted, "Batch should be marked as "+
"committed")

// NoOpReplayLog doesn't detect intra-batch replays (that's done by
// Batch itself), but it should return an empty set from its own
// detection.
require.NotNil(t, replays)

// Create another batch with the same hash prefix - should not detect
// replay since NoOpReplayLog doesn't store anything.
batch2 := NewBatch([]byte{2})
require.NoError(t, batch2.Put(1, &hashPrefix1, 1))
require.NoError(t, batch2.Put(2, &hashPrefix2, 2))

replays, err = rl.PutBatch(batch2)
require.NoError(t, err)
require.True(t, batch2.IsCommitted, "Batch should be marked as "+
"committed")

// Should report no replays since NoOpReplayLog doesn't track state.
require.Equal(t, 0, replays.Size(), "Expected empty replay set")
}
25 changes: 19 additions & 6 deletions sphinx.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,11 +606,17 @@ func WithTLVPayloadOnly() ProcessOnionOpt {
// in the onion packet to derive the shared secret. Finally, if the MAC doesn't
// check the packet is again rejected.
//
// In the case of a successful packet processing, and ProcessedPacket struct is
// The replayData parameter is passed to the ReplayLog and can be used by
// implementations to store auxiliary data alongside the packet hash. For
// example, in HTLC forwarding this could be the incoming CLTV value. When using
// NoOpReplayLog (no replay protection), this value is ignored and can be set
// to 0.
//
// In the case of a successful packet processing, a ProcessedPacket struct is
// returned which houses the newly parsed packet, along with instructions on
// what to do next.
func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte,
incomingCltv uint32, opts ...ProcessOnionOpt) (*ProcessedPacket,
replayData uint32, opts ...ProcessOnionOpt) (*ProcessedPacket,
error) {

cfg := &processOnionCfg{}
Expand Down Expand Up @@ -642,7 +648,8 @@ func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte,

// Atomically compare this hash prefix with the contents of the on-disk
// log, persisting it only if this entry was not detected as a replay.
if err := r.log.Put(hashPrefix, incomingCltv); err != nil {
err = r.log.Put(hashPrefix, replayData)
if err != nil {
return nil, err
}

Expand Down Expand Up @@ -853,11 +860,17 @@ func (r *Router) BeginTxn(id []byte, nels int) *Tx {
// in the onion packet to derive the shared secret. Finally, if the MAC doesn't
// check the packet is again rejected.
//
// In the case of a successful packet processing, and ProcessedPacket struct is
// The replayData parameter is passed to the ReplayLog and can be used by
// implementations to store auxiliary data alongside the packet hash. For
// example, in HTLC forwarding this could be the incoming CLTV value. When using
// NoOpReplayLog (no replay protection), this value is ignored and can be set
// to 0.
//
// In the case of a successful packet processing, a ProcessedPacket struct is
// returned which houses the newly parsed packet, along with instructions on
// what to do next.
func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket,
assocData []byte, incomingCltv uint32, opts ...ProcessOnionOpt) error {
assocData []byte, replayData uint32, opts ...ProcessOnionOpt) error {

@ziggie1984 ziggie1984 Feb 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call it replayValue because it is still just a uint32 not a real "data" type ?


cfg := &processOnionCfg{}
for _, o := range opts {
Expand Down Expand Up @@ -891,7 +904,7 @@ func (t *Tx) ProcessOnionPacket(seqNum uint16, onionPkt *OnionPacket,

// Add the hash prefix to pending batch of shared secrets that will be
// written later via Commit().
err = t.batch.Put(seqNum, hashPrefix, incomingCltv)
err = t.batch.Put(seqNum, hashPrefix, replayData)
if err != nil {
return err
}
Expand Down