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

Add merkle proof module #1101

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0bc1ca0
feat: add main logic
ericnordelo Aug 13, 2024
3199da1
feat: add hashes tests
ericnordelo Aug 14, 2024
c345375
feat: add some tests and update hashes
ericnordelo Aug 15, 2024
c564b66
feat: add tests for multi proofs
ericnordelo Aug 15, 2024
69f1ef3
feat: format files
ericnordelo Aug 15, 2024
1b4b8ec
feat: add more tests
ericnordelo Aug 15, 2024
d67535c
feat: finish poseidon tests
ericnordelo Aug 16, 2024
5a63fe6
Merge branch 'main' of github.com:OpenZeppelin/cairo-contracts into f…
ericnordelo Aug 16, 2024
9e70146
fix: version
ericnordelo Aug 16, 2024
a76ef71
fix: typo
ericnordelo Aug 16, 2024
4c3068d
refactor: remove unnecessary check
ericnordelo Aug 21, 2024
2b2a035
refactor: merkle_tree into a separated package
ericnordelo Aug 21, 2024
a6155ad
feat: update CHANGELOG
ericnordelo Aug 21, 2024
1732282
feat: remove common module from utils
ericnordelo Aug 22, 2024
1d15ee0
Merge branch 'main' of github.com:OpenZeppelin/cairo-contracts into f…
ericnordelo Aug 22, 2024
8cd2b51
docs: add page for merkle tree
ericnordelo Aug 22, 2024
9020139
feat: format files
ericnordelo Aug 22, 2024
11cb1cc
feat: update index from scarb bump
ericnordelo Aug 22, 2024
31cdc10
feat: add typos config file
ericnordelo Aug 22, 2024
ec11531
refactor: add empty line
ericnordelo Aug 22, 2024
34664b3
Update packages/utils/src/lib.cairo
ericnordelo Aug 23, 2024
7976a2d
Update packages/merkle_tree/src/hashes.cairo
ericnordelo Aug 23, 2024
65c67cc
feat: apple review updates
ericnordelo Aug 23, 2024
c599582
Merge branch 'feat/merkle-proof-verifier-#936' of github.com:ericnord…
ericnordelo Aug 23, 2024
e4758fd
feat: update test
ericnordelo Aug 26, 2024
198918c
Update docs/modules/ROOT/pages/api/merkle-tree.adoc
ericnordelo Aug 27, 2024
b2a8fd6
Update packages/merkle_tree/src/tests/merkle_proof/test_with_poseidon…
ericnordelo Aug 27, 2024
de1a13d
Update packages/merkle_tree/src/tests/merkle_proof/test_with_poseidon…
ericnordelo Aug 27, 2024
63928f9
Update docs/modules/ROOT/pages/api/merkle-tree.adoc
ericnordelo Aug 28, 2024
56f8491
Update docs/modules/ROOT/pages/api/merkle-tree.adoc
ericnordelo Aug 28, 2024
f9cae0e
feat: apply review updates
ericnordelo Aug 29, 2024
f7b839c
feat: format files
ericnordelo Aug 29, 2024
013aa57
Merge branch 'feat/merkle-proof-verifier-#936' of github.com:ericnord…
ericnordelo Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- Merkle tree utilities to verify proofs and multi proofs (#1101)

### Changed (Breaking)

- Changed ABI suffix to Trait in dual case account and eth account modules (#1096).
- `DualCaseAccountABI` renamed to `DualCaseAccountTrait`
- `DualCaseEthAccountABI` renamed to `DualCaseEthAccountTrait`
- Bump scarb to v2.7.1 (#1025)
andrew-fleming marked this conversation as resolved.
Show resolved Hide resolved

## 0.15.1 (2024-08-13)

Expand Down
6 changes: 3 additions & 3 deletions Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ version.workspace = true
[workspace.package]
version = "0.15.1"
edition = "2023_11"
cairo-version = "2.7.0"
scarb-version = "2.7.0"
cairo-version = "2.7.1"
scarb-version = "2.7.1"
authors = ["OpenZeppelin Community <[email protected]>"]
description = "OpenZeppelin Contracts written in Cairo for Starknet, a decentralized ZK Rollup"
documentation = "https://docs.openzeppelin.com/contracts-cairo"
Expand All @@ -38,7 +38,7 @@ keywords = [
]

[workspace.dependencies]
starknet = "2.7.0"
starknet = "2.7.1"
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.26.0" }

[dependencies]
Expand Down
27 changes: 27 additions & 0 deletions packages/utils/src/common.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts for Cairo v0.15.1 (utils/common.cairo)

use core::traits::PartialOrd;

pub impl Felt252PartialOrd of PartialOrd<felt252> {
ericnordelo marked this conversation as resolved.
Show resolved Hide resolved
#[inline(always)]
fn le(lhs: felt252, rhs: felt252) -> bool {
let lhs: u256 = lhs.into();
lhs <= rhs.into()
}
#[inline(always)]
fn ge(lhs: felt252, rhs: felt252) -> bool {
let lhs: u256 = lhs.into();
lhs >= rhs.into()
}
#[inline(always)]
fn lt(lhs: felt252, rhs: felt252) -> bool {
let lhs: u256 = lhs.into();
lhs < rhs.into()
}
#[inline(always)]
fn gt(lhs: felt252, rhs: felt252) -> bool {
let lhs: u256 = lhs.into();
lhs > rhs.into()
}
}
2 changes: 2 additions & 0 deletions packages/utils/src/cryptography.cairo
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod hashes;
pub mod interface;
pub mod merkle_proof;
pub mod nonces;
pub mod snip12;
45 changes: 45 additions & 0 deletions packages/utils/src/cryptography/hashes.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts for Cairo v0.15.1 (utils/cryptography/hashes.cairo)

use core::hash::HashStateTrait;
use core::pedersen::PedersenTrait;
use core::poseidon::PoseidonTrait;
use openzeppelin_utils::common::Felt252PartialOrd;

/// Computes a commutative hash of a sorted pair of felt252.
///
/// This is usually implemented as an extension of a non-commutative hash function, like
/// Pedersen or Poseidon, returning the hash of the concatenation of the two values by first
/// sorting them.
///
/// Frequently used when working with merkle proofs.
pub trait CommutativeHasher {
fn commutative_hash(a: felt252, b: felt252) -> felt252;
}

/// Computes Pedersen's commutative hash of a sorted pair of felt252.
pub impl PedersenCHasher of CommutativeHasher {
/// Computes the Pedersen hash of chaining the two values
/// with the len, sorting the pair first.
fn commutative_hash(a: felt252, b: felt252) -> felt252 {
let hash_state = PedersenTrait::new(0);
if a < b {
hash_state.update(a).update(b).update(2).finalize()
} else {
hash_state.update(b).update(a).update(2).finalize()
}
}
}

/// Computes Poseidon's commutative hash of a sorted pair of felt252.
pub impl PoseidonCHasher of CommutativeHasher {
/// Computes the Poseidon hash of the concatenation of two values, sorting the pair first.
fn commutative_hash(a: felt252, b: felt252) -> felt252 {
let hash_state = PoseidonTrait::new();
if a < b {
hash_state.update(a).update(b).finalize()
} else {
hash_state.update(b).update(a).finalize()
}
}
}
149 changes: 149 additions & 0 deletions packages/utils/src/cryptography/merkle_proof.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts for Cairo v0.15.1 (utils/cryptography/merkle_proof.cairo)

/// These functions deal with verification of Merkle Tree proofs.
///
/// WARNING: You should avoid using leaf values that are 62 bytes long prior to
/// hashing, or use a different hash function for hashing leaves and pre-images.
/// This is because the concatenation of a sorted pair of internal nodes in
/// the Merkle tree could be reinterpreted as a leaf value.
///
/// NOTE: This library supports proof verification for merkle trees built using
/// custom _commutative_ hashing functions (i.e. `H(a, b) == H(b, a)`). Proving
/// leaf inclusion in trees built using non-commutative hashing functions requires
/// additional logic that is not supported by this library.

use openzeppelin_utils::cryptography::hashes::{CommutativeHasher, PedersenCHasher, PoseidonCHasher};

/// Version of `verify` using perdersen as the hashing function.
pub fn verify_pedersen(proof: Span<felt252>, root: felt252, leaf: felt252) -> bool {
verify::<PedersenCHasher>(proof, root, leaf)
}

/// Version of `verify` using poseidon as the hashing function.
pub fn verify_poseidon(proof: Span<felt252>, root: felt252, leaf: felt252) -> bool {
verify::<PoseidonCHasher>(proof, root, leaf)
}

/// Returns true if a `leaf` can be proved to be a part of a Merkle tree
/// defined by `root`. For this, a `proof` must be provided, containing
/// sibling hashes on the branch from the leaf to the root of the tree. Each
/// pair of leaves and each pair of pre-images are assumed to be sorted.
pub fn verify<impl Hasher: CommutativeHasher>(
proof: Span<felt252>, root: felt252, leaf: felt252
) -> bool {
process_proof::<Hasher>(proof, leaf) == root
}

/// Returns the rebuilt hash obtained by traversing a Merkle tree up
/// from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
/// hash matches the root of the tree. When processing the proof, the pairs
/// of leaves & pre-images are assumed to be sorted.
pub fn process_proof<impl Hasher: CommutativeHasher>(
proof: Span<felt252>, leaf: felt252
) -> felt252 {
let mut computed_hash = leaf;
for hash in proof {
computed_hash = Hasher::commutative_hash(computed_hash, *hash);
};
computed_hash
}

/// Returns True if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined
/// by `root`, according to `proof` and `proof_flags` as described in `process_multi_proof`.
///
/// CAUTION: Not all Merkle trees admit multiproofs. See `process_multi_proof` for details.
///
/// NOTE: Consider the case where `root == proof[0] && leaves.len() == 0` as it will return `True`.
/// The `leaves` must be validated independently. See `process_multi_proof`.
pub fn verify_multi_proof<impl Hasher: CommutativeHasher>(
proof: Span<felt252>, proof_flags: Span<bool>, root: felt252, leaves: Span<felt252>
) -> bool {
process_multi_proof::<Hasher>(proof, proof_flags, leaves) == root
}

/// Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The
/// reconstruction proceeds by incrementally reconstructing all inner nodes by combining a
/// leaf/inner node with either another leaf/inner node or a proof sibling node, depending on
/// whether each `proof_flags` item is true or false respectively.
///
/// CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure
/// that: 1) the tree is complete (but not necessarily perfect), 2) the leaves to be proven are in
/// the opposite order they are in the tree (i.e., as seen from right to left starting at the
/// deepest layer and continuing at the next layer).
///
/// NOTE: The _empty set_ (i.e. the case where `proof.len() == 1 && leaves.len() == 0`) is
/// considered a no-op, and therefore a valid multiproof (i.e. it returns `proof.at(0)`). Consider
/// disallowing this case if you're not validating the leaves elsewhere.
pub fn process_multi_proof<impl Hasher: CommutativeHasher>(
proof: Span<felt252>, proof_flags: Span<bool>, leaves: Span<felt252>
) -> felt252 {
// This function rebuilds the root hash by traversing the tree up from the leaves. The root is
// rebuilt by consuming and producing values on a queue. The queue starts with the `leaves`
// span, then goes onto the `hashes` span. At the end of the process, the last hash in the
// `hashes` span should contain the root of the Merkle tree.
let leaves_len = leaves.len();
let proof_flags_len = proof_flags.len();

// Check proof validity.
if (leaves_len + proof.len() != proof_flags_len + 1) {
panic!("MerkleProof: invalid multi proof");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like a perfect case to use assert_eq

Copy link
Member Author

@ericnordelo ericnordelo Aug 16, 2024

Choose a reason for hiding this comment

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

Sadly assert_eq only works on tests.


// The x_pos values are "pointers" to the next value to consume in each array.
// By incrementing the value we simulate a queue's pop operation.
let mut hashes = array![];
let mut leaf_pos = 0;
let mut hash_pos = 0;
let mut proof_pos = 0;
let mut i = 0;

// At each step, we compute the next hash using two values:
// - a value from the "main queue". If not all leaves have been consumed, we get the next leaf,
// otherwise we get the next hash.
// - depending on the flag, either another value from the "main queue" (merging branches) or an
// element from the `proof` array.
while i < proof_flags_len {
let a = if leaf_pos < leaves_len {
leaf_pos += 1;
leaves.at(leaf_pos - 1)
} else {
hash_pos += 1;
hashes.at(hash_pos - 1)
};

let b = if *proof_flags.at(i) {
if leaf_pos < leaves_len {
leaf_pos += 1;
leaves.at(leaf_pos - 1)
} else {
hash_pos += 1;
hashes.at(hash_pos - 1)
}
} else {
proof_pos += 1;
proof.at(proof_pos - 1)
};

hashes.append(Hasher::commutative_hash(*a, *b));
i += 1;
};

let root = if proof_flags_len > 0 {
// If `proof_flags` is not empty, assert that every
// proof was used in the validation process.
if proof_pos != proof.len() {
// TODO: check if this is not unrechable code.

Check warning on line 136 in packages/utils/src/cryptography/merkle_proof.cairo

View workflow job for this annotation

GitHub Actions / check-for-typos

"unrechable" should be "unreachable".
panic!("MerkleProof: invalid multi proof");
ericnordelo marked this conversation as resolved.
Show resolved Hide resolved
}
hashes.at(proof_flags_len - 1)
} else if leaves_len > 0 {
// If `proof_flags_len` is zero, and `leaves_len` is greater then zero,
// then `leaves_len` can only be 1, because of the proof validity check.
leaves.at(0)
ericnordelo marked this conversation as resolved.
Show resolved Hide resolved
} else {
proof.at(0)
};

*root
}
2 changes: 1 addition & 1 deletion packages/utils/src/deployments.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ pub const CONTRACT_ADDRESS_PREFIX: felt252 = 'STARKNET_CONTRACT_ADDRESS';
/// Returns the contract address from a `deploy_syscall`.
/// `deployer_address` should be the zero address if the deployment is origin-independent (deployed
/// from zero).
/// For more information, see
///
/// For more information, see
/// https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/contract-address/
pub fn calculate_contract_address_from_deploy_syscall(
salt: felt252,
Expand Down
3 changes: 2 additions & 1 deletion packages/utils/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts for Cairo v0.15.1 (utils.cairo)
// OpenZeppelin Contracts for Cairo v0.15.1 (utils/utils.cairo)
ericnordelo marked this conversation as resolved.
Show resolved Hide resolved

pub mod common;
pub mod cryptography;
pub mod deployments;
pub mod interfaces;
Expand Down
3 changes: 2 additions & 1 deletion packages/utils/src/tests.cairo
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod merkle_proof;
pub(crate) mod mocks;

mod test_hashes;
mod test_nonces;
mod test_snip12;
4 changes: 4 additions & 0 deletions packages/utils/src/tests/merkle_proof.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub(crate) mod common;

mod test_with_pedersen;
mod test_with_poseidon;
36 changes: 36 additions & 0 deletions packages/utils/src/tests/merkle_proof/common.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use starknet::{ContractAddress, contract_address_const};

#[derive(Serde, Copy, Drop, Hash)]
pub(crate) struct Leaf {
pub address: ContractAddress,
pub amount: u128,
}

pub(crate) fn LEAVES() -> Span<Leaf> {
[
Leaf {
address: contract_address_const::<
0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc8
>(),
amount: 0xfc104e31d098d1ab488fc1acaeb0269
},
Leaf {
address: contract_address_const::<
0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffc66ca5c000
>(),
amount: 0xfc104e31d098d1ab488fc1acaeb0269
},
Leaf {
address: contract_address_const::<
0x6a1f098854799debccf2d3c4059ff0f02dbfef6673dc1fcbfffffffffffffc8
>(),
amount: 0xfc104e31d098d1ab488fc1acaeb0269
},
Leaf {
address: contract_address_const::<
0xfa6541b7909bfb5e8585f1222fcf272eea352c7e0e8ed38c988bd1e2a85e82
>(),
amount: 0xaa8565d732c2c9fa5f6c001d89d5c219
},
].span()
}
Loading
Loading