Skip to content

Commit 21b871e

Browse files
committed
starknet_committer: define StarknetForestProofs serde
1 parent 22afd6e commit 21b871e

6 files changed

Lines changed: 299 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ license.workspace = true
77
description = "Computes and manages Starknet state."
88

99
[features]
10-
os_input = ["dep:blake2", "dep:digest"]
10+
os_input = ["dep:bincode", "dep:blake2", "dep:digest", "dep:zstd"]
1111
testing = ["starknet_patricia/testing"]
1212

1313
[dependencies]
1414
apollo_config.workspace = true
1515
async-trait.workspace = true
16+
bincode = { workspace = true, optional = true }
1617
blake2 = { workspace = true, optional = true }
1718
derive_more = { workspace = true, features = ["as_ref", "from", "into"] }
1819
digest = { workspace = true, optional = true }
20+
zstd = { workspace = true, optional = true }
1921
ethnum.workspace = true
2022
pretty_assertions.workspace = true
2123
rand.workspace = true

crates/starknet_committer/src/patricia_merkle_tree.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
pub mod leaf;
2+
#[cfg(feature = "os_input")]
3+
mod starknet_forest_proofs_serde;
4+
#[cfg(all(test, feature = "os_input"))]
5+
mod starknet_forest_proofs_serialization_test;
26
pub mod tree;
37
pub mod types;
48
#[cfg(test)]
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use std::collections::HashMap;
2+
3+
use starknet_api::core::{ClassHash, ContractAddress, Nonce};
4+
use starknet_api::hash::HashOutput;
5+
use starknet_patricia::patricia_merkle_tree::node_data::inner_node::{
6+
flatten_preimages,
7+
Preimage,
8+
PreimageMap,
9+
};
10+
use starknet_patricia_storage::errors::{
11+
DeserializationError,
12+
SerializationError,
13+
SerializationResult,
14+
};
15+
use starknet_patricia_storage::storage_trait::DbValue;
16+
use starknet_types_core::felt::Felt;
17+
18+
use crate::patricia_merkle_tree::leaf::leaf_impl::ContractState;
19+
use crate::patricia_merkle_tree::types::{ContractsTrieProof, StarknetForestProofs};
20+
21+
type RawPreimages = Vec<(HashOutput, Vec<Felt>)>;
22+
23+
impl StarknetForestProofs {
24+
/// Zstd-compressed bincode payload for the OS-input witnesses.
25+
///
26+
/// The inner bincode payload is a 4-tuple, in order:
27+
///
28+
/// 1. Classes trie inner nodes — `RawPreimages`.
29+
/// 2. Contract trie inner nodes — `RawPreimages`.
30+
/// 3. Contract trie leaves — `Vec<(ContractAddress, (Nonce, HashOutput, ClassHash))>`.
31+
/// 4. Storage tries inner nodes — `Vec<(ContractAddress, RawPreimages)>`.
32+
///
33+
/// Each `commitment_facts` entry uses the same encoding as [`CommitmentInfo::commitment_facts`]
34+
/// and OS Patricia hints:
35+
///
36+
/// - Binary node — `[left: Felt, right: Felt]`.
37+
/// - Edge node — `[length: Felt, path: Felt, bottom: Felt]`.
38+
pub fn serialize(&self) -> SerializationResult<DbValue> {
39+
let classes: RawPreimages =
40+
flatten_preimages(&self.classes_trie_proof).into_iter().collect();
41+
let contract_nodes: RawPreimages =
42+
flatten_preimages(&self.contracts_trie_proof.nodes).into_iter().collect();
43+
let contract_leaves: Vec<(ContractAddress, (Nonce, HashOutput, ClassHash))> = self
44+
.contracts_trie_proof
45+
.leaves
46+
.iter()
47+
.map(|(addr, contract_state)| {
48+
(
49+
*addr,
50+
(
51+
contract_state.nonce,
52+
contract_state.storage_root_hash,
53+
contract_state.class_hash,
54+
),
55+
)
56+
})
57+
.collect();
58+
59+
let storage: Vec<(ContractAddress, RawPreimages)> = self
60+
.contracts_trie_storage_proofs
61+
.iter()
62+
.map(|(addr, preimage_map)| {
63+
(*addr, flatten_preimages(preimage_map).into_iter().collect())
64+
})
65+
.collect();
66+
67+
let bincode_payload =
68+
bincode::serialize(&(classes, contract_nodes, contract_leaves, storage))
69+
.map_err(bincode_ser_err)?;
70+
let compressed =
71+
zstd::encode_all(bincode_payload.as_slice(), zstd::DEFAULT_COMPRESSION_LEVEL)
72+
.map_err(SerializationError::IOSerialize)?;
73+
Ok(DbValue(compressed))
74+
}
75+
76+
pub fn deserialize(value: &DbValue) -> Result<Self, DeserializationError> {
77+
let bincode_payload = zstd::decode_all(value.0.as_slice()).map_err(|error| {
78+
DeserializationError::ValueError(Box::new(std::io::Error::new(
79+
std::io::ErrorKind::InvalidData,
80+
error.to_string(),
81+
)))
82+
})?;
83+
let (classes, contract_nodes, contract_leaves, storage): (
84+
RawPreimages,
85+
RawPreimages,
86+
Vec<(ContractAddress, (Nonce, HashOutput, ClassHash))>,
87+
Vec<(ContractAddress, RawPreimages)>,
88+
) = bincode::deserialize(&bincode_payload).map_err(|error| {
89+
DeserializationError::ValueError(Box::new(std::io::Error::new(
90+
std::io::ErrorKind::InvalidData,
91+
error.to_string(),
92+
)))
93+
})?;
94+
95+
let classes_trie_proof = preimage_map_from_commitment_facts(classes)?;
96+
let contracts_trie_proof = ContractsTrieProof {
97+
nodes: preimage_map_from_commitment_facts(contract_nodes)?,
98+
leaves: contract_leaves.into_iter().try_fold(
99+
HashMap::new(),
100+
|mut leaves, (addr, (nonce, storage_root_hash, class_hash))| {
101+
if leaves
102+
.insert(addr, ContractState { nonce, storage_root_hash, class_hash })
103+
.is_some()
104+
{
105+
return Err(DeserializationError::KeyDuplicate(format!(
106+
"duplicate contracts trie leaf {addr:?}"
107+
)));
108+
}
109+
Ok(leaves)
110+
},
111+
)?,
112+
};
113+
114+
let contracts_trie_storage_proofs =
115+
storage.into_iter().try_fold(HashMap::new(), |mut proofs, (addr, facts)| {
116+
if proofs.insert(addr, preimage_map_from_commitment_facts(facts)?).is_some() {
117+
return Err(DeserializationError::KeyDuplicate(format!(
118+
"duplicate storage trie witness address {addr:?}"
119+
)));
120+
}
121+
Ok(proofs)
122+
})?;
123+
124+
Ok(Self { classes_trie_proof, contracts_trie_proof, contracts_trie_storage_proofs })
125+
}
126+
}
127+
128+
fn bincode_ser_err(error: bincode::Error) -> SerializationError {
129+
SerializationError::IOSerialize(std::io::Error::other(error))
130+
}
131+
132+
fn preimage_map_from_commitment_facts(
133+
facts: RawPreimages,
134+
) -> Result<PreimageMap, DeserializationError> {
135+
let mut preimage_map = PreimageMap::new();
136+
for (hash, raw_preimage) in facts {
137+
let preimage = Preimage::try_from(&raw_preimage).map_err(|error| {
138+
DeserializationError::ValueError(Box::new(std::io::Error::new(
139+
std::io::ErrorKind::InvalidData,
140+
error.to_string(),
141+
)))
142+
})?;
143+
if preimage_map.insert(hash, preimage).is_some() {
144+
return Err(DeserializationError::KeyDuplicate(format!(
145+
"duplicate preimage node hash {hash:?}"
146+
)));
147+
}
148+
}
149+
Ok(preimage_map)
150+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use std::collections::HashMap;
2+
3+
use ethnum::U256;
4+
use pretty_assertions::assert_eq;
5+
use rstest::rstest;
6+
use starknet_api::core::{ClassHash, ContractAddress, Nonce};
7+
use starknet_api::hash::HashOutput;
8+
use starknet_patricia::patricia_merkle_tree::node_data::inner_node::{
9+
BinaryData,
10+
EdgeData,
11+
EdgePath,
12+
EdgePathLength,
13+
PathToBottom,
14+
Preimage,
15+
PreimageMap,
16+
};
17+
use starknet_types_core::felt::Felt;
18+
19+
use crate::patricia_merkle_tree::leaf::leaf_impl::ContractState;
20+
use crate::patricia_merkle_tree::types::{ContractsTrieProof, StarknetForestProofs};
21+
22+
fn binary_preimage(node_hash: u128, left: u128, right: u128) -> (HashOutput, Preimage) {
23+
(
24+
HashOutput(Felt::from(node_hash)),
25+
Preimage::Binary(BinaryData {
26+
left_data: HashOutput(Felt::from(left)),
27+
right_data: HashOutput(Felt::from(right)),
28+
}),
29+
)
30+
}
31+
32+
fn edge_preimage(node_hash: u128, bottom: u128, path: u128, length: u8) -> (HashOutput, Preimage) {
33+
(
34+
HashOutput(Felt::from(node_hash)),
35+
Preimage::Edge(EdgeData {
36+
bottom_data: HashOutput(Felt::from(bottom)),
37+
path_to_bottom: PathToBottom::new(
38+
EdgePath(U256::from(path)),
39+
EdgePathLength::new(length).unwrap(),
40+
)
41+
.unwrap(),
42+
}),
43+
)
44+
}
45+
46+
fn contract_state(nonce: u128, storage_root: u128, class_hash: u128) -> ContractState {
47+
ContractState {
48+
nonce: Nonce(Felt::from(nonce)),
49+
storage_root_hash: HashOutput(Felt::from(storage_root)),
50+
class_hash: ClassHash(Felt::from(class_hash)),
51+
}
52+
}
53+
54+
fn empty_contracts_trie_proof() -> ContractsTrieProof {
55+
ContractsTrieProof { nodes: PreimageMap::new(), leaves: HashMap::new() }
56+
}
57+
58+
fn only_classes() -> StarknetForestProofs {
59+
StarknetForestProofs {
60+
classes_trie_proof: PreimageMap::from([
61+
binary_preimage(100, 101, 102),
62+
edge_preimage(200, 201, 0, 1),
63+
]),
64+
contracts_trie_proof: empty_contracts_trie_proof(),
65+
contracts_trie_storage_proofs: HashMap::new(),
66+
}
67+
}
68+
69+
fn only_contracts() -> StarknetForestProofs {
70+
let address = ContractAddress::try_from(Felt::from(7)).unwrap();
71+
StarknetForestProofs {
72+
classes_trie_proof: PreimageMap::new(),
73+
contracts_trie_proof: ContractsTrieProof {
74+
nodes: PreimageMap::from([binary_preimage(300, 301, 302)]),
75+
leaves: HashMap::from([(address, contract_state(1, 400, 500))]),
76+
},
77+
contracts_trie_storage_proofs: HashMap::new(),
78+
}
79+
}
80+
81+
fn classes_and_contracts() -> StarknetForestProofs {
82+
let address = ContractAddress::try_from(Felt::from(9)).unwrap();
83+
StarknetForestProofs {
84+
classes_trie_proof: PreimageMap::from([binary_preimage(110, 111, 112)]),
85+
contracts_trie_proof: ContractsTrieProof {
86+
nodes: PreimageMap::from([edge_preimage(310, 311, 1, 1)]),
87+
leaves: HashMap::from([(address, contract_state(2, 401, 501))]),
88+
},
89+
contracts_trie_storage_proofs: HashMap::new(),
90+
}
91+
}
92+
93+
fn all_leaf_types_single_storage() -> StarknetForestProofs {
94+
let address = ContractAddress::try_from(Felt::from(11)).unwrap();
95+
StarknetForestProofs {
96+
classes_trie_proof: PreimageMap::from([binary_preimage(120, 121, 122)]),
97+
contracts_trie_proof: ContractsTrieProof {
98+
nodes: PreimageMap::from([binary_preimage(320, 321, 322)]),
99+
leaves: HashMap::from([(address, contract_state(3, 402, 502))]),
100+
},
101+
contracts_trie_storage_proofs: HashMap::from([(
102+
address,
103+
PreimageMap::from([edge_preimage(410, 411, 0, 1)]),
104+
)]),
105+
}
106+
}
107+
108+
fn all_leaf_types_multiple_storages() -> StarknetForestProofs {
109+
let address_a = ContractAddress::try_from(Felt::from(13)).unwrap();
110+
let address_b = ContractAddress::try_from(Felt::from(14)).unwrap();
111+
StarknetForestProofs {
112+
classes_trie_proof: PreimageMap::from([edge_preimage(130, 131, 1, 1)]),
113+
contracts_trie_proof: ContractsTrieProof {
114+
nodes: PreimageMap::from([binary_preimage(330, 331, 332)]),
115+
leaves: HashMap::from([
116+
(address_a, contract_state(4, 403, 503)),
117+
(address_b, contract_state(5, 404, 504)),
118+
]),
119+
},
120+
contracts_trie_storage_proofs: HashMap::from([
121+
(address_a, PreimageMap::from([binary_preimage(420, 421, 422)])),
122+
(address_b, PreimageMap::from([edge_preimage(430, 431, 1, 1)])),
123+
]),
124+
}
125+
}
126+
127+
#[rstest]
128+
#[case::only_classes(only_classes())]
129+
#[case::only_contracts(only_contracts())]
130+
#[case::classes_and_contracts(classes_and_contracts())]
131+
#[case::all_leaf_types_single_storage(all_leaf_types_single_storage())]
132+
#[case::all_leaf_types_multiple_storages(all_leaf_types_multiple_storages())]
133+
fn test_starknet_forest_proofs_serialization_round_trip(#[case] proofs: StarknetForestProofs) {
134+
let encoded = proofs.serialize().unwrap();
135+
let decoded = StarknetForestProofs::deserialize(&encoded).unwrap();
136+
assert_eq!(proofs, decoded);
137+
}

crates/starknet_committer/src/patricia_merkle_tree/types.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,21 @@ pub type ClassesTrie = FilledTreeImpl<CompiledClassHash>;
3636
pub type ContractsTrie = FilledTreeImpl<ContractState>;
3737
pub type StorageTrieMap = HashMap<ContractAddress, StorageTrie>;
3838

39+
#[derive(Debug, PartialEq)]
3940
pub struct ContractsTrieProof {
4041
pub nodes: PreimageMap,
4142
pub leaves: HashMap<ContractAddress, ContractState>,
4243
}
4344

45+
#[derive(Debug, PartialEq)]
4446
pub struct StarknetForestProofs {
4547
pub classes_trie_proof: PreimageMap,
4648
pub contracts_trie_proof: ContractsTrieProof,
4749
pub contracts_trie_storage_proofs: HashMap<ContractAddress, PreimageMap>,
4850
}
4951

5052
impl StarknetForestProofs {
51-
pub(crate) fn extend(&mut self, other: Self) {
53+
pub fn extend(&mut self, other: Self) {
5254
self.classes_trie_proof.extend(other.classes_trie_proof);
5355
self.contracts_trie_proof.nodes.extend(other.contracts_trie_proof.nodes);
5456
self.contracts_trie_proof.leaves.extend(other.contracts_trie_proof.leaves);

0 commit comments

Comments
 (0)