Skip to content

Commit 13b9881

Browse files
ArniStarkwaregiladchaseGilad Chase
authored
apollo_integration_tests: add mocked starknet contract (#8459) (#9724)
* apollo_integration_tests: ban `anvil` in unit tests * l1: add mocked starknet contract Mocked contract has mocked initialize and update state functions, but is otherwise identical, is intended for use in anvil-based integraiton tests. Scraper test needed a fix, it wasn't passing fee, and not that the contract is initialized it checks this. Also the cancellation request wasn't sent from the correct sender, which was also not previously checked. --------- Co-authored-by: giladchase <[email protected]> Co-authored-by: Gilad Chase <[email protected]>
1 parent 2f65482 commit 13b9881

File tree

5 files changed

+795
-20
lines changed

5 files changed

+795
-20
lines changed

crates/apollo_integration_tests/src/anvil_base_layer.rs

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::ops::RangeInclusive;
22

33
use alloy::node_bindings::NodeError as AnvilError;
4-
use alloy::primitives::U256;
4+
use alloy::primitives::{I256, U256};
55
use alloy::providers::{DynProvider, Provider, ProviderBuilder};
66
use alloy::rpc::types::TransactionReceipt;
7+
use alloy::sol;
8+
use alloy::sol_types::SolValue;
79
use async_trait::async_trait;
810
use colored::*;
911
use 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+
}

crates/apollo_integration_tests/tests/l1_events_scraper_end_to_end.rs

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,35 @@ async fn scraper_end_to_end() {
3737
// Send messages from L1 to L2.
3838
let l2_contract_address = "0x12";
3939
let l2_entry_point = "0x34";
40-
let message_to_l2_0 = contract.sendMessageToL2(
41-
l2_contract_address.parse().unwrap(),
42-
l2_entry_point.parse().unwrap(),
43-
vec![U256::from(1_u8), U256::from(2_u8)],
44-
);
45-
let message_to_l2_1 = contract.sendMessageToL2(
46-
l2_contract_address.parse().unwrap(),
47-
l2_entry_point.parse().unwrap(),
48-
vec![U256::from(3_u8), U256::from(4_u8)],
49-
);
40+
let fee = 1_u8;
41+
let message_to_l2_0 = contract
42+
.sendMessageToL2(
43+
l2_contract_address.parse().unwrap(),
44+
l2_entry_point.parse().unwrap(),
45+
vec![U256::from(1_u8), U256::from(2_u8)],
46+
)
47+
.value(U256::from(fee));
48+
let message_to_l2_1 = contract
49+
.sendMessageToL2(
50+
l2_contract_address.parse().unwrap(),
51+
l2_entry_point.parse().unwrap(),
52+
vec![U256::from(3_u8), U256::from(4_u8)],
53+
)
54+
.value(U256::from(fee));
5055
let nonce_of_message_to_l2_0 = U256::from(0_u8);
51-
let request_cancel_message_0 = contract.startL1ToL2MessageCancellation(
52-
l2_contract_address.parse().unwrap(),
53-
l2_entry_point.parse().unwrap(),
54-
vec![U256::from(1_u8), U256::from(2_u8)],
55-
nonce_of_message_to_l2_0,
56-
);
56+
let request_cancel_message_0 = contract
57+
.startL1ToL2MessageCancellation(
58+
l2_contract_address.parse().unwrap(),
59+
l2_entry_point.parse().unwrap(),
60+
vec![U256::from(1_u8), U256::from(2_u8)],
61+
nonce_of_message_to_l2_0,
62+
)
63+
.from(DEFAULT_ANVIL_L1_ACCOUNT_ADDRESS.to_hex_string().parse().unwrap());
5764

5865
// Send the transactions to Anvil, and record the timestamps of the blocks they are included in.
5966
let mut l1_handler_timestamps: Vec<BlockTimestamp> = Vec::with_capacity(2);
6067
for msg in &[message_to_l2_0, message_to_l2_1] {
68+
msg.call().await.unwrap(); // Query for errors.
6169
let receipt = msg.send().await.unwrap().get_receipt().await.unwrap();
6270
l1_handler_timestamps.push(
6371
base_layer
@@ -69,6 +77,7 @@ async fn scraper_end_to_end() {
6977
);
7078
}
7179

80+
request_cancel_message_0.call().await.unwrap(); // Query for errors;
7281
let cancel_receipt =
7382
request_cancel_message_0.send().await.unwrap().get_receipt().await.unwrap();
7483
let cancel_timestamp = base_layer
@@ -95,7 +104,7 @@ async fn scraper_end_to_end() {
95104
let expected_executable_l1_handler_0 = ExecutableL1HandlerTransaction {
96105
tx_hash: tx_hash_first_tx,
97106
tx: expected_l1_handler_0,
98-
paid_fee_on_l1: Fee(0),
107+
paid_fee_on_l1: Fee(fee.into()),
99108
};
100109
let first_expected_log = Event::L1HandlerTransaction {
101110
l1_handler_tx: expected_executable_l1_handler_0.clone(),
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use alloy::primitives::{I256, U256};
2+
use alloy::providers::Provider;
3+
use alloy::rpc::types::eth::Filter as EthEventFilter;
4+
use alloy::sol_types::SolEventInterface;
5+
use apollo_integration_tests::anvil_base_layer::{AnvilBaseLayer, MockedStateUpdate};
6+
use papyrus_base_layer::ethereum_base_layer_contract::Starknet;
7+
use papyrus_base_layer::BaseLayerContract;
8+
use pretty_assertions::assert_eq;
9+
use starknet_api::block::{BlockHash, BlockHashAndNumber, BlockNumber};
10+
11+
#[tokio::test]
12+
async fn test_mocked_starknet_state_update() {
13+
let base_layer = AnvilBaseLayer::new().await;
14+
15+
// Check that the contract was initialized (during the construction above).
16+
let no_finality = 0;
17+
let genesis_block_number = 1;
18+
let genesis_block_hash = 0;
19+
let initial_state = base_layer.latest_proved_block(no_finality).await.unwrap().unwrap();
20+
assert_eq!(
21+
initial_state.number,
22+
BlockNumber(genesis_block_number),
23+
"Starknet contract was not initiailized."
24+
);
25+
assert_eq!(
26+
initial_state.hash,
27+
BlockHash(genesis_block_hash.into()),
28+
"Starknet contract was not initiailized."
29+
);
30+
31+
// Negative flow: update state should always have sequential block numbers.
32+
let wrong_next_block_number = genesis_block_number + 2;
33+
let incorrect_new_block_number_result = base_layer
34+
.update_mocked_starknet_contract_state(MockedStateUpdate {
35+
new_block_number: wrong_next_block_number,
36+
new_block_hash: 2,
37+
prev_block_hash: genesis_block_hash,
38+
})
39+
.await;
40+
assert!(
41+
incorrect_new_block_number_result
42+
.unwrap_err()
43+
.to_string()
44+
.contains("INVALID_PREV_BLOCK_NUMBER")
45+
);
46+
47+
// Happy flow.
48+
let next_block_number = genesis_block_number + 1;
49+
let new_block_hash = 2;
50+
base_layer
51+
.update_mocked_starknet_contract_state(MockedStateUpdate {
52+
new_block_number: next_block_number,
53+
new_block_hash,
54+
prev_block_hash: genesis_block_hash,
55+
})
56+
.await
57+
.unwrap();
58+
59+
let updated_block_number_and_hash =
60+
base_layer.latest_proved_block(no_finality).await.unwrap().unwrap();
61+
assert_eq!(
62+
updated_block_number_and_hash,
63+
BlockHashAndNumber {
64+
number: BlockNumber(next_block_number),
65+
hash: BlockHash(new_block_hash.into())
66+
}
67+
);
68+
69+
// Check that LogStateUpdate event was emitted (we don't use this event in the sequencer at the
70+
// time this was written).
71+
let event = base_layer
72+
.ethereum_base_layer
73+
.contract
74+
.provider()
75+
.get_logs(&EthEventFilter::new().from_block(1))
76+
.await
77+
.unwrap();
78+
let event = event.first().unwrap();
79+
80+
match Starknet::StarknetEvents::decode_log(&event.inner).unwrap().data {
81+
Starknet::StarknetEvents::LogStateUpdate(state_update) => {
82+
assert_eq!(
83+
state_update.blockNumber,
84+
I256::from_dec_str(&next_block_number.to_string()).unwrap(),
85+
);
86+
assert_eq!(state_update.blockHash, U256::from(new_block_hash));
87+
}
88+
_ => panic!("Expected LogStateUpdate event"),
89+
}
90+
}

0 commit comments

Comments
 (0)