Skip to content

Commit ec09738

Browse files
committed
starknet_committer,starknet_patricia: layout-based original tree creation tests
1 parent fd680df commit ec09738

File tree

6 files changed

+325
-5
lines changed

6 files changed

+325
-5
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/starknet_committer/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ tokio = { workspace = true, features = ["rt"] }
3131
tracing.workspace = true
3232

3333
[dev-dependencies]
34+
async-recursion.workspace = true
3435
rstest.workspace = true
3536
rstest_reuse.workspace = true
3637
starknet_api = { workspace = true, features = ["testing"] }
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use rstest::rstest;
2+
use rstest_reuse::apply;
3+
use starknet_api::hash::HashOutput;
4+
use starknet_patricia::patricia_merkle_tree::external_test_utils::MockLeaf;
5+
use starknet_patricia::patricia_merkle_tree::node_data::inner_node::NodeData;
6+
use starknet_patricia::patricia_merkle_tree::updated_skeleton_tree::hash_function::TreeHashFunction;
7+
use starknet_patricia_storage::db_object::EmptyKeyContext;
8+
use starknet_types_core::felt::Felt;
9+
10+
use crate::db::create_original_skeleton_tests::case_helpers::CreateTreeCase;
11+
use crate::db::create_original_skeleton_tests::{create_tree_cases, test_create_original_skeleton};
12+
use crate::db::index_db::db::IndexNodeLayout;
13+
use crate::db::index_db::test_utils::convert_facts_db_to_index_db;
14+
use crate::hash_function::hash::TreeHashFunctionImpl;
15+
16+
impl TreeHashFunction<MockLeaf> for TreeHashFunctionImpl {
17+
fn compute_leaf_hash(leaf_data: &MockLeaf) -> HashOutput {
18+
HashOutput(leaf_data.0)
19+
}
20+
21+
fn compute_node_hash(_node_data: &NodeData<MockLeaf, HashOutput>) -> HashOutput {
22+
HashOutput(Felt::ZERO)
23+
}
24+
}
25+
26+
#[apply(create_tree_cases)]
27+
#[rstest]
28+
#[tokio::test]
29+
async fn test_create_tree_index_layout(
30+
#[case] mut case: CreateTreeCase,
31+
#[values(true, false)] compare_modified_leaves: bool,
32+
) {
33+
let mut storage = convert_facts_db_to_index_db::<MockLeaf, MockLeaf, EmptyKeyContext>(
34+
&mut case.storage,
35+
case.root_hash,
36+
&EmptyKeyContext,
37+
&mut None,
38+
)
39+
.await;
40+
41+
test_create_original_skeleton::<MockLeaf, IndexNodeLayout>(
42+
&mut storage,
43+
&case.leaf_modifications,
44+
case.root_hash,
45+
&case.expected_skeleton_nodes,
46+
case.subtree_height,
47+
compare_modified_leaves,
48+
)
49+
.await;
50+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
#[cfg(test)]
2+
pub mod create_index_tree_test;
13
pub mod db;
24
pub mod leaves;
35
#[cfg(test)]
46
pub mod serde_tests;
7+
#[cfg(any(feature = "testing", test))]
8+
pub mod test_utils;
59
pub mod types;
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
use async_recursion::async_recursion;
2+
use starknet_api::core::ContractAddress;
3+
use starknet_api::hash::{HashOutput, StateRoots};
4+
use starknet_patricia::patricia_merkle_tree::filled_tree::node::{FactDbFilledNode, FilledNode};
5+
use starknet_patricia::patricia_merkle_tree::filled_tree::node_serde::{
6+
FactNodeDeserializationContext,
7+
PatriciaPrefix,
8+
FACT_LAYOUT_DB_KEY_SEPARATOR,
9+
};
10+
use starknet_patricia::patricia_merkle_tree::node_data::inner_node::{
11+
BinaryData,
12+
EdgeData,
13+
NodeData,
14+
};
15+
use starknet_patricia::patricia_merkle_tree::node_data::leaf::Leaf;
16+
use starknet_patricia::patricia_merkle_tree::types::NodeIndex;
17+
use starknet_patricia::patricia_merkle_tree::updated_skeleton_tree::hash_function::TreeHashFunction;
18+
use starknet_patricia_storage::db_object::{DBObject, EmptyKeyContext, HasStaticPrefix};
19+
use starknet_patricia_storage::map_storage::MapStorage;
20+
use starknet_patricia_storage::storage_trait::{create_db_key, DbHashMap, DbValue, Storage};
21+
22+
use crate::block_committer::input::{try_node_index_into_contract_address, StarknetStorageValue};
23+
use crate::db::index_db::leaves::{
24+
IndexLayoutCompiledClassHash,
25+
IndexLayoutContractState,
26+
IndexLayoutStarknetStorageValue,
27+
INDEX_LAYOUT_DB_KEY_SEPARATOR,
28+
};
29+
use crate::db::index_db::types::{EmptyNodeData, IndexFilledNode};
30+
use crate::hash_function::hash::TreeHashFunctionImpl;
31+
use crate::patricia_merkle_tree::leaf::leaf_impl::ContractState;
32+
use crate::patricia_merkle_tree::types::CompiledClassHash;
33+
34+
/// Converts a complete Facts-layout forest DB to Index-layout.
35+
/// Expects all nodes to exist (panics if a node is missing).
36+
/// Use this for converting input storage before running tests.
37+
pub async fn convert_facts_forest_db_to_index_db(
38+
storage: &mut MapStorage,
39+
roots: StateRoots,
40+
) -> MapStorage {
41+
convert_forest(storage, roots, false).await
42+
}
43+
44+
/// Converts a Facts-layout filled forest (delta) to Index-layout.
45+
/// Skips missing branches (nodes that weren't updated).
46+
/// Use this for converting expected_facts which only contains updated nodes.
47+
pub async fn convert_facts_filled_forest_to_index(
48+
storage: &mut MapStorage,
49+
roots: StateRoots,
50+
) -> MapStorage {
51+
convert_forest(storage, roots, true).await
52+
}
53+
54+
/// Converts a single Facts-layout trie to Index-layout.
55+
/// Expects all nodes to exist (panics if a node is missing).
56+
pub async fn convert_facts_db_to_index_db<FactsLeaf, IndexLeaf, KeyContext>(
57+
storage: &mut MapStorage,
58+
root_hash: HashOutput,
59+
key_context: &KeyContext,
60+
current_leaves: &mut Option<Vec<(NodeIndex, FactsLeaf)>>,
61+
) -> MapStorage
62+
where
63+
FactsLeaf: Leaf + Into<IndexLeaf> + HasStaticPrefix<KeyContext = KeyContext>,
64+
IndexLeaf: Leaf + HasStaticPrefix<KeyContext = KeyContext>,
65+
TreeHashFunctionImpl: TreeHashFunction<IndexLeaf>,
66+
KeyContext: Sync,
67+
{
68+
convert_single_trie(storage, root_hash, key_context, current_leaves, false).await
69+
}
70+
71+
/// Converts all tries in a forest from Facts-layout to Index-layout.
72+
///
73+
/// If skip_missing is true, missing nodes are silently skipped. Used when the original facts DB
74+
/// only contains the tree subset that was necessary for tests. Use false when the input is known to
75+
/// contain a full tree.
76+
async fn convert_forest(
77+
storage: &mut MapStorage,
78+
roots: StateRoots,
79+
skip_missing: bool,
80+
) -> MapStorage {
81+
let mut contract_leaves: Option<Vec<(NodeIndex, ContractState)>> = Some(Vec::new());
82+
let mut index_storage =
83+
convert_single_trie::<ContractState, IndexLayoutContractState, EmptyKeyContext>(
84+
storage,
85+
roots.contracts_trie_root_hash,
86+
&EmptyKeyContext,
87+
&mut contract_leaves,
88+
skip_missing,
89+
)
90+
.await
91+
.0;
92+
93+
for (index, contract_leaf) in contract_leaves.unwrap() {
94+
let contract_address = try_node_index_into_contract_address(&index).unwrap();
95+
let storage_root = contract_leaf.storage_root_hash;
96+
let contract_storage_entries =
97+
convert_single_trie::<
98+
StarknetStorageValue,
99+
IndexLayoutStarknetStorageValue,
100+
ContractAddress,
101+
>(storage, storage_root, &contract_address, &mut None, skip_missing)
102+
.await;
103+
index_storage.extend(contract_storage_entries.0);
104+
}
105+
106+
let classes_storage =
107+
convert_single_trie::<CompiledClassHash, IndexLayoutCompiledClassHash, EmptyKeyContext>(
108+
storage,
109+
roots.classes_trie_root_hash,
110+
&EmptyKeyContext,
111+
&mut None,
112+
skip_missing,
113+
)
114+
.await;
115+
index_storage.extend(classes_storage.0);
116+
117+
MapStorage(index_storage)
118+
}
119+
120+
/// Converts a single trie from Facts-layout to Index-layout.
121+
async fn convert_single_trie<FactsLeaf, IndexLeaf, KeyContext>(
122+
storage: &mut MapStorage,
123+
root_hash: HashOutput,
124+
key_context: &KeyContext,
125+
current_leaves: &mut Option<Vec<(NodeIndex, FactsLeaf)>>,
126+
skip_missing: bool,
127+
) -> MapStorage
128+
where
129+
FactsLeaf: Leaf + Into<IndexLeaf> + HasStaticPrefix<KeyContext = KeyContext>,
130+
IndexLeaf: Leaf + HasStaticPrefix<KeyContext = KeyContext>,
131+
TreeHashFunctionImpl: TreeHashFunction<IndexLeaf>,
132+
KeyContext: Sync,
133+
{
134+
let mut index_layout_storage = MapStorage(DbHashMap::new());
135+
traverse_and_convert::<FactsLeaf, IndexLeaf, KeyContext>(
136+
storage,
137+
&mut index_layout_storage,
138+
NodeIndex::ROOT,
139+
root_hash,
140+
key_context,
141+
current_leaves,
142+
skip_missing,
143+
)
144+
.await;
145+
index_layout_storage
146+
}
147+
148+
/// Recursively traverses a Facts-layout trie and converts each node to Index-layout.
149+
#[async_recursion]
150+
async fn traverse_and_convert<FactsLeaf, IndexLeaf, KeyContext>(
151+
facts_storage: &mut MapStorage,
152+
index_layout_storage: &mut MapStorage,
153+
current_index: NodeIndex,
154+
current_hash: HashOutput,
155+
key_context: &KeyContext,
156+
current_leaves: &mut Option<Vec<(NodeIndex, FactsLeaf)>>,
157+
skip_missing: bool,
158+
) where
159+
FactsLeaf: Leaf + Into<IndexLeaf> + HasStaticPrefix<KeyContext = KeyContext>,
160+
IndexLeaf: Leaf + HasStaticPrefix<KeyContext = KeyContext>,
161+
TreeHashFunctionImpl: TreeHashFunction<IndexLeaf>,
162+
KeyContext: Sync,
163+
{
164+
if current_hash == HashOutput::ROOT_OF_EMPTY_TREE {
165+
return;
166+
}
167+
let db_prefix = if current_index.is_leaf() {
168+
FactsLeaf::get_static_prefix(key_context)
169+
} else {
170+
PatriciaPrefix::InnerNode.into()
171+
};
172+
let facts_db_key =
173+
create_db_key(db_prefix, FACT_LAYOUT_DB_KEY_SEPARATOR, &current_hash.0.to_bytes_be());
174+
175+
// Try to get the node from storage.
176+
let filled_node_raw: Option<DbValue> = facts_storage.get(&facts_db_key).await.unwrap();
177+
178+
// Handle missing nodes based on the skip_missing flag.
179+
let Some(filled_node_raw) = filled_node_raw else {
180+
if skip_missing {
181+
return;
182+
} else {
183+
panic!(
184+
"Node not found in storage: index={:?}, hash={:?}. If converting a filled forest, \
185+
use convert_facts_filled_forest_to_index.",
186+
current_index, current_hash
187+
);
188+
}
189+
};
190+
191+
let facts_filled_node = FactDbFilledNode::<FactsLeaf>::deserialize(
192+
&filled_node_raw,
193+
&FactNodeDeserializationContext {
194+
node_hash: current_hash,
195+
is_leaf: current_index.is_leaf(),
196+
},
197+
)
198+
.unwrap();
199+
200+
let index_filled_node: IndexFilledNode<IndexLeaf> = match facts_filled_node.data {
201+
NodeData::Binary(binary_data) => {
202+
let children_indices = current_index.get_children_indices();
203+
traverse_and_convert::<FactsLeaf, IndexLeaf, KeyContext>(
204+
facts_storage,
205+
index_layout_storage,
206+
children_indices[0],
207+
binary_data.left_data,
208+
key_context,
209+
current_leaves,
210+
skip_missing,
211+
)
212+
.await;
213+
traverse_and_convert::<FactsLeaf, IndexLeaf, KeyContext>(
214+
facts_storage,
215+
index_layout_storage,
216+
children_indices[1],
217+
binary_data.right_data,
218+
key_context,
219+
current_leaves,
220+
skip_missing,
221+
)
222+
.await;
223+
IndexFilledNode(FilledNode {
224+
hash: current_hash,
225+
data: NodeData::Binary(BinaryData {
226+
left_data: EmptyNodeData,
227+
right_data: EmptyNodeData,
228+
}),
229+
})
230+
}
231+
NodeData::Edge(edge_data) => {
232+
let bottom_index =
233+
NodeIndex::compute_bottom_index(current_index, &edge_data.path_to_bottom);
234+
traverse_and_convert::<FactsLeaf, IndexLeaf, KeyContext>(
235+
facts_storage,
236+
index_layout_storage,
237+
bottom_index,
238+
edge_data.bottom_data,
239+
key_context,
240+
current_leaves,
241+
skip_missing,
242+
)
243+
.await;
244+
IndexFilledNode(FilledNode {
245+
hash: current_hash,
246+
data: NodeData::Edge(EdgeData {
247+
bottom_data: EmptyNodeData,
248+
path_to_bottom: edge_data.path_to_bottom,
249+
}),
250+
})
251+
}
252+
NodeData::Leaf(leaf) => {
253+
if let Some(leaves) = current_leaves {
254+
leaves.push((current_index, leaf.clone()));
255+
}
256+
257+
IndexFilledNode(FilledNode { hash: current_hash, data: NodeData::Leaf(leaf.into()) })
258+
}
259+
};
260+
261+
let index_db_key = create_db_key(
262+
IndexLeaf::get_static_prefix(key_context),
263+
INDEX_LAYOUT_DB_KEY_SEPARATOR,
264+
&current_index.0.to_be_bytes(),
265+
);
266+
index_layout_storage.set(index_db_key, index_filled_node.serialize().unwrap()).await.unwrap();
267+
}

crates/starknet_patricia/src/patricia_merkle_tree/types.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,12 @@ impl NodeIndex {
6868
}
6969

7070
// TODO(Amos, 1/5/2024): Move to EdgePath.
71-
pub(crate) fn compute_bottom_index(
72-
index: NodeIndex,
73-
path_to_bottom: &PathToBottom,
74-
) -> NodeIndex {
71+
pub fn compute_bottom_index(index: NodeIndex, path_to_bottom: &PathToBottom) -> NodeIndex {
7572
let PathToBottom { path, length, .. } = path_to_bottom;
7673
(index << u8::from(*length)) + Self::new(path.into())
7774
}
7875

79-
pub(crate) fn get_children_indices(&self) -> [Self; 2] {
76+
pub fn get_children_indices(&self) -> [Self; 2] {
8077
let left_child = *self << 1;
8178
[left_child, left_child + 1]
8279
}

0 commit comments

Comments
 (0)