@@ -769,6 +769,180 @@ func TestAggregatorV3_BuildFiles_WithReorgDepth(t *testing.T) {
769769 require .Equal (t , kv .Step (6 ), kv .Step (agg .EndTxNumMinimax ()/ agg .StepSize ()))
770770}
771771
772+ // With txnsPerBlock=1, fakeFrozenBlocks(N) yields capTxNum=N.
773+ type fakeFrozenBlocks uint64
774+
775+ func (f fakeFrozenBlocks ) FrozenBlocks () uint64 { return uint64 (f ) }
776+
777+ // Regression for #20701: cap must clamp the loop, not just early-return.
778+ func TestAggregatorV3_BuildFiles_RespectsMaxCollatableTxNumCap (t * testing.T ) {
779+ if testing .Short () {
780+ t .Skip ("slow test" )
781+ }
782+ ctx := t .Context ()
783+ logger := log .New ()
784+ dirs := datadir .New (t .TempDir ())
785+ db := mdbx .New (dbcfg .ChainDB , logger ).InMem (t , dirs .Chaindata ).GrowthStep (32 * datasize .MB ).MapSize (2 * datasize .GB ).MustOpen ()
786+ t .Cleanup (db .Close )
787+ agg := state .NewTest (dirs ).ReorgBlockDepth (1 ).StepSize (2 ).Logger (logger ).MustOpen (ctx , db )
788+ t .Cleanup (agg .Close )
789+ require .NoError (t , agg .OpenFolder ())
790+
791+ // Cap at 4 steps; write 9 steps' worth into the DB.
792+ const capTxNum = uint64 (8 )
793+ agg .SetFrozenBlocksProvider (fakeFrozenBlocks (capTxNum ))
794+
795+ tdb , err := temporal .New (db , agg )
796+ require .NoError (t , err )
797+ t .Cleanup (tdb .Close )
798+ tx , err := tdb .BeginTemporalRw (ctx )
799+ require .NoError (t , err )
800+ t .Cleanup (tx .Rollback )
801+ doms , err := execctx .NewSharedDomains (context .Background (), tx , logger )
802+ require .NoError (t , err )
803+ t .Cleanup (doms .Close )
804+
805+ const txnNums = uint64 (18 )
806+ const txnsPerBlock = uint64 (1 )
807+ for i := uint64 (0 ); i < txnNums / txnsPerBlock ; i ++ {
808+ require .NoError (t , rawdbv3 .TxNums .Append (tx , i + 1 , (i + 1 )* txnsPerBlock ))
809+ }
810+ generateSharedDomainsUpdates (t , doms , tx , txnNums , newRnd (0 ), length .Addr , 10 , txnsPerBlock )
811+ require .NoError (t , doms .Flush (ctx , tx ))
812+ require .NoError (t , tx .Commit ())
813+
814+ require .NoError (t , agg .BuildFiles (txnNums ))
815+
816+ require .Equal (t , capTxNum , agg .EndTxNumMinimax (),
817+ "EndTxNumMinimax must be clamped to cap (%d), not driven by lastInDB" , capTxNum )
818+ require .Equal (t , kv .Step (capTxNum / agg .StepSize ()), kv .Step (agg .EndTxNumMinimax ()/ agg .StepSize ()))
819+ }
820+
821+ // nil provider = no cap; behavior unchanged.
822+ func TestAggregatorV3_BuildFiles_NoCapHookBuildsAll (t * testing.T ) {
823+ if testing .Short () {
824+ t .Skip ("slow test" )
825+ }
826+ ctx := t .Context ()
827+ logger := log .New ()
828+ dirs := datadir .New (t .TempDir ())
829+ db := mdbx .New (dbcfg .ChainDB , logger ).InMem (t , dirs .Chaindata ).GrowthStep (32 * datasize .MB ).MapSize (2 * datasize .GB ).MustOpen ()
830+ t .Cleanup (db .Close )
831+ agg := state .NewTest (dirs ).ReorgBlockDepth (1 ).StepSize (2 ).Logger (logger ).MustOpen (ctx , db )
832+ t .Cleanup (agg .Close )
833+ require .NoError (t , agg .OpenFolder ())
834+
835+ tdb , err := temporal .New (db , agg )
836+ require .NoError (t , err )
837+ t .Cleanup (tdb .Close )
838+ tx , err := tdb .BeginTemporalRw (ctx )
839+ require .NoError (t , err )
840+ t .Cleanup (tx .Rollback )
841+ doms , err := execctx .NewSharedDomains (context .Background (), tx , logger )
842+ require .NoError (t , err )
843+ t .Cleanup (doms .Close )
844+
845+ const txnNums = uint64 (18 )
846+ const txnsPerBlock = uint64 (1 )
847+ for i := uint64 (0 ); i < txnNums / txnsPerBlock ; i ++ {
848+ require .NoError (t , rawdbv3 .TxNums .Append (tx , i + 1 , (i + 1 )* txnsPerBlock ))
849+ }
850+ generateSharedDomainsUpdates (t , doms , tx , txnNums , newRnd (0 ), length .Addr , 10 , txnsPerBlock )
851+ require .NoError (t , doms .Flush (ctx , tx ))
852+ require .NoError (t , tx .Commit ())
853+
854+ require .NoError (t , agg .BuildFiles (txnNums ))
855+
856+ // reorg-depth=1 holds the last block back; cap unset.
857+ require .Equal (t , uint64 (16 ), agg .EndTxNumMinimax ())
858+ }
859+
860+ // BuildFiles2 path (stage_custom_trace, squeeze) must honor the cap too.
861+ func TestAggregatorV3_BuildFiles2_RespectsMaxCollatableTxNumCap (t * testing.T ) {
862+ if testing .Short () {
863+ t .Skip ("slow test" )
864+ }
865+ ctx := t .Context ()
866+ logger := log .New ()
867+ dirs := datadir .New (t .TempDir ())
868+ db := mdbx .New (dbcfg .ChainDB , logger ).InMem (t , dirs .Chaindata ).GrowthStep (32 * datasize .MB ).MapSize (2 * datasize .GB ).MustOpen ()
869+ t .Cleanup (db .Close )
870+ agg := state .NewTest (dirs ).ReorgBlockDepth (1 ).StepSize (2 ).Logger (logger ).MustOpen (ctx , db )
871+ t .Cleanup (agg .Close )
872+ require .NoError (t , agg .OpenFolder ())
873+
874+ const capTxNum = uint64 (8 )
875+ agg .SetFrozenBlocksProvider (fakeFrozenBlocks (capTxNum ))
876+
877+ tdb , err := temporal .New (db , agg )
878+ require .NoError (t , err )
879+ t .Cleanup (tdb .Close )
880+ tx , err := tdb .BeginTemporalRw (ctx )
881+ require .NoError (t , err )
882+ t .Cleanup (tx .Rollback )
883+ doms , err := execctx .NewSharedDomains (context .Background (), tx , logger )
884+ require .NoError (t , err )
885+ t .Cleanup (doms .Close )
886+
887+ const txnNums = uint64 (18 )
888+ for i := uint64 (0 ); i < txnNums ; i ++ {
889+ require .NoError (t , rawdbv3 .TxNums .Append (tx , i + 1 , i + 1 ))
890+ }
891+ generateSharedDomainsUpdates (t , doms , tx , txnNums , newRnd (0 ), length .Addr , 10 , 1 )
892+ require .NoError (t , doms .Flush (ctx , tx ))
893+ require .NoError (t , tx .Commit ())
894+
895+ require .NoError (t , agg .BuildFiles2 (ctx , 0 , kv .Step (txnNums / agg .StepSize ()), false ))
896+ agg .WaitForFiles ()
897+
898+ require .Equal (t , capTxNum , agg .EndTxNumMinimax (),
899+ "BuildFiles2 must clamp to cap (%d), not toStep" , capTxNum )
900+ }
901+
902+ // Cap below visibleFilesMinimaxTxNum: defensive only — no rollback.
903+ func TestAggregatorV3_BuildFiles_CapBelowVisibleIsNoop (t * testing.T ) {
904+ if testing .Short () {
905+ t .Skip ("slow test" )
906+ }
907+ ctx := t .Context ()
908+ logger := log .New ()
909+ dirs := datadir .New (t .TempDir ())
910+ db := mdbx .New (dbcfg .ChainDB , logger ).InMem (t , dirs .Chaindata ).GrowthStep (32 * datasize .MB ).MapSize (2 * datasize .GB ).MustOpen ()
911+ t .Cleanup (db .Close )
912+ agg := state .NewTest (dirs ).ReorgBlockDepth (1 ).StepSize (2 ).Logger (logger ).MustOpen (ctx , db )
913+ t .Cleanup (agg .Close )
914+ require .NoError (t , agg .OpenFolder ())
915+
916+ tdb , err := temporal .New (db , agg )
917+ require .NoError (t , err )
918+ t .Cleanup (tdb .Close )
919+ tx , err := tdb .BeginTemporalRw (ctx )
920+ require .NoError (t , err )
921+ t .Cleanup (tx .Rollback )
922+ doms , err := execctx .NewSharedDomains (context .Background (), tx , logger )
923+ require .NoError (t , err )
924+ t .Cleanup (doms .Close )
925+
926+ const txnNums = uint64 (18 )
927+ const txnsPerBlock = uint64 (1 )
928+ for i := uint64 (0 ); i < txnNums / txnsPerBlock ; i ++ {
929+ require .NoError (t , rawdbv3 .TxNums .Append (tx , i + 1 , (i + 1 )* txnsPerBlock ))
930+ }
931+ generateSharedDomainsUpdates (t , doms , tx , txnNums , newRnd (0 ), length .Addr , 10 , txnsPerBlock )
932+ require .NoError (t , doms .Flush (ctx , tx ))
933+ require .NoError (t , tx .Commit ())
934+
935+ // build state up to txNum 8.
936+ agg .SetFrozenBlocksProvider (fakeFrozenBlocks (8 ))
937+ require .NoError (t , agg .BuildFiles (txnNums ))
938+ require .Equal (t , uint64 (8 ), agg .EndTxNumMinimax ())
939+
940+ // lower cap below current visible — must be a no-op, no rollback.
941+ agg .SetFrozenBlocksProvider (fakeFrozenBlocks (4 ))
942+ require .NoError (t , agg .BuildFiles (txnNums ))
943+ require .Equal (t , uint64 (8 ), agg .EndTxNumMinimax ())
944+ }
945+
772946func compareMapsBytes (t * testing.T , m1 , m2 map [string ][]byte ) {
773947 t .Helper ()
774948 for k , v := range m1 {
0 commit comments