Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(proof): pathfinder_getClassProof endpoint #2179

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Pathfinder JSON-RPC extension methods are now also exposed on the `/rpc/pathfinder/v0_1` endpoint.
- `--sync.l1-poll-interval` CLI option has been added to set the poll interval for L1 state. Defaults to 30s.
- Added the `pathfinder_getClassProof` endpoint to retrieve the Merkle proof of any class hash in the class trie.

## [0.14.1] - 2024-07-29

Expand Down
63 changes: 63 additions & 0 deletions crates/merkle-tree/src/class.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use anyhow::Context;
use bitvec::order::Msb0;
use bitvec::prelude::BitSlice;
use pathfinder_common::hash::PoseidonHash;
use pathfinder_common::trie::TrieNode;
use pathfinder_common::{
BlockNumber,
ClassCommitment,
Expand Down Expand Up @@ -71,6 +74,66 @@ 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<Option<Vec<TrieNode>>> {
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),
};

MerkleTree::<PoseidonHash, 251>::get_proof(root, &storage, class_hash.0.view_bits())
}
}

struct ClassTrieStorage<'tx> {
tx: &'tx Transaction<'tx>,
block: Option<BlockNumber>,
}

impl crate::storage::Storage for ClassTrieStorage<'_> {
fn get(&self, index: u64) -> anyhow::Result<Option<pathfinder_storage::StoredNode>> {
self.tx.class_trie_node(index)
}

fn hash(&self, index: u64) -> anyhow::Result<Option<Felt>> {
self.tx.class_trie_node_hash(index)
}

fn leaf(&self, path: &BitSlice<u8, Msb0>) -> anyhow::Result<Option<Felt>> {
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)
}
Comment on lines +115 to +136
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this seems a bit strange. How is this actually different from ClassStorage::leaf()?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not, it's an artifact from earlier attempts. Will remove it.

}

struct ClassStorage<'tx> {
Expand Down
2 changes: 1 addition & 1 deletion crates/merkle-tree/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pub mod merkle_node;
pub mod storage;
pub mod tree;

mod class;
pub mod class;
mod contract;
mod transaction;

Expand Down
1 change: 1 addition & 0 deletions crates/rpc/src/pathfinder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ pub fn register_routes() -> RpcRouterBuilder {
RpcRouter::builder(crate::RpcVersion::PathfinderV01)
.register("pathfinder_version", || { pathfinder_common::consts::VERGEN_GIT_DESCRIBE })
.register("pathfinder_getProof", methods::get_proof)
.register("pathfinder_getClassProof", methods::get_proof_class)
.register("pathfinder_getTransactionStatus", methods::get_transaction_status)
}
2 changes: 1 addition & 1 deletion crates/rpc/src/pathfinder/methods.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod get_proof;
mod get_transaction_status;

pub(crate) use get_proof::get_proof;
pub(crate) use get_proof::{get_proof, get_proof_class};
pub(crate) use get_transaction_status::get_transaction_status;
75 changes: 74 additions & 1 deletion crates/rpc/src/pathfinder/methods/get_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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::{ClassCommitmentTree, ContractsStorageTree, StorageCommitmentTree};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;

Expand All @@ -16,6 +16,12 @@ pub struct GetProofInput {
pub keys: Vec<StorageAddress>,
}

#[derive(Deserialize, Debug, PartialEq, Eq)]
pub struct GetClassProofInput {
pub block_id: BlockId,
pub class_hash: ClassHash,
}

// FIXME: allow `generate_rpc_error_subset!` to work with enum struct variants.
// This may not actually be possible though.
#[derive(Debug)]
Expand Down Expand Up @@ -151,6 +157,18 @@ pub struct GetProofOutput {
contract_data: Option<ContractData>,
}

#[derive(Debug, Serialize)]
#[skip_serializing_none]
pub struct GetClassProofOutput {
/// 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<ClassCommitment>,
/// 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(
Expand Down Expand Up @@ -278,6 +296,61 @@ 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: GetClassProofInput,
) -> Result<GetClassProofOutput, GetProofError> {
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 class proof")?
.ok_or(GetProofError::ProofMissing)?;
let class_proof = ProofNodes(class_proof);

Ok(GetClassProofOutput {
class_commitment,
class_proof,
})
});

jh.await.context("Database read panic or shutting down")?
}

#[cfg(test)]
mod tests {
use pathfinder_common::macro_prelude::*;
Expand Down