Skip to content

Commit

Permalink
fix: Replace Map simulating Array with Vec - streamline md file expla…
Browse files Browse the repository at this point in the history
…nations
  • Loading branch information
hudem1 committed Sep 28, 2024
1 parent dfcf526 commit d37b1a3
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 50 deletions.
39 changes: 12 additions & 27 deletions listings/applications/merkle_tree/src/contract.cairo
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
use core::poseidon::PoseidonTrait;
use core::hash::{HashStateTrait};
use core::poseidon::poseidon_hash_span;

#[generate_trait]
pub impl ByteArrayHashTraitImpl of ByteArrayHashTrait {
fn hash(self: @ByteArray) -> felt252 {
let mut serialized_byte_arr: Array<felt252> = ArrayTrait::new();
self.serialize(ref serialized_byte_arr);

let mut hash = PoseidonTrait::new().update(poseidon_hash_span(serialized_byte_arr.span()));

hash.finalize()
core::poseidon::poseidon_hash_span(serialized_byte_arr.span())
}
}

Expand All @@ -24,7 +18,7 @@ pub trait IMerkleTree<TContractState> {
) -> bool;
}

mod Errors {
mod errors {
pub const NOT_POW_2: felt252 = 'Data length is not a power of 2';
pub const NOT_PRESENT: felt252 = 'No element in merkle tree';
}
Expand All @@ -34,16 +28,14 @@ pub mod MerkleTree {
use core::poseidon::PoseidonTrait;
use core::hash::{HashStateTrait, HashStateExTrait};
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerWriteAccess,
StoragePointerReadAccess
StoragePointerWriteAccess, StoragePointerReadAccess,
Vec, MutableVecTrait, VecTrait
};
use super::ByteArrayHashTrait;

#[storage]
struct Storage {
// cannot store Array, therefore use a LegacyMap to simulate an array
pub hashes: Map::<usize, felt252>,
pub hashes_length: usize
pub hashes: Vec<felt252>
}

#[derive(Drop, Serde, Copy)]
Expand All @@ -56,16 +48,13 @@ pub mod MerkleTree {
impl IMerkleTreeImpl of super::IMerkleTree<ContractState> {
fn build_tree(ref self: ContractState, mut data: Array<ByteArray>) -> Array<felt252> {
let data_len = data.len();
assert(data_len > 0 && (data_len & (data_len - 1)) == 0, super::Errors::NOT_POW_2);
assert(data_len > 0 && (data_len & (data_len - 1)) == 0, super::errors::NOT_POW_2);

let mut _hashes: Array<felt252> = ArrayTrait::new();

// first, hash every leaf
let mut i = 0;
while let Option::Some(value) = data.pop_front() {
for value in data {
_hashes.append(value.hash());

i += 1;
};

// then, hash all levels above leaves
Expand All @@ -89,22 +78,18 @@ pub mod MerkleTree {
};

// write to the contract state (useful for the get_root function)
let mut i = 0;
let hashes_span = _hashes.span();
while i < hashes_span.len() {
self.hashes.write(i, *hashes_span.at(i));
i += 1;
for hash in _hashes.span() {
self.hashes.append().write(*hash);
};
self.hashes_length.write(hashes_span.len());

_hashes
}

fn get_root(self: @ContractState) -> felt252 {
let merkle_tree_length = self.hashes_length.read();
assert(merkle_tree_length > 0, super::Errors::NOT_PRESENT);
let merkle_tree_length = self.hashes.len();
assert(merkle_tree_length > 0, super::errors::NOT_PRESENT);

self.hashes.read(merkle_tree_length - 1)
self.hashes.at(merkle_tree_length - 1).read()
}

fn verify(
Expand Down
14 changes: 6 additions & 8 deletions listings/applications/merkle_tree/src/tests.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use starknet::{ContractAddress, SyscallResultTrait};
use starknet::testing::set_contract_address;
use core::poseidon::PoseidonTrait;
use core::hash::{HashStateTrait, HashStateExTrait};
use starknet::storage::{StorageMapReadAccess, StoragePointerReadAccess};
use starknet::storage::{VecTrait, StoragePointerReadAccess};

fn deploy_util(class_hash: felt252, calldata: Array<felt252>) -> ContractAddress {
let (address, _) = deploy_syscall(class_hash.try_into().unwrap(), 0, calldata.span(), false)
Expand All @@ -28,7 +28,7 @@ fn should_deploy() {
// in order to access its internal state fields for assertions
set_contract_address(deploy.contract_address);

assert_eq!(state.hashes_length.read(), 0);
assert_eq!(state.hashes.len(), 0);
}

#[test]
Expand Down Expand Up @@ -81,13 +81,11 @@ fn build_tree_succeeds() {
// in order to access its internal state fields for assertions
set_contract_address(deploy.contract_address);

assert_eq!(state.hashes_length.read(), expected_hashes.len());
assert_eq!(state.hashes.len(), expected_hashes.len().into());

let mut i = 0;
while i < expected_hashes.len() {
assert_eq!(state.hashes.read(i), *expected_hashes.at(i));
i += 1;
};
for i in 0..expected_hashes.len() {
assert_eq!(state.hashes.at(i.into()).read(), *expected_hashes.at(i));
}
}

#[test]
Expand Down
24 changes: 9 additions & 15 deletions src/applications/merkle_tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,16 @@ Here's a quick summary of how it operates and what functionalities it supports:
### Examples of use cases:

1. Fundamental use case: Ethereum blockchain integrity
- Cryptocurrencies like Bitcoin and Ethereum use Merkle trees to efficiently summarize and validate transactions within a block. This process is crucial for ensuring the integrity and immutability of the blockchain.

- Here is the process detailed:
1. In a block, each transaction is hashed to form the leaf nodes of the merkle tree.
2. As explained, pairwise hashes of the leaf nodes are combined and hashed again to create parent nodes. We do this recursively until we obtain a single hash, the merkle root. Here, the Merkle root represents a compact and efficient summary of all transactions within the block
3. The Merkle root is included in the block header, along with other important information like the previous block hash, timestamp, and nonce.
4. This header is then hashed to form the block's hash, which uniquely identifies the block in the blockchain.

- Guaranteed Integrity: If a node tries to tamper with a transaction, it will change the transaction hash, changing the merkle root hash of the transactions of the block, resulting in a different block header hash, resulting in a different block hash. Therefore, the other nodes will easily see the change and will not accept the block.

- Transaction verification: When a node needs to verify a specific transaction, it does not need to download the entire block. Instead, it can use a Merkle proof (a path of hashes from the transaction to the Merkle root) to prove that the transaction is included in the block.
- Cryptocurrencies like Ethereum use Merkle trees to efficiently verify and maintain transaction integrity within blocks.
- Each transaction in a block is hashed to form leaf nodes, and these hashes are recursively combined to form a single Merkle root, summarizing all transactions.
- The Merkle root is stored in the block header, which is hashed to generate the block's unique identifier.
- <u>Guaranteed Integrity:</u> Any change to a transaction alters the Merkle root, block header, and block hash, making it easy for nodes to detect tampering.
- <u>Transaction verification:</u> Nodes can verify specific transactions via Merkle proofs without downloading the entire block.

2. Whitelist inclusion
- One common use case of Merkle trees in blockchain is to verify whether a particular address is part of a whitelist, without having to store the entire list on-chain. This is efficient and cost-effective due to reduced on-chain storage costs.

- This method consists in saving the merkle root of the whitelist on-chain, while the entire list is stored elsewhere off-chain. To verify inclusion of an address, a user provides a Merkle proof and the address whose inclusion they want to verify. The merkle root will be re-computed thanks to the provided data (address here) and the merkle proof, before being verified against the merkle proof stored on-chain. If they match, the address passed is indeed included in the whitelist. Otherwise, it is not included.
- Merkle trees allow efficient whitelist verification without storing the full list on-chain, reducing storage costs.
- The Merkle root of the whitelist is stored on-chain, while the full list remains off-chain.
- To verify if an address is on the whitelist, a user provides a Merkle proof and the address. The Merkle root is recalculated using the provided data and compared to the stored on-chain root. If they match, the address is included; if not, it's excluded.

3. Decentralized Identity Verification
- Merkle trees can be used in decentralized identity systems to verify credentials.
Expand All @@ -60,7 +54,7 @@ Each other node is the hash of the combination of both children nodes.
If we were to `verify` the `hash 6`, the merkle proof would need to contain the `hash 5`, `hash 12`and `hash 13`:
1. The `hash 5` would be combined with the `hash 6` to re-compute the `hash 11`.
2. The newly computed `hash 11` in step 1 would be combined with `hash 12` to re-compute `hash 14`.
3. The newly computed `hash 14` in step 2 would be combined with `hash 13` to re-compute the merkle root.
3. The `hash 13` would be combined with the newly computed `hash 14` in step 2 to re-compute the merkle root.
4. We can then compare the computed resultant merkle root with the one provided to the `verify` function.

### Code
Expand Down

0 comments on commit d37b1a3

Please sign in to comment.