@@ -10,6 +10,7 @@ import (
1010 "net/http"
1111 "os"
1212 "path/filepath"
13+ "syscall"
1314 "testing"
1415 "time"
1516
@@ -560,3 +561,307 @@ func TestEvmSyncerMaliciousSequencerForceInclusionE2E(t *testing.T) {
560561 require .False (t , evm .CheckTxIncluded (seqClient , txForce .Hash ()),
561562 "Malicious sequencer should NOT have included the forced inclusion transaction" )
562563}
564+
565+ // setDAStartHeightInGenesis modifies the genesis file to set da_start_height.
566+ // This is needed because the based sequencer requires non-zero DAStartHeight,
567+ // and catch-up detection via CalculateEpochNumber also depends on it.
568+ func setDAStartHeightInGenesis (t * testing.T , homeDir string , height uint64 ) {
569+ t .Helper ()
570+ genesisPath := filepath .Join (homeDir , "config" , "genesis.json" )
571+ data , err := os .ReadFile (genesisPath )
572+ require .NoError (t , err )
573+
574+ var genesis map [string ]interface {}
575+ err = json .Unmarshal (data , & genesis )
576+ require .NoError (t , err )
577+
578+ genesis ["da_start_height" ] = height
579+
580+ newData , err := json .MarshalIndent (genesis , "" , " " )
581+ require .NoError (t , err )
582+
583+ err = os .WriteFile (genesisPath , newData , 0644 )
584+ require .NoError (t , err )
585+ }
586+
587+ // TestEvmSequencerCatchUpBasedSequencerE2E tests that when a sequencer restarts after
588+ // extended downtime (multiple DA epochs), it correctly enters catch-up mode, replays
589+ // missed forced inclusion transactions from DA (matching what a based sequencer would
590+ // produce), and then resumes normal operation.
591+ //
592+ // Test Scenario:
593+ // A sequencer goes down. During downtime, forced inclusion txs accumulate on DA.
594+ // A based sequencer keeps the chain alive using those DA txs. When the original
595+ // sequencer restarts, it must catch up by replaying the same DA epochs — producing
596+ // identical forced-inclusion-only blocks — before resuming normal mempool-based operation.
597+ //
598+ // Architecture:
599+ // - 2 Reth instances (via setupCommonEVMTest with needsFullNode=true)
600+ // - Reth1 -> Sequencer (phases 1 and 4)
601+ // - Reth2 -> Based sequencer (phase 3, reuses full node Reth/port slots)
602+ //
603+ // Phases:
604+ // 0. Setup: DA, 2 Reth containers, init sequencer with force inclusion epoch=2 and da_start_height=1
605+ // 1. Normal sequencer operation: submit normal txs
606+ // 2. Sequencer downtime: stop sequencer, submit forced inclusion txs directly to DA
607+ // 3. Start based sequencer: verify it includes forced inclusion txs
608+ // 4. Restart original sequencer: it catches up via DA replay
609+ // 5. Verification: forced txs on both nodes, sequencer resumes normal operation
610+ func TestEvmSequencerCatchUpBasedSequencerE2E (t * testing.T ) {
611+ sut := NewSystemUnderTest (t )
612+ workDir := t .TempDir ()
613+ sequencerHome := filepath .Join (workDir , "sequencer" )
614+ basedSeqHome := filepath .Join (workDir , "based-sequencer" )
615+
616+ // ===== PHASE 0: Setup =====
617+ t .Log ("Phase 0: Setup" )
618+
619+ jwtSecret , fullNodeJwtSecret , genesisHash , endpoints := setupCommonEVMTest (t , sut , true )
620+
621+ // Create passphrase and JWT secret files for sequencer
622+ passphraseFile := createPassphraseFile (t , sequencerHome )
623+ jwtSecretFile := createJWTSecretFile (t , sequencerHome , jwtSecret )
624+
625+ // Initialize sequencer node
626+ output , err := sut .RunCmd (evmSingleBinaryPath ,
627+ "init" ,
628+ "--evnode.node.aggregator=true" ,
629+ "--evnode.signer.passphrase_file" , passphraseFile ,
630+ "--home" , sequencerHome ,
631+ )
632+ require .NoError (t , err , "failed to init sequencer" , output )
633+
634+ // Modify genesis: enable force inclusion with epoch=2, set da_start_height=1
635+ enableForceInclusionInGenesis (t , sequencerHome , 2 )
636+ setDAStartHeightInGenesis (t , sequencerHome , 1 )
637+
638+ // Start sequencer with forced inclusion namespace
639+ seqProcess := sut .ExecCmd (evmSingleBinaryPath ,
640+ "start" ,
641+ "--evm.jwt-secret-file" , jwtSecretFile ,
642+ "--evm.genesis-hash" , genesisHash ,
643+ "--evnode.node.block_time" , DefaultBlockTime ,
644+ "--evnode.node.aggregator=true" ,
645+ "--evnode.signer.passphrase_file" , passphraseFile ,
646+ "--home" , sequencerHome ,
647+ "--evnode.da.block_time" , DefaultDABlockTime ,
648+ "--evnode.da.address" , endpoints .GetDAAddress (),
649+ "--evnode.da.namespace" , DefaultDANamespace ,
650+ "--evnode.da.forced_inclusion_namespace" , "forced-inc" ,
651+ "--evnode.rpc.address" , endpoints .GetRollkitRPCListen (),
652+ "--evnode.p2p.listen_address" , endpoints .GetRollkitP2PAddress (),
653+ "--evm.engine-url" , endpoints .GetSequencerEngineURL (),
654+ "--evm.eth-url" , endpoints .GetSequencerEthURL (),
655+ )
656+ sut .AwaitNodeUp (t , endpoints .GetRollkitRPCAddress (), NodeStartupTimeout )
657+ t .Log ("Sequencer is up with force inclusion enabled" )
658+
659+ // ===== PHASE 1: Normal Sequencer Operation =====
660+ t .Log ("Phase 1: Normal Sequencer Operation" )
661+
662+ seqClient , err := ethclient .Dial (endpoints .GetSequencerEthURL ())
663+ require .NoError (t , err )
664+ defer seqClient .Close ()
665+
666+ ctx := context .Background ()
667+ var nonce uint64 = 0
668+
669+ // Submit 2 normal transactions
670+ var normalTxHashes []common.Hash
671+ for i := 0 ; i < 2 ; i ++ {
672+ tx := evm .GetRandomTransaction (t , TestPrivateKey , TestToAddress , DefaultChainID , DefaultGasLimit , & nonce )
673+ err = seqClient .SendTransaction (ctx , tx )
674+ require .NoError (t , err )
675+ normalTxHashes = append (normalTxHashes , tx .Hash ())
676+ t .Logf ("Submitted normal tx %d: %s (nonce=%d)" , i + 1 , tx .Hash ().Hex (), tx .Nonce ())
677+ }
678+
679+ // Wait for normal txs to be included
680+ for i , txHash := range normalTxHashes {
681+ require .Eventually (t , func () bool {
682+ return evm .CheckTxIncluded (seqClient , txHash )
683+ }, 15 * time .Second , 500 * time .Millisecond , "Normal tx %d not included" , i + 1 )
684+ t .Logf ("Normal tx %d included" , i + 1 )
685+ }
686+
687+ // Record sequencer height
688+ seqHeader , err := seqClient .HeaderByNumber (ctx , nil )
689+ require .NoError (t , err )
690+ preDowntimeHeight := seqHeader .Number .Uint64 ()
691+ t .Logf ("Sequencer height before downtime: %d" , preDowntimeHeight )
692+
693+ // ===== PHASE 2: Sequencer Downtime + Submit Forced Inclusion Txs to DA =====
694+ t .Log ("Phase 2: Sequencer Downtime + Submit Forced Inclusion Txs to DA" )
695+
696+ // Stop sequencer process (SIGTERM to just this process, not all evm processes)
697+ err = seqProcess .Signal (syscall .SIGTERM )
698+ require .NoError (t , err , "failed to stop sequencer process" )
699+ time .Sleep (1 * time .Second ) // Wait for process to exit
700+
701+ // Submit forced inclusion transactions directly to DA
702+ blobClient , err := blobrpc .NewClient (ctx , endpoints .GetDAAddress (), "" , "" )
703+ require .NoError (t , err , "Failed to create blob RPC client" )
704+ defer blobClient .Close ()
705+
706+ daClient := block .NewDAClient (
707+ blobClient ,
708+ config.Config {
709+ DA : config.DAConfig {
710+ Namespace : DefaultDANamespace ,
711+ ForcedInclusionNamespace : "forced-inc" ,
712+ },
713+ },
714+ zerolog .Nop (),
715+ )
716+
717+ // Create and submit 3 forced inclusion txs to DA
718+ var forcedTxHashes []common.Hash
719+ for i := 0 ; i < 3 ; i ++ {
720+ txForce := evm .GetRandomTransaction (t , TestPrivateKey , TestToAddress , DefaultChainID , DefaultGasLimit , & nonce )
721+ txBytes , err := txForce .MarshalBinary ()
722+ require .NoError (t , err )
723+
724+ result := daClient .Submit (ctx , [][]byte {txBytes }, - 1 , daClient .GetForcedInclusionNamespace (), nil )
725+ require .Equal (t , da .StatusSuccess , result .Code , "Failed to submit forced tx %d to DA: %s" , i + 1 , result .Message )
726+
727+ forcedTxHashes = append (forcedTxHashes , txForce .Hash ())
728+ t .Logf ("Submitted forced inclusion tx %d to DA: %s (nonce=%d)" , i + 1 , txForce .Hash ().Hex (), txForce .Nonce ())
729+ }
730+
731+ // Wait for DA to advance past multiple epochs (epoch=2, DA block time=1s)
732+ // Need missedEpochs > 1, so DA must be at least 2 full epochs ahead
733+ t .Log ("Waiting for DA to advance past multiple epochs..." )
734+ time .Sleep (6 * time .Second )
735+
736+ // ===== PHASE 3: Start Based Sequencer =====
737+ t .Log ("Phase 3: Start Based Sequencer" )
738+
739+ // Initialize based sequencer node
740+ output , err = sut .RunCmd (evmSingleBinaryPath ,
741+ "init" ,
742+ "--home" , basedSeqHome ,
743+ )
744+ require .NoError (t , err , "failed to init based sequencer" , output )
745+
746+ // Copy genesis from sequencer to based sequencer (shares chain config)
747+ MustCopyFile (t ,
748+ filepath .Join (sequencerHome , "config" , "genesis.json" ),
749+ filepath .Join (basedSeqHome , "config" , "genesis.json" ),
750+ )
751+
752+ // Create JWT secret file for based sequencer using fullNodeJwtSecret
753+ basedSeqJwtSecretFile := createJWTSecretFile (t , basedSeqHome , fullNodeJwtSecret )
754+
755+ // Start based sequencer (uses full node Reth/port slots)
756+ sut .ExecCmd (evmSingleBinaryPath ,
757+ "start" ,
758+ "--evnode.node.aggregator=true" ,
759+ "--evnode.node.based_sequencer=true" ,
760+ "--evm.jwt-secret-file" , basedSeqJwtSecretFile ,
761+ "--evm.genesis-hash" , genesisHash ,
762+ "--evnode.node.block_time" , DefaultBlockTime ,
763+ "--home" , basedSeqHome ,
764+ "--evnode.da.block_time" , DefaultDABlockTime ,
765+ "--evnode.da.address" , endpoints .GetDAAddress (),
766+ "--evnode.da.namespace" , DefaultDANamespace ,
767+ "--evnode.da.forced_inclusion_namespace" , "forced-inc" ,
768+ "--evnode.rpc.address" , endpoints .GetFullNodeRPCListen (),
769+ "--evnode.p2p.listen_address" , endpoints .GetFullNodeP2PAddress (),
770+ "--evm.engine-url" , endpoints .GetFullNodeEngineURL (),
771+ "--evm.eth-url" , endpoints .GetFullNodeEthURL (),
772+ )
773+
774+ // Based sequencer may take longer to be ready (processing DA from start height)
775+ sut .AwaitNodeLive (t , endpoints .GetFullNodeRPCAddress (), NodeStartupTimeout )
776+ t .Log ("Based sequencer is live" )
777+
778+ // Connect ethclient to based sequencer
779+ basedSeqClient , err := ethclient .Dial (endpoints .GetFullNodeEthURL ())
780+ require .NoError (t , err )
781+ defer basedSeqClient .Close ()
782+
783+ // Verify based sequencer includes forced inclusion txs
784+ t .Log ("Waiting for based sequencer to include forced inclusion txs..." )
785+ for i , txHash := range forcedTxHashes {
786+ require .Eventually (t , func () bool {
787+ return evm .CheckTxIncluded (basedSeqClient , txHash )
788+ }, 60 * time .Second , 1 * time .Second ,
789+ "Forced inclusion tx %d (%s) not included in based sequencer" , i + 1 , txHash .Hex ())
790+ t .Logf ("Based sequencer included forced tx %d: %s" , i + 1 , txHash .Hex ())
791+ }
792+ t .Log ("All forced inclusion txs verified on based sequencer" )
793+
794+ // ===== PHASE 4: Restart Original Sequencer (Catch-Up) =====
795+ t .Log ("Phase 4: Restart Original Sequencer (Catch-Up)" )
796+
797+ // Restart the sequencer using existing home directory (no init needed)
798+ sut .ExecCmd (evmSingleBinaryPath ,
799+ "start" ,
800+ "--evm.jwt-secret-file" , jwtSecretFile ,
801+ "--evm.genesis-hash" , genesisHash ,
802+ "--evnode.node.block_time" , DefaultBlockTime ,
803+ "--evnode.node.aggregator=true" ,
804+ "--evnode.signer.passphrase_file" , passphraseFile ,
805+ "--home" , sequencerHome ,
806+ "--evnode.da.block_time" , DefaultDABlockTime ,
807+ "--evnode.da.address" , endpoints .GetDAAddress (),
808+ "--evnode.da.namespace" , DefaultDANamespace ,
809+ "--evnode.da.forced_inclusion_namespace" , "forced-inc" ,
810+ "--evnode.rpc.address" , endpoints .GetRollkitRPCListen (),
811+ "--evnode.p2p.listen_address" , endpoints .GetRollkitP2PAddress (),
812+ "--evm.engine-url" , endpoints .GetSequencerEngineURL (),
813+ "--evm.eth-url" , endpoints .GetSequencerEthURL (),
814+ )
815+ sut .AwaitNodeUp (t , endpoints .GetRollkitRPCAddress (), NodeStartupTimeout )
816+ t .Log ("Sequencer restarted successfully" )
817+
818+ // Reconnect ethclient to sequencer
819+ seqClient .Close ()
820+ seqClient , err = ethclient .Dial (endpoints .GetSequencerEthURL ())
821+ require .NoError (t , err )
822+
823+ // ===== PHASE 5: Verification =====
824+ t .Log ("Phase 5: Verification" )
825+
826+ // 5a. Verify sequencer includes forced inclusion txs after catch-up
827+ t .Log ("Verifying sequencer includes forced inclusion txs after catch-up..." )
828+ for i , txHash := range forcedTxHashes {
829+ require .Eventually (t , func () bool {
830+ return evm .CheckTxIncluded (seqClient , txHash )
831+ }, 30 * time .Second , 1 * time .Second ,
832+ "Forced inclusion tx %d (%s) should be included after catch-up" , i + 1 , txHash .Hex ())
833+ t .Logf ("Sequencer caught up with forced tx %d: %s" , i + 1 , txHash .Hex ())
834+ }
835+ t .Log ("All forced inclusion txs verified on sequencer after catch-up" )
836+
837+ // 5b. Verify sequencer resumes normal operation
838+ t .Log ("Verifying sequencer resumes normal mempool-based operation..." )
839+ txNormal := evm .GetRandomTransaction (t , TestPrivateKey , TestToAddress , DefaultChainID , DefaultGasLimit , & nonce )
840+ err = seqClient .SendTransaction (ctx , txNormal )
841+ require .NoError (t , err )
842+ t .Logf ("Submitted post-catchup normal tx: %s (nonce=%d)" , txNormal .Hash ().Hex (), txNormal .Nonce ())
843+
844+ require .Eventually (t , func () bool {
845+ return evm .CheckTxIncluded (seqClient , txNormal .Hash ())
846+ }, 15 * time .Second , 500 * time .Millisecond ,
847+ "Normal tx after catch-up should be included" )
848+ t .Log ("Post-catchup normal tx included - sequencer resumed normal operation" )
849+
850+ // 5c. Verify both nodes have the forced inclusion txs
851+ t .Log ("Verifying both nodes have all forced inclusion txs..." )
852+ for i , txHash := range forcedTxHashes {
853+ seqIncluded := evm .CheckTxIncluded (seqClient , txHash )
854+ basedIncluded := evm .CheckTxIncluded (basedSeqClient , txHash )
855+ require .True (t , seqIncluded , "Forced tx %d should be on sequencer" , i + 1 )
856+ require .True (t , basedIncluded , "Forced tx %d should be on based sequencer" , i + 1 )
857+ t .Logf ("Forced tx %d verified on both nodes: %s" , i + 1 , txHash .Hex ())
858+ }
859+
860+ t .Log ("Test PASSED: Sequencer catch-up with based sequencer verified successfully" )
861+ t .Logf (" - Sequencer processed %d normal txs before downtime" , len (normalTxHashes ))
862+ t .Logf (" - %d forced inclusion txs submitted to DA during downtime" , len (forcedTxHashes ))
863+ t .Logf (" - Based sequencer included all forced txs from DA" )
864+ t .Logf (" - Sequencer caught up and replayed all forced txs after restart" )
865+ t .Logf (" - Sequencer resumed normal mempool-based operation" )
866+ t .Logf (" - Both nodes have identical forced inclusion tx set" )
867+ }
0 commit comments