@@ -201,14 +201,12 @@ func TestSameChunkAddress(t *testing.T) {
201201 bin := swarm .Proximity (baseAddr .Bytes (), ch1 .Address ().Bytes ())
202202 binBinIDs [bin ] += 1
203203 err = r .Put (ctx , ch2 )
204- if err != nil {
205- t .Fatal (err )
204+ if ! errors . Is ( err , storage . ErrOverwriteNewerChunk ) {
205+ t .Fatal ("expected error" )
206206 }
207- bin2 := swarm .Proximity (baseAddr .Bytes (), ch2 .Address ().Bytes ())
208- binBinIDs [bin2 ] += 1
209207 size2 := r .Size ()
210- if size2 - size1 != 2 {
211- t .Fatalf ("expected reserve size to increase by 2 , got %d" , size2 - size1 )
208+ if size2 - size1 != 1 {
209+ t .Fatalf ("expected reserve size to increase by 1 , got %d" , size2 - size1 )
212210 }
213211 })
214212
@@ -1241,7 +1239,7 @@ func TestSOCSiblingSumRefresh(t *testing.T) {
12411239 s3 := soctesting .GenerateMockSocWithSigner (t , []byte ("v3" ), signer )
12421240
12431241 stampA := postagetesting .MustNewFields (batchA .ID , 0 , 1 )
1244- stampB := postagetesting .MustNewFields (batchB .ID , 0 , 1 )
1242+ stampB := postagetesting .MustNewFields (batchB .ID , 0 , 2 )
12451243 chA := s1 .Chunk ().WithStamp (stampA )
12461244 chB := s2 .Chunk ().WithStamp (stampB )
12471245
@@ -1308,7 +1306,7 @@ func TestSOCSiblingSumRefresh(t *testing.T) {
13081306 // the same-batch replacement path (higher stamp timestamp) must refresh
13091307 // batch B's entry the same way.
13101308 staleSumB := sumOf (batchB .ID , stampHashB )
1311- chA2 := s3 .Chunk ().WithStamp (postagetesting .MustNewFields (batchA .ID , 0 , 2 ))
1309+ chA2 := s3 .Chunk ().WithStamp (postagetesting .MustNewFields (batchA .ID , 0 , 3 ))
13121310 if err := r .Put (ctx , chA2 ); err != nil {
13131311 t .Fatal (err )
13141312 }
@@ -1447,6 +1445,169 @@ func TestChunkSumIndexRandomOps(t *testing.T) {
14471445 checkInvariant (200 )
14481446}
14491447
1448+ // TestSOCCrossBatchTimestamp covers two single owner chunks that share an
1449+ // address but are stamped under different batches. The shared chunkstore
1450+ // payload is replaced when the incoming stamp timestamp is strictly higher,
1451+ // or when timestamps are equal and the incoming stamp hash is lower. An older
1452+ // stamp is rejected so neighborhoods converge.
1453+ func TestSOCCrossBatchTimestamp (t * testing.T ) {
1454+ t .Parallel ()
1455+
1456+ ctx := context .Background ()
1457+ signer := getSigner (t )
1458+ batchA := postagetesting .MustNewBatch ()
1459+ batchB := postagetesting .MustNewBatch ()
1460+
1461+ sOlder := soctesting .GenerateMockSocWithSigner (t , []byte ("older" ), signer )
1462+ sNewer := soctesting .GenerateMockSocWithSigner (t , []byte ("newer" ), signer )
1463+ if ! sOlder .Chunk ().Address ().Equal (sNewer .Chunk ().Address ()) {
1464+ t .Fatal ("expected shared SOC address" )
1465+ }
1466+
1467+ t .Run ("higher timestamp replaces" , func (t * testing.T ) {
1468+ t .Parallel ()
1469+
1470+ baseAddr := swarm .RandAddress (t )
1471+ ts := internal .NewInmemStorage ()
1472+ r , err := reserve .New (baseAddr , ts , 0 , kademlia .NewTopologyDriver (), log .Noop )
1473+ if err != nil {
1474+ t .Fatal (err )
1475+ }
1476+
1477+ older := sOlder .Chunk ().WithStamp (postagetesting .MustNewFields (batchA .ID , 0 , 1 ))
1478+ newer := sNewer .Chunk ().WithStamp (postagetesting .MustNewFields (batchB .ID , 0 , 2 ))
1479+
1480+ if err := r .Put (ctx , older ); err != nil {
1481+ t .Fatal (err )
1482+ }
1483+ if err := r .Put (ctx , newer ); err != nil {
1484+ t .Fatal (err )
1485+ }
1486+
1487+ got , err := ts .ChunkStore ().Get (ctx , newer .Address ())
1488+ if err != nil {
1489+ t .Fatal (err )
1490+ }
1491+ if ! bytes .Equal (got .Data (), newer .Data ()) {
1492+ t .Fatal ("expected payload from the higher-timestamp stamp" )
1493+ }
1494+ })
1495+
1496+ t .Run ("equal timestamp stamp hash tie-break" , func (t * testing.T ) {
1497+ t .Parallel ()
1498+
1499+ chA := sOlder .Chunk ().WithStamp (postagetesting .MustNewFields (batchA .ID , 0 , 5 ))
1500+ chB := sNewer .Chunk ().WithStamp (postagetesting .MustNewFields (batchB .ID , 0 , 5 ))
1501+ hashA , err := chA .Stamp ().Hash ()
1502+ if err != nil {
1503+ t .Fatal (err )
1504+ }
1505+ hashB , err := chB .Stamp ().Hash ()
1506+ if err != nil {
1507+ t .Fatal (err )
1508+ }
1509+ var winner , loser swarm.Chunk
1510+ if bytes .Compare (hashA , hashB ) < 0 {
1511+ winner , loser = chA , chB
1512+ } else {
1513+ winner , loser = chB , chA
1514+ }
1515+
1516+ for _ , order := range [][]swarm.Chunk {{winner , loser }, {loser , winner }} {
1517+ baseAddr := swarm .RandAddress (t )
1518+ ts := internal .NewInmemStorage ()
1519+ r , err := reserve .New (baseAddr , ts , 0 , kademlia .NewTopologyDriver (), log .Noop )
1520+ if err != nil {
1521+ t .Fatal (err )
1522+ }
1523+ if err := r .Put (ctx , order [0 ]); err != nil {
1524+ t .Fatal (err )
1525+ }
1526+ _ = r .Put (ctx , order [1 ]) // may reject when winner is already stored
1527+
1528+ got , err := ts .ChunkStore ().Get (ctx , winner .Address ())
1529+ if err != nil {
1530+ t .Fatal (err )
1531+ }
1532+ if ! bytes .Equal (got .Data (), winner .Data ()) {
1533+ t .Fatal ("expected payload from the lower stamp-hash claim" )
1534+ }
1535+ }
1536+ })
1537+
1538+ t .Run ("lower timestamp rejected" , func (t * testing.T ) {
1539+ t .Parallel ()
1540+
1541+ baseAddr := swarm .RandAddress (t )
1542+ ts := internal .NewInmemStorage ()
1543+ r , err := reserve .New (baseAddr , ts , 0 , kademlia .NewTopologyDriver (), log .Noop )
1544+ if err != nil {
1545+ t .Fatal (err )
1546+ }
1547+
1548+ newer := sNewer .Chunk ().WithStamp (postagetesting .MustNewFields (batchA .ID , 0 , 9 ))
1549+ older := sOlder .Chunk ().WithStamp (postagetesting .MustNewFields (batchB .ID , 0 , 3 ))
1550+
1551+ if err := r .Put (ctx , newer ); err != nil {
1552+ t .Fatal (err )
1553+ }
1554+ err = r .Put (ctx , older )
1555+ if ! errors .Is (err , storage .ErrOverwriteNewerChunk ) {
1556+ t .Fatalf ("expected ErrOverwriteNewerChunk, got %v" , err )
1557+ }
1558+
1559+ got , err := ts .ChunkStore ().Get (ctx , newer .Address ())
1560+ if err != nil {
1561+ t .Fatal (err )
1562+ }
1563+ if ! bytes .Equal (got .Data (), newer .Data ()) {
1564+ t .Fatal ("expected newer payload to remain" )
1565+ }
1566+ })
1567+
1568+ t .Run ("same batch different stamp index" , func (t * testing.T ) {
1569+ t .Parallel ()
1570+
1571+ chLow := sOlder .Chunk ().WithStamp (postagetesting .MustNewFields (batchA .ID , 0 , 5 ))
1572+ chHigh := sNewer .Chunk ().WithStamp (postagetesting .MustNewFields (batchA .ID , 1 , 5 ))
1573+ hashLow , err := chLow .Stamp ().Hash ()
1574+ if err != nil {
1575+ t .Fatal (err )
1576+ }
1577+ hashHigh , err := chHigh .Stamp ().Hash ()
1578+ if err != nil {
1579+ t .Fatal (err )
1580+ }
1581+ var winner , loser swarm.Chunk
1582+ if bytes .Compare (hashLow , hashHigh ) < 0 {
1583+ winner , loser = chLow , chHigh
1584+ } else {
1585+ winner , loser = chHigh , chLow
1586+ }
1587+
1588+ for _ , order := range [][]swarm.Chunk {{winner , loser }, {loser , winner }} {
1589+ baseAddr := swarm .RandAddress (t )
1590+ ts := internal .NewInmemStorage ()
1591+ r , err := reserve .New (baseAddr , ts , 0 , kademlia .NewTopologyDriver (), log .Noop )
1592+ if err != nil {
1593+ t .Fatal (err )
1594+ }
1595+ if err := r .Put (ctx , order [0 ]); err != nil {
1596+ t .Fatal (err )
1597+ }
1598+ _ = r .Put (ctx , order [1 ])
1599+
1600+ got , err := ts .ChunkStore ().Get (ctx , winner .Address ())
1601+ if err != nil {
1602+ t .Fatal (err )
1603+ }
1604+ if ! bytes .Equal (got .Data (), winner .Data ()) {
1605+ t .Fatal ("expected payload from the lower stamp-hash claim" )
1606+ }
1607+ }
1608+ })
1609+ }
1610+
14501611// TestSOCDivergence covers two single owner chunks that share an address, batch
14511612// and stamp while wrapping different content. Both are valid, so the storage
14521613// layer settles which one the neighborhood keeps, and it must reach the same
0 commit comments