Skip to content

Commit 91077f1

Browse files
committed
apollo_committer: add request_paths_and_commit_block tests
1 parent 39fe75d commit 91077f1

4 files changed

Lines changed: 385 additions & 0 deletions

File tree

crates/apollo_committer/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ tracing.workspace = true
2424
[dev-dependencies]
2525
assert_matches.workspace = true
2626
indexmap.workspace = true
27+
starknet_committer = { workspace = true, features = ["testing"] }
2728
starknet_patricia = { workspace = true, features = ["testing"] }
2829
tokio.workspace = true
2930

crates/apollo_committer/src/committer_test.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ use starknet_patricia_storage::map_storage::MapStorage;
2020
use super::Committer;
2121
use crate::committer::StorageConstructor;
2222

23+
#[cfg(feature = "os_input")]
24+
#[path = "request_paths_and_commit_block_tests.rs"]
25+
mod request_paths_and_commit_block_tests;
26+
2327
pub type ApolloTestStorage = MapStorage;
2428
pub type ApolloTestCommitter = Committer<ApolloTestStorage, IndexDb<ApolloTestStorage>>;
2529

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
use std::collections::{BTreeSet, HashMap};
2+
use std::sync::LazyLock;
3+
4+
use apollo_committer_types::committer_types::{
5+
AccessedKeys,
6+
CommitBlockRequest,
7+
ReadPathsAndCommitBlockRequest,
8+
RevertBlockRequest,
9+
};
10+
use indexmap::indexmap;
11+
use starknet_api::block::BlockNumber;
12+
use starknet_api::block_hash::state_diff_hash::calculate_state_diff_hash;
13+
use starknet_api::core::{ClassHash, CompiledClassHash, ContractAddress, StateDiffCommitment};
14+
use starknet_api::hash::HashOutput;
15+
use starknet_api::state::ThinStateDiff;
16+
use starknet_committer::block_committer::input::{
17+
contract_address_into_node_index,
18+
StarknetStorageKey,
19+
StarknetStorageValue,
20+
};
21+
use starknet_committer::db::forest_trait::forest_trait_witnesses::ForestReaderWithWitnesses;
22+
use starknet_committer::db::forest_trait::{EmptyInitialReadContext, ForestReader};
23+
use starknet_committer::db::index_db::IndexDbReadContext;
24+
use starknet_committer::db::serde_db_utils::accessed_keys_digest;
25+
use starknet_committer::hash_function::hash::TreeHashFunctionImpl;
26+
use starknet_committer::patricia_merkle_tree::leaf::leaf_impl::ContractState;
27+
use starknet_committer::patricia_merkle_tree::tree::LeavesRequest;
28+
use starknet_committer::patricia_merkle_tree::types::{
29+
class_hash_into_node_index,
30+
CompiledClassHash as CommitterCompiledClassHash,
31+
StarknetForestProofs,
32+
};
33+
use starknet_patricia::patricia_merkle_tree::node_data::leaf::Leaf;
34+
use starknet_patricia::patricia_merkle_tree::storage_proof_verification::verify_patricia_proof;
35+
use starknet_patricia::patricia_merkle_tree::types::NodeIndex;
36+
use starknet_patricia::patricia_merkle_tree::updated_skeleton_tree::hash_function::TreeHashFunction;
37+
38+
use crate::committer::committer_test::{new_test_committer, ApolloTestCommitter};
39+
40+
const ACCESSED_STORAGE_VALUE_1: u128 = 100;
41+
const ACCESSED_STORAGE_VALUE_2: u128 = 200;
42+
const UNACCESSED_CLASS_HASH: u64 = 2;
43+
const UNACCESSED_CONTRACT: u128 = 3;
44+
const UNACCESSED_STORAGE_KEY: u128 = 11;
45+
const UNACCESSED_STORAGE_VALUE: u128 = 300;
46+
47+
static ACCESSED_CLASS_HASH: LazyLock<ClassHash> = LazyLock::new(|| ClassHash(1u64.into()));
48+
static ACCESSED_CONTRACT_1: LazyLock<ContractAddress> =
49+
LazyLock::new(|| ContractAddress::from(1u128));
50+
static ACCESSED_CONTRACT_2: LazyLock<ContractAddress> =
51+
LazyLock::new(|| ContractAddress::from(2u128));
52+
static ACCESSED_STORAGE_KEY_1: LazyLock<StarknetStorageKey> =
53+
LazyLock::new(|| StarknetStorageKey::from(10u128));
54+
static ACCESSED_STORAGE_KEY_2: LazyLock<StarknetStorageKey> =
55+
LazyLock::new(|| StarknetStorageKey::from(20u128));
56+
57+
static ACCESSED_KEYS: LazyLock<AccessedKeys> = LazyLock::new(|| AccessedKeys {
58+
accessed_class_hashes: BTreeSet::from([*ACCESSED_CLASS_HASH]),
59+
accessed_contracts: BTreeSet::from([*ACCESSED_CONTRACT_1, *ACCESSED_CONTRACT_2]),
60+
storage_keys: BTreeSet::from([
61+
(*ACCESSED_CONTRACT_1, ACCESSED_STORAGE_KEY_1.0),
62+
(*ACCESSED_CONTRACT_2, ACCESSED_STORAGE_KEY_2.0),
63+
]),
64+
});
65+
66+
static EXPECTED_ACCESSED_KEYS_DIGEST: LazyLock<[u8; 32]> = LazyLock::new(|| {
67+
let mut leaves_request = LeavesRequest::from(&*ACCESSED_KEYS);
68+
let sorted_leaves = leaves_request.sorted();
69+
accessed_keys_digest(&sorted_leaves)
70+
});
71+
72+
/// Leaf values for accessed class indices. Required to build facts-layout storage from the
73+
/// returned [`PreimageMap`], which contains inner nodes only.
74+
static ACCESSED_CLASS_LEAVES: LazyLock<HashMap<ClassHash, CommitterCompiledClassHash>> =
75+
LazyLock::new(|| {
76+
HashMap::from([(*ACCESSED_CLASS_HASH, CommitterCompiledClassHash(ACCESSED_CLASS_HASH.0))])
77+
});
78+
79+
/// Leaf values for accessed storage indices, per contract. Required to build facts-layout
80+
/// storage from the returned [`PreimageMap`], which contains inner nodes only.
81+
static ACCESSED_STORAGE_LEAVES: LazyLock<
82+
HashMap<ContractAddress, HashMap<StarknetStorageKey, StarknetStorageValue>>,
83+
> = LazyLock::new(|| {
84+
HashMap::from([
85+
(
86+
*ACCESSED_CONTRACT_1,
87+
HashMap::from([(
88+
*ACCESSED_STORAGE_KEY_1,
89+
StarknetStorageValue(ACCESSED_STORAGE_VALUE_1.into()),
90+
)]),
91+
),
92+
(
93+
*ACCESSED_CONTRACT_2,
94+
HashMap::from([(
95+
*ACCESSED_STORAGE_KEY_2,
96+
StarknetStorageValue(ACCESSED_STORAGE_VALUE_2.into()),
97+
)]),
98+
),
99+
])
100+
});
101+
102+
static BLOCK_0_STATE_DIFF: LazyLock<ThinStateDiff> = LazyLock::new(|| {
103+
let class_hash = *ACCESSED_CLASS_HASH;
104+
let unaccessed_class_hash = ClassHash(UNACCESSED_CLASS_HASH.into());
105+
let contract_1 = *ACCESSED_CONTRACT_1;
106+
let contract_2 = *ACCESSED_CONTRACT_2;
107+
let unaccessed_contract = ContractAddress::from(UNACCESSED_CONTRACT);
108+
109+
ThinStateDiff {
110+
deployed_contracts: indexmap! {
111+
contract_1 => class_hash,
112+
contract_2 => class_hash,
113+
unaccessed_contract => class_hash,
114+
},
115+
storage_diffs: indexmap! {
116+
contract_1 => indexmap! {
117+
ACCESSED_STORAGE_KEY_1.0 => ACCESSED_STORAGE_VALUE_1.into(),
118+
UNACCESSED_STORAGE_KEY.into() => UNACCESSED_STORAGE_VALUE.into(),
119+
},
120+
contract_2 => indexmap! {
121+
ACCESSED_STORAGE_KEY_2.0 => ACCESSED_STORAGE_VALUE_2.into(),
122+
},
123+
},
124+
class_hash_to_compiled_class_hash: indexmap! {
125+
class_hash => CompiledClassHash(ACCESSED_CLASS_HASH.0),
126+
unaccessed_class_hash => CompiledClassHash(UNACCESSED_CLASS_HASH.into()),
127+
},
128+
..Default::default()
129+
}
130+
});
131+
132+
static BLOCK_1_STATE_DIFF: LazyLock<ThinStateDiff> = LazyLock::new(|| ThinStateDiff {
133+
storage_diffs: indexmap! {
134+
*ACCESSED_CONTRACT_1 => indexmap! {
135+
ACCESSED_STORAGE_KEY_1.0 => 101_u128.into(),
136+
UNACCESSED_STORAGE_KEY.into() => 301_u128.into(),
137+
},
138+
},
139+
..Default::default()
140+
});
141+
142+
static BLOCK_1_REVERSED_STATE_DIFF: LazyLock<ThinStateDiff> = LazyLock::new(|| ThinStateDiff {
143+
storage_diffs: indexmap! {
144+
*ACCESSED_CONTRACT_1 => indexmap! {
145+
ACCESSED_STORAGE_KEY_1.0 => ACCESSED_STORAGE_VALUE_1.into(),
146+
UNACCESSED_STORAGE_KEY.into() => UNACCESSED_STORAGE_VALUE.into(),
147+
},
148+
},
149+
..Default::default()
150+
});
151+
152+
fn read_paths_and_commit_block_request(
153+
state_diff: ThinStateDiff,
154+
state_diff_commitment: Option<StateDiffCommitment>,
155+
height: u64,
156+
accessed_keys: AccessedKeys,
157+
) -> ReadPathsAndCommitBlockRequest {
158+
ReadPathsAndCommitBlockRequest {
159+
commit: CommitBlockRequest {
160+
state_diff,
161+
state_diff_commitment,
162+
height: BlockNumber(height),
163+
},
164+
accessed_keys,
165+
}
166+
}
167+
168+
fn leaf_hashes<Key, PatriciaLeaf>(
169+
leaves: &HashMap<Key, PatriciaLeaf>,
170+
key_into_node_index: impl Fn(&Key) -> NodeIndex,
171+
) -> HashMap<NodeIndex, HashOutput>
172+
where
173+
PatriciaLeaf: Leaf,
174+
TreeHashFunctionImpl: TreeHashFunction<PatriciaLeaf>,
175+
{
176+
leaves
177+
.iter()
178+
.map(|(key, leaf)| {
179+
(key_into_node_index(key), TreeHashFunctionImpl::compute_leaf_hash(leaf))
180+
})
181+
.collect()
182+
}
183+
184+
/// Verifies that `patricia_proofs` contains a valid proof for the membership of each leaf in
185+
/// `accessed_leaves`.
186+
fn verify_witness_patricia_paths(
187+
patricia_proofs: &StarknetForestProofs,
188+
accessed_keys: &AccessedKeys,
189+
class_leaves: &HashMap<ClassHash, CommitterCompiledClassHash>,
190+
storage_leaves: &HashMap<ContractAddress, HashMap<StarknetStorageKey, StarknetStorageValue>>,
191+
classes_trie_root: HashOutput,
192+
contracts_trie_root: HashOutput,
193+
) {
194+
verify_patricia_proof::<CommitterCompiledClassHash, TreeHashFunctionImpl>(
195+
classes_trie_root,
196+
&patricia_proofs.classes_trie_proof,
197+
&leaf_hashes(class_leaves, class_hash_into_node_index),
198+
)
199+
.unwrap_or_else(|error| panic!("classes trie proof verification failed: {error}"));
200+
201+
verify_patricia_proof::<ContractState, TreeHashFunctionImpl>(
202+
contracts_trie_root,
203+
&patricia_proofs.contracts_trie_proof.nodes,
204+
&leaf_hashes(
205+
&patricia_proofs.contracts_trie_proof.leaves,
206+
contract_address_into_node_index,
207+
),
208+
)
209+
.unwrap_or_else(|error| panic!("contracts trie proof verification failed: {error}"));
210+
211+
for contract_address in &accessed_keys.accessed_contracts {
212+
let storage_proof = patricia_proofs
213+
.contracts_trie_storage_proofs
214+
.get(contract_address)
215+
.unwrap_or_else(|| panic!("missing storage trie proof for {contract_address:?}"));
216+
let contract_state = patricia_proofs
217+
.contracts_trie_proof
218+
.leaves
219+
.get(contract_address)
220+
.unwrap_or_else(|| panic!("missing contracts trie leaf for {contract_address:?}"));
221+
verify_patricia_proof::<StarknetStorageValue, TreeHashFunctionImpl>(
222+
contract_state.storage_root_hash,
223+
storage_proof,
224+
&leaf_hashes(
225+
storage_leaves.get(contract_address).unwrap_or_else(|| {
226+
panic!("missing storage leaves for contract {contract_address:?}")
227+
}),
228+
|key| NodeIndex::from(key),
229+
),
230+
)
231+
.unwrap_or_else(|error| {
232+
panic!("storage trie proof verification failed for {contract_address:?}: {error}")
233+
});
234+
}
235+
}
236+
237+
async fn assert_witnesses_and_digest_present(
238+
committer: &mut ApolloTestCommitter,
239+
height: BlockNumber,
240+
expected_patricia_proofs: &StarknetForestProofs,
241+
) {
242+
assert_eq!(
243+
committer.load_witnesses_digest(height).await.unwrap(),
244+
Some(*EXPECTED_ACCESSED_KEYS_DIGEST),
245+
);
246+
assert_eq!(
247+
committer.forest_storage.read_witnesses(height).await.unwrap().as_ref(),
248+
Some(expected_patricia_proofs),
249+
);
250+
}
251+
252+
async fn assert_witnesses_and_digest_absent(
253+
committer: &mut ApolloTestCommitter,
254+
height: BlockNumber,
255+
) {
256+
assert!(committer.load_witnesses_digest(height).await.unwrap().is_none());
257+
assert!(committer.forest_storage.read_witnesses(height).await.unwrap().is_none());
258+
}
259+
260+
/// Flow overview:
261+
/// 1. Commit block 0 via [crate::committer::Committer::read_paths_and_commit_block], requesting
262+
/// witnesses for [`ACCESSED_KEYS`].
263+
/// 2. Verify the returned Patricia proofs via [verify_witness_patricia_paths].
264+
/// 3. Clear trie storage and replay the same request to verify witnesses are loaded from storage
265+
/// rather than recomputed.
266+
/// 4. Assert witnesses and the accessed-keys digest are stored for block 0 via
267+
/// [assert_witnesses_and_digest_present].
268+
#[tokio::test]
269+
async fn read_paths_and_commit_block_happy_flow() {
270+
let mut committer = new_test_committer().await;
271+
let height = 0;
272+
let state_diff = BLOCK_0_STATE_DIFF.clone();
273+
let state_diff_commitment = Some(calculate_state_diff_hash(&state_diff));
274+
let accessed_keys = ACCESSED_KEYS.clone();
275+
let request = read_paths_and_commit_block_request(
276+
state_diff,
277+
state_diff_commitment,
278+
height,
279+
accessed_keys.clone(),
280+
);
281+
282+
let response = committer.read_paths_and_commit_block(request.clone()).await.unwrap();
283+
assert_eq!(committer.offset, BlockNumber(height + 1));
284+
let roots =
285+
committer.forest_storage.read_roots(IndexDbReadContext::create_empty()).await.unwrap();
286+
verify_witness_patricia_paths(
287+
&response.patricia_proofs,
288+
&accessed_keys,
289+
&ACCESSED_CLASS_LEAVES,
290+
&ACCESSED_STORAGE_LEAVES,
291+
roots.classes_trie_root_hash,
292+
roots.contracts_trie_root_hash,
293+
);
294+
295+
// Historical replay should load persisted witnesses, removing trie nodes to assert this.
296+
committer.forest_storage.clear_patricia_trie_nodes_for_test();
297+
298+
let replay_response = committer.read_paths_and_commit_block(request).await.unwrap();
299+
assert_eq!(response.global_root, replay_response.global_root);
300+
assert_eq!(response.patricia_proofs, replay_response.patricia_proofs);
301+
assert_witnesses_and_digest_present(
302+
&mut committer,
303+
BlockNumber(height),
304+
&response.patricia_proofs,
305+
)
306+
.await;
307+
}
308+
309+
/// Flow overview:
310+
/// 1. Commit block 0 via [crate::committer::Committer::commit_block] (no witnesses fetched).
311+
/// 2. Commit block 1 via [crate::committer::Committer::read_paths_and_commit_block], requesting
312+
/// witnesses for [`ACCESSED_KEYS`].
313+
/// 3. Assert witnesses and the accessed-keys digest are present for block 1 via
314+
/// [assert_witnesses_and_digest_present].
315+
/// 4. Revert block 1 via [crate::committer::Committer::revert_block].
316+
/// 5. Assert witnesses and the accessed-keys digest are absent for block 1 via
317+
/// [assert_witnesses_and_digest_absent].
318+
#[tokio::test]
319+
async fn revert_removes_witnesses_and_digest() {
320+
let mut committer = new_test_committer().await;
321+
let height_0 = 0;
322+
let height_1 = 1;
323+
let block_0_state_diff = BLOCK_0_STATE_DIFF.clone();
324+
let block_1_state_diff = BLOCK_1_STATE_DIFF.clone();
325+
let accessed_keys = ACCESSED_KEYS.clone();
326+
327+
committer
328+
.commit_block(CommitBlockRequest {
329+
state_diff: block_0_state_diff.clone(),
330+
state_diff_commitment: Some(calculate_state_diff_hash(&block_0_state_diff)),
331+
height: BlockNumber(height_0),
332+
})
333+
.await
334+
.unwrap();
335+
336+
let block_1_response = committer
337+
.read_paths_and_commit_block(read_paths_and_commit_block_request(
338+
block_1_state_diff.clone(),
339+
Some(calculate_state_diff_hash(&block_1_state_diff)),
340+
height_1,
341+
accessed_keys.clone(),
342+
))
343+
.await
344+
.unwrap();
345+
assert_witnesses_and_digest_present(
346+
&mut committer,
347+
BlockNumber(height_1),
348+
&block_1_response.patricia_proofs,
349+
)
350+
.await;
351+
352+
committer
353+
.revert_block(RevertBlockRequest {
354+
reversed_state_diff: BLOCK_1_REVERSED_STATE_DIFF.clone(),
355+
height: BlockNumber(height_1),
356+
})
357+
.await
358+
.unwrap();
359+
assert_witnesses_and_digest_absent(&mut committer, BlockNumber(height_1)).await;
360+
assert_eq!(committer.offset, BlockNumber(height_1));
361+
}

0 commit comments

Comments
 (0)