From e3b783b2e7db53644a819901315cd5389afa776b Mon Sep 17 00:00:00 2001 From: Arun Jangra Date: Sat, 13 Jul 2024 13:20:33 +0530 Subject: [PATCH 1/3] feat : added pathfinder_getClassProof method --- .gitignore | 5 +- crates/merkle-tree/src/class.rs | 173 +++++++++++++++++- crates/merkle-tree/src/lib.rs | 2 +- crates/rpc/src/pathfinder.rs | 1 + crates/rpc/src/pathfinder/methods.rs | 1 + .../rpc/src/pathfinder/methods/get_proof.rs | 111 ++++++++++- 6 files changed, 283 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index c44283c5da..050cdb9ab0 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,7 @@ pathfinder-var.env .vscode/ # mdbook compilation -**/book/ \ No newline at end of file +**/book/ + +# Intellij IDE generated files +.idea \ No newline at end of file diff --git a/crates/merkle-tree/src/class.rs b/crates/merkle-tree/src/class.rs index 273060c9f3..1131a2f770 100644 --- a/crates/merkle-tree/src/class.rs +++ b/crates/merkle-tree/src/class.rs @@ -1,16 +1,19 @@ +use crate::merkle_node::InternalNode; use anyhow::Context; -use pathfinder_common::hash::PoseidonHash; +use bitvec::order::Msb0; +use bitvec::prelude::BitSlice; +use bitvec::view::BitView; +use pathfinder_common::hash::{PedersenHash, PoseidonHash}; +use pathfinder_common::trie::TrieNode; use pathfinder_common::{ - BlockNumber, - ClassCommitment, - ClassCommitmentLeafHash, - ClassHash, - SierraHash, + BlockNumber, CasmHash, ClassCommitment, ClassCommitmentLeafHash, ClassHash, ContractAddress, + ContractRoot, SierraHash, StorageAddress, StorageValue, }; use pathfinder_crypto::Felt; use pathfinder_storage::{Transaction, TrieUpdate}; +use std::ops::ControlFlow; -use crate::tree::MerkleTree; +use crate::tree::{MerkleTree, Visit}; /// A [Patricia Merkle tree](MerkleTree) used to calculate commitments to /// Starknet's Sierra classes. @@ -71,6 +74,162 @@ impl<'tx> ClassCommitmentTree<'tx> { let commitment = ClassCommitment(update.root_commitment); Ok((commitment, update)) } + + /// Generates a proof for a given `key` + pub fn get_proof( + tx: &'tx Transaction<'tx>, + block: BlockNumber, + class_hash: ClassHash, + ) -> anyhow::Result>> { + let root = tx + .class_root_index(block) + .context("Querying class root index")?; + + let Some(root) = root else { + return Ok(None); + }; + + let storage = ClassTrieStorage { + tx, + block: Some(block), + }; + + let casm = tx + .casm_hash_at(block.into(), class_hash) + .context("Querying CASM hash")?; + + let Some(casm) = casm else { + return Ok(None); + }; + + MerkleTree::::get_proof(root, &storage, casm.view_bits()) + } +} + +/// A [Patricia Merkle tree](MerkleTree) used to calculate commitments to +/// Starknet's Sierra classes. +/// +/// It maps a class's [SierraHash] to its [ClassCommitmentLeafHash] +/// +/// Tree data is persisted by a sqlite table 'tree_class'. + +pub struct ClassStorageTree<'tx> { + tree: MerkleTree, + storage: ClassStorage<'tx>, +} + +impl<'tx> ClassStorageTree<'tx> { + pub fn empty(tx: &'tx Transaction<'tx>) -> Self { + let storage = ClassStorage { tx, block: None }; + let tree = MerkleTree::empty(); + + Self { tree, storage } + } + + pub fn load(tx: &'tx Transaction<'tx>, block: BlockNumber) -> anyhow::Result { + let root = tx + .class_root_index(block) + .context("Querying class root index")?; + + let Some(root) = root else { + return Ok(Self::empty(tx)); + }; + + let storage = ClassStorage { + tx, + block: Some(block), + }; + + let tree = MerkleTree::new(root); + + Ok(Self { tree, storage }) + } + + pub fn with_verify_hashes(mut self, verify_hashes: bool) -> Self { + self.tree = self.tree.with_verify_hashes(verify_hashes); + self + } + + /// Generates a proof for `key`. See [`MerkleTree::get_proof`]. + pub fn get_proof( + tx: &'tx Transaction<'tx>, + block: BlockNumber, + key: &BitSlice, + ) -> anyhow::Result>> { + let root = tx + .class_root_index(block) + .context("Querying class root index")?; + + let Some(root) = root else { + return Ok(None); + }; + + let storage = ClassStorage { + tx, + block: Some(block), + }; + + MerkleTree::::get_proof(root, &storage, key) + } + + pub fn set(&mut self, address: StorageAddress, value: StorageValue) -> anyhow::Result<()> { + let key = address.view_bits().to_owned(); + self.tree.set(&self.storage, key, value.0) + } + + /// Commits the changes and calculates the new node hashes. Returns the new + /// commitment and any potentially newly created nodes. + pub fn commit(self) -> anyhow::Result<(CasmHash, TrieUpdate)> { + let update = self.tree.commit(&self.storage)?; + let commitment = CasmHash(update.root_commitment); + Ok((commitment, update)) + } + + /// See [`MerkleTree::dfs`] + pub fn dfs) -> ControlFlow>( + &mut self, + f: &mut F, + ) -> anyhow::Result> { + self.tree.dfs(&self.storage, f) + } +} + +struct ClassTrieStorage<'tx> { + tx: &'tx Transaction<'tx>, + block: Option, +} + +impl crate::storage::Storage for ClassTrieStorage<'_> { + fn get(&self, index: u64) -> anyhow::Result> { + self.tx.storage_trie_node(index) + } + + fn hash(&self, index: u64) -> anyhow::Result> { + self.tx.storage_trie_node_hash(index) + } + + fn leaf(&self, path: &BitSlice) -> anyhow::Result> { + assert!(path.len() == 251); + + let Some(block) = self.block else { + return Ok(None); + }; + + let sierra = + ClassHash(Felt::from_bits(path).context("Mapping leaf path to contract address")?); + + let casm = self + .tx + .casm_hash_at(block.into(), sierra) + .context("Querying CASM hash")?; + let Some(casm) = casm else { + return Ok(None); + }; + + let value = self.tx.class_commitment_leaf(block, &casm)?.map(|x| x.0); + + Ok(value) + } } struct ClassStorage<'tx> { diff --git a/crates/merkle-tree/src/lib.rs b/crates/merkle-tree/src/lib.rs index 07ef9424f1..0bf2bbcdce 100644 --- a/crates/merkle-tree/src/lib.rs +++ b/crates/merkle-tree/src/lib.rs @@ -3,7 +3,7 @@ pub mod merkle_node; pub mod storage; pub mod tree; -mod class; +pub mod class; mod contract; mod transaction; diff --git a/crates/rpc/src/pathfinder.rs b/crates/rpc/src/pathfinder.rs index 4b0ca8ec4a..f491080f6e 100644 --- a/crates/rpc/src/pathfinder.rs +++ b/crates/rpc/src/pathfinder.rs @@ -8,4 +8,5 @@ pub fn register_routes() -> RpcRouterBuilder { .register("pathfinder_version", || { pathfinder_common::consts::VERGEN_GIT_DESCRIBE }) .register("pathfinder_getProof", methods::get_proof) .register("pathfinder_getTransactionStatus", methods::get_transaction_status) + .register("pathfinder_getClassProof", methods::get_proof_class) } diff --git a/crates/rpc/src/pathfinder/methods.rs b/crates/rpc/src/pathfinder/methods.rs index eab78fb2bd..a0e8bd5a7f 100644 --- a/crates/rpc/src/pathfinder/methods.rs +++ b/crates/rpc/src/pathfinder/methods.rs @@ -2,4 +2,5 @@ mod get_proof; mod get_transaction_status; pub(crate) use get_proof::get_proof; +pub(crate) use get_proof::get_proof_class; pub(crate) use get_transaction_status::get_transaction_status; diff --git a/crates/rpc/src/pathfinder/methods/get_proof.rs b/crates/rpc/src/pathfinder/methods/get_proof.rs index 3b5118f09d..99cf00d3ce 100644 --- a/crates/rpc/src/pathfinder/methods/get_proof.rs +++ b/crates/rpc/src/pathfinder/methods/get_proof.rs @@ -3,7 +3,8 @@ use pathfinder_common::prelude::*; use pathfinder_common::trie::TrieNode; use pathfinder_common::BlockId; use pathfinder_crypto::Felt; -use pathfinder_merkle_tree::{ContractsStorageTree, StorageCommitmentTree}; +use pathfinder_merkle_tree::class::ClassStorageTree; +use pathfinder_merkle_tree::{ClassCommitmentTree, ContractsStorageTree, StorageCommitmentTree}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -16,6 +17,13 @@ pub struct GetProofInput { pub keys: Vec, } +#[derive(Deserialize, Debug, PartialEq, Eq)] +pub struct GetProofInputClass { + pub block_id: BlockId, + pub class_hash: ClassHash, + pub keys: Vec, +} + // FIXME: allow `generate_rpc_error_subset!` to work with enum struct variants. // This may not actually be possible though. #[derive(Debug)] @@ -151,6 +159,18 @@ pub struct GetProofOutput { contract_data: Option, } +#[derive(Debug, Serialize)] +#[skip_serializing_none] +pub struct GetProofOutputClass { + /// Required to verify that the hash of the class commitment and the root of + /// the [contract_proof](GetProofOutput::contract_proof) matches the + /// [state_commitment](Self#state_commitment). Present only for Starknet + /// blocks 0.11.0 onwards. + class_commitment: Option, + /// Membership / Non-membership proof for the queried contract classes + class_proof: ProofNodes, +} + /// Returns all the necessary data to trustlessly verify storage slots for a /// particular contract. pub async fn get_proof( @@ -278,6 +298,95 @@ pub async fn get_proof( jh.await.context("Database read panic or shutting down")? } +/// Returns all the necessary data to trustlessly verify class changes for a +/// particular contract. +pub async fn get_proof_class( + context: RpcContext, + input: GetProofInputClass, +) -> Result { + const MAX_KEYS: usize = 100; + if input.keys.len() > MAX_KEYS { + return Err(GetProofError::ProofLimitExceeded { + limit: MAX_KEYS as u32, + requested: input.keys.len() as u32, + }); + } + + let block_id = match input.block_id { + BlockId::Pending => { + return Err(GetProofError::Internal(anyhow!( + "'pending' is not currently supported by this method!" + ))) + } + other => other.try_into().expect("Only pending cast should fail"), + }; + + let storage = context.storage.clone(); + let span = tracing::Span::current(); + + let jh = tokio::task::spawn_blocking(move || { + let _g = span.enter(); + let mut db = storage + .connection() + .context("Opening database connection")?; + + let tx = db.transaction().context("Creating database transaction")?; + + // Use internal error to indicate that the process of querying for a particular + // block failed, which is not the same as being sure that the block is + // not in the db. + let header = tx + .block_header(block_id) + .context("Fetching block header")? + .ok_or(GetProofError::BlockNotFound)?; + + let class_commitment = match header.class_commitment { + ClassCommitment::ZERO => None, + other => Some(other), + }; + + // Generate a proof for this class. If the class does not exist, this will + // be a "non membership" proof. + let class_proof = ClassCommitmentTree::get_proof(&tx, header.number, input.class_hash) + .context("Creating contract proof")? + .ok_or(GetProofError::ProofMissing)?; + let class_proof = ProofNodes(class_proof); + + let class_root_exists = tx + .class_root_exists(header.number) + .context("Fetching class root existence")?; + + if !class_root_exists { + return Ok(GetProofOutputClass { + class_commitment, + class_proof, + }); + }; + + let mut class_proofs = Vec::new(); + for k in &input.keys { + let proof = ClassStorageTree::get_proof(&tx, header.number, k.view_bits()) + .context("Get proof from class tree")? + .ok_or_else(|| { + let e = anyhow!( + "Storage proof missing for key {:?}, but should be present", + k + ); + tracing::warn!("{e}"); + e + })?; + class_proofs.push(ProofNodes(proof)); + } + + Ok(GetProofOutputClass { + class_commitment, + class_proof, + }) + }); + + jh.await.context("Database read panic or shutting down")? +} + #[cfg(test)] mod tests { use pathfinder_common::macro_prelude::*; From 4c1de0cb75317822251b81191e1937ae922a57d6 Mon Sep 17 00:00:00 2001 From: Arun Jangra Date: Mon, 15 Jul 2024 17:02:25 +0530 Subject: [PATCH 2/3] feat : added poseidon hash for class tree function and fixed lint tests --- .gitignore | 3 --- .idea/.gitignore | 5 ++++ .idea/modules.xml | 8 ++++++ .idea/pathfinder.iml | 37 ++++++++++++++++++++++++++++ .idea/vcs.xml | 6 +++++ crates/merkle-tree/src/class.rs | 20 +++++++++------ crates/rpc/src/pathfinder/methods.rs | 3 +-- 7 files changed, 70 insertions(+), 12 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/pathfinder.iml create mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index 050cdb9ab0..ed158cfecf 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,3 @@ pathfinder-var.env # mdbook compilation **/book/ - -# Intellij IDE generated files -.idea \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000..b58b603fea --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000..921c18fcf8 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/pathfinder.iml b/.idea/pathfinder.iml new file mode 100644 index 0000000000..e4c15ed1a5 --- /dev/null +++ b/.idea/pathfinder.iml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..35eb1ddfbb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/crates/merkle-tree/src/class.rs b/crates/merkle-tree/src/class.rs index 1131a2f770..e211ccc469 100644 --- a/crates/merkle-tree/src/class.rs +++ b/crates/merkle-tree/src/class.rs @@ -1,18 +1,24 @@ -use crate::merkle_node::InternalNode; +use std::ops::ControlFlow; + use anyhow::Context; use bitvec::order::Msb0; use bitvec::prelude::BitSlice; -use bitvec::view::BitView; use pathfinder_common::hash::{PedersenHash, PoseidonHash}; use pathfinder_common::trie::TrieNode; use pathfinder_common::{ - BlockNumber, CasmHash, ClassCommitment, ClassCommitmentLeafHash, ClassHash, ContractAddress, - ContractRoot, SierraHash, StorageAddress, StorageValue, + BlockNumber, + CasmHash, + ClassCommitment, + ClassCommitmentLeafHash, + ClassHash, + SierraHash, + StorageAddress, + StorageValue, }; use pathfinder_crypto::Felt; use pathfinder_storage::{Transaction, TrieUpdate}; -use std::ops::ControlFlow; +use crate::merkle_node::InternalNode; use crate::tree::{MerkleTree, Visit}; /// A [Patricia Merkle tree](MerkleTree) used to calculate commitments to @@ -102,7 +108,7 @@ impl<'tx> ClassCommitmentTree<'tx> { return Ok(None); }; - MerkleTree::::get_proof(root, &storage, casm.view_bits()) + MerkleTree::::get_proof(root, &storage, casm.view_bits()) } } @@ -114,7 +120,7 @@ impl<'tx> ClassCommitmentTree<'tx> { /// Tree data is persisted by a sqlite table 'tree_class'. pub struct ClassStorageTree<'tx> { - tree: MerkleTree, + tree: MerkleTree, storage: ClassStorage<'tx>, } diff --git a/crates/rpc/src/pathfinder/methods.rs b/crates/rpc/src/pathfinder/methods.rs index a0e8bd5a7f..d2c6f85074 100644 --- a/crates/rpc/src/pathfinder/methods.rs +++ b/crates/rpc/src/pathfinder/methods.rs @@ -1,6 +1,5 @@ mod get_proof; mod get_transaction_status; -pub(crate) use get_proof::get_proof; -pub(crate) use get_proof::get_proof_class; +pub(crate) use get_proof::{get_proof, get_proof_class}; pub(crate) use get_transaction_status::get_transaction_status; From 08969c525e02a2ea8421dd6936cd746617712f8b Mon Sep 17 00:00:00 2001 From: Arun Jangra Date: Mon, 15 Jul 2024 17:02:41 +0530 Subject: [PATCH 3/3] feat : added poseidon hash for class tree function and fixed lint tests --- .idea/.gitignore | 5 ----- .idea/modules.xml | 8 -------- .idea/pathfinder.iml | 37 ------------------------------------- .idea/vcs.xml | 6 ------ 4 files changed, 56 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/modules.xml delete mode 100644 .idea/pathfinder.iml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index b58b603fea..0000000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 921c18fcf8..0000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/pathfinder.iml b/.idea/pathfinder.iml deleted file mode 100644 index e4c15ed1a5..0000000000 --- a/.idea/pathfinder.iml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddfbb..0000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file