Skip to content

Commit 804d8ae

Browse files
committed
feat(storer): improve SOC/CAC stamp collision convergence for pullsync
1 parent 38d5daa commit 804d8ae

7 files changed

Lines changed: 491 additions & 180 deletions

File tree

pkg/puller/puller.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,12 +396,35 @@ func (p *Puller) syncPeerBin(parentCtx context.Context, peer *syncPeer, bin uint
396396

397397
// pulled at least one chunk
398398
if top >= start {
399+
if err != nil {
400+
p.logger.Debug("syncWorker advancing interval despite sync errors",
401+
"error", err,
402+
"peer_address", address,
403+
"bin", bin,
404+
"cursor", cursor,
405+
"start", start,
406+
"topmost", top,
407+
"count", count,
408+
"historical", isHistorical,
409+
)
410+
}
399411
if err := p.addPeerInterval(address, bin, start, top); err != nil {
400412
p.metrics.SyncWorkerErrCounter.Inc()
401413
p.logger.Error(err, "syncWorker could not persist interval for peer, quitting", "peer_address", address)
402414
return
403415
}
404416
start = top + 1
417+
} else if err != nil {
418+
p.logger.Debug("syncWorker not advancing interval after sync error",
419+
"error", err,
420+
"peer_address", address,
421+
"bin", bin,
422+
"cursor", cursor,
423+
"start", start,
424+
"topmost", top,
425+
"count", count,
426+
"historical", isHistorical,
427+
)
405428
}
406429
}
407430
}

pkg/pullsync/pullsync.go

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package pullsync
88

99
import (
1010
"context"
11+
"encoding/binary"
1112
"encoding/hex"
1213
"errors"
1314
"fmt"
@@ -303,7 +304,32 @@ func (s *Syncer) Sync(ctx context.Context, peer swarm.Address, bin uint8, start
303304
ctr++
304305
s.metrics.Wanted.Inc()
305306
bv.Set(i)
307+
s.logger.Debug("pullsync want chunk",
308+
"peer_address", peer,
309+
"bin", bin,
310+
"offer_idx", i,
311+
"chunk_address", a,
312+
"sum", hex.EncodeToString(sum),
313+
"start", start,
314+
"offer_topmost", topmost,
315+
)
316+
} else {
317+
s.logger.Debug("pullsync skip offer, already have sum",
318+
"peer_address", peer,
319+
"bin", bin,
320+
"offer_idx", i,
321+
"chunk_address", a,
322+
"sum", hex.EncodeToString(sum),
323+
"start", start,
324+
"offer_topmost", topmost,
325+
)
306326
}
327+
} else {
328+
s.logger.Debug("pullsync skip offer, outside storage radius",
329+
"peer_address", peer,
330+
"bin", bin,
331+
"chunk_address", a,
332+
)
307333
}
308334
}
309335

@@ -313,6 +339,7 @@ func (s *Syncer) Sync(ctx context.Context, peer swarm.Address, bin uint8, start
313339
}
314340

315341
chunksToPut := make([]swarm.Chunk, 0, ctr)
342+
wanted := ctr
316343

317344
var chunkErr error
318345
for ; ctr > 0; ctr-- {
@@ -345,17 +372,53 @@ func (s *Syncer) Sync(ctx context.Context, peer swarm.Address, bin uint8, start
345372
}
346373

347374
wantChunkID := addr.ByteString() + string(sum)
375+
stampHash, hashErr := stamp.Hash()
376+
stampHashHex := ""
377+
if hashErr == nil {
378+
stampHashHex = hex.EncodeToString(stampHash)
379+
}
348380
if _, ok := wantChunks[wantChunkID]; !ok {
349-
s.logger.Debug("want chunks", "error", ErrUnsolicitedChunk, "peer_address", peer, "chunk_address", addr)
381+
s.logger.Debug("pullsync unsolicited delivery",
382+
"error", ErrUnsolicitedChunk,
383+
"peer_address", peer,
384+
"chunk_address", addr,
385+
"batch_id", hex.EncodeToString(stamp.BatchID()),
386+
"stamp_hash", stampHashHex,
387+
"stamp_index", hex.EncodeToString(stamp.Index()),
388+
"stamp_timestamp", binary.BigEndian.Uint64(stamp.Timestamp()),
389+
"recomputed_sum", hex.EncodeToString(sum),
390+
"bin", bin,
391+
"offer_topmost", topmost,
392+
)
350393
chunkErr = errors.Join(chunkErr, ErrUnsolicitedChunk)
351394
continue
352395
}
353396

354397
delete(wantChunks, wantChunkID)
355398

399+
s.logger.Debug("pullsync delivery accepted for want",
400+
"peer_address", peer,
401+
"chunk_address", addr,
402+
"batch_id", hex.EncodeToString(stamp.BatchID()),
403+
"stamp_hash", stampHashHex,
404+
"stamp_index", hex.EncodeToString(stamp.Index()),
405+
"stamp_timestamp", binary.BigEndian.Uint64(stamp.Timestamp()),
406+
"recomputed_sum", hex.EncodeToString(sum),
407+
"bin", bin,
408+
"offer_topmost", topmost,
409+
)
410+
356411
chunk, err := s.validStamp(newChunk.WithStamp(stamp))
357412
if err != nil {
358-
s.logger.Debug("unverified stamp", "error", err, "peer_address", peer, "chunk_address", newChunk)
413+
s.logger.Debug("unverified stamp",
414+
"error", err,
415+
"peer_address", peer,
416+
"chunk_address", addr,
417+
"batch_id", hex.EncodeToString(stamp.BatchID()),
418+
"stamp_timestamp", binary.BigEndian.Uint64(stamp.Timestamp()),
419+
"bin", bin,
420+
"offer_topmost", topmost,
421+
)
359422
chunkErr = errors.Join(chunkErr, err)
360423
continue
361424
}
@@ -390,24 +453,66 @@ func (s *Syncer) Sync(ctx context.Context, peer swarm.Address, bin uint8, start
390453
// in case of these errors, no new items are added to the storage, so it
391454
// is safe to continue with the next chunk
392455
if errors.Is(err, storage.ErrOverwriteNewerChunk) {
393-
s.logger.Debug("overwrite newer chunk", "error", err, "peer_address", peer, "chunk", c)
456+
s.logger.Debug("overwrite newer chunk",
457+
"error", err,
458+
"peer_address", peer,
459+
"chunk_address", c.Address(),
460+
"batch_id", hex.EncodeToString(c.Stamp().BatchID()),
461+
"stamp_timestamp", binary.BigEndian.Uint64(c.Stamp().Timestamp()),
462+
"bin", bin,
463+
)
394464
chunkErr = errors.Join(chunkErr, err)
395465
continue
396466
}
397467
// the chunk diverged from the one already stored and lost the
398468
// tie-break. The neighborhood converges on the stored chunk, so
399469
// this is an expected outcome rather than a sync error.
400470
if errors.Is(err, storage.ErrDivergentChunkRejected) {
401-
s.logger.Debug("divergent chunk rejected", "error", err, "peer_address", peer, "chunk", c)
471+
s.logger.Debug("divergent chunk rejected",
472+
"error", err,
473+
"peer_address", peer,
474+
"chunk_address", c.Address(),
475+
"batch_id", hex.EncodeToString(c.Stamp().BatchID()),
476+
"stamp_timestamp", binary.BigEndian.Uint64(c.Stamp().Timestamp()),
477+
"bin", bin,
478+
)
402479
s.metrics.DivergentRejected.Inc()
403480
continue
404481
}
482+
s.logger.Debug("pullsync reserve put failed",
483+
"error", err,
484+
"peer_address", peer,
485+
"chunk_address", c.Address(),
486+
"batch_id", hex.EncodeToString(c.Stamp().BatchID()),
487+
"stamp_timestamp", binary.BigEndian.Uint64(c.Stamp().Timestamp()),
488+
"bin", bin,
489+
)
405490
return 0, 0, errors.Join(chunkErr, err)
406491
}
492+
s.logger.Debug("pullsync reserve put ok",
493+
"peer_address", peer,
494+
"chunk_address", c.Address(),
495+
"batch_id", hex.EncodeToString(c.Stamp().BatchID()),
496+
"stamp_index", hex.EncodeToString(c.Stamp().Index()),
497+
"stamp_timestamp", binary.BigEndian.Uint64(c.Stamp().Timestamp()),
498+
"bin", bin,
499+
)
407500
chunksPut++
408501
}
409502
}
410503

504+
if chunkErr != nil {
505+
s.logger.Debug("pullsync sync finished with chunk errors",
506+
"error", chunkErr,
507+
"peer_address", peer,
508+
"bin", bin,
509+
"start", start,
510+
"offer_topmost", topmost,
511+
"chunks_put", chunksPut,
512+
"wanted", wanted,
513+
)
514+
}
515+
411516
return topmost, chunksPut, chunkErr
412517
}
413518

@@ -424,9 +529,26 @@ func (s *Syncer) makeOffer(ctx context.Context, rn pb.Get) (*pb.Offer, []*storer
424529
o := new(pb.Offer)
425530
o.Topmost = top
426531
o.Chunks = make([]*pb.Chunk, 0, len(bincs))
427-
for _, v := range bincs {
532+
for i, v := range bincs {
428533
o.Chunks = append(o.Chunks, &pb.Chunk{Address: v.Address.Bytes(), Sum: v.Sum})
534+
s.logger.Debug("pullsync offer chunk",
535+
"bin", rn.Bin,
536+
"start", rn.Start,
537+
"offer_idx", i,
538+
"bin_id", v.BinID,
539+
"chunk_address", v.Address,
540+
"batch_id", hex.EncodeToString(v.BatchID),
541+
"stamp_hash", hex.EncodeToString(v.StampHash),
542+
"sum", hex.EncodeToString(v.Sum),
543+
"offer_topmost", top,
544+
)
429545
}
546+
s.logger.Debug("pullsync offer summary",
547+
"bin", rn.Bin,
548+
"start", rn.Start,
549+
"count", len(bincs),
550+
"offer_topmost", top,
551+
)
430552
return o, bincs, nil
431553
}
432554

@@ -465,7 +587,7 @@ func (s *Syncer) collectAddrs(ctx context.Context, bin uint8, start uint64) ([]*
465587
break LOOP // The stream has been closed.
466588
}
467589

468-
chs = append(chs, &storer.BinC{Address: c.Address, BatchID: c.BatchID, StampHash: c.StampHash, Sum: c.Sum})
590+
chs = append(chs, &storer.BinC{Address: c.Address, BinID: c.BinID, BatchID: c.BatchID, StampHash: c.StampHash, Sum: c.Sum})
469591
if c.BinID > topmost {
470592
topmost = c.BinID
471593
}
@@ -516,11 +638,34 @@ func (s *Syncer) processWant(ctx context.Context, bincs []*storer.BinC, w *pb.Wa
516638
s.metrics.SentWanted.Inc()
517639
ch, err := s.store.ReserveGet(ctx, c.Address, c.BatchID, c.StampHash)
518640
if err != nil {
519-
s.logger.Debug("processing want: unable to find chunk", "chunk_address", c.Address, "batch_id", hex.EncodeToString(c.BatchID))
641+
s.logger.Debug("processing want: unable to find chunk",
642+
"chunk_address", c.Address,
643+
"batch_id", hex.EncodeToString(c.BatchID),
644+
"stamp_hash", hex.EncodeToString(c.StampHash),
645+
"bin_id", c.BinID,
646+
"sum", hex.EncodeToString(c.Sum),
647+
"offer_idx", i,
648+
)
520649
chunks = append(chunks, swarm.NewChunk(swarm.ZeroAddress, nil))
521650
s.metrics.MissingChunks.Inc()
522651
continue
523652
}
653+
stampTS := uint64(0)
654+
stampIndex := ""
655+
if ch.Stamp() != nil {
656+
stampTS = binary.BigEndian.Uint64(ch.Stamp().Timestamp())
657+
stampIndex = hex.EncodeToString(ch.Stamp().Index())
658+
}
659+
s.logger.Debug("pullsync deliver chunk",
660+
"offer_idx", i,
661+
"bin_id", c.BinID,
662+
"chunk_address", c.Address,
663+
"batch_id", hex.EncodeToString(c.BatchID),
664+
"stamp_hash", hex.EncodeToString(c.StampHash),
665+
"stamp_index", stampIndex,
666+
"stamp_timestamp", stampTS,
667+
"sum", hex.EncodeToString(c.Sum),
668+
)
524669
chunks = append(chunks, ch)
525670
}
526671
}

pkg/pushsync/pushsync.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ package pushsync
88

99
import (
1010
"context"
11+
"encoding/binary"
12+
"encoding/hex"
1113
"errors"
1214
"fmt"
1315
"strconv"
@@ -266,13 +268,51 @@ func (ps *PushSync) handler(ctx context.Context, p p2p.Peer, stream p2p.Stream)
266268

267269
chunkToPut, err := ps.validStamp(chunk)
268270
if err != nil {
271+
ps.logger.Debug("pushsync invalid stamp",
272+
"error", err,
273+
"peer_address", p.Address,
274+
"chunk_address", chunkAddress,
275+
"batch_id", hex.EncodeToString(chunk.Stamp().BatchID()),
276+
"stamp_index", hex.EncodeToString(chunk.Stamp().Index()),
277+
"stamp_timestamp", binary.BigEndian.Uint64(chunk.Stamp().Timestamp()),
278+
)
269279
return fmt.Errorf("invalid stamp: %w", err)
270280
}
271281

282+
sumHex := ""
283+
if sum, sumErr := storage.ChunkSum(chunkToPut); sumErr == nil {
284+
sumHex = hex.EncodeToString(sum)
285+
}
286+
wrappedHex := ""
287+
if sch, socErr := soc.FromChunk(chunkToPut); socErr == nil {
288+
wrappedHex = sch.WrappedChunk().Address().String()
289+
}
290+
272291
err = ps.store.ReservePutter().Put(ctx, chunkToPut)
273292
if err != nil {
293+
ps.logger.Debug("pushsync reserve put failed",
294+
"error", err,
295+
"peer_address", p.Address,
296+
"chunk_address", chunkAddress,
297+
"batch_id", hex.EncodeToString(chunkToPut.Stamp().BatchID()),
298+
"stamp_index", hex.EncodeToString(chunkToPut.Stamp().Index()),
299+
"stamp_timestamp", binary.BigEndian.Uint64(chunkToPut.Stamp().Timestamp()),
300+
"sum", sumHex,
301+
"wrapped_chunk_address", wrappedHex,
302+
"path", reason,
303+
)
274304
return fmt.Errorf("reserve put: %w", err)
275305
}
306+
ps.logger.Debug("pushsync reserve put ok",
307+
"peer_address", p.Address,
308+
"chunk_address", chunkAddress,
309+
"batch_id", hex.EncodeToString(chunkToPut.Stamp().BatchID()),
310+
"stamp_index", hex.EncodeToString(chunkToPut.Stamp().Index()),
311+
"stamp_timestamp", binary.BigEndian.Uint64(chunkToPut.Stamp().Timestamp()),
312+
"sum", sumHex,
313+
"wrapped_chunk_address", wrappedHex,
314+
"path", reason,
315+
)
276316

277317
signature, err := ps.signer.Sign(chunkToPut.Address().Bytes())
278318
if err != nil {
@@ -307,6 +347,13 @@ func (ps *PushSync) handler(ctx context.Context, p p2p.Peer, stream p2p.Stream)
307347
return store(ctx)
308348
case err == nil:
309349
ps.metrics.Forwarder.Inc()
350+
ps.logger.Debug("pushsync forward chunk",
351+
"peer_address", p.Address,
352+
"chunk_address", chunkAddress,
353+
"batch_id", hex.EncodeToString(chunk.Stamp().BatchID()),
354+
"stamp_index", hex.EncodeToString(chunk.Stamp().Index()),
355+
"stamp_timestamp", binary.BigEndian.Uint64(chunk.Stamp().Timestamp()),
356+
)
310357

311358
debit, err := ps.accounting.PrepareDebit(ctx, p.Address, price)
312359
if err != nil {

pkg/storer/internal/reserve/convergence_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,37 @@ func TestPutOrderConvergence(t *testing.T) {
330330
}
331331
},
332332
},
333+
{
334+
// Same SOC address, same batch, equal timestamp, different stamp
335+
// indices. putSOC treats this as a new stamp entry and replaces the
336+
// shared payload unconditionally (last-write wins). Desired: settle
337+
// on the lexicographically lower stamp hash like the same-slot path.
338+
name: "divergent socs, equal timestamp, distinct stamp indices",
339+
unresolved: true,
340+
chunks: func(t *testing.T) []swarm.Chunk {
341+
t.Helper()
342+
return []swarm.Chunk{
343+
newTestSOC(t, signer, id1, []byte("soc payload one")).WithStamp(postagetesting.MustNewFields(batchA.ID, 0, 5)),
344+
newTestSOC(t, signer, id1, []byte("soc payload two")).WithStamp(postagetesting.MustNewFields(batchA.ID, 1, 5)),
345+
}
346+
},
347+
},
348+
{
349+
// Same SOC address under two batches at the same timestamp.
350+
// putSOC currently replaces the shared payload on the second stamp
351+
// unconditionally (last-write wins), so arrival order decides the
352+
// payload. Desired: settle on the lexicographically lower stamp
353+
// hash, matching the same-slot equal-timestamp path.
354+
name: "divergent socs, equal timestamp, distinct batches",
355+
unresolved: true,
356+
chunks: func(t *testing.T) []swarm.Chunk {
357+
t.Helper()
358+
return []swarm.Chunk{
359+
newTestSOC(t, signer, id1, []byte("soc payload one")).WithStamp(postagetesting.MustNewFields(batchA.ID, 0, 5)),
360+
newTestSOC(t, signer, id1, []byte("soc payload two")).WithStamp(postagetesting.MustNewFields(batchB.ID, 0, 5)),
361+
}
362+
},
363+
},
333364
{
334365
// Three-way conflict across batches: the batch B entry must end
335366
// serving whatever payload the batch A conflict settles on, with

0 commit comments

Comments
 (0)