Skip to content

Commit c60c9cd

Browse files
committed
apollo_l1_provider: add test for each type of L1 event we filter on
1 parent 3fe86e2 commit c60c9cd

File tree

8 files changed

+266
-19
lines changed

8 files changed

+266
-19
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/apollo_l1_provider/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,6 @@ rstest.workspace = true
5252
starknet-types-core.workspace = true
5353
starknet_api = { workspace = true, features = ["testing"] }
5454
tokio = { workspace = true, features = ["test-util"] }
55+
url.workspace = true
5556
[lints]
5657
workspace = true

crates/apollo_l1_provider/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,13 @@ impl From<L1ProviderConfig> for TransactionManagerConfig {
3636

3737
pub const fn event_identifiers_to_track() -> &'static [EventIdentifier] {
3838
&[
39+
// LogMessageToL2(address,uint256,uint256,uint256[],uint256,uint256)
3940
LOG_MESSAGE_TO_L2_EVENT_IDENTIFIER,
41+
// MessageToL2CancellationStarted(address,uint256,uint256)
4042
MESSAGE_TO_L2_CANCELLATION_STARTED_EVENT_IDENTIFIER,
43+
// MessageToL2Canceled(address,uint256,uint256,uint256[],uint256,uint256)
4144
MESSAGE_TO_L2_CANCELED_EVENT_IDENTIFIER,
45+
// ConsumedMessageToL2(address,uint256,uint256,uint256[],uint256,uint256)
4246
CONSUMED_MESSAGE_TO_L2_EVENT_IDENTIFIER,
4347
]
4448
}

crates/apollo_l1_provider/tests/flow_test_consumed.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use papyrus_base_layer::{
1313
MockBaseLayerContract,
1414
};
1515
use starknet_api::block::{BlockNumber, BlockTimestamp};
16-
use starknet_api::core::{EntryPointSelector, Nonce};
16+
use starknet_api::core::{ContractAddress, EntryPointSelector, Nonce};
1717
use starknet_api::transaction::fields::{Calldata, Fee};
1818
use starknet_api::transaction::{L1HandlerTransaction, TransactionVersion};
1919
use starknet_types_core::felt::Felt;
@@ -45,8 +45,8 @@ async fn l1_handler_tx_consumed_txs() {
4545
let l1_handler_tx = L1HandlerTransaction {
4646
version: TransactionVersion::default(),
4747
nonce: Nonce::default(),
48-
contract_address: L1_CONTRACT_ADDRESS.parse().unwrap(),
49-
entry_point_selector: EntryPointSelector(Felt::from_hex_unchecked(L2_ENTRY_POINT)),
48+
contract_address: ContractAddress::from(L1_CONTRACT_ADDRESS),
49+
entry_point_selector: EntryPointSelector(Felt::from(L2_ENTRY_POINT)),
5050
calldata: Calldata(call_data.into()),
5151
};
5252

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
use std::str::FromStr;
2+
3+
use alloy::consensus::Header as HeaderInner;
4+
use alloy::primitives::{
5+
keccak256,
6+
BlockHash,
7+
Bytes,
8+
Log as LogInner,
9+
LogData,
10+
TxHash,
11+
B256,
12+
U256,
13+
};
14+
use alloy::providers::{Provider, ProviderBuilder};
15+
use alloy::rpc::types::{Block, BlockTransactions, Header, Log};
16+
use alloy::transports::mock::Asserter;
17+
use apollo_l1_provider::event_identifiers_to_track;
18+
use papyrus_base_layer::ethereum_base_layer_contract::{
19+
EthereumBaseLayerConfig,
20+
EthereumBaseLayerContract,
21+
};
22+
use papyrus_base_layer::test_utils::{
23+
DEFAULT_ANVIL_L1_ACCOUNT_ADDRESS,
24+
DEFAULT_ANVIL_L1_DEPLOYED_ADDRESS,
25+
};
26+
mod utils;
27+
use papyrus_base_layer::BaseLayerContract;
28+
use utils::{L1_CONTRACT_ADDRESS, L2_ENTRY_POINT};
29+
30+
// This test requires that we do some manual work to produce the logs we expect to get from the
31+
// Starknet L1 contract. The reason we don't just post events to L1 and have them scraped is that
32+
// some of the log types don't correspond to actions we can just do to the base layer, like marking
33+
// a tx as consumed on L2 (which requires a state update). We also don't know which additional logs
34+
// may be added to the list of filtered logs, which is the point of this test (to protect against
35+
// future additions). So we leave it to anyone that adds that new message from L1 to L2 to also make
36+
// an example log and post it as part of the test, to make sure it is properly parsed all the way up
37+
// to the provider.
38+
39+
const FAKE_HASH: &str = "0x1234567890123456789012345678901234567890123456789012345678901234";
40+
41+
#[tokio::test]
42+
async fn all_event_types_must_be_filtered_and_parsed() {
43+
// Setup.
44+
// Make a mock L1
45+
let asserter = Asserter::new();
46+
let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
47+
48+
let mut base_layer = EthereumBaseLayerContract::new_with_provider(
49+
EthereumBaseLayerConfig::default(),
50+
provider.root().clone(),
51+
);
52+
53+
// We can just return the same block all the time, it will only affect the timestamps.
54+
let dummy_block: Block<B256, Header> = dummy_block();
55+
56+
// Put together the log that corresponds to each type of event in event_identifiers_to_track().
57+
// Then filter them one at a time. If any iteration doesn't return an event, it means we fail to
58+
// filter for it. If any iteration returns an error, we know something is wrong.
59+
// TODO(guyn): add the scraper and provider parsing.
60+
let mut block_number = 1;
61+
let filters = event_identifiers_to_track();
62+
63+
let mut expected_logs = Vec::with_capacity(filters.len());
64+
65+
// This log is for LOG_MESSAGE_TO_L2_EVENT_IDENTIFIER (must check that this is the first log in
66+
// filters)
67+
let expected_message_to_l2_log = encode_message_into_log(
68+
filters[0],
69+
block_number,
70+
&[U256::from(15), U256::from(202)],
71+
U256::from(127),
72+
Some(U256::from(420)),
73+
);
74+
block_number += 1;
75+
asserter.push_success(&vec![expected_message_to_l2_log.clone()]);
76+
expected_logs.push(expected_message_to_l2_log);
77+
asserter.push_success(&dummy_block);
78+
79+
// This log is for MESSAGE_TO_L2_CANCELLATION_STARTED_EVENT_IDENTIFIER (must check that this is
80+
// the second log in filters)
81+
let expected_message_to_l2_cancellation_started_log = encode_message_into_log(
82+
filters[1],
83+
block_number,
84+
&[U256::from(1), U256::from(2)],
85+
U256::from(0),
86+
None,
87+
);
88+
block_number += 1;
89+
asserter.push_success(&vec![expected_message_to_l2_cancellation_started_log.clone()]);
90+
expected_logs.push(expected_message_to_l2_cancellation_started_log);
91+
asserter.push_success(&dummy_block);
92+
93+
// This log is for MESSAGE_TO_L2_CANCELED_EVENT_IDENTIFIER (must check that this is the third
94+
// log in filters)
95+
let expected_message_to_l2_canceled_log = encode_message_into_log(
96+
filters[2],
97+
block_number,
98+
&[U256::from(1), U256::from(2)],
99+
U256::from(0),
100+
None,
101+
);
102+
block_number += 1;
103+
asserter.push_success(&vec![expected_message_to_l2_canceled_log.clone()]);
104+
expected_logs.push(expected_message_to_l2_canceled_log);
105+
asserter.push_success(&dummy_block);
106+
107+
// This log is for CONSUMED_MESSAGE_TO_L2_EVENT_IDENTIFIER (must check that this is the fourth
108+
// log in filters)
109+
let expected_consumed_message_to_l2_log = encode_message_into_log(
110+
filters[3],
111+
block_number,
112+
&[U256::from(1), U256::from(2)],
113+
U256::from(0),
114+
Some(U256::from(1)),
115+
);
116+
block_number += 1;
117+
asserter.push_success(&vec![expected_consumed_message_to_l2_log.clone()]);
118+
expected_logs.push(expected_consumed_message_to_l2_log);
119+
asserter.push_success(&dummy_block);
120+
121+
// If new log types are needed, they must be added here.
122+
123+
// Check that each event type has a corresponding log.
124+
for filter in filters {
125+
// Only filter for one event at a time, to make sure we trigger on all events.
126+
let events = base_layer.events(0..=block_number, &[filter]).await.unwrap_or_else(|_| {
127+
panic!("should succeed in getting events for filter: {:?}", filter)
128+
});
129+
assert!(events.len() == 1, "Expected 1 event for filter: {:?}", filter);
130+
}
131+
}
132+
133+
fn dummy_block<T>() -> Block<T, Header> {
134+
Block {
135+
header: Header {
136+
hash: BlockHash::from_str(FAKE_HASH).unwrap(),
137+
inner: HeaderInner { number: 3, base_fee_per_gas: Some(5), ..Default::default() },
138+
total_difficulty: None,
139+
size: None,
140+
},
141+
transactions: BlockTransactions::<T>::default(),
142+
uncles: vec![],
143+
withdrawals: None,
144+
}
145+
}
146+
147+
// Each function signature is hashed using keccak256 to get the selector.
148+
// For example, LogMessageToL2(address,uint256,uint256,uint256[],uint256,uint256)
149+
// becomes "db80dd488acf86d17c747445b0eabb5d57c541d3bd7b6b87af987858e5066b2b".
150+
fn filter_to_hash(filter: &str) -> String {
151+
format!("{:x}", keccak256(filter.as_bytes()))
152+
}
153+
154+
/// Encodes the non-indexed parameters of LogMessageToL2 event data.
155+
/// Parameters: (payload: uint256[], nonce: uint256, fee: uint256)
156+
///
157+
/// ABI encoding for tuple (uint256[], uint256, uint256):
158+
/// - Offset to array (32 bytes)
159+
/// - nonce value (32 bytes)
160+
/// - fee value (32 bytes)
161+
/// - Array length (32 bytes)
162+
/// - Array elements (32 bytes each)
163+
fn encode_log_message_to_l2_data(payload: &[U256], nonce: U256, fee: U256) -> Bytes {
164+
// Instead of the payload array data, we only put in the head section the offset to where the
165+
// data will be stored. This would be 3 words from the start of the head (offset, nonce, fee =
166+
// 96 bytes = 0x60).
167+
let offset = U256::from(96u64);
168+
169+
let mut encoded = Vec::new();
170+
// Offset to the array data (96 bytes).
171+
encoded.extend_from_slice(&offset.to_be_bytes::<32>());
172+
// nonce.
173+
encoded.extend_from_slice(&nonce.to_be_bytes::<32>());
174+
// fee.
175+
encoded.extend_from_slice(&fee.to_be_bytes::<32>());
176+
// Tail section has the payload array data only. It starts with the length of the array.
177+
let array_len = U256::from(payload.len());
178+
encoded.extend_from_slice(&array_len.to_be_bytes::<32>());
179+
// Finally, write the array elements.
180+
for item in payload {
181+
encoded.extend_from_slice(&item.to_be_bytes::<32>());
182+
}
183+
184+
Bytes::from(encoded)
185+
}
186+
187+
// Same as above, but for the other event types (that don't include a fee).
188+
fn encode_other_event_data(payload: &[U256], nonce: U256) -> Bytes {
189+
// Instead of the payload array data, we only put in the head section the offset to where the
190+
// data will be stored. This would be 2 words from the start of the head (offset, nonce = 64
191+
// bytes = 0x40).
192+
let offset = U256::from(64u64);
193+
194+
let mut encoded = Vec::new();
195+
// Offset to the array data (96 bytes).
196+
encoded.extend_from_slice(&offset.to_be_bytes::<32>());
197+
// nonce.
198+
encoded.extend_from_slice(&nonce.to_be_bytes::<32>());
199+
// Tail section has the payload array data only. It starts with the length of the array.
200+
let array_len = U256::from(payload.len());
201+
encoded.extend_from_slice(&array_len.to_be_bytes::<32>());
202+
// Finally, write the array elements.
203+
for item in payload {
204+
encoded.extend_from_slice(&item.to_be_bytes::<32>());
205+
}
206+
207+
Bytes::from(encoded)
208+
}
209+
210+
fn encode_message_into_log(
211+
selector: &str,
212+
block_number: u64,
213+
payload: &[U256],
214+
nonce: U256,
215+
fee: Option<U256>,
216+
) -> Log {
217+
// Add zero padding to the address to make it 32 bytes
218+
let starknet_address = DEFAULT_ANVIL_L1_ACCOUNT_ADDRESS.to_bigint().to_str_radix(16);
219+
let starknet_address = format!("{:0>64}", starknet_address);
220+
221+
let encoded_data = match fee {
222+
Some(fee) => encode_log_message_to_l2_data(payload, nonce, fee),
223+
None => encode_other_event_data(payload, nonce),
224+
};
225+
Log {
226+
inner: LogInner {
227+
address: DEFAULT_ANVIL_L1_DEPLOYED_ADDRESS.parse().unwrap(),
228+
data: LogData::new_unchecked(
229+
vec![
230+
filter_to_hash(selector).parse().unwrap(),
231+
starknet_address.parse().unwrap(),
232+
U256::from(L1_CONTRACT_ADDRESS).into(),
233+
U256::from(L2_ENTRY_POINT).into(),
234+
],
235+
encoded_data,
236+
),
237+
},
238+
block_hash: Some(BlockHash::from_str(FAKE_HASH).unwrap()),
239+
block_number: Some(block_number),
240+
block_timestamp: None,
241+
transaction_hash: Some(TxHash::from_str(FAKE_HASH).unwrap()),
242+
transaction_index: Some(block_number + 1),
243+
log_index: Some(block_number + 2),
244+
removed: false,
245+
}
246+
}

crates/apollo_l1_provider/tests/utils/mod.rs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// TODO(guyn): remove this after sorting out the compilation flags on all the flow tests.
2+
#![allow(dead_code)]
13
use std::error::Error;
24
use std::fmt::Debug;
35
use std::future::Future;
@@ -47,10 +49,9 @@ pub(crate) const TIMELOCK_DURATION: Duration = Duration::from_secs(30);
4749
pub(crate) const WAIT_FOR_ASYNC_PROCESSING_DURATION: Duration = Duration::from_millis(50);
4850
const NUMBER_OF_BLOCKS_TO_MINE: u64 = 100;
4951
const CHAIN_ID: ChainId = ChainId::Mainnet;
50-
pub(crate) const L1_CONTRACT_ADDRESS: &str = "0x12";
51-
pub(crate) const L2_ENTRY_POINT: &str = "0x34";
52+
pub(crate) const L1_CONTRACT_ADDRESS: u64 = 1;
53+
pub(crate) const L2_ENTRY_POINT: u64 = 34;
5254
pub(crate) const CALL_DATA: &[u8] = &[1_u8, 2_u8];
53-
#[allow(dead_code)]
5455
pub(crate) const CALL_DATA_2: &[u8] = &[3_u8, 4_u8];
5556

5657
const START_L1_BLOCK: L1BlockReference = L1BlockReference { number: 0, hash: L1BlockHash([0; 32]) };
@@ -182,11 +183,7 @@ pub(crate) async fn send_message_from_l1_to_l2(
182183
let call_data = convert_call_data_to_u256(call_data);
183184
let fee = 1_u8;
184185
let message_to_l2 = contract
185-
.sendMessageToL2(
186-
L1_CONTRACT_ADDRESS.parse().unwrap(),
187-
L2_ENTRY_POINT.parse().unwrap(),
188-
call_data,
189-
)
186+
.sendMessageToL2(U256::from(L1_CONTRACT_ADDRESS), U256::from(L2_ENTRY_POINT), call_data)
190187
.value(U256::from(fee));
191188
let receipt = message_to_l2.send().await.unwrap().get_receipt().await.unwrap();
192189

@@ -239,8 +236,8 @@ pub(crate) async fn send_cancellation_request(
239236
let contract = &base_layer.ethereum_base_layer.contract;
240237
let call_data = convert_call_data_to_u256(call_data);
241238
let cancellation_request = contract.startL1ToL2MessageCancellation(
242-
L1_CONTRACT_ADDRESS.parse().unwrap(),
243-
L2_ENTRY_POINT.parse().unwrap(),
239+
U256::from(L1_CONTRACT_ADDRESS),
240+
U256::from(L2_ENTRY_POINT),
244241
call_data,
245242
nonce,
246243
);
@@ -261,8 +258,8 @@ pub(crate) async fn send_cancellation_finalization(
261258
let contract = &base_layer.ethereum_base_layer.contract;
262259
let call_data = convert_call_data_to_u256(call_data);
263260
let cancellation_finalization = contract.cancelL1ToL2Message(
264-
L1_CONTRACT_ADDRESS.parse().unwrap(),
265-
L2_ENTRY_POINT.parse().unwrap(),
261+
U256::from(L1_CONTRACT_ADDRESS),
262+
U256::from(L2_ENTRY_POINT),
266263
call_data,
267264
nonce,
268265
);

crates/papyrus_base_layer/src/base_layer_test.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ pub fn in_ci() -> bool {
1717
fn base_layer_with_mocked_provider() -> (EthereumBaseLayerContract, Asserter) {
1818
// See alloy docs, functions as a queue of mocked responses, success or failure.
1919
let asserter = Asserter::new();
20-
2120
let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone()).root().clone();
2221
let config = EthereumBaseLayerConfig::default();
2322
let base_layer = EthereumBaseLayerContract::new_with_provider(config, provider);

crates/papyrus_base_layer/src/ethereum_base_layer_contract.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,14 @@ impl BaseLayerContract for EthereumBaseLayerContract {
152152
async fn events<'a>(
153153
&'a mut self,
154154
block_range: RangeInclusive<u64>,
155-
events: &'a [&'a str],
155+
event_types_to_filter: &'a [&'a str],
156156
) -> EthereumBaseLayerResult<Vec<L1Event>> {
157157
// Don't actually need mutability here, and using mut self doesn't work with async move in
158158
// the loop below.
159159
let immutable_self = &*self;
160160
let filter = EthEventFilter::new()
161161
.select(block_range.clone())
162-
.events(events)
162+
.events(event_types_to_filter)
163163
.address(immutable_self.config.starknet_contract_address);
164164
let matching_logs = tokio::time::timeout(
165165
immutable_self.config.timeout_millis,
@@ -178,7 +178,6 @@ impl BaseLayerContract for EthereumBaseLayerContract {
178178
parse_event(log, header.timestamp)
179179
}
180180
});
181-
182181
futures::future::join_all(block_header_futures).await.into_iter().collect()
183182
}
184183

0 commit comments

Comments
 (0)