Skip to content

Commit ea10993

Browse files
authored
Merge pull request #75 from gijswijs/noop-replay-log
Add NoOpReplayLog for optional replay protection
2 parents c8214d7 + f1b6fcf commit ea10993

4 files changed

Lines changed: 149 additions & 7 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
vendor/
22
.idea
33
.aider*
4+
CLAUDE.md

replaylog.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,5 +184,53 @@ func (rl *MemoryReplayLog) PutBatch(batch *Batch) (*ReplaySet, error) {
184184
return replays, nil
185185
}
186186

187-
// A compile time asserting *MemoryReplayLog implements the RelayLog interface.
187+
// A compile time asserting *MemoryReplayLog implements the ReplayLog interface.
188188
var _ ReplayLog = (*MemoryReplayLog)(nil)
189+
190+
// NoOpReplayLog is a ReplayLog implementation that performs no replay
191+
// protection. This can be used when replay protection is handled externally
192+
// or is not needed (e.g., for onion messaging where replay protection is
193+
// not required).
194+
type NoOpReplayLog struct{}
195+
196+
// NewNoOpReplayLog constructs a new NoOpReplayLog, which is an implementation
197+
// of ReplayLog that performs no replay protection.
198+
func NewNoOpReplayLog() *NoOpReplayLog {
199+
return &NoOpReplayLog{}
200+
}
201+
202+
// Start is a no-op.
203+
func (NoOpReplayLog) Start() error {
204+
return nil
205+
}
206+
207+
// Stop is a no-op.
208+
func (NoOpReplayLog) Stop() error {
209+
return nil
210+
}
211+
212+
// Get always returns ErrLogEntryNotFound since no entries are ever stored.
213+
func (NoOpReplayLog) Get(*HashPrefix) (uint32, error) {
214+
return 0, ErrLogEntryNotFound
215+
}
216+
217+
// Put is a no-op and always returns nil, allowing all packets through.
218+
func (NoOpReplayLog) Put(*HashPrefix, uint32) error {
219+
return nil
220+
}
221+
222+
// Delete is a no-op.
223+
func (NoOpReplayLog) Delete(*HashPrefix) error {
224+
return nil
225+
}
226+
227+
// PutBatch marks the batch as committed and returns an empty replay set,
228+
// indicating no replays were detected.
229+
func (NoOpReplayLog) PutBatch(batch *Batch) (*ReplaySet, error) {
230+
batch.IsCommitted = true
231+
return NewReplaySet(), nil
232+
}
233+
234+
// A compile time assertion that *NoOpReplayLog implements the ReplayLog
235+
// interface.
236+
var _ ReplayLog = (*NoOpReplayLog)(nil)

replaylog_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package sphinx
22

33
import (
44
"testing"
5+
6+
"github.com/stretchr/testify/require"
57
)
68

79
// TestMemoryReplayLogStorageAndRetrieval tests that the non-batch methods on
@@ -130,3 +132,81 @@ func TestMemoryReplayLogPutBatch(t *testing.T) {
130132
t.Fatalf("Unexpected replay set after adding batch 2 to log: %v", err)
131133
}
132134
}
135+
136+
// TestNoOpReplayLog tests that NoOpReplayLog performs no replay protection,
137+
// allowing all packets through without storing any state.
138+
func TestNoOpReplayLog(t *testing.T) {
139+
t.Parallel()
140+
141+
rl := NewNoOpReplayLog()
142+
143+
// Start and Stop should succeed without error.
144+
require.NoError(t, rl.Start())
145+
defer func() {
146+
require.NoError(t, rl.Stop())
147+
}()
148+
149+
var hashPrefix HashPrefix
150+
151+
hashPrefix[0] = 1
152+
153+
// Get should always return ErrLogEntryNotFound since nothing is stored.
154+
_, err := rl.Get(&hashPrefix)
155+
require.ErrorIs(t, err, ErrLogEntryNotFound)
156+
157+
// Put should always succeed.
158+
require.NoError(t, rl.Put(&hashPrefix, 1))
159+
160+
// Put the same packet again - should still succeed (no replay
161+
// detection).
162+
require.NoError(t, rl.Put(&hashPrefix, 1))
163+
164+
// Get should still return ErrLogEntryNotFound (nothing is stored).
165+
_, err = rl.Get(&hashPrefix)
166+
require.ErrorIs(t, err, ErrLogEntryNotFound)
167+
168+
// Delete should succeed.
169+
require.NoError(t, rl.Delete(&hashPrefix))
170+
}
171+
172+
// TestNoOpReplayLogPutBatch tests that NoOpReplayLog's PutBatch marks batches
173+
// as committed and never reports replays.
174+
func TestNoOpReplayLogPutBatch(t *testing.T) {
175+
t.Parallel()
176+
177+
rl := NewNoOpReplayLog()
178+
179+
var hashPrefix1, hashPrefix2 HashPrefix
180+
181+
hashPrefix1[0] = 1
182+
hashPrefix2[0] = 2
183+
184+
// Create a batch with duplicate packets.
185+
batch1 := NewBatch([]byte{1})
186+
require.NoError(t, batch1.Put(1, &hashPrefix1, 1))
187+
require.NoError(t, batch1.Put(2, &hashPrefix1, 1))
188+
189+
replays, err := rl.PutBatch(batch1)
190+
require.NoError(t, err)
191+
require.True(t, batch1.IsCommitted, "Batch should be marked as "+
192+
"committed")
193+
194+
// NoOpReplayLog doesn't detect intra-batch replays (that's done by
195+
// Batch itself), but it should return an empty set from its own
196+
// detection.
197+
require.NotNil(t, replays)
198+
199+
// Create another batch with the same hash prefix - should not detect
200+
// replay since NoOpReplayLog doesn't store anything.
201+
batch2 := NewBatch([]byte{2})
202+
require.NoError(t, batch2.Put(1, &hashPrefix1, 1))
203+
require.NoError(t, batch2.Put(2, &hashPrefix2, 2))
204+
205+
replays, err = rl.PutBatch(batch2)
206+
require.NoError(t, err)
207+
require.True(t, batch2.IsCommitted, "Batch should be marked as "+
208+
"committed")
209+
210+
// Should report no replays since NoOpReplayLog doesn't track state.
211+
require.Equal(t, 0, replays.Size(), "Expected empty replay set")
212+
}

sphinx.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -606,11 +606,17 @@ func WithTLVPayloadOnly() ProcessOnionOpt {
606606
// in the onion packet to derive the shared secret. Finally, if the MAC doesn't
607607
// check the packet is again rejected.
608608
//
609-
// In the case of a successful packet processing, and ProcessedPacket struct is
609+
// The replayData parameter is passed to the ReplayLog and can be used by
610+
// implementations to store auxiliary data alongside the packet hash. For
611+
// example, in HTLC forwarding this could be the incoming CLTV value. When using
612+
// NoOpReplayLog (no replay protection), this value is ignored and can be set
613+
// to 0.
614+
//
615+
// In the case of a successful packet processing, a ProcessedPacket struct is
610616
// returned which houses the newly parsed packet, along with instructions on
611617
// what to do next.
612618
func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte,
613-
incomingCltv uint32, opts ...ProcessOnionOpt) (*ProcessedPacket,
619+
replayData uint32, opts ...ProcessOnionOpt) (*ProcessedPacket,
614620
error) {
615621

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

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

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

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

892905
// Add the hash prefix to pending batch of shared secrets that will be
893906
// written later via Commit().
894-
err = t.batch.Put(seqNum, hashPrefix, incomingCltv)
907+
err = t.batch.Put(seqNum, hashPrefix, replayData)
895908
if err != nil {
896909
return err
897910
}

0 commit comments

Comments
 (0)