@@ -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"
@@ -330,6 +331,37 @@ func TestPutOrderConvergence(t *testing.T) {
330331 }
331332 },
332333 },
334+ {
335+ // Same SOC address, same batch, equal timestamp, different stamp
336+ // indices. putSOC treats this as a new stamp entry and replaces the
337+ // shared payload unconditionally (last-write wins). Desired: settle
338+ // on the lexicographically lower stamp hash like the same-slot path.
339+ name : "divergent socs, equal timestamp, distinct stamp indices" ,
340+ unresolved : true ,
341+ chunks : func (t * testing.T ) []swarm.Chunk {
342+ t .Helper ()
343+ return []swarm.Chunk {
344+ newTestSOC (t , signer , id1 , []byte ("soc payload one" )).WithStamp (postagetesting .MustNewFields (batchA .ID , 0 , 5 )),
345+ newTestSOC (t , signer , id1 , []byte ("soc payload two" )).WithStamp (postagetesting .MustNewFields (batchA .ID , 1 , 5 )),
346+ }
347+ },
348+ },
349+ {
350+ // Same SOC address under two batches at the same timestamp.
351+ // putSOC currently replaces the shared payload on the second stamp
352+ // unconditionally (last-write wins), so arrival order decides the
353+ // payload. Desired: settle on the lexicographically lower stamp
354+ // hash, matching the same-slot equal-timestamp path.
355+ name : "divergent socs, equal timestamp, distinct batches" ,
356+ unresolved : true ,
357+ chunks : func (t * testing.T ) []swarm.Chunk {
358+ t .Helper ()
359+ return []swarm.Chunk {
360+ newTestSOC (t , signer , id1 , []byte ("soc payload one" )).WithStamp (postagetesting .MustNewFields (batchA .ID , 0 , 5 )),
361+ newTestSOC (t , signer , id1 , []byte ("soc payload two" )).WithStamp (postagetesting .MustNewFields (batchB .ID , 0 , 5 )),
362+ }
363+ },
364+ },
333365 {
334366 // Three-way conflict across batches: the batch B entry must end
335367 // serving whatever payload the batch A conflict settles on, with
@@ -353,3 +385,87 @@ func TestPutOrderConvergence(t *testing.T) {
353385 })
354386 }
355387}
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+ }
0 commit comments