@@ -56,13 +56,15 @@ const (
5656// Interface is the PullSync interface.
5757type Interface interface {
5858 // Sync syncs a batch of chunks starting at a start BinID.
59- // It returns the BinID of the highest offered chunk (topmost), the BinID
60- // of the highest chunk that was actually stored (stored; 0 on any delivery
61- // error), and the total number of chunks successfully stored.
62- // Callers must use stored — not topmost — to advance their sync interval,
63- // so that BinIDs whose chunks failed validation are not silently skipped.
64- // topmost is retained for overflow detection and future HIST-completion use.
65- Sync (ctx context.Context , peer swarm.Address , bin uint8 , start uint64 ) (topmost uint64 , stored uint64 , count int , err error )
59+ // It returns the topmost BinID safe for interval advancement and the total
60+ // number of chunks successfully stored.
61+ // topmost equals offer.Topmost (capped at the server's historical cursor)
62+ // when all delivered chunks passed validation. topmost is 0 when any chunk
63+ // failed validation (invalid stamp, unsolicited, or structural error), so
64+ // callers that use topmost to advance their interval will not skip
65+ // unverified BinIDs. ErrOverwriteNewerChunk does not zero topmost because
66+ // the chunk is already present in the reserve.
67+ Sync (ctx context.Context , peer swarm.Address , bin uint8 , start uint64 ) (topmost uint64 , count int , err error )
6668 // GetCursors retrieves all cursors from a downstream peer.
6769 GetCursors (ctx context.Context , peer swarm.Address ) ([]uint64 , uint64 , error )
6870}
@@ -229,16 +231,17 @@ func (s *Syncer) handler(streamCtx context.Context, p p2p.Peer, stream p2p.Strea
229231}
230232
231233// Sync syncs a batch of chunks starting at a start BinID.
232- // topmost is the highest BinID offered by the peer (used for overflow detection).
233- // stored is the highest BinID that was successfully validated and stored
234- // (0 if any chunk failed validation); callers must advance their sync interval
235- // using stored, not topmost.
236- // count is the number of chunks successfully stored.
237- func (s * Syncer ) Sync (ctx context.Context , peer swarm.Address , bin uint8 , start uint64 ) (topmost uint64 , stored uint64 , count int , err error ) {
234+ // topmost equals offer.Topmost capped at the server's historical cursor
235+ // (see collectAddrs). topmost is 0 when any delivered chunk failed validation
236+ // (invalid stamp, unsolicited, or structural error), preventing the caller
237+ // from silently skipping unverified BinIDs.
238+ // ErrOverwriteNewerChunk does not zero topmost: the chunk is already present.
239+ // count is the number of chunks successfully written to the reserve.
240+ func (s * Syncer ) Sync (ctx context.Context , peer swarm.Address , bin uint8 , start uint64 ) (topmost uint64 , count int , err error ) {
238241
239242 stream , err := s .streamer .NewStream (ctx , peer , nil , protocolName , protocolVersion , streamName )
240243 if err != nil {
241- return 0 , 0 , 0 , fmt .Errorf ("new stream: %w" , err )
244+ return 0 , 0 , fmt .Errorf ("new stream: %w" , err )
242245 }
243246 defer func () {
244247 if err != nil {
@@ -252,20 +255,18 @@ func (s *Syncer) Sync(ctx context.Context, peer swarm.Address, bin uint8, start
252255
253256 rangeMsg := & pb.Get {Bin : int32 (bin ), Start : start }
254257 if err = w .WriteMsgWithContext (ctx , rangeMsg ); err != nil {
255- return 0 , 0 , 0 , fmt .Errorf ("write get range: %w" , err )
258+ return 0 , 0 , fmt .Errorf ("write get range: %w" , err )
256259 }
257260
258261 var offer pb.Offer
259262 if err = r .ReadMsgWithContext (ctx , & offer ); err != nil {
260- return 0 , 0 , 0 , fmt .Errorf ("read offer: %w" , err )
263+ return 0 , 0 , fmt .Errorf ("read offer: %w" , err )
261264 }
262265
263- // empty interval (no chunks present in interval).
264- // return the end of the requested range as topmost.
265- // stored equals topmost: an empty offer advances the interval to the peer's
266- // watermark without delivering any chunks, which is correct.
266+ // empty interval: no chunks in range.
267+ // return the peer's watermark so the caller can advance the interval.
267268 if len (offer .Chunks ) == 0 {
268- return offer .Topmost , offer . Topmost , 0 , nil
269+ return offer .Topmost , 0 , nil
269270 }
270271
271272 topmost = offer .Topmost
@@ -279,7 +280,7 @@ func (s *Syncer) Sync(ctx context.Context, peer swarm.Address, bin uint8, start
279280
280281 bv , err := bitvector .New (bvLen )
281282 if err != nil {
282- return 0 , 0 , 0 , fmt .Errorf ("new bitvector: %w" , err )
283+ return 0 , 0 , fmt .Errorf ("new bitvector: %w" , err )
283284 }
284285
285286 for i := 0 ; i < len (offer .Chunks ); i ++ {
@@ -288,7 +289,7 @@ func (s *Syncer) Sync(ctx context.Context, peer swarm.Address, bin uint8, start
288289 batchID := offer .Chunks [i ].BatchID
289290 stampHash := offer .Chunks [i ].StampHash
290291 if len (addr ) != swarm .HashSize {
291- return 0 , 0 , 0 , fmt .Errorf ("inconsistent hash length" )
292+ return 0 , 0 , fmt .Errorf ("inconsistent hash length" )
292293 }
293294
294295 a := swarm .NewAddress (addr )
@@ -302,7 +303,7 @@ func (s *Syncer) Sync(ctx context.Context, peer swarm.Address, bin uint8, start
302303 have , err = s .store .ReserveHas (a , batchID , stampHash )
303304 if err != nil {
304305 s .logger .Debug ("storage has" , "error" , err )
305- return 0 , 0 , 0 , err
306+ return 0 , 0 , err
306307 }
307308
308309 if ! have {
@@ -316,16 +317,19 @@ func (s *Syncer) Sync(ctx context.Context, peer swarm.Address, bin uint8, start
316317
317318 wantMsg := & pb.Want {BitVector : bv .Bytes ()}
318319 if err = w .WriteMsgWithContext (ctx , wantMsg ); err != nil {
319- return 0 , 0 , 0 , fmt .Errorf ("write want: %w" , err )
320+ return 0 , 0 , fmt .Errorf ("write want: %w" , err )
320321 }
321322
322323 chunksToPut := make ([]swarm.Chunk , 0 , ctr )
323324
324- var chunkErr error
325+ var (
326+ chunkErr error
327+ hasValidationError bool // true when a chunk was not stored due to a validation failure
328+ )
325329 for ; ctr > 0 ; ctr -- {
326330 var delivery pb.Delivery
327331 if err = r .ReadMsgWithContext (ctx , & delivery ); err != nil {
328- return 0 , 0 , 0 , errors .Join (chunkErr , fmt .Errorf ("read delivery: %w" , err ))
332+ return 0 , 0 , errors .Join (chunkErr , fmt .Errorf ("read delivery: %w" , err ))
329333 }
330334
331335 addr := swarm .NewAddress (delivery .Address )
@@ -340,18 +344,21 @@ func (s *Syncer) Sync(ctx context.Context, peer swarm.Address, bin uint8, start
340344 stamp := new (postage.Stamp )
341345 if err = stamp .UnmarshalBinary (delivery .Stamp ); err != nil {
342346 chunkErr = errors .Join (chunkErr , err )
347+ hasValidationError = true
343348 continue
344349 }
345350 stampHash , err := stamp .Hash ()
346351 if err != nil {
347352 chunkErr = errors .Join (chunkErr , err )
353+ hasValidationError = true
348354 continue
349355 }
350356
351357 wantChunkID := addr .ByteString () + string (stamp .BatchID ()) + string (stampHash )
352358 if _ , ok := wantChunks [wantChunkID ]; ! ok {
353359 s .logger .Debug ("want chunks" , "error" , ErrUnsolicitedChunk , "peer_address" , peer , "chunk_address" , addr )
354360 chunkErr = errors .Join (chunkErr , ErrUnsolicitedChunk )
361+ hasValidationError = true
355362 continue
356363 }
357364
@@ -361,6 +368,7 @@ func (s *Syncer) Sync(ctx context.Context, peer swarm.Address, bin uint8, start
361368 if err != nil {
362369 s .logger .Debug ("unverified stamp" , "error" , err , "peer_address" , peer , "chunk_address" , newChunk )
363370 chunkErr = errors .Join (chunkErr , err )
371+ hasValidationError = true
364372 continue
365373 }
366374
@@ -370,13 +378,15 @@ func (s *Syncer) Sync(ctx context.Context, peer swarm.Address, bin uint8, start
370378 addr , err := chunk .Address ()
371379 if err != nil {
372380 chunkErr = errors .Join (chunkErr , err )
381+ hasValidationError = true
373382 continue
374383 }
375384 s .logger .Debug ("sync gsoc" , "peer_address" , peer , "chunk_address" , addr , "wrapped_chunk_address" , chunk .WrappedChunk ().Address ())
376385 s .gsocHandler (chunk )
377386 } else {
378387 s .logger .Debug ("invalid cac/soc chunk" , "error" , swarm .ErrInvalidChunk , "peer_address" , peer , "chunk" , chunk )
379388 chunkErr = errors .Join (chunkErr , swarm .ErrInvalidChunk )
389+ hasValidationError = true
380390 s .metrics .ReceivedInvalidChunk .Inc ()
381391 continue
382392 }
@@ -396,23 +406,27 @@ func (s *Syncer) Sync(ctx context.Context, peer swarm.Address, bin uint8, start
396406 if errors .Is (err , storage .ErrOverwriteNewerChunk ) {
397407 s .logger .Debug ("overwrite newer chunk" , "error" , err , "peer_address" , peer , "chunk" , c )
398408 chunkErr = errors .Join (chunkErr , err )
409+ // ErrOverwriteNewerChunk is not a validation failure: the chunk is
410+ // already present in the reserve with a newer stamp. It is safe to
411+ // advance the interval past it.
399412 continue
400413 }
401- return 0 , 0 , 0 , errors .Join (chunkErr , err )
414+ return 0 , 0 , errors .Join (chunkErr , err )
402415 }
403416 chunksPut ++
404417 }
405418 }
406419
407- // stored is the BinID callers should use to advance their sync interval.
408- // When any chunk failed validation or storage, stored is 0 so the interval
409- // does not advance past unverified BinIDs.
410- stored = topmost
411- if chunkErr != nil {
412- stored = 0
420+ // Zero topmost when any chunk failed validation (stamp, solicitation,
421+ // or structural check). This prevents the caller from advancing its
422+ // interval past BinIDs whose chunks were never stored.
423+ // ErrOverwriteNewerChunk does not zero topmost: the chunk is already
424+ // present in the reserve with a newer stamp, so advancement is safe.
425+ if hasValidationError {
426+ topmost = 0
413427 }
414428
415- return topmost , stored , chunksPut , chunkErr
429+ return topmost , chunksPut , chunkErr
416430}
417431
418432// makeOffer tries to assemble an offer for a given requested interval.
@@ -443,6 +457,19 @@ type collectAddrsResult struct {
443457// after which the function returns the collected slice of chunks.
444458func (s * Syncer ) collectAddrs (ctx context.Context , bin uint8 , start uint64 ) ([]* storer.BinC , uint64 , error ) {
445459 v , _ , err := s .intervalsSF .Do (ctx , sfKey (bin , start ), func (ctx context.Context ) (* collectAddrsResult , error ) {
460+ // Snapshot the server's historical cursor for this bin before subscribing.
461+ // Any chunk with BinID beyond this cursor arrived live after the snapshot;
462+ // capping topmost at the cursor prevents live chunks from inflating the
463+ // interval the client records for this peer.
464+ cursors , _ , err := s .store .ReserveLastBinIDs ()
465+ if err != nil {
466+ return nil , fmt .Errorf ("reserve last bin IDs: %w" , err )
467+ }
468+ var historicalCursor uint64
469+ if int (bin ) < len (cursors ) {
470+ historicalCursor = cursors [bin ]
471+ }
472+
446473 var (
447474 chs []* storer.BinC
448475 topmost uint64
@@ -492,6 +519,13 @@ func (s *Syncer) collectAddrs(ctx context.Context, bin uint8, start uint64) ([]*
492519 }
493520 }
494521
522+ // Cap topmost at the historical cursor. Live chunks (BinID > historicalCursor)
523+ // are included in the offer so the client can store them, but Topmost must
524+ // not advance the client's interval past the server's historical frontier.
525+ if topmost > historicalCursor {
526+ topmost = historicalCursor
527+ }
528+
495529 return & collectAddrsResult {chs : chs , topmost : topmost }, nil
496530 })
497531 if err != nil {
0 commit comments