@@ -6,17 +6,23 @@ package batchservice_test
66
77import (
88 "bytes"
9+ "compress/gzip"
910 "context"
11+ "encoding/json"
1012 "errors"
1113 "hash"
1214 "math/big"
1315 "testing"
16+ "time"
1417
18+ "github.com/ethereum/go-ethereum/accounts/abi"
1519 "github.com/ethereum/go-ethereum/common"
20+ "github.com/ethereum/go-ethereum/core/types"
1621 "github.com/ethersphere/bee/v2/pkg/log"
1722 "github.com/ethersphere/bee/v2/pkg/postage"
1823 "github.com/ethersphere/bee/v2/pkg/postage/batchservice"
1924 "github.com/ethersphere/bee/v2/pkg/postage/batchstore/mock"
25+ "github.com/ethersphere/bee/v2/pkg/postage/snapshot"
2026 postagetesting "github.com/ethersphere/bee/v2/pkg/postage/testing"
2127 mocks "github.com/ethersphere/bee/v2/pkg/statestore/mock"
2228 "github.com/ethersphere/bee/v2/pkg/storage"
@@ -615,15 +621,20 @@ func TestResyncControlsReset(t *testing.T) {
615621
616622type recordingListener struct {
617623 from uint64
624+ syncedTo uint64 // when non-zero, the replay advances the chain state to this block
618625 listened bool
619626 closed bool
620627 listenErr error
621628 closeErr error
622629}
623630
624- func (r * recordingListener ) Listen (_ context.Context , from uint64 , _ postage.EventUpdater ) <- chan error {
631+ func (r * recordingListener ) Listen (_ context.Context , from uint64 , updater postage.EventUpdater ) <- chan error {
625632 r .listened = true
626633 r .from = from
634+ // Mimic the real listener advancing the chain state during replay.
635+ if r .syncedTo != 0 && r .listenErr == nil {
636+ _ = updater .UpdateBlockNumber (r .syncedTo )
637+ }
627638 c := make (chan error , 1 )
628639 c <- r .listenErr
629640 return c
@@ -634,28 +645,33 @@ func (r *recordingListener) Close() error {
634645 return r .closeErr
635646}
636647
637- // TestSnapshotRebuild covers the snapshot rebuild path and the #5495 fix: live
638- // sync resumes from the snapshot's block height, and the store is reset at most
639- // once even when --resync is set alongside a snapshot (never twice, which would
640- // discard the freshly loaded snapshot).
648+ // TestSnapshotRebuild covers the snapshot rebuild path: live sync resumes from
649+ // the block the replay reached (not the snapshot tip), and the store is reset at
650+ // most once even when --resync is set alongside a snapshot (#5495).
641651func TestSnapshotRebuild (t * testing.T ) {
642652 t .Parallel ()
643653
644654 newSnapshot := func () (* recordingListener , * batchservice.Snapshot ) {
645655 snapListener := & recordingListener {}
646656 return snapListener , & batchservice.Snapshot {
647- Listener : snapListener ,
648- StartBlock : 100 ,
649- ResumeBlock : 4096 ,
657+ Listener : snapListener ,
658+ StartBlock : 100 ,
650659 }
651660 }
652661
653- t .Run ("snapshot replays and live sync resumes from its block " , func (t * testing.T ) {
662+ t .Run ("live sync resumes from where the replay stopped, not the snapshot tip " , func (t * testing.T ) {
654663 t .Parallel ()
655664
656665 s := mocks .NewStateStore ()
657666 store := mock .New ()
658- snapListener , snapshot := newSnapshot ()
667+ // A valid chain state must exist before the replay advances it.
668+ putChainState (t , store , & postage.ChainState {Block : 0 , TotalAmount : big .NewInt (0 ), CurrentPrice : big .NewInt (0 )})
669+
670+ // The real replay stops a few blocks below the snapshot tip (the listener
671+ // trims its tip), so live sync must resume where it actually stopped, not
672+ // at the tip — otherwise the trimmed blocks are skipped (#5495).
673+ snapListener := & recordingListener {syncedTo : 4090 }
674+ snapshot := & batchservice.Snapshot {Listener : snapListener , StartBlock : 100 }
659675 liveListener := & recordingListener {}
660676
661677 svc , loaded , err := batchservice .New (context .Background (), s , store , testLog , liveListener , nil , nil , nil , snapshot , false )
@@ -677,13 +693,12 @@ func TestSnapshotRebuild(t *testing.T) {
677693 t .Fatal ("expected snapshot listener to be closed" )
678694 }
679695
680- // Live sync resumes from the snapshot's block height, not the requested
681- // start block.
696+ // Live sync resumes from cs.Block+1, where the replay stopped.
682697 if err := svc .Start (context .Background (), snapshot .StartBlock ); err != nil {
683698 t .Fatal (err )
684699 }
685- if liveListener .from != snapshot . ResumeBlock + 1 {
686- t .Fatalf ("expect live sync from %d got %d" , snapshot . ResumeBlock + 1 , liveListener .from )
700+ if liveListener .from != 4091 {
701+ t .Fatalf ("expect live sync to resume from 4091 (replay stop +1) got %d" , liveListener .from )
687702 }
688703 if c := store .ResetCalls (); c != 0 {
689704 t .Fatalf ("expect store never reset, got %d" , c )
@@ -730,9 +745,8 @@ func TestSnapshotCornerCases(t *testing.T) {
730745 newSnapshot := func (listenErr error ) (* recordingListener , * batchservice.Snapshot ) {
731746 snapListener := & recordingListener {listenErr : listenErr }
732747 return snapListener , & batchservice.Snapshot {
733- Listener : snapListener ,
734- StartBlock : 100 ,
735- ResumeBlock : 4096 ,
748+ Listener : snapListener ,
749+ StartBlock : 100 ,
736750 }
737751 }
738752
@@ -848,7 +862,7 @@ func TestSnapshotCornerCases(t *testing.T) {
848862
849863 s := mocks .NewStateStore ()
850864 store := mock .New ()
851- _ , snapshot := newSnapshot (nil ) // snapshot block 4096
865+ _ , snapshot := newSnapshot (nil )
852866 liveListener := & recordingListener {}
853867
854868 svc , loaded , err := batchservice .New (context .Background (), s , store , testLog , liveListener , nil , nil , nil , snapshot , false )
@@ -859,7 +873,7 @@ func TestSnapshotCornerCases(t *testing.T) {
859873 t .Fatal ("expected snapshot to be loaded" )
860874 }
861875
862- // A chain state further ahead than the snapshot must take precedence so
876+ // A chain state further ahead than the replay must take precedence so
863877 // live sync never rewinds and reprocesses events.
864878 putChainState (t , store , & postage.ChainState {Block : 5000 , TotalAmount : big .NewInt (0 ), CurrentPrice : big .NewInt (0 )})
865879
@@ -895,6 +909,76 @@ func TestSnapshotCornerCases(t *testing.T) {
895909 })
896910}
897911
912+ // TestSnapshotHandoffNoGap guards the snapshot->RPC handoff (#5495): after a
913+ // snapshot replay, live sync must resume from where the replay stopped
914+ // (cs.Block+1), not the snapshot's nominal tip (maxBlock+1), which would skip the
915+ // blocks the listener trims off the tip.
916+ func TestSnapshotHandoffNoGap (t * testing.T ) {
917+ t .Parallel ()
918+
919+ const maxBlock = uint64 (5000 )
920+
921+ // Newest log at maxBlock; a non-matching address makes the listener filter
922+ // the events out, so it only advances the chain state per page.
923+ logs := []types.Log {
924+ {BlockNumber : 10 , Address : common .HexToAddress ("0x1" ), Topics : []common.Hash {}},
925+ {BlockNumber : maxBlock , Address : common .HexToAddress ("0x1" ), Topics : []common.Hash {}},
926+ }
927+ snap , err := snapshot .New (context .Background (), testLog , rawSnapshotGetter (gzipSnapshot (t , logs )), nil ,
928+ common.Address {}, abi.ABI {}, time .Second , time .Minute , time .Second , 0 )
929+ if err != nil {
930+ t .Fatalf ("snapshot.New: %v" , err )
931+ }
932+
933+ s := mocks .NewStateStore ()
934+ store := mock .New ()
935+ // Valid chain state so the replay can advance it.
936+ putChainState (t , store , & postage.ChainState {Block : 0 , TotalAmount : big .NewInt (0 ), CurrentPrice : big .NewInt (0 )})
937+
938+ live := & recordingListener {}
939+ svc , loaded , err := batchservice .New (context .Background (), s , store , testLog , live , nil , nil , nil , snap , false )
940+ if err != nil {
941+ t .Fatalf ("batchservice.New: %v" , err )
942+ }
943+ if ! loaded {
944+ t .Fatal ("expected snapshot to be loaded" )
945+ }
946+
947+ cs := store .GetChainState ()
948+ if cs .Block >= maxBlock {
949+ t .Fatalf ("replay reached %d, expected to stop below the snapshot max block %d" , cs .Block , maxBlock )
950+ }
951+
952+ if err := svc .Start (context .Background (), 0 ); err != nil {
953+ t .Fatalf ("start: %v" , err )
954+ }
955+
956+ // Must resume where the replay stopped, not at the snapshot tip.
957+ if live .from != cs .Block + 1 {
958+ t .Fatalf ("live sync resumed from %d; must resume from cs.Block+1 = %d (resuming higher skips the snapshot's trimmed tail — see #5495)" , live .from , cs .Block + 1 )
959+ }
960+ }
961+
962+ func gzipSnapshot (t * testing.T , logs []types.Log ) []byte {
963+ t .Helper ()
964+ var buf bytes.Buffer
965+ gz := gzip .NewWriter (& buf )
966+ enc := json .NewEncoder (gz )
967+ for _ , l := range logs {
968+ if err := enc .Encode (l ); err != nil {
969+ t .Fatalf ("encode log: %v" , err )
970+ }
971+ }
972+ if err := gz .Close (); err != nil {
973+ t .Fatalf ("gzip close: %v" , err )
974+ }
975+ return buf .Bytes ()
976+ }
977+
978+ type rawSnapshotGetter []byte
979+
980+ func (g rawSnapshotGetter ) GetBatchSnapshot () []byte { return g }
981+
898982func TestChecksum (t * testing.T ) {
899983 t .Parallel ()
900984
0 commit comments