Skip to content

Commit e6d71c6

Browse files
committed
starknet_committer: add index layout nodes
1 parent 08c16bf commit e6d71c6

File tree

6 files changed

+427
-80
lines changed

6 files changed

+427
-80
lines changed

crates/starknet_committer/src/db/index_db/leaves_test.rs

Lines changed: 0 additions & 76 deletions
This file was deleted.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
pub mod leaves;
22
#[cfg(test)]
3-
pub mod leaves_test;
3+
pub mod serde_tests;
44
pub mod types;
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
use std::iter;
2+
use std::sync::LazyLock;
3+
4+
use ethnum::U256;
5+
use rstest::rstest;
6+
use starknet_api::core::{ClassHash, Nonce};
7+
use starknet_api::hash::HashOutput;
8+
use starknet_patricia::patricia_merkle_tree::filled_tree::node::FilledNode;
9+
use starknet_patricia::patricia_merkle_tree::node_data::inner_node::{
10+
BinaryData,
11+
EdgeData,
12+
EdgePath,
13+
EdgePathLength,
14+
NodeData,
15+
PathToBottom,
16+
};
17+
use starknet_patricia::patricia_merkle_tree::node_data::leaf::Leaf;
18+
use starknet_patricia::patricia_merkle_tree::updated_skeleton_tree::hash_function::TreeHashFunction;
19+
use starknet_patricia_storage::db_object::{DBObject, EmptyDeserializationContext};
20+
use starknet_patricia_storage::storage_trait::DbValue;
21+
use starknet_types_core::felt::Felt;
22+
23+
use crate::block_committer::input::StarknetStorageValue;
24+
use crate::db::index_db::leaves::{
25+
IndexLayoutCompiledClassHash,
26+
IndexLayoutContractState,
27+
IndexLayoutStarknetStorageValue,
28+
};
29+
use crate::db::index_db::types::{IndexFilledNode, IndexNodeContext};
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 leaf node data from NodeData<L, HashOutput> to NodeData<L, ()>.
35+
///
36+
/// Useful for using the same data for computing the hash and creating an index layout leaf
37+
/// instance.
38+
fn index_leaf_data_from_hash_data<L: Leaf>(data: NodeData<L, HashOutput>) -> NodeData<L, ()> {
39+
match data {
40+
NodeData::Binary(_) | NodeData::Edge(_) => {
41+
unreachable!("this helper is intended for leaf-only test data")
42+
}
43+
NodeData::Leaf(leaf) => NodeData::Leaf(leaf),
44+
}
45+
}
46+
47+
static CONTRACT_STATE_LEAF: LazyLock<IndexFilledNode<IndexLayoutContractState>> =
48+
LazyLock::new(|| {
49+
let data = NodeData::Leaf(IndexLayoutContractState(ContractState {
50+
class_hash: ClassHash(Felt::from(1)),
51+
storage_root_hash: HashOutput(Felt::from(2)),
52+
nonce: Nonce(Felt::from(3)),
53+
}));
54+
let hash = TreeHashFunctionImpl::compute_node_hash(&data);
55+
IndexFilledNode(FilledNode { hash, data: index_leaf_data_from_hash_data(data) })
56+
});
57+
58+
static COMPILED_CLASS_HASH_LEAF: LazyLock<IndexFilledNode<IndexLayoutCompiledClassHash>> =
59+
LazyLock::new(|| {
60+
let data = NodeData::Leaf(IndexLayoutCompiledClassHash(CompiledClassHash(Felt::from(1))));
61+
let hash = TreeHashFunctionImpl::compute_node_hash(&data);
62+
IndexFilledNode(FilledNode { hash, data: index_leaf_data_from_hash_data(data) })
63+
});
64+
65+
static STARKNET_STORAGE_VALUE_LEAF: LazyLock<IndexFilledNode<IndexLayoutStarknetStorageValue>> =
66+
LazyLock::new(|| {
67+
let data =
68+
NodeData::Leaf(IndexLayoutStarknetStorageValue(StarknetStorageValue(Felt::from(1))));
69+
let hash = TreeHashFunctionImpl::compute_node_hash(&data);
70+
IndexFilledNode(FilledNode { hash, data: index_leaf_data_from_hash_data(data) })
71+
});
72+
73+
fn starknet_storage_value_leaf_96_bits() -> IndexLayoutStarknetStorageValue {
74+
// 2^96 (12 bytes, under the 27 nibbles threshold)
75+
IndexLayoutStarknetStorageValue(StarknetStorageValue(Felt::from(1_u128 << 95)))
76+
}
77+
78+
fn starknet_storage_value_leaf_136_bits() -> IndexLayoutStarknetStorageValue {
79+
// 2^136 (reaching the 34 nibbles / 17 bytes serialization threshold)
80+
let mut bytes = [0u8; 32];
81+
bytes[15] = 128;
82+
IndexLayoutStarknetStorageValue(StarknetStorageValue(Felt::from_bytes_be(&bytes)))
83+
}
84+
85+
fn binary_node() -> IndexFilledNode<IndexLayoutContractState> {
86+
IndexFilledNode(FilledNode {
87+
hash: HashOutput(Felt::from(1)),
88+
data: NodeData::Binary(BinaryData { left_data: (), right_data: () }),
89+
})
90+
}
91+
92+
fn edge_node_short_path_len_3() -> IndexFilledNode<IndexLayoutContractState> {
93+
IndexFilledNode(FilledNode {
94+
hash: HashOutput(Felt::from(1)),
95+
data: NodeData::Edge(EdgeData {
96+
bottom_data: (),
97+
// 110, right, right, left
98+
path_to_bottom: PathToBottom::new(
99+
EdgePath(U256::from(6_u128)),
100+
EdgePathLength::new(3).unwrap(),
101+
)
102+
.unwrap(),
103+
}),
104+
})
105+
}
106+
107+
fn edge_node_short_path_len_10() -> IndexFilledNode<IndexLayoutContractState> {
108+
IndexFilledNode(FilledNode {
109+
hash: HashOutput(Felt::from(1)),
110+
data: NodeData::Edge(EdgeData {
111+
bottom_data: (),
112+
// 0...0 seven times followed by 110
113+
path_to_bottom: PathToBottom::new(
114+
EdgePath(U256::from(6_u128)),
115+
EdgePathLength::new(10).unwrap(),
116+
)
117+
.unwrap(),
118+
}),
119+
})
120+
}
121+
122+
fn edge_node_path_divisible_by_8() -> IndexFilledNode<IndexLayoutContractState> {
123+
IndexFilledNode(FilledNode {
124+
hash: HashOutput(Felt::from(1)),
125+
data: NodeData::Edge(EdgeData {
126+
bottom_data: (),
127+
path_to_bottom: PathToBottom::new(
128+
// 1...1 24 times
129+
EdgePath(U256::from((1_u128 << 24) - 1)),
130+
EdgePathLength::new(24).unwrap(),
131+
)
132+
.unwrap(),
133+
}),
134+
})
135+
}
136+
137+
fn edge_node_path_not_divisible_by_8() -> IndexFilledNode<IndexLayoutContractState> {
138+
IndexFilledNode(FilledNode {
139+
hash: HashOutput(Felt::from(1)),
140+
data: NodeData::Edge(EdgeData {
141+
bottom_data: (),
142+
// 000 followed by 1...1 19 times
143+
path_to_bottom: PathToBottom::new(
144+
EdgePath(U256::from((1_u128 << 19) - 1)),
145+
EdgePathLength::new(22).unwrap(),
146+
)
147+
.unwrap(),
148+
}),
149+
})
150+
}
151+
152+
fn edge_node_long_zero_path() -> IndexFilledNode<IndexLayoutContractState> {
153+
IndexFilledNode(FilledNode {
154+
hash: HashOutput(Felt::from(1)),
155+
data: NodeData::Edge(EdgeData {
156+
bottom_data: (),
157+
// 0...0 path of length 250
158+
path_to_bottom: PathToBottom::new(
159+
EdgePath(U256::ZERO),
160+
EdgePathLength::new(250).unwrap(),
161+
)
162+
.unwrap(),
163+
}),
164+
})
165+
}
166+
167+
fn edge_node_long_non_zero_path() -> IndexFilledNode<IndexLayoutContractState> {
168+
IndexFilledNode(FilledNode {
169+
hash: HashOutput(Felt::from(1)),
170+
data: NodeData::Edge(EdgeData {
171+
bottom_data: (),
172+
// 1 followed by 250 zeros path of length 251
173+
path_to_bottom: PathToBottom::new(
174+
EdgePath(U256::from(1u8) << 250),
175+
EdgePathLength::new(251).unwrap(),
176+
)
177+
.unwrap(),
178+
}),
179+
})
180+
}
181+
182+
fn extract_leaf<L: Leaf>(node: &IndexFilledNode<L>) -> L {
183+
if let NodeData::Leaf(leaf) = &node.0.data {
184+
leaf.clone()
185+
} else {
186+
unreachable!("attempted to extract a leaf from a non-leaf node");
187+
}
188+
}
189+
190+
#[rstest]
191+
#[case::index_layout_contract_state(extract_leaf(&CONTRACT_STATE_LEAF))]
192+
#[case::index_layout_compiled_class_hash(extract_leaf(&COMPILED_CLASS_HASH_LEAF))]
193+
#[case::index_layout_starknet_storage_value(extract_leaf(&STARKNET_STORAGE_VALUE_LEAF))]
194+
fn test_index_layout_leaf_serde<L: Leaf>(#[case] leaf: L) {
195+
let serialized = leaf.serialize().unwrap();
196+
let deserialized = L::deserialize(&serialized, &EmptyDeserializationContext).unwrap();
197+
assert_eq!(leaf, deserialized);
198+
}
199+
200+
#[rstest]
201+
#[case(extract_leaf(&CONTRACT_STATE_LEAF), DbValue(vec![1, 2, 3]))]
202+
#[case(extract_leaf(&COMPILED_CLASS_HASH_LEAF), DbValue(vec![1]))]
203+
#[case(extract_leaf(&STARKNET_STORAGE_VALUE_LEAF), DbValue(vec![1]))]
204+
// We are serializing 2^96. The 4 MSB of the first byte are the chooser. For values >= 16 but under
205+
// 27 nibbles, the chooser is the number of bytes. In this case, the first byte will be 11000000
206+
// (chooser 12, i.e. we need 12 bytes) followed by the value.
207+
#[case(starknet_storage_value_leaf_96_bits(), DbValue([vec![192, 128], vec![0; 11]].concat()))]
208+
// We are serializing 2^136, which exceeds the 34 nibbles threshold where the encoding utilizes the
209+
// full 32 bytes. This case is marked by chooser = 15, followed by the value, starting immediately
210+
// after the chooser (hence the first 116 bits after the chooser are 0).
211+
#[case(starknet_storage_value_leaf_136_bits(), DbValue([
212+
vec![240], vec![0; 14], vec![128], vec![0; 16]
213+
].concat()))]
214+
fn test_leaf_serialization_regression<L: Leaf>(
215+
#[case] leaf: L,
216+
#[case] expected_serialize: DbValue,
217+
) {
218+
let actual_serialize = leaf.serialize().unwrap();
219+
assert_eq!(actual_serialize, expected_serialize);
220+
}
221+
222+
#[rstest]
223+
#[case::index_layout_contract_state_leaf(&CONTRACT_STATE_LEAF, IndexNodeContext { is_leaf: true })]
224+
#[case::index_layout_compiled_class_hash_leaf(&COMPILED_CLASS_HASH_LEAF, IndexNodeContext { is_leaf: true })]
225+
#[case::index_layout_starknet_storage_value_leaf(&STARKNET_STORAGE_VALUE_LEAF, IndexNodeContext { is_leaf: true })]
226+
#[case::index_layout_binary_node(&binary_node(), IndexNodeContext { is_leaf: false })]
227+
#[case::index_layout_edge_node_short_path(&edge_node_short_path_len_3(), IndexNodeContext { is_leaf: false })]
228+
#[case::index_layout_edge_node_short_path(&edge_node_short_path_len_10(), IndexNodeContext { is_leaf: false })]
229+
#[case::index_layout_edge_node_path_divisible_by_8(&edge_node_path_divisible_by_8(), IndexNodeContext { is_leaf: false })]
230+
#[case::index_layout_edge_node_path_not_divisible_by_8(&edge_node_path_not_divisible_by_8(), IndexNodeContext { is_leaf: false })]
231+
#[case::index_layout_edge_node_long_zero_path(&edge_node_long_zero_path(), IndexNodeContext { is_leaf: false })]
232+
#[case::index_layout_edge_node_long_non_zero_path(&edge_node_long_non_zero_path(), IndexNodeContext { is_leaf: false })]
233+
234+
fn test_index_layout_node_serde<L: Leaf>(
235+
#[case] node: &IndexFilledNode<L>,
236+
#[case] deserialization_context: IndexNodeContext,
237+
) where
238+
TreeHashFunctionImpl: TreeHashFunction<L>,
239+
{
240+
let serialized = node.serialize().unwrap();
241+
let deserialized =
242+
IndexFilledNode::<L>::deserialize(&serialized, &deserialization_context).unwrap();
243+
assert_eq!(node, &deserialized);
244+
}
245+
246+
#[rstest]
247+
#[case::index_layout_binary_node(&binary_node(), DbValue(Felt::ONE.to_bytes_be().to_vec()))]
248+
// Dummy hash 0...01 followed by length 10 and 6 written over two bytes in little endian.
249+
#[case::index_layout_binary_node(&edge_node_short_path_len_10(), DbValue(Felt::ONE.to_bytes_be().into_iter().chain(vec![10_u8, 6, 0]).collect()))]
250+
// Dummy hash 0...01 followed by length 251 and 10...0 written over 32 bytes in little endian.
251+
#[case::index_layout_edge_node_long_non_zero_path(&edge_node_long_non_zero_path(), DbValue(
252+
Felt::ONE.to_bytes_be().into_iter()
253+
.chain(iter::once(251_u8))
254+
.chain(vec![0_u8; 31])
255+
.chain(iter::once(4_u8))
256+
.collect()
257+
))]
258+
fn test_node_serialization_regression<L: Leaf>(
259+
#[case] node: &IndexFilledNode<L>,
260+
#[case] expected_serialize: DbValue,
261+
) where
262+
TreeHashFunctionImpl: TreeHashFunction<L>,
263+
{
264+
let actual_serialize = node.serialize().unwrap();
265+
assert_eq!(actual_serialize, expected_serialize);
266+
}

0 commit comments

Comments
 (0)