11use std:: ops:: RangeInclusive ;
22
33use alloy:: node_bindings:: NodeError as AnvilError ;
4- use alloy:: primitives:: U256 ;
4+ use alloy:: primitives:: { I256 , U256 } ;
55use alloy:: providers:: { DynProvider , Provider , ProviderBuilder } ;
66use alloy:: rpc:: types:: TransactionReceipt ;
7+ use alloy:: sol;
8+ use alloy:: sol_types:: SolValue ;
79use async_trait:: async_trait;
810use colored:: * ;
911use papyrus_base_layer:: ethereum_base_layer_contract:: {
@@ -84,10 +86,13 @@ impl AnvilBaseLayer {
8486 let root_client = anvil_client. root ( ) . clone ( ) ;
8587 let contract = Starknet :: new ( config. starknet_contract_address , root_client) ;
8688
87- Self {
89+ let anvil_base_layer = Self {
8890 anvil_provider : anvil_client. erased ( ) ,
8991 ethereum_base_layer : EthereumBaseLayerContract { config, contract, url } ,
90- }
92+ } ;
93+ anvil_base_layer. initialize_mocked_starknet_contract ( ) . await ;
94+
95+ anvil_base_layer
9196 }
9297
9398 pub async fn send_message_to_l2 (
@@ -107,6 +112,66 @@ impl AnvilBaseLayer {
107112 ..Default :: default ( )
108113 }
109114 }
115+
116+ pub async fn update_mocked_starknet_contract_state (
117+ & self ,
118+ update : MockedStateUpdate ,
119+ ) -> Result < ( ) , EthereumBaseLayerError > {
120+ // Size out output in the starknet contract, most of these aren't checked in the mock.
121+ let mut output = vec ! [ U256 :: from( 0 ) ; starknet_output:: HEADER_SIZE + 2 ] ;
122+
123+ output[ starknet_output:: PREV_BLOCK_NUMBER_OFFSET ] = U256 :: from ( update. new_block_number - 1 ) ;
124+ output[ starknet_output:: NEW_BLOCK_NUMBER_OFFSET ] = U256 :: from ( update. new_block_number ) ;
125+ output[ starknet_output:: PREV_BLOCK_HASH_OFFSET ] = U256 :: from ( update. prev_block_hash ) ;
126+ output[ starknet_output:: NEW_BLOCK_HASH_OFFSET ] = U256 :: from ( update. new_block_hash ) ;
127+
128+ // Run eth_call first, which simulates the tx and returns errors, unlike eth_send which
129+ // executes without returning errors or output from the contract.
130+ // Note: if this fails and this is the first state update, make sure to set the previous
131+ // block number as 1 and the previous block hash as 0, as documented in the contract
132+ // initializer.
133+ self . ethereum_base_layer
134+ . contract
135+ . updateState ( output. clone ( ) , Default :: default ( ) )
136+ . call ( )
137+ . await ?;
138+
139+ self . ethereum_base_layer
140+ . contract
141+ . updateState ( output, Default :: default ( ) )
142+ . send ( )
143+ . await
144+ . unwrap ( )
145+ . get_receipt ( )
146+ . await
147+ . unwrap ( ) ;
148+
149+ Ok ( ( ) )
150+ }
151+
152+ /// Initialize the mocked Starknet contract with default test values and first block number and
153+ /// hash as 1.
154+ ///
155+ /// Other values are boilerplate to match the offsets in the Starknet solidity contract, a
156+ /// better mock can remove those as well.
157+ /// NOTE: right now this is coupled with the conditionally compiled mocked starknet account at
158+ /// EthereumBaseLayer; It'd be best if it could be included here instead, but this seems
159+ /// nontrivial without duplicating EthereumBaseLayer, which is self-defeating for a test.
160+ async fn initialize_mocked_starknet_contract ( & self ) {
161+ let init_data = InitializeData {
162+ programHash : U256 :: from ( 1 ) ,
163+ configHash : U256 :: from ( 1 ) ,
164+ initialState : StateUpdate {
165+ blockNumber : I256 :: from_dec_str ( "1" ) . unwrap ( ) ,
166+ ..Default :: default ( )
167+ } ,
168+ ..Default :: default ( )
169+ } ;
170+
171+ let encoded_data = init_data. abi_encode ( ) ;
172+ let builder = self . ethereum_base_layer . contract . initializeMock ( encoded_data. into ( ) ) ;
173+ builder. send ( ) . await . unwrap ( ) . get_receipt ( ) . await . unwrap ( ) ;
174+ }
110175}
111176
112177#[ async_trait]
@@ -198,3 +263,47 @@ pub async fn send_message_to_l2(
198263 // Waits until the transaction is received on L1 and then fetches its receipt.
199264 . get_receipt ( ) . await . expect ( "Transaction was not received on L1 or receipt retrieval failed." )
200265}
266+
267+ pub struct MockedStateUpdate {
268+ pub new_block_number : u64 ,
269+ pub new_block_hash : u64 ,
270+ // Consider caching and auto-filling this for better UX.
271+ pub prev_block_hash : u64 ,
272+ }
273+
274+ // The following structures are used with a mocked version of the Starknet L1 contract.
275+ // This mocked contract (starknet_for_testing.json) differs from the production contract:
276+ // - Includes an `initializeMock` function that bypasses governance requirements.
277+ // - The `updateState` function doesn't require special permissions.
278+ // - Removed a bunch of checks and functionality not necessary and that was difficult to mock,and
279+ // that are not called from the sequencer at this time.
280+ // - Used exclusively for integration testing with Anvil.
281+ sol ! {
282+ #[ derive( Debug , Default ) ]
283+ struct StateUpdate {
284+ uint256 globalRoot;
285+ int256 blockNumber;
286+ uint256 blockHash;
287+ }
288+
289+ #[ derive( Debug , Default ) ]
290+ struct InitializeData {
291+ uint256 programHash;
292+ uint256 aggregatorProgramHash;
293+ address verifier;
294+ uint256 configHash;
295+ StateUpdate initialState;
296+ }
297+ }
298+
299+ /// Output offsets from the Starknet solidity contract. These correspond to `StarknetOutput`,
300+ /// defined in the public `Output.sol` contract. These are the only offsets for values currently
301+ /// used in the starknet contract mock, if more are needed, which isn't likely, grab the offsets
302+ /// from Output.sol.
303+ mod starknet_output {
304+ pub const PREV_BLOCK_NUMBER_OFFSET : usize = 2 ;
305+ pub const NEW_BLOCK_NUMBER_OFFSET : usize = 3 ;
306+ pub const PREV_BLOCK_HASH_OFFSET : usize = 4 ;
307+ pub const NEW_BLOCK_HASH_OFFSET : usize = 5 ;
308+ pub const HEADER_SIZE : usize = 10 ;
309+ }
0 commit comments