@@ -13,7 +13,7 @@ use eyre::Result;
1313use futures_util:: future:: BoxFuture ;
1414use reth_node_api:: { EngineTypes , PayloadTypes } ;
1515use reth_rpc_api:: clients:: { EngineApiClient , EthApiClient } ;
16- use std:: { marker:: PhantomData , time:: Duration } ;
16+ use std:: { collections :: HashSet , marker:: PhantomData , time:: Duration } ;
1717use tokio:: time:: sleep;
1818use tracing:: debug;
1919
@@ -697,3 +697,271 @@ where
697697 } )
698698 }
699699}
700+
701+ /// Action to test forkchoice update to a tagged block with expected status
702+ #[ derive( Debug ) ]
703+ pub struct TestFcuToTag {
704+ /// Tag name of the target block
705+ pub tag : String ,
706+ /// Expected payload status
707+ pub expected_status : PayloadStatusEnum ,
708+ }
709+
710+ impl TestFcuToTag {
711+ /// Create a new `TestFcuToTag` action
712+ pub fn new ( tag : impl Into < String > , expected_status : PayloadStatusEnum ) -> Self {
713+ Self { tag : tag. into ( ) , expected_status }
714+ }
715+ }
716+
717+ impl < Engine > Action < Engine > for TestFcuToTag
718+ where
719+ Engine : EngineTypes ,
720+ {
721+ fn execute < ' a > ( & ' a mut self , env : & ' a mut Environment < Engine > ) -> BoxFuture < ' a , Result < ( ) > > {
722+ Box :: pin ( async move {
723+ // get the target block from the registry
724+ let target_block = env
725+ . block_registry
726+ . get ( & self . tag )
727+ . copied ( )
728+ . ok_or_else ( || eyre:: eyre!( "Block tag '{}' not found in registry" , self . tag) ) ?;
729+
730+ let engine_client = env. node_clients [ 0 ] . engine . http_client ( ) ;
731+ let fcu_state = ForkchoiceState {
732+ head_block_hash : target_block. hash ,
733+ safe_block_hash : target_block. hash ,
734+ finalized_block_hash : target_block. hash ,
735+ } ;
736+
737+ let fcu_response =
738+ EngineApiClient :: < Engine > :: fork_choice_updated_v2 ( & engine_client, fcu_state, None )
739+ . await ?;
740+
741+ // validate the response matches expected status
742+ match ( & fcu_response. payload_status . status , & self . expected_status ) {
743+ ( PayloadStatusEnum :: Valid , PayloadStatusEnum :: Valid ) => {
744+ debug ! ( "FCU to '{}' returned VALID as expected" , self . tag) ;
745+ }
746+ ( PayloadStatusEnum :: Invalid { .. } , PayloadStatusEnum :: Invalid { .. } ) => {
747+ debug ! ( "FCU to '{}' returned INVALID as expected" , self . tag) ;
748+ }
749+ ( PayloadStatusEnum :: Syncing , PayloadStatusEnum :: Syncing ) => {
750+ debug ! ( "FCU to '{}' returned SYNCING as expected" , self . tag) ;
751+ }
752+ ( PayloadStatusEnum :: Accepted , PayloadStatusEnum :: Accepted ) => {
753+ debug ! ( "FCU to '{}' returned ACCEPTED as expected" , self . tag) ;
754+ }
755+ ( actual, expected) => {
756+ return Err ( eyre:: eyre!(
757+ "FCU to '{}': expected status {:?}, but got {:?}" ,
758+ self . tag,
759+ expected,
760+ actual
761+ ) ) ;
762+ }
763+ }
764+
765+ Ok ( ( ) )
766+ } )
767+ }
768+ }
769+
770+ /// Action to expect a specific FCU status when targeting a tagged block
771+ #[ derive( Debug ) ]
772+ pub struct ExpectFcuStatus {
773+ /// Tag name of the target block
774+ pub target_tag : String ,
775+ /// Expected payload status
776+ pub expected_status : PayloadStatusEnum ,
777+ }
778+
779+ impl ExpectFcuStatus {
780+ /// Create a new `ExpectFcuStatus` action expecting VALID status
781+ pub fn valid ( target_tag : impl Into < String > ) -> Self {
782+ Self { target_tag : target_tag. into ( ) , expected_status : PayloadStatusEnum :: Valid }
783+ }
784+
785+ /// Create a new `ExpectFcuStatus` action expecting INVALID status
786+ pub fn invalid ( target_tag : impl Into < String > ) -> Self {
787+ Self {
788+ target_tag : target_tag. into ( ) ,
789+ expected_status : PayloadStatusEnum :: Invalid {
790+ validation_error : "corrupted block" . to_string ( ) ,
791+ } ,
792+ }
793+ }
794+
795+ /// Create a new `ExpectFcuStatus` action expecting SYNCING status
796+ pub fn syncing ( target_tag : impl Into < String > ) -> Self {
797+ Self { target_tag : target_tag. into ( ) , expected_status : PayloadStatusEnum :: Syncing }
798+ }
799+
800+ /// Create a new `ExpectFcuStatus` action expecting ACCEPTED status
801+ pub fn accepted ( target_tag : impl Into < String > ) -> Self {
802+ Self { target_tag : target_tag. into ( ) , expected_status : PayloadStatusEnum :: Accepted }
803+ }
804+ }
805+
806+ impl < Engine > Action < Engine > for ExpectFcuStatus
807+ where
808+ Engine : EngineTypes ,
809+ {
810+ fn execute < ' a > ( & ' a mut self , env : & ' a mut Environment < Engine > ) -> BoxFuture < ' a , Result < ( ) > > {
811+ Box :: pin ( async move {
812+ let mut test_fcu = TestFcuToTag :: new ( & self . target_tag , self . expected_status . clone ( ) ) ;
813+ test_fcu. execute ( env) . await
814+ } )
815+ }
816+ }
817+
818+ /// Action to validate that a tagged block remains canonical by performing FCU to it
819+ #[ derive( Debug ) ]
820+ pub struct ValidateCanonicalTag {
821+ /// Tag name of the block to validate as canonical
822+ pub tag : String ,
823+ }
824+
825+ impl ValidateCanonicalTag {
826+ /// Create a new `ValidateCanonicalTag` action
827+ pub fn new ( tag : impl Into < String > ) -> Self {
828+ Self { tag : tag. into ( ) }
829+ }
830+ }
831+
832+ impl < Engine > Action < Engine > for ValidateCanonicalTag
833+ where
834+ Engine : EngineTypes ,
835+ {
836+ fn execute < ' a > ( & ' a mut self , env : & ' a mut Environment < Engine > ) -> BoxFuture < ' a , Result < ( ) > > {
837+ Box :: pin ( async move {
838+ let mut expect_valid = ExpectFcuStatus :: valid ( & self . tag ) ;
839+ expect_valid. execute ( env) . await ?;
840+
841+ debug ! ( "Successfully validated that '{}' remains canonical" , self . tag) ;
842+ Ok ( ( ) )
843+ } )
844+ }
845+ }
846+
847+ /// Action that produces a sequence of blocks where some blocks are intentionally invalid
848+ #[ derive( Debug ) ]
849+ pub struct ProduceInvalidBlocks < Engine > {
850+ /// Number of blocks to produce
851+ pub num_blocks : u64 ,
852+ /// Set of indices (0-based) where blocks should be made invalid
853+ pub invalid_indices : HashSet < u64 > ,
854+ /// Tracks engine type
855+ _phantom : PhantomData < Engine > ,
856+ }
857+
858+ impl < Engine > ProduceInvalidBlocks < Engine > {
859+ /// Create a new `ProduceInvalidBlocks` action
860+ pub fn new ( num_blocks : u64 , invalid_indices : HashSet < u64 > ) -> Self {
861+ Self { num_blocks, invalid_indices, _phantom : Default :: default ( ) }
862+ }
863+
864+ /// Create a new `ProduceInvalidBlocks` action with a single invalid block at the specified
865+ /// index
866+ pub fn with_invalid_at ( num_blocks : u64 , invalid_index : u64 ) -> Self {
867+ let mut invalid_indices = HashSet :: new ( ) ;
868+ invalid_indices. insert ( invalid_index) ;
869+ Self :: new ( num_blocks, invalid_indices)
870+ }
871+ }
872+
873+ impl < Engine > Action < Engine > for ProduceInvalidBlocks < Engine >
874+ where
875+ Engine : EngineTypes + PayloadTypes ,
876+ Engine :: PayloadAttributes : From < PayloadAttributes > + Clone ,
877+ Engine :: ExecutionPayloadEnvelopeV3 : Into < ExecutionPayloadEnvelopeV3 > ,
878+ {
879+ fn execute < ' a > ( & ' a mut self , env : & ' a mut Environment < Engine > ) -> BoxFuture < ' a , Result < ( ) > > {
880+ Box :: pin ( async move {
881+ for block_index in 0 ..self . num_blocks {
882+ let is_invalid = self . invalid_indices . contains ( & block_index) ;
883+
884+ if is_invalid {
885+ debug ! ( "Producing invalid block at index {}" , block_index) ;
886+
887+ // produce a valid block first, then corrupt it
888+ let mut sequence = Sequence :: new ( vec ! [
889+ Box :: new( PickNextBlockProducer :: default ( ) ) ,
890+ Box :: new( GeneratePayloadAttributes :: default ( ) ) ,
891+ Box :: new( GenerateNextPayload :: default ( ) ) ,
892+ ] ) ;
893+ sequence. execute ( env) . await ?;
894+
895+ // get the latest payload and corrupt it
896+ let latest_envelope = env
897+ . latest_payload_envelope
898+ . as_ref ( )
899+ . ok_or_else ( || eyre:: eyre!( "No payload envelope available to corrupt" ) ) ?;
900+
901+ let envelope_v3: ExecutionPayloadEnvelopeV3 = latest_envelope. clone ( ) . into ( ) ;
902+ let mut corrupted_payload = envelope_v3. execution_payload ;
903+
904+ // corrupt the state root to make the block invalid
905+ corrupted_payload. payload_inner . payload_inner . state_root = B256 :: random ( ) ;
906+
907+ debug ! (
908+ "Corrupted state root for block {} to: {}" ,
909+ block_index, corrupted_payload. payload_inner. payload_inner. state_root
910+ ) ;
911+
912+ // send the corrupted payload via newPayload
913+ let engine_client = env. node_clients [ 0 ] . engine . http_client ( ) ;
914+ // for simplicity, we'll use empty versioned hashes for invalid block testing
915+ let versioned_hashes = Vec :: new ( ) ;
916+ // use a random parent beacon block root since this is for invalid block testing
917+ let parent_beacon_block_root = B256 :: random ( ) ;
918+
919+ let new_payload_response = EngineApiClient :: < Engine > :: new_payload_v3 (
920+ & engine_client,
921+ corrupted_payload. clone ( ) ,
922+ versioned_hashes,
923+ parent_beacon_block_root,
924+ )
925+ . await ?;
926+
927+ // expect the payload to be rejected as invalid
928+ match new_payload_response. status {
929+ PayloadStatusEnum :: Invalid { validation_error } => {
930+ debug ! (
931+ "Block {} correctly rejected as invalid: {:?}" ,
932+ block_index, validation_error
933+ ) ;
934+ }
935+ other_status => {
936+ return Err ( eyre:: eyre!(
937+ "Expected block {} to be rejected as INVALID, but got: {:?}" ,
938+ block_index,
939+ other_status
940+ ) ) ;
941+ }
942+ }
943+
944+ // update block info with the corrupted block (for potential future reference)
945+ env. current_block_info = Some ( BlockInfo {
946+ hash : corrupted_payload. payload_inner . payload_inner . block_hash ,
947+ number : corrupted_payload. payload_inner . payload_inner . block_number ,
948+ timestamp : corrupted_payload. timestamp ( ) ,
949+ } ) ;
950+ } else {
951+ debug ! ( "Producing valid block at index {}" , block_index) ;
952+
953+ // produce a valid block normally
954+ let mut sequence = Sequence :: new ( vec ! [
955+ Box :: new( PickNextBlockProducer :: default ( ) ) ,
956+ Box :: new( GeneratePayloadAttributes :: default ( ) ) ,
957+ Box :: new( GenerateNextPayload :: default ( ) ) ,
958+ Box :: new( BroadcastNextNewPayload :: default ( ) ) ,
959+ Box :: new( UpdateBlockInfoToLatestPayload :: default ( ) ) ,
960+ ] ) ;
961+ sequence. execute ( env) . await ?;
962+ }
963+ }
964+ Ok ( ( ) )
965+ } )
966+ }
967+ }
0 commit comments