Skip to content

Commit

Permalink
feat: Starknet storage proof verifier (#189)
Browse files Browse the repository at this point in the history
Starknet storage proof verifier.

## Pull Request type

<!-- Please try to limit your pull request to one type; submit multiple
pull requests if needed. -->

Please check the type of change your PR introduces:

- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no API changes)
- [ ] Build-related changes
- [ ] Documentation content changes
- [ ] Other (please describe):

## What is the current behavior?

<!-- Please describe the current behavior that you are modifying, or
link to a relevant issue. -->

Issue Number: N/A

## What is the new behavior?

<!-- Please describe the behavior or changes that are being added by
this PR. -->

-
-
-

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this does introduce a breaking change, please describe the
impact and migration path for existing applications below. -->

## Other information

<!-- Any other information that is important to this PR, such as
screenshots of how the component looks before and after the change. -->
  • Loading branch information
maciejka authored Oct 17, 2023
1 parent 3356bf0 commit 2cd5cff
Show file tree
Hide file tree
Showing 16 changed files with 886 additions and 24 deletions.
1 change: 1 addition & 0 deletions Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"src/encoding",
"src/linalg",
"src/math",
"src/merkle_tree",
"src/numeric",
"src/searching",
"src/sorting",
Expand Down
22 changes: 7 additions & 15 deletions src/data_structures/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
# Data structures
## [Array extension](./src/array_ext.cairo)
A collection of handy functions to help with array manipulation.

## [Merkle Tree](./src/merkle_tree.cairo)

The Merkle tree algorithm is a cryptographic hashing algorithm used to create a hash tree, which is a tree data structure where each leaf node represents a data block and each non-leaf node represents a hash of its child nodes.
The purpose of the Merkle tree algorithm is to provide a way to verify the integrity and authenticity of large amounts of data without needing to store the entire data set. The algorithm has applications in various areas of computer science, including cryptocurrency, file sharing, and database management.
The Merkle tree algorithm is also used in creating digital signatures and verifying the authenticity of transactions.
By providing a secure and efficient way to verify data integrity, the Merkle tree algorithm is an important tool in cryptography and information security.
A generic implementation is available to manage both pedersen (legacy) and poseidon hash methods.
A collection of handy functions to help with array manipulation.

## [Queue](./src/queue.cairo)

The queue is used to store and manipulate a collection of elements where the elements are processed in a first-in, first-out (FIFO) order.
The purpose of the queue algorithm is to provide a way to manage and process elements in a specific order, where the oldest element is processed first.
The queue algorithm has applications in various areas of computer science, including operating systems, networking, and data processing. It is used to manage tasks that are processed in a specific order and ensure that the order is maintained.
The queue is used to store and manipulate a collection of elements where the elements are processed in a first-in, first-out (FIFO) order.
The purpose of the queue algorithm is to provide a way to manage and process elements in a specific order, where the oldest element is processed first.
The queue algorithm has applications in various areas of computer science, including operating systems, networking, and data processing. It is used to manage tasks that are processed in a specific order and ensure that the order is maintained.
By providing a simple and efficient way to manage elements in a specific order, the queue algorithm is an important tool in computer science and software development.

## [Stack](./src/stack.cairo)

The stack is used to store and manipulate a collection of elements where the elements are processed in a last-in, first-out (LIFO) order.
The purpose of the stack algorithm is to provide a way to manage and process elements in a specific order, where the most recently added element is processed first.
The stack algorithm has applications in various areas of computer science, including programming languages, operating systems, and network protocols. It is used to manage tasks that require a temporary storage of data or information, and also for processing recursive function calls.
The stack is used to store and manipulate a collection of elements where the elements are processed in a last-in, first-out (LIFO) order.
The purpose of the stack algorithm is to provide a way to manage and process elements in a specific order, where the most recently added element is processed first.
The stack algorithm has applications in various areas of computer science, including programming languages, operating systems, and network protocols. It is used to manage tasks that require a temporary storage of data or information, and also for processing recursive function calls.
By providing a simple and efficient way to manage elements in a specific order, the stack algorithm is an important tool in computer science and software development.
1 change: 0 additions & 1 deletion src/data_structures/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
mod array_ext;
mod byte_array_ext;
mod byte_array_reader;
mod merkle_tree;
mod queue;
mod stack;
mod vec;
Expand Down
1 change: 0 additions & 1 deletion src/data_structures/src/tests.cairo
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
mod array_ext_test;
mod byte_array_ext_test;
mod byte_array_reader_test;
mod merkle_tree_test;
mod queue_test;
mod stack_test;
mod vec_test;
12 changes: 12 additions & 0 deletions src/merkle_tree/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Merkle Tree related stuff

## [Merkle Tree](./src/merkle_tree.cairo)

The Merkle tree algorithm is a cryptographic hashing algorithm used to create a hash tree, which is a tree data structure where each leaf node represents a data block and each non-leaf node represents a hash of its child nodes.
The purpose of the Merkle tree algorithm is to provide a way to verify the integrity and authenticity of large amounts of data without needing to store the entire data set. The algorithm has applications in various areas of computer science, including cryptocurrency, file sharing, and database management.
The Merkle tree algorithm is also used in creating digital signatures and verifying the authenticity of transactions.
By providing a secure and efficient way to verify data integrity, the Merkle tree algorithm is an important tool in cryptography and information security.
A generic implementation is available to manage both pedersen (legacy) and poseidon hash methods.

## [Starknet Storage Proof Verifier](./src/storage_proof.cairo)
Implementation of Starknet ([storage proofs](https://docs.starknet.io/documentation/architecture_and_concepts/State/starknet-state/)) returned by `pathfinder_getproof` API endpoint ([see](https://github.com/eqlabs/pathfinder/blob/main/doc/rpc/pathfinder_rpc_api.json)).
8 changes: 8 additions & 0 deletions src/merkle_tree/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "alexandria_merkle_tree"
version = "0.1.0"
description = "Merkle tree related stuff"
homepage = "https://github.com/keep-starknet-strange/alexandria/tree/src/merkle_tree"

[dependencies]
starknet.workspace = true
6 changes: 6 additions & 0 deletions src/merkle_tree/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mod merkle_tree;
mod storage_proof;


#[cfg(test)]
mod tests;
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@
//! let root = merkle_tree.compute_root(leaf, proof);
//! ```

// Core lib imports
use array::{ArrayTrait, SpanTrait};
use traits::{Into, Copy, Drop};

/// Hasher trait.

trait HasherTrait<T> {
Expand Down
163 changes: 163 additions & 0 deletions src/merkle_tree/src/storage_proof.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
use pedersen::PedersenTrait;
use poseidon::PoseidonTrait;
use hash::HashStateTrait;

#[derive(Drop)]
struct BinaryNode {
left: felt252,
right: felt252,
}

#[derive(Drop, Copy)]
struct EdgeNode {
child: felt252,
path: felt252,
length: u8,
}

#[derive(Drop)]
enum TrieNode {
Binary: BinaryNode,
Edge: EdgeNode,
}

#[derive(Destruct)]
struct ContractData {
class_hash: felt252,
nonce: felt252,
contract_state_hash_version: felt252,
storage_proof: Array<TrieNode>
}

#[derive(Destruct)]
struct ContractStateProof {
class_commitment: felt252,
contract_proof: Array<TrieNode>,
contract_data: ContractData
}

/// Verify Starknet storage proof. For reference see:
/// - ([state](https://docs.starknet.io/documentation/architecture_and_concepts/State/starknet-state/))
/// - ([pathfinder_getproof API endpoint](https://github.com/eqlabs/pathfinder/blob/main/doc/rpc/pathfinder_rpc_api.json))
/// - ([pathfinder storage implementation](https://github.com/eqlabs/pathfinder/blob/main/crates/merkle-tree/src/tree.rs))
/// # Arguments
/// * `expected_state_commitment` - state root `proof` is going to be verified against
/// * `contract_address` - `contract_address` of the value to be verified
/// * `storage_address` - `storage_address` of the value to be verified
/// * `proof` - `ContractStateProof` representing storage proof
/// # Returns
/// * `felt252` - `value` at `storage_address` if verified, panic otherwise.
fn verify(
expected_state_commitment: felt252,
contract_address: felt252,
storage_address: felt252,
proof: ContractStateProof
) -> felt252 {
let contract_data = proof.contract_data;

let (contract_root_hash, storage_value) = traverse(
storage_address, contract_data.storage_proof
);

let contract_state_hash = pedersen_hash_4(
contract_data.class_hash,
contract_root_hash,
contract_data.nonce,
contract_data.contract_state_hash_version
);

let (contracts_tree_root, expected_contract_state_hash) = traverse(
contract_address, proof.contract_proof
);

assert(expected_contract_state_hash == contract_state_hash, 'wrong contract_state_hash');

let state_commitment = poseidon_hash(
'STARKNET_STATE_V0', contracts_tree_root, proof.class_commitment
);

assert(expected_state_commitment == state_commitment, 'wrong state_commitment');

storage_value
}

fn traverse(expected_path: felt252, proof: Array<TrieNode>) -> (felt252, felt252) {
let mut path: felt252 = 0;
let mut remaining_depth: u8 = 251;

let mut nodes = proof.span();
let expected_path_u256: u256 = expected_path.into();

let leaf = *match nodes.pop_back().unwrap() {
TrieNode::Binary(_) => panic_with_felt252('invalid leaf type'),
TrieNode::Edge(edge) => edge
};

let mut expected_hash = node_hash(@TrieNode::Edge(leaf));
let value = leaf.child;
let mut path = leaf.path;
let mut path_length_pow2 = pow(2, leaf.length);

loop {
match nodes.pop_back() {
Option::Some(node) => {
match node {
TrieNode::Binary(binary_node) => {
if expected_path_u256 & path_length_pow2.into() > 0 {
assert(expected_hash == *binary_node.right, 'invalid node hash');
path += path_length_pow2;
} else {
assert(expected_hash == *binary_node.left, 'invalid node hash');
};
path_length_pow2 *= 2;
},
TrieNode::Edge(edge_node) => {
assert(expected_hash == *edge_node.child, 'invalid node hash');
path += *edge_node.path * path_length_pow2;
path_length_pow2 *= pow(2, *edge_node.length);
}
}
expected_hash = node_hash(node);
},
Option::None => { break; }
};
};
assert(expected_path == path, 'invalid proof path');
(expected_hash, value)
}

#[inline]
fn node_hash(node: @TrieNode) -> felt252 {
match node {
TrieNode::Binary(binary) => pedersen_hash(*binary.left, *binary.right),
TrieNode::Edge(edge) => pedersen_hash(*edge.child, *edge.path) + (*edge.length).into()
}
}

// TODO: replace with lookup table once array constants are available in Cairo
fn pow(x: felt252, n: u8) -> felt252 {
if n == 0 {
1
} else if n == 1 {
x
} else if (n & 1) == 1 {
x * pow(x * x, n / 2)
} else {
pow(x * x, n / 2)
}
}

#[inline(always)]
fn pedersen_hash(a: felt252, b: felt252) -> felt252 {
PedersenTrait::new(a).update(b).finalize()
}

#[inline(always)]
fn pedersen_hash_4(a: felt252, b: felt252, c: felt252, d: felt252) -> felt252 {
PedersenTrait::new(a).update(b).update(c).update(d).finalize()
}

#[inline(always)]
fn poseidon_hash(a: felt252, b: felt252, c: felt252) -> felt252 {
PoseidonTrait::new().update(a).update(b).update(c).finalize()
}
3 changes: 3 additions & 0 deletions src/merkle_tree/src/tests.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod merkle_tree_test;
mod storage_proof_test;
mod storage_proof_test_data;
18 changes: 18 additions & 0 deletions src/merkle_tree/src/tests/get_storage_proof.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#! /usr/bin/env bash
set -e;
set -o pipefail;

curl -s -X POST \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"method": "pathfinder_getProof",
"params": {
"block_id": { "block_hash": "'${1}'"},
"contract_address": "'${2}'",
"keys": ["'${3}'"]
},
"id": 0
}' \
$STARKNET_RPC \
| jq -r -f storage_proof_filter.jq
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// Internal imports
use alexandria_data_structures::merkle_tree::{
use alexandria_merkle_tree::merkle_tree::{
Hasher, MerkleTree, pedersen::PedersenHasherImpl, poseidon::PoseidonHasherImpl, MerkleTreeTrait,
MerkleTreeImpl
};


mod regular_call_merkle_tree_pedersen {
// Internal imports
use alexandria_data_structures::merkle_tree::{
use alexandria_merkle_tree::merkle_tree::{
Hasher, MerkleTree, pedersen::PedersenHasherImpl, MerkleTreeTrait,
};
#[test]
Expand Down
34 changes: 34 additions & 0 deletions src/merkle_tree/src/tests/storage_proof_filter.jq
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
def node:
if has("binary")
then
"TrieNode::Binary(
BinaryNode {
left: \(.binary.left),
right: \(.binary.right),
}
)"
else
"TrieNode::Edge(
EdgeNode {
path: \(.edge.path.value),
length: \(.edge.path.len),
child: \(.edge.child),
}
)"
end
;

def proof:
"array![\n\(.|map(node) | join(",\n"))\n]"
;

.result|"ContractStateProof {
class_commitment: \(.class_commitment),
contract_proof: \(.contract_proof | proof),
contract_data: \(.contract_data|"ContractData {
class_hash: \(.class_hash),
nonce: \(.nonce),
contract_state_hash_version: 0,
storage_proof: \(.storage_proofs | .[0] | proof),
}")
}"
58 changes: 58 additions & 0 deletions src/merkle_tree/src/tests/storage_proof_test.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use alexandria_merkle_tree::storage_proof::{
ContractStateProof, ContractData, TrieNode, BinaryNode, EdgeNode, verify
};

use alexandria_merkle_tree::tests::storage_proof_test_data::{balance_proof, total_balance_proof};

const DAI: felt252 = 0x00da114221cb83fa859dbdb4c44beeaa0bb37c7537ad5ae66fe5e0efd20e6eb3;
const ETH: felt252 = 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7;

#[test]
#[available_gas(2000000)]
fn balance_lsb_proof_test() {
let state_commitment = 0x07dc88984a2d8f9c2a6d2d431b2d8f2c32957da514c16ceef0761b6933121708;
let contract_address = DAI;
let storage_address = 0x4ae51d08cd202d1472587dfe63dbf2d5ec767cbf4218b59b7ab71956780c6ee;
let expected_value = 8700000000000000005;
let proof = balance_proof();
let value = verify(state_commitment, contract_address, storage_address, proof);
assert(expected_value == value, 'wrong value');
}

#[test]
#[should_panic(expected: ('invalid proof path',))]
#[available_gas(2000000)]
fn balance_msb_proof_test() {
let state_commitment = 0x07dc88984a2d8f9c2a6d2d431b2d8f2c32957da514c16ceef0761b6933121708;
let contract_address = DAI;
let storage_address = 0x4ae51d08cd202d1472587dfe63dbf2d5ec767cbf4218b59b7ab71956780c6ef;
let expected_value = 8700000000000000005;
let proof = balance_proof();
let value = verify(state_commitment, contract_address, storage_address, proof);
assert(expected_value == value, 'wrong value');
}

#[test]
#[should_panic(expected: ('invalid node hash',))]
#[available_gas(2000000)]
fn wrong_contract_address_proof_test() {
let state_commitment = 0x07dc88984a2d8f9c2a6d2d431b2d8f2c32957da514c16ceef0761b6933121708;
let contract_address = ETH;
let storage_address = 0x4ae51d08cd202d1472587dfe63dbf2d5ec767cbf4218b59b7ab71956780c6ee;
let expected_value = 8700000000000000005;
let proof = balance_proof();
let value = verify(state_commitment, contract_address, storage_address, proof);
assert(expected_value == value, 'wrong value');
}

#[test]
#[available_gas(50000000)]
fn total_balance_lsb_proof_test() {
let state_commitment = 0x07dc88984a2d8f9c2a6d2d431b2d8f2c32957da514c16ceef0761b6933121708;
let contract_address = DAI;
let storage_address = 0x37a9774624a0e3e0d8e6b72bd35514f62b3e8e70fbaff4ed27181de4ffd4604;
let expected_value = 2970506847688829412026631;
let proof = total_balance_proof();
let value = verify(state_commitment, contract_address, storage_address, proof);
assert(expected_value == value, 'wrong value');
}
Loading

0 comments on commit 2cd5cff

Please sign in to comment.