Skip to content

Commit d6f65c6

Browse files
committed
fix(storer): enforce timestamp and stamp-hash precedence in resolveDivergence
1 parent 804d8ae commit d6f65c6

2 files changed

Lines changed: 116 additions & 1 deletion

File tree

pkg/storer/internal/reserve/convergence_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/ethersphere/bee/v2/pkg/cac"
1818
"github.com/ethersphere/bee/v2/pkg/crypto"
1919
"github.com/ethersphere/bee/v2/pkg/log"
20+
"github.com/ethersphere/bee/v2/pkg/postage"
2021
postagetesting "github.com/ethersphere/bee/v2/pkg/postage/testing"
2122
"github.com/ethersphere/bee/v2/pkg/soc"
2223
"github.com/ethersphere/bee/v2/pkg/storage"
@@ -384,3 +385,87 @@ func TestPutOrderConvergence(t *testing.T) {
384385
})
385386
}
386387
}
388+
389+
func TestSOCMultiStampDivergenceCornerCase(t *testing.T) {
390+
t.Parallel()
391+
392+
baseAddr := swarm.RandAddress(t)
393+
ts := internal.NewInmemStorage()
394+
r, err := reserve.New(baseAddr, ts, 0, kademlia.NewTopologyDriver(), log.Noop)
395+
if err != nil {
396+
t.Fatal(err)
397+
}
398+
399+
privKey, err := crypto.GenerateSecp256k1Key()
400+
if err != nil {
401+
t.Fatal(err)
402+
}
403+
signer := crypto.NewDefaultSigner(privKey)
404+
idBytes := make([]byte, 32)
405+
406+
chCAC1, err := cac.New([]byte("payload-1-alpha"))
407+
if err != nil {
408+
t.Fatal(err)
409+
}
410+
chCAC2, err := cac.New([]byte("payload-2-beta"))
411+
if err != nil {
412+
t.Fatal(err)
413+
}
414+
if bytes.Compare(chCAC1.Address().Bytes(), chCAC2.Address().Bytes()) > 0 {
415+
chCAC1, chCAC2 = chCAC2, chCAC1
416+
}
417+
418+
soc1, err := soc.New(idBytes, chCAC1).Sign(signer)
419+
if err != nil {
420+
t.Fatal(err)
421+
}
422+
soc2, err := soc.New(idBytes, chCAC2).Sign(signer)
423+
if err != nil {
424+
t.Fatal(err)
425+
}
426+
427+
var stampA, stampB *postage.Stamp
428+
for {
429+
batchA := postagetesting.MustNewBatch()
430+
batchB := postagetesting.MustNewBatch()
431+
stA := postagetesting.MustNewFields(batchA.ID, 0, 1000)
432+
stB := postagetesting.MustNewFields(batchB.ID, 0, 1000)
433+
shA, _ := stA.Hash()
434+
shB, _ := stB.Hash()
435+
if bytes.Compare(shB, shA) < 0 {
436+
stampA, stampB = stA, stB
437+
break
438+
}
439+
}
440+
441+
ctx := context.Background()
442+
443+
// 1. Put Stamp A + Payload P1 (soc1)
444+
err = r.Put(ctx, soc1.WithStamp(stampA))
445+
if err != nil {
446+
t.Fatalf("put soc1 stampA failed: %v", err)
447+
}
448+
449+
// 2. Put Stamp B + Payload P2 (soc2) under same timestamp.
450+
// Since stampHashB < stampHashA, Stamp B wins over Stamp A.
451+
err = r.Put(ctx, soc2.WithStamp(stampB))
452+
if err != nil {
453+
t.Fatalf("put soc2 stampB failed: %v", err)
454+
}
455+
456+
// 3. Re-offer Stamp A + Payload P1 (soc1).
457+
// Stamp A lost to Stamp B at timestamp 1000. Re-offering Stamp A + P1 MUST NOT restore P1!
458+
err = r.Put(ctx, soc1.WithStamp(stampA))
459+
if err == nil {
460+
t.Fatalf("expected ErrDivergentChunkRejected when re-offering weaker stampA, got nil")
461+
}
462+
463+
// Verify that active chunk in ChunkStore STILL has Payload P2 (soc2)
464+
finalCh, err := ts.ChunkStore().Get(ctx, soc1.Address())
465+
if err != nil {
466+
t.Fatalf("get final chunk failed: %v", err)
467+
}
468+
if !bytes.Equal(finalCh.Data(), soc2.Data()) {
469+
t.Fatalf("re-offered Stamp A restored payload P1 over Stamp B's winning payload P2!")
470+
}
471+
}

pkg/storer/internal/reserve/reserve.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,8 @@ func (r *Reserve) putCAC(ctx context.Context, chunk swarm.Chunk, sum, stampHash
371371
func (r *Reserve) resolveStampIndexCollision(
372372
ctx context.Context, s transaction.Store,
373373
chunk swarm.Chunk, oldStampIndex *stampindex.Item,
374-
sum, stampHash []byte, bin uint8) (sameAddr bool, err error) {
374+
sum, stampHash []byte, bin uint8,
375+
) (sameAddr bool, err error) {
375376
prev := binary.BigEndian.Uint64(oldStampIndex.StampTimestamp)
376377
curr := binary.BigEndian.Uint64(chunk.Stamp().Timestamp())
377378
if prev > curr {
@@ -674,6 +675,31 @@ func (r *Reserve) resolveDivergence(
674675
return fmt.Errorf("failed loading diverging chunk %s: %w", chunk.Address(), err)
675676
}
676677

678+
// Verify timestamp precedence: an incoming chunk with an older timestamp
679+
// can never displace a stored chunk.
680+
prevTimestamp := binary.BigEndian.Uint64(stored.Stamp().Timestamp())
681+
currTimestamp := binary.BigEndian.Uint64(chunk.Stamp().Timestamp())
682+
if prevTimestamp > currTimestamp {
683+
return fmt.Errorf("overwrite same chunk. prev %d cur %d batch %s: %w", prevTimestamp, currTimestamp, hex.EncodeToString(chunk.Stamp().BatchID()), storage.ErrOverwriteNewerChunk)
684+
}
685+
686+
// At equal timestamp, if the stamps differ, the lower stamp hash wins.
687+
if prevTimestamp == currTimestamp {
688+
storedStampHash, err := stored.Stamp().Hash()
689+
if err != nil {
690+
return err
691+
}
692+
if !bytes.Equal(storedStampHash, stampHash) && bytes.Compare(storedStampHash, stampHash) < 0 {
693+
r.logger.Debug(
694+
"discarding diverging chunk (weaker stamp hash at equal timestamp)",
695+
"address", chunk.Address(),
696+
"stored_stamp_hash", hex.EncodeToString(storedStampHash),
697+
"incoming_stamp_hash", hex.EncodeToString(stampHash),
698+
)
699+
return fmt.Errorf("diverging chunk %s lost stamp-hash tie-break: %w", chunk.Address(), storage.ErrDivergentChunkRejected)
700+
}
701+
}
702+
677703
wins, err := storage.DivergentChunkWins(stored, chunk)
678704
if err != nil {
679705
return fmt.Errorf("divergence tie-break for chunk %s: %w", chunk.Address(), err)
@@ -1307,6 +1333,9 @@ func (r *Reserve) IncBinID(store storage.IndexStore, bin uint8) (uint64, error)
13071333
}
13081334

13091335
func wrappedAddrHex(ch swarm.Chunk) string {
1336+
if ch == nil {
1337+
return ""
1338+
}
13101339
if !soc.Valid(ch) {
13111340
return ""
13121341
}
@@ -1328,6 +1357,7 @@ func (r *Reserve) logAddressStampState(addr swarm.Address, event string) {
13281357
err := chunkstamp.IterateAll(r.st.IndexStore(), reserveScope, addr, func(stamp swarm.Stamp) (bool, error) {
13291358
stampHash, err := stamp.Hash()
13301359
if err != nil {
1360+
// nolint:nilerr
13311361
return false, nil
13321362
}
13331363
item := &BatchRadiusItem{Bin: bin, BatchID: stamp.BatchID(), Address: addr, StampHash: stampHash}

0 commit comments

Comments
 (0)