diff --git a/.tool-versions b/.tool-versions index 2175a33..e129096 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -scarb 0.7.0 +scarb 2.6.4 diff --git a/README.md b/README.md index 3b50ac8..97930c5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![](/banner.png) + # `cairo-lib`: Comprehensive Library for Cairo 🐺 [![CI](https://github.com/HerodotusDev/cairo-lib/actions/workflows/ci.yml/badge.svg)](https://github.com/HerodotusDev/cairo-lib/actions/workflows/ci.yml) diff --git a/Scarb.lock b/Scarb.lock new file mode 100644 index 0000000..8b41a8f --- /dev/null +++ b/Scarb.lock @@ -0,0 +1,6 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "cairo_lib" +version = "0.2.0" diff --git a/audit/ChainSecurity_Herodotus_CairoLib_audit.pdf b/audit/ChainSecurity_Herodotus_CairoLib_audit.pdf new file mode 100644 index 0000000..15005e0 Binary files /dev/null and b/audit/ChainSecurity_Herodotus_CairoLib_audit.pdf differ diff --git a/banner.png b/banner.png new file mode 100644 index 0000000..6160822 Binary files /dev/null and b/banner.png differ diff --git a/src/data_structures/eth_mpt.cairo b/src/data_structures/eth_mpt.cairo index 6976766..30e398a 100644 --- a/src/data_structures/eth_mpt.cairo +++ b/src/data_structures/eth_mpt.cairo @@ -1,8 +1,8 @@ use cairo_lib::hashing::keccak::keccak_cairo_words64; -use cairo_lib::encoding::rlp::{RLPItem, rlp_decode}; +use cairo_lib::encoding::rlp::{RLPItem, rlp_decode, rlp_decode_list_lazy}; use cairo_lib::utils::types::byte::{Byte, ByteTrait}; use cairo_lib::utils::bitwise::{right_shift, left_shift}; -use cairo_lib::utils::types::words64::{Words64, Words64Trait, Words64TryIntoU256LE}; +use cairo_lib::utils::types::words64::{Words64, Words64Trait}; use cairo_lib::utils::math::pow; // @notice Ethereum Merkle Patricia Trie struct @@ -19,19 +19,23 @@ impl MPTDefault of Default { } // @notice Represents a node in the MPT -#[derive(Drop)] +#[derive(Drop, PartialEq)] enum MPTNode { - // @param 16 hashes of children - // @param Value of the node + // @param hashes 16 hashes of children + // @param value value of the node Branch: (Span, Words64), + // @param hash hash of the next node + LazyBranch: u256, // @param shared_nibbles // @param next_node - // @param nibbles_skip Number of nibbles to skip in shared nibbles - Extension: (Words64, u256, usize), + // @param nibbles_skip number of nibbles to skip in shared nibbles + // @param n_nibbles number of shared nibbles + Extension: (Words64, u256, usize, usize), // @param key_end - // @param value of the node - // @param nibbles_skip Number of nibbles to skip in the key end - Leaf: (Words64, Words64, usize) + // @param value value of the node + // @param nibbles_skip number of nibbles to skip in the key end + // @param n_nibbles number of nibbles in key_end + Leaf: (Words64, Words64, usize, usize) } #[generate_trait] @@ -47,30 +51,52 @@ impl MPTImpl of MPTTrait { // @param key Key to verify // @param key_len Length of they key in nibbles // @param proof Merkle proof, collection of rlp encoded nodes - // @return Result with the value associated with the key if it exists + // @return Result with the value associated with the key, empty in case of non inclusion fn verify( self: @MPT, key: u256, key_len: usize, proof: Span ) -> Result { let mut current_hash = *self.root; let mut proof_index: usize = 0; - let mut key_pow2: u256 = pow(2, (key_len.into() - 1) * 4); + let mut key_pow2: u256 = if key_len == 64 { + 0x1000000000000000000000000000000000000000000000000000000000000000 + } else { + pow(2, (key_len.into() - 1) * 4) + }; + + let proof_len = proof.len(); loop { - if proof_index == proof.len() { + if proof_index == proof_len { break Result::Err('Proof reached end'); } let node = *proof.at(proof_index); - let hash = MPTTrait::hash_rlp_node(node); - assert(hash == current_hash, 'Element not matching'); - - let decoded = match MPTTrait::decode_rlp_node(node) { - Result::Ok(decoded) => decoded, - Result::Err(e) => { - break Result::Err(e); + // If it's not the last node and more than 9 words, it must be a branch node + let (decoded, rlp_byte_len) = if proof_index != proof_len - 1 && node.len() > 9 { + let current_nibble = (key / key_pow2) & 0xf; + // Unwrap impossible to fail, as we are masking with 0xf, meaning the result is always a nibble + match MPTTrait::lazy_rlp_decode_branch_node( + node, current_nibble.try_into().unwrap() + ) { + Result::Ok(d) => d, + Result::Err(e) => { break Result::Err(e); } + } + } else { + match MPTTrait::decode_rlp_node(node) { + Result::Ok(d) => d, + Result::Err(e) => { break Result::Err(e); } } }; + + let mut last_word_byte_len = rlp_byte_len % 8; + if last_word_byte_len == 0 { + last_word_byte_len = 8; + } + + let hash = MPTTrait::hash_rlp_node(node, last_word_byte_len); + assert(hash == current_hash, 'Element not matching'); + match decoded { MPTNode::Branch(( nibbles, value @@ -85,19 +111,24 @@ impl MPTImpl of MPTTrait { let current_hash_words = *nibbles.at(current_nibble.try_into().unwrap()); current_hash = if current_hash_words.len() == 0 { - 0 + break Result::Ok(array![].span()); } else { - match current_hash_words.try_into() { - Option::Some(h) => h, - Option::None(_) => { - break Result::Err('Invalid hash'); - } + match current_hash_words.as_u256_le(32) { + Result::Ok(h) => h, + Result::Err(_) => { break Result::Err('Invalid hash'); } } }; key_pow2 = key_pow2 / 16; }, + MPTNode::LazyBranch(next_node) => { + if next_node == 0 { + break Result::Ok(array![].span()); + } + current_hash = next_node; + key_pow2 = key_pow2 / 16; + }, MPTNode::Extension(( - shared_nibbles, next_node, nibbles_skip + shared_nibbles, next_node, nibbles_skip, n_nibbles )) => { let mut shared_nibbles_pow2 = pow(2, nibbles_skip.into() * 4); @@ -113,18 +144,25 @@ impl MPTImpl of MPTTrait { let mut shared_nibbles_word_idx = nibbles_skip / 16; let mut shared_nibbles_word = *shared_nibbles.at(shared_nibbles_word_idx); + let mut i_nibbles = 0; let next_hash = loop { - if key_pow2 == 0 { - break Result::Err('Key reached end'); - } - let current_nibble_shared_nibbles = (shared_nibbles_word / shared_nibbles_pow2) & 0xf; let current_nibble_key = (key / key_pow2) & 0xf; if current_nibble_shared_nibbles.into() != current_nibble_key { + break Result::Ok(0); + } + + key_pow2 = key_pow2 / 16; + i_nibbles += 1; + + if i_nibbles == n_nibbles { break Result::Ok(next_node); } + if key_pow2 == 0 { + break Result::Err('Key reached end'); + } if shared_nibbles_pow2 == 0x100000000000000 { shared_nibbles_pow2 = 16; @@ -139,20 +177,20 @@ impl MPTImpl of MPTTrait { }; in_byte = !in_byte; - key_pow2 = key_pow2 / 16; }; match next_hash { Result::Ok(next_hash) => { + if next_hash == 0 { + break Result::Ok(array![].span()); + } current_hash = next_hash; }, - Result::Err(e) => { - break Result::Err(e); - } + Result::Err(e) => { break Result::Err(e); } } }, MPTNode::Leaf(( - key_end, value, nibbles_skip + key_end, value, nibbles_skip, n_nibbles )) => { let mut key_end_pow2 = pow(2, nibbles_skip.into() * 4); @@ -168,15 +206,19 @@ impl MPTImpl of MPTTrait { let mut key_end_word_idx = nibbles_skip / 16; let mut key_end_word = *key_end.at(key_end_word_idx); + let mut i_nibbles = 0; break loop { - if key_pow2 == 0 { - break Result::Ok(value); - } - let current_nibble_key_end = (key_end_word / key_end_pow2) & 0xf; let current_nibble_key = (key / key_pow2) & 0xf; if current_nibble_key_end.into() != current_nibble_key { - break Result::Err('Key not matching'); + break Result::Ok(array![].span()); + } + + key_pow2 = key_pow2 / 16; + i_nibbles += 1; + + if key_pow2 == 0 && i_nibbles == n_nibbles { + break Result::Ok(value); } if key_end_pow2 == 0x100000000000000 { @@ -192,7 +234,6 @@ impl MPTImpl of MPTTrait { }; in_byte = !in_byte; - key_pow2 = key_pow2 / 16; }; } }; @@ -203,9 +244,9 @@ impl MPTImpl of MPTTrait { // @notice Decodes an RLP encoded node // @param rlp RLP encoded node - // @return Result with the decoded node - fn decode_rlp_node(rlp: Words64) -> Result { - let (item, _) = rlp_decode(rlp)?; + // @return Result with the decoded node and the RLP byte length + fn decode_rlp_node(rlp: Words64) -> Result<(MPTNode, usize), felt252> { + let (item, rlp_byte_len) = rlp_decode(rlp)?; match item { RLPItem::Bytes(_) => Result::Err('Invalid RLP for node'), RLPItem::List(l) => { @@ -215,33 +256,43 @@ impl MPTImpl of MPTTrait { let mut i: usize = 0; loop { if i == 16 { - let value = *l.at(16); - break Result::Ok(MPTNode::Branch((nibble_hashes.span(), value))); + let (value, _) = *l.at(16); + break Result::Ok( + (MPTNode::Branch((nibble_hashes.span(), value)), rlp_byte_len) + ); } - nibble_hashes.append(*l.at(i)); + let (current_hash, _) = (*l.at(i)); + nibble_hashes.append(current_hash); i += 1; } } else if len == 2 { - let first = *l.at(0); + let (first, first_len) = *l.at(0); + let (second, _) = *l.at(1); // Unwrap impossible to fail, as we are making with 0xff, meaning the result always fits in a byte let prefix_byte: Byte = (*first.at(0) & 0xff).try_into().unwrap(); let (prefix, _) = prefix_byte.extract_nibbles(); + let n_nibbles = (first_len * 2) - 1; + if prefix == 0 { - match (*l.at(1)).try_into() { - Option::Some(n) => Result::Ok(MPTNode::Extension((first, n, 2))), - Option::None(_) => Result::Err('Invalid next node') + match second.as_u256_le(32) { + Result::Ok(n) => Result::Ok( + (MPTNode::Extension((first, n, 2, n_nibbles - 1)), rlp_byte_len) + ), + Result::Err(_) => Result::Err('Invalid next node') } } else if prefix == 1 { - match (*l.at(1)).try_into() { - Option::Some(n) => Result::Ok(MPTNode::Extension((first, n, 1))), - Option::None(_) => Result::Err('Invalid next node') + match second.as_u256_le(32) { + Result::Ok(n) => Result::Ok( + (MPTNode::Extension((first, n, 1, n_nibbles)), rlp_byte_len) + ), + Result::Err(_) => Result::Err('Invalid next node') } } else if prefix == 2 { - Result::Ok(MPTNode::Leaf((first, *l.at(1), 2))) + Result::Ok((MPTNode::Leaf((first, second, 2, n_nibbles - 1)), rlp_byte_len)) } else if prefix == 3 { - Result::Ok(MPTNode::Leaf((first, *l.at(1), 1))) + Result::Ok((MPTNode::Leaf((first, second, 1, n_nibbles)), rlp_byte_len)) } else { Result::Err('Invalid RLP prefix') } @@ -252,76 +303,30 @@ impl MPTImpl of MPTTrait { } } - // @notice keccak256 hashes an RLP encoded node - // @param rlp RLP encoded node - // @return keccak256 hash of the node - fn hash_rlp_node(rlp: Words64) -> u256 { - keccak_cairo_words64(rlp) - } -} -impl MPTNodePartialEq of PartialEq { - fn eq(lhs: @MPTNode, rhs: @MPTNode) -> bool { - match lhs { - MPTNode::Branch(( - lhs_nibbles, lhs_value - )) => { - match rhs { - MPTNode::Branch(( - rhs_nibbles, rhs_value - )) => { - if (*lhs_nibbles).len() != (*rhs_nibbles).len() { - return false; - } - let mut i: usize = 0; - loop { - if i >= (*lhs_nibbles).len() { - break lhs_value == rhs_value; - } - if (*lhs_nibbles).at(i) != (*rhs_nibbles).at(i) { - break false; - } - i += 1; - } - }, - MPTNode::Extension(_) => false, - MPTNode::Leaf(_) => false - } - }, - MPTNode::Extension(( - lhs_shared_nibbles, lhs_next_node, lhs_nibbles_skip - )) => { - match rhs { - MPTNode::Branch(_) => false, - MPTNode::Extension(( - rhs_shared_nibbles, rhs_next_node, rhs_nibbles_skip - )) => { - lhs_shared_nibbles == rhs_shared_nibbles - && lhs_next_node == rhs_next_node - && lhs_nibbles_skip == rhs_nibbles_skip - }, - MPTNode::Leaf(_) => false - } - }, - MPTNode::Leaf(( - lhs_key_end, lhs_value, lhs_nibbles_skip - )) => { - match rhs { - MPTNode::Branch(_) => false, - MPTNode::Extension(_) => false, - MPTNode::Leaf(( - rhs_key_end, rhs_value, rhs_nibbles_skip - )) => { - lhs_key_end == rhs_key_end - && lhs_value == rhs_value - && lhs_nibbles_skip == rhs_nibbles_skip - } + fn lazy_rlp_decode_branch_node( + rlp: Words64, current_nibble: u8 + ) -> Result<(MPTNode, usize), felt252> { + let (lazy_item, rlp_byte_len) = rlp_decode_list_lazy( + rlp, array![current_nibble.into()].span() + )?; + match lazy_item { + RLPItem::Bytes(_) => Result::Err('Invalid RLP for node'), + RLPItem::List(l) => { + let (hash_words, _) = *l.at(0); + match hash_words.as_u256_le(32) { + Result::Ok(h) => Result::Ok((MPTNode::LazyBranch(h), rlp_byte_len)), + Result::Err(_) => Result::Err('Invalid hash') } } } } - fn ne(lhs: @MPTNode, rhs: @MPTNode) -> bool { - !(lhs == rhs) + // @notice keccak256 hashes an RLP encoded node + // @param rlp RLP encoded node + // @param last_word_bytes number of bytes in the last worf of the RLP encoded node + // @return keccak256 hash of the node + fn hash_rlp_node(rlp: Words64, last_word_bytes: usize) -> u256 { + keccak_cairo_words64(rlp, last_word_bytes) } } diff --git a/src/data_structures/mmr/mmr.cairo b/src/data_structures/mmr/mmr.cairo index 1620c82..5a8240d 100644 --- a/src/data_structures/mmr/mmr.cairo +++ b/src/data_structures/mmr/mmr.cairo @@ -37,10 +37,10 @@ impl MMRImpl of MMRTrait { // @param peaks The peaks of the MMR // @return Result with the new root and new peaks of the MMR fn append(ref self: MMR, hash: felt252, peaks: Peaks) -> Result<(felt252, Peaks), felt252> { - let leaf_count = mmr_size_to_leaf_count(self.last_pos); + let leaf_count = mmr_size_to_leaf_count(self.last_pos.into()); let peaks_count = peaks.len(); - if leaf_count_to_peaks_count(leaf_count) != peaks_count { + if leaf_count_to_peaks_count(leaf_count) != peaks_count.into() { return Result::Err('Invalid peaks count'); } if !peaks.valid(self.last_pos, self.root) { @@ -91,9 +91,14 @@ impl MMRImpl of MMRTrait { fn verify_proof( self: @MMR, index: usize, hash: felt252, peaks: Peaks, proof: Proof ) -> Result { + let leaf_count = mmr_size_to_leaf_count((*self.last_pos).into()); + if leaf_count_to_peaks_count(leaf_count) != peaks.len().into() { + return Result::Err('Invalid peaks count'); + } if !peaks.valid(*self.last_pos, *self.root) { return Result::Err('Invalid peaks'); } + let (peak_index, peak_height) = get_peak_info(*self.last_pos, index); if proof.len() != peak_height { diff --git a/src/data_structures/mmr/tests/test_mmr.cairo b/src/data_structures/mmr/tests/test_mmr.cairo index 36327ef..a797a0b 100644 --- a/src/data_structures/mmr/tests/test_mmr.cairo +++ b/src/data_structures/mmr/tests/test_mmr.cairo @@ -23,11 +23,14 @@ fn test_append_initial() { let mut mmr: MMR = Default::default(); let peaks = array![].span(); - mmr.append(*elems.at(0), peaks); + let (new_root, new_peaks) = mmr.append(*elems.at(0), peaks).unwrap(); - let root = PoseidonHasher::hash_double(1, *elems.at(0)); + let expected_root = PoseidonHasher::hash_double(1, *elems.at(0)); assert(mmr.last_pos == 1, 'Wrong last_pos'); - assert(mmr.root == root, 'Wrong root'); + assert(mmr.root == expected_root, 'Wrong updated root'); + assert(new_root == expected_root, 'Wrong returned root'); + + assert(new_peaks == array![*elems.at(0)].span(), 'Wrong new_peaks'); } #[test] @@ -35,16 +38,25 @@ fn test_append_initial() { fn test_append_1() { let elems = helper_test_get_elements(); let mut mmr: MMR = Default::default(); + let mmr_peaks_0 = array![].span(); + + let (mmr_root_1, mmr_peaks_1) = mmr.append(*elems.at(0), mmr_peaks_0).unwrap(); + + let expected_peaks_1 = array![*elems.at(0)].span(); + let expected_root_1 = PoseidonHasher::hash_double(1, *elems.at(0)); + assert(expected_peaks_1 == mmr_peaks_1, 'Wrong peaks after 1 append'); + assert(mmr.root == expected_root_1, 'Wrong updated root after 2 a.'); + assert(mmr_root_1 == expected_root_1, 'Wrong returned root after 1 a.'); - let mut peaks = array![].span(); - mmr.append(*elems.at(0), peaks); + let (mmr_root_2, mmr_peaks_2) = mmr.append(*elems.at(1), mmr_peaks_1).unwrap(); - peaks = array![*elems.at(0)].span(); - mmr.append(*elems.at(1), peaks); + let expected_peaks_2 = array![*elems.at(2)].span(); + let expected_root_2 = PoseidonHasher::hash_double(3, *elems.at(2)); + assert(expected_peaks_2 == mmr_peaks_2, 'Wrong peaks after 2 appends'); + assert(mmr.root == expected_root_2, 'Wrong updated root after 2 a.'); + assert(mmr_root_2 == expected_root_2, 'Wrong reeturned root after 2 a.'); - let root = PoseidonHasher::hash_double(3, *elems.at(2)); assert(mmr.last_pos == 3, 'Wrong last_pos'); - assert(mmr.root == root, 'Wrong root'); } #[test] @@ -52,21 +64,35 @@ fn test_append_1() { fn test_append_2() { let elems = helper_test_get_elements(); let mut mmr: MMR = Default::default(); + let mmr_peaks_0 = array![].span(); - let mut peaks = array![].span(); - mmr.append(*elems.at(0), peaks); + let (mmr_root_1, mmr_peaks_1) = mmr.append(*elems.at(0), mmr_peaks_0).unwrap(); - peaks = array![*elems.at(0)].span(); - mmr.append(*elems.at(1), peaks); + let expected_peaks_1 = array![*elems.at(0)].span(); + let expected_root_1 = PoseidonHasher::hash_double(1, *elems.at(0)); + assert(expected_peaks_1 == mmr_peaks_1, 'Wrong peaks after 1 append'); + assert(mmr.root == expected_root_1, 'Wrong updated root after 2 a.'); + assert(mmr_root_1 == expected_root_1, 'Wrong returned root after 1 a.'); - peaks = array![*elems.at(2)].span(); - mmr.append(*elems.at(3), peaks); + let (mmr_root_2, mmr_peaks_2) = mmr.append(*elems.at(1), mmr_peaks_1).unwrap(); - let root = PoseidonHasher::hash_double( + let expected_peaks_2 = array![*elems.at(2)].span(); + let expected_root_2 = PoseidonHasher::hash_double(3, *elems.at(2)); + assert(expected_peaks_2 == mmr_peaks_2, 'Wrong peaks after 2 appends'); + assert(mmr.root == expected_root_2, 'Wrong updated root after 2 a.'); + assert(mmr_root_2 == expected_root_2, 'Wrong reeturned root after 2 a.'); + + let (mmr_root_3, mmr_peaks_3) = mmr.append(*elems.at(3), mmr_peaks_2).unwrap(); + + let expected_peaks_3 = array![*elems.at(2), *elems.at(3)].span(); + let expected_root_3 = PoseidonHasher::hash_double( 4, PoseidonHasher::hash_double(*elems.at(2), *elems.at(3)) ); + assert(expected_peaks_3 == mmr_peaks_3, 'Wrong peaks after 3 appends'); + assert(mmr.root == expected_root_3, 'Wrong updated root after 3 a.'); + assert(mmr_root_3 == expected_root_3, 'Wrong reeturned root after 3 a.'); + assert(mmr.last_pos == 4, 'Wrong last_pos'); - assert(mmr.root == root, 'Wrong root'); } #[test] @@ -74,22 +100,43 @@ fn test_append_2() { fn test_append_3() { let elems = helper_test_get_elements(); let mut mmr: MMR = Default::default(); + let mmr_peaks_0 = array![].span(); - let mut peaks = array![].span(); - mmr.append(*elems.at(0), peaks); + let (mmr_root_1, mmr_peaks_1) = mmr.append(*elems.at(0), mmr_peaks_0).unwrap(); - peaks = array![*elems.at(0)].span(); - mmr.append(*elems.at(1), peaks); + let expected_peaks_1 = array![*elems.at(0)].span(); + let expected_root_1 = PoseidonHasher::hash_double(1, *elems.at(0)); + assert(expected_peaks_1 == mmr_peaks_1, 'Wrong peaks after 1 append'); + assert(mmr.root == expected_root_1, 'Wrong updated root after 2 a.'); + assert(mmr_root_1 == expected_root_1, 'Wrong returned root after 1 a.'); - peaks = array![*elems.at(2)].span(); - mmr.append(*elems.at(3), peaks); + let (mmr_root_2, mmr_peaks_2) = mmr.append(*elems.at(1), mmr_peaks_1).unwrap(); + + let expected_peaks_2 = array![*elems.at(2)].span(); + let expected_root_2 = PoseidonHasher::hash_double(3, *elems.at(2)); + assert(expected_peaks_2 == mmr_peaks_2, 'Wrong peaks after 2 appends'); + assert(mmr.root == expected_root_2, 'Wrong updated root after 2 a.'); + assert(mmr_root_2 == expected_root_2, 'Wrong reeturned root after 2 a.'); + + let (mmr_root_3, mmr_peaks_3) = mmr.append(*elems.at(3), mmr_peaks_2).unwrap(); + + let expected_peaks_3 = array![*elems.at(2), *elems.at(3)].span(); + let expected_root_3 = PoseidonHasher::hash_double( + 4, PoseidonHasher::hash_double(*elems.at(2), *elems.at(3)) + ); + assert(expected_peaks_3 == mmr_peaks_3, 'Wrong peaks after 3 appends'); + assert(mmr.root == expected_root_3, 'Wrong updated root after 3 a.'); + assert(mmr_root_3 == expected_root_3, 'Wrong reeturned root after 3 a.'); - peaks = array![*elems.at(2), *elems.at(3)].span(); - mmr.append(*elems.at(4), peaks); + let (mmr_root_4, mmr_peaks_4) = mmr.append(*elems.at(4), mmr_peaks_3).unwrap(); + + let expected_peaks_4 = array![*elems.at(6)].span(); + let expected_root_4 = PoseidonHasher::hash_double(7, *elems.at(6)); + assert(expected_peaks_4 == mmr_peaks_4, 'Wrong peaks after 4 appends'); + assert(mmr.root == expected_root_4, 'Wrong updated root after 4 a.'); + assert(mmr_root_4 == expected_root_4, 'Wrong reeturned root after 4 a.'); - let root = PoseidonHasher::hash_double(7, *elems.at(6)); assert(mmr.last_pos == 7, 'Wrong last_pos'); - assert(mmr.root == root, 'Wrong root'); } #[test] @@ -97,27 +144,53 @@ fn test_append_3() { fn test_append_4() { let elems = helper_test_get_elements(); let mut mmr: MMR = Default::default(); + let mmr_peaks_0 = array![].span(); + + let (mmr_root_1, mmr_peaks_1) = mmr.append(*elems.at(0), mmr_peaks_0).unwrap(); + + let expected_peaks_1 = array![*elems.at(0)].span(); + let expected_root_1 = PoseidonHasher::hash_double(1, *elems.at(0)); + assert(expected_peaks_1 == mmr_peaks_1, 'Wrong peaks after 1 append'); + assert(mmr.root == expected_root_1, 'Wrong updated root after 2 a.'); + assert(mmr_root_1 == expected_root_1, 'Wrong returned root after 1 a.'); + + let (mmr_root_2, mmr_peaks_2) = mmr.append(*elems.at(1), mmr_peaks_1).unwrap(); + + let expected_peaks_2 = array![*elems.at(2)].span(); + let expected_root_2 = PoseidonHasher::hash_double(3, *elems.at(2)); + assert(expected_peaks_2 == mmr_peaks_2, 'Wrong peaks after 2 appends'); + assert(mmr.root == expected_root_2, 'Wrong updated root after 2 a.'); + assert(mmr_root_2 == expected_root_2, 'Wrong reeturned root after 2 a.'); - let mut peaks = array![].span(); - mmr.append(*elems.at(0), peaks); + let (mmr_root_3, mmr_peaks_3) = mmr.append(*elems.at(3), mmr_peaks_2).unwrap(); - peaks = array![*elems.at(0)].span(); - mmr.append(*elems.at(1), peaks); + let expected_peaks_3 = array![*elems.at(2), *elems.at(3)].span(); + let expected_root_3 = PoseidonHasher::hash_double( + 4, PoseidonHasher::hash_double(*elems.at(2), *elems.at(3)) + ); + assert(expected_peaks_3 == mmr_peaks_3, 'Wrong peaks after 3 appends'); + assert(mmr.root == expected_root_3, 'Wrong updated root after 3 a.'); + assert(mmr_root_3 == expected_root_3, 'Wrong reeturned root after 3 a.'); - peaks = array![*elems.at(2)].span(); - mmr.append(*elems.at(3), peaks); + let (mmr_root_4, mmr_peaks_4) = mmr.append(*elems.at(4), mmr_peaks_3).unwrap(); - peaks = array![*elems.at(2), *elems.at(3)].span(); - mmr.append(*elems.at(4), peaks); + let expected_peaks_4 = array![*elems.at(6)].span(); + let expected_root_4 = PoseidonHasher::hash_double(7, *elems.at(6)); + assert(expected_peaks_4 == mmr_peaks_4, 'Wrong peaks after 4 appends'); + assert(mmr.root == expected_root_4, 'Wrong updated root after 4 a.'); + assert(mmr_root_4 == expected_root_4, 'Wrong reeturned root after 4 a.'); - peaks = array![*elems.at(6)].span(); - mmr.append(*elems.at(7), peaks); + let (mmr_root_5, mmr_peaks_5) = mmr.append(*elems.at(7), mmr_peaks_4).unwrap(); - let root = PoseidonHasher::hash_double( + let expected_peaks_5 = array![*elems.at(6), *elems.at(7)].span(); + let expected_root_5 = PoseidonHasher::hash_double( 8, PoseidonHasher::hash_double(*elems.at(6), *elems.at(7)) ); + assert(expected_peaks_5 == mmr_peaks_5, 'Wrong peaks after 5 appends'); + assert(mmr.root == expected_root_5, 'Wrong updated root after 5 a.'); + assert(mmr_root_5 == expected_root_5, 'Wrong reeturned root after 5 a.'); + assert(mmr.last_pos == 8, 'Wrong last_pos'); - assert(mmr.root == root, 'Wrong root'); } #[test] @@ -125,20 +198,20 @@ fn test_append_4() { fn test_append_wrong_peaks() { let elems = helper_test_get_elements(); let mut mmr: MMR = Default::default(); + let peaks = array![].span(); + + let (_, peaks) = mmr.append(*elems.at(0), peaks).unwrap(); - let mut peaks = array![].span(); - mmr.append(*elems.at(0), peaks); + let (_, peaks) = mmr.append(*elems.at(1), peaks).unwrap(); - peaks = array![*elems.at(0)].span(); - mmr.append(*elems.at(1), peaks); + let (_, peaks) = mmr.append(*elems.at(3), peaks).unwrap(); - peaks = array![*elems.at(2)].span(); - mmr.append(*elems.at(3), peaks); + assert(peaks == array![*elems.at(2), *elems.at(3)].span(), 'Wrong peaks returned by append'); - peaks = array![*elems.at(2), *elems.at(4)].span(); - let res = mmr.append(*elems.at(4), peaks); + let wrong_peaks = array![*elems.at(2), *elems.at(4)].span(); + let res = mmr.append(*elems.at(4), wrong_peaks); - assert(res.is_err(), 'Wrong peaks'); + assert(res.is_err(), 'Appnd accepted with wrong peaks'); } #[test] @@ -241,7 +314,7 @@ fn test_attack_forge_peaks() { // add the next element normally to mmr_real and get the root; let peaks_real = array![*elems.at(6), *elems.at(7)].span(); - mmr_real.append(9, peaks_real); + let _ = mmr_real.append(9, peaks_real); // add the next element abnormally to mmr_real and get the root; let forged_peak = PoseidonHasher::hash_double(*elems.at(6), *elems.at(7)); diff --git a/src/data_structures/mmr/tests/test_utils.cairo b/src/data_structures/mmr/tests/test_utils.cairo index 511f9a4..4a80033 100644 --- a/src/data_structures/mmr/tests/test_utils.cairo +++ b/src/data_structures/mmr/tests/test_utils.cairo @@ -1,5 +1,6 @@ use cairo_lib::data_structures::mmr::utils::{ get_height, compute_root, count_ones, leaf_index_to_mmr_index, get_peak_info, + mmr_size_to_leaf_count, }; use cairo_lib::hashing::poseidon::PoseidonHasher; use cairo_lib::data_structures::mmr::peaks::PeaksTrait; @@ -73,6 +74,19 @@ fn test_leaf_index_to_mmr_index() { assert(leaf_index_to_mmr_index(11) == 19, 'leaf_..._index(11) != 19'); } +#[test] +#[available_gas(999999999)] +fn test_mmr_size_to_leaf_count() { + assert(mmr_size_to_leaf_count(1) == 1, 'mmr_size_to_leaf_count(1) != 1'); + assert(mmr_size_to_leaf_count(3) == 2, 'mmr_size_to_leaf_count(3) != 2'); + assert(mmr_size_to_leaf_count(4) == 3, 'mmr_size_to_leaf_count(4) != 3'); + assert(mmr_size_to_leaf_count(7) == 4, 'mmr_size_to_leaf_count(7) != 4'); + assert(mmr_size_to_leaf_count(8) == 5, 'mmr_size_to_leaf_count(8) != 5'); + assert(mmr_size_to_leaf_count(10) == 6, 'mmr_size_to_leaf_count(10) != 6'); + assert(mmr_size_to_leaf_count(11) == 7, 'mmr_size_to_leaf_count(11) != 7'); + assert(mmr_size_to_leaf_count(15) == 8, 'mmr_size_to_leaf_count(15) != 8'); +} + #[test] #[available_gas(999999999)] fn test_get_peak_info() { diff --git a/src/data_structures/mmr/utils.cairo b/src/data_structures/mmr/utils.cairo index e3ce301..966f620 100644 --- a/src/data_structures/mmr/utils.cairo +++ b/src/data_structures/mmr/utils.cairo @@ -28,7 +28,7 @@ fn compute_root(last_pos: felt252, peaks: Peaks) -> felt252 { } // @notice Count the number of bits set to 1 in a 256-bit unsigned integer -// @param n The 256-bit unsigned integer +// @param arg The 256-bit unsigned integer // @return The number of bits set to 1 in n fn count_ones(n: u32) -> u32 { let mut n = n; @@ -54,19 +54,19 @@ fn leaf_index_to_mmr_index(n: u32) -> u32 { // @result Number of leaves fn mmr_size_to_leaf_count(n: u32) -> u32 { let mut mmr_size = n; - let bits = bit_length(mmr_size); - let mut i = pow(2, bits); + let bits = bit_length(mmr_size + 1); + let mut mountain_leaf_count = pow(2, bits - 1); let mut leaf_count = 0; loop { - if i == 0 { + if mountain_leaf_count == 0 { break leaf_count; } - let x = 2 * i - 1; - if x <= mmr_size { - mmr_size -= x; - leaf_count += i; + let mountain_size = 2 * mountain_leaf_count - 1; + if mountain_size <= mmr_size { + mmr_size -= mountain_size; + leaf_count += mountain_leaf_count; } - i /= 2; + mountain_leaf_count /= 2; } } diff --git a/src/data_structures/tests/test_eth_mpt.cairo b/src/data_structures/tests/test_eth_mpt.cairo index 8544bf2..a520f76 100644 --- a/src/data_structures/tests/test_eth_mpt.cairo +++ b/src/data_structures/tests/test_eth_mpt.cairo @@ -1,5 +1,6 @@ use cairo_lib::data_structures::eth_mpt::{MPTNode, MPTTrait}; -use cairo_lib::utils::types::words64::Words64TryIntoU256LE; +use cairo_lib::utils::types::words64::{Words64, Words64Trait}; +use debug::PrintTrait; #[test] #[available_gas(9999999999)] @@ -94,7 +95,8 @@ fn test_decode_rlp_node_branch() { ] .span(); - let decoded = MPTTrait::decode_rlp_node(rlp_node.span()).unwrap(); + let (decoded, rlp_byte_len) = MPTTrait::decode_rlp_node(rlp_node.span()).unwrap(); + assert(rlp_byte_len == 66 * 8 + 4, 'Wrong RLP len'); match decoded { MPTNode::Branch(( hashes, value @@ -106,19 +108,99 @@ fn test_decode_rlp_node_branch() { if i >= hashes.len() { break (); } - assert((*hashes.at(i)).try_into().unwrap() == *expected.at(i), 'Wrong hash'); + assert((*hashes.at(i)).as_u256_le(32).unwrap() == *expected.at(i), 'Wrong hash'); i += 1; }; }, - MPTNode::Extension(_) => { - panic_with_felt252('Branch node differs'); - }, - MPTNode::Leaf(_) => { - panic_with_felt252('Branch node differs'); - }, + MPTNode::LazyBranch(_) => { panic_with_felt252('Branch node differs'); }, + MPTNode::Extension(_) => { panic_with_felt252('Branch node differs'); }, + MPTNode::Leaf(_) => { panic_with_felt252('Branch node differs'); }, } } +#[test] +#[available_gas(9999999999)] +fn test_lazy_rlp_node_branch() { + let rlp_node = array![ + 0x09cf7077a01102f9, + 0xa962df351b7a06b5, + 0xadecaece75818924, + 0x0c4044a8b4cd681f, + 0x85a31ea0f44ac173, + 0x045c6d4661b25ad0, + 0x1a9fc1344568fe87, + 0x35361adc184b5c4b, + 0x4c2ca0b471500260, + 0x1846d1d34035ce04, + 0x8366e5a5533c3072, + 0x0c80a8368d4f30c1, + 0xa9a0eecd3ffaf56a, + 0xc4d37d4bc58d77dc, + 0xb0fe61d139e72282, + 0x3717d5dcb2ceeec0, + 0xa05138a6378e5bf0, + 0xdd62df56554d5fa9, + 0x9b56ae97049962c2, + 0x9307207bdafd8ecd, + 0xd71897db4cded3f8, + 0x2238146d06d439a0, + 0x74a843e9c94aaf6e, + 0xb91dd8b05fc2a9a9, + 0x03e2b336138c1d86, + 0x6ab4637ccc7aa04c, + 0x25a141a0c9b318a4, + 0x7a396b316173cb6b, + 0x13bb1b4967885ada, + 0x25818a3515a03001, + 0xc736fe137193c42e, + 0x3497a1fb11b74680, + 0x5f78007a1829bb91, + 0xd3429168a0ae52f8, + 0xdfce8b1ca7faab16, + 0x254e10b2db1d2049, + 0x1f2256e8c490dc0a, + 0x5036dca058964a53, + 0xa714a3a8fd342599, + 0xb59dc7a83baeb0db, + 0xc060242ace690c55, + 0xb020a0a3c1c4ad07, + 0xe19e05b055663b68, + 0xc1cb6b504b4ed003, + 0x11b1dab792630039, + 0x8ea0e7420366c278, + 0xd91c0f63fb45ebed, + 0xcb17225718eb3697, + 0x03e21bb715f3d5c6, + 0xa014269bd9e83cb0, + 0x6f985af63da32379, + 0x69b9c2e4e6f9e7d5, + 0x3999be4e94086b73, + 0xf309e62f6114864a, + 0x71201ad0d73465a0, + 0xce46b9552afba44a, + 0xa22aadff2d22c364, + 0xb12ac97334928ad1, + 0xb8fe8bc2f9bfa0fd, + 0xb0c3c818b6a92dbf, + 0x4714bdc0b10ce86f, + 0xe229ff6121c4f738, + 0x3c6961147fa02f50, + 0x5ea3bb1b02a54e70, + 0x8f459e43f602c572, + 0x8fea4837d02e2498, + 0x805fb3e2 + ]; + + let expected = 0xE7420366C27811B1DAB792630039C1CB6B504B4ED003E19E05B055663B68B020; + + let (decoded, rlp_byte_len) = MPTTrait::lazy_rlp_decode_branch_node(rlp_node.span(), 0xa) + .unwrap(); + assert(rlp_byte_len == 66 * 8 + 4, 'Wrong RLP len'); + + let expected_node = MPTNode::LazyBranch(expected); + assert(decoded == expected_node, 'Lazy branch node diffes'); +} + #[test] #[available_gas(9999999999)] fn test_decode_rlp_node_leaf_odd() { @@ -153,8 +235,10 @@ fn test_decode_rlp_node_leaf_odd() { 0xc71a5df8340f ]; - let decoded = MPTTrait::decode_rlp_node(rlp_node.span()).unwrap(); - let expected_node = MPTNode::Leaf((expected_key_end.span(), expected_value.span(), 1)); + let (decoded, rlp_byte_len) = MPTTrait::decode_rlp_node(rlp_node.span()).unwrap(); + assert(rlp_byte_len == 13 * 8, 'Wrong RLP len'); + + let expected_node = MPTNode::Leaf((expected_key_end.span(), expected_value.span(), 1, 57)); assert(decoded == expected_node, 'Even leaf node differs'); } @@ -192,8 +276,10 @@ fn test_decode_rlp_node_leaf_even() { 0xc71a5df8340f ]; - let decoded = MPTTrait::decode_rlp_node(rlp_node.span()).unwrap(); - let expected_node = MPTNode::Leaf((expected_key_end.span(), expected_value.span(), 2)); + let (decoded, rlp_byte_len) = MPTTrait::decode_rlp_node(rlp_node.span()).unwrap(); + assert(rlp_byte_len == 13 * 8, 'Wrong RLP len'); + + let expected_node = MPTNode::Leaf((expected_key_end.span(), expected_value.span(), 2, 56)); assert(decoded == expected_node, 'Even leaf node differs'); } @@ -216,7 +302,7 @@ fn test_hash_rlp_node() { 0xc71a5df8340f6249 ]; - let hash = MPTTrait::hash_rlp_node(rlp_node.span()); + let hash = MPTTrait::hash_rlp_node(rlp_node.span(), 8); assert( hash == 0x035F9A54E8BEE015293EB9791C7FEC6A4A111DB8B32464597B6F8E63B1167FA1, 'Wrong node rlp hash' @@ -677,3 +763,473 @@ fn test_full_verify() { let res = mpt.verify(key, 64, proof.span()).unwrap(); assert(res == expected_res.span(), 'Result not matching'); } + +#[test] +#[available_gas(9999999999999)] +fn test_full_verify_2_nodes() { + let proof = array![ + array![ + 0x5c0b6aa0808051f8, + 0x9720a2845ec292e7, + 0xdde909402854fc40, + 0x8b0ff9103babb578, + 0x808080c17ea1cc35, + 0xf86ba08080808080, + 0x652dee110f0a9fd1, + 0x98195bc1e2e398a8, + 0xcb959918a8d08b95, + 0x8080d2e4ec07dcfc, + 0x808080 + ] + .span(), + array![ + 0x76522d0e31a043f8, + 0x71fdcdee263b0712, + 0xfa4a4bf40c326a7e, + 0xb7e2cb9f2d73b0c2, + 0x636f4da0a1f60cfa, + 0x000000000000006b, + 0x0000000000000000, + 0x0000000000000000, + 0x0800000000 + ] + .span() + ]; + + let expected_res = array![0x6b636f4da0, 0x0, 0x0, 0x0, 0x8]; + + let key = 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6; + + let mpt = MPTTrait::new(0x8051DC27EC55E8B88C99B97B08DFF3E99B5106E4BED472E53B9B7D53C0129A6E); + let res = mpt.verify(key, 64, proof.span()).unwrap(); + assert(res == expected_res.span(), 'Result not matching'); +} + +#[test] +#[available_gas(9999999999999)] +fn test_verify_non_inclusion() { + let proof = array![ + array![ + 0xec98f822a01102f9, + 0xa6dd8f1214e1f11f, + 0xff4ad6f843ecf036, + 0xf6a7e88f76b1c824, + 0x5e47eba0f38aad9d, + 0x7ab225aee4e37bc0, + 0x504eb61cf06f663d, + 0xbb3b7ede40eadac7, + 0x9824a09076b7c780, + 0x919f6fa982b02b36, + 0x58d20214605f9948, + 0x56b041c561468afe, + 0x97a03430bd08cf66, + 0x62bbd866a3c8fb66, + 0x54d883fae6fe8129, + 0xa028191e12dfd561, + 0xa0670d84e1aa857b, + 0x55ccec057a3dd6cd, + 0x362b3118d757701e, + 0x146b17eeaf0ad6fd, + 0x1319c45144710cf9, + 0x46f757a79362bba0, + 0xa6ef7d98da737a41, + 0x02195eb29cd3e26d, + 0x7dfd57449c4c589a, + 0x38440d73892ca0e3, + 0xb4489211b479dbbd, + 0x0d4de704e5849607, + 0xb25d3b17d9f4d02a, + 0x7364a11c42a06df8, + 0x6d86c52845ef2567, + 0x724871838ca2dcdd, + 0xc9d2cecc83e6e9a0, + 0x02300c35a0620e9c, + 0x6b8dc74c237580ab, + 0xdbce8d27070a6c4f, + 0xb8cc16227e6f82a5, + 0x36d813a037bd63f4, + 0x44ec4fe517ce274f, + 0x64e998642529587c, + 0x7a4288f48bba7ea4, + 0x278ea0fdba64dc87, + 0x8da7ef8f781077aa, + 0x1ab52ff8420c677b, + 0x071a58e621bd3a13, + 0x9ea0ed688e392ff2, + 0x1c64a2e0cf0280e6, + 0x56e6d81a8d9c782a, + 0x59a9c64f4f680642, + 0xa056099859cfc1fd, + 0x8609c42dd7f52a4b, + 0x39e7c1999c1e73d4, + 0x9e8282fb3f570af7, + 0x11333251697454e3, + 0x1a397b07c8246aa0, + 0xed8e88c7a947b661, + 0x229c111bf67c959e, + 0xec403782f21939ba, + 0x9fa9d5f8fc07a085, + 0xa3f59303f4f8fd89, + 0xb00533f3b4daba74, + 0x382c892f74849089, + 0x454f994c81a09b53, + 0x07e3f1381c062a08, + 0xec28c36be4a9f162, + 0x51ace64e0718855f, + 0x804af09c, + ] + .span(), + array![ + 0xa18b1734a01102f9, + 0x624c4dd4e96fec64, + 0x8923e53f4e4a5c08, + 0xc1cca86fa2faaf4b, + 0xd8ba0ba0fc96285c, + 0xcddc7ebea7be7113, + 0x46ecd259ea06047d, + 0xa421c443d6f9a71a, + 0x8102a03f4d1d0680, + 0xaec223e9d168da7b, + 0x5cd2cdcc97098098, + 0x09d04cafc495736d, + 0xdea02f35e2bc92b2, + 0x436b95f3e26fb0df, + 0xc1bbe2d660b38aca, + 0xc0a370f7287dea21, + 0xa01b3d90b2772163, + 0x1fe1a4120535ee81, + 0x1961fea5b81063da, + 0xd4a5dd507ff58dcf, + 0x9332183e378bdfc9, + 0x7d9a1054ac0aa3a0, + 0x9bd80710831fefeb, + 0x83c7f00efd8667f3, + 0x5cb588453ef8ae96, + 0x1064588e1e69a0a3, + 0x6641154313a7cc3f, + 0xef0f04edc1fea6af, + 0x73277c83d5f6512a, + 0xe0e9c4b3b9a0ff7a, + 0x2a6357e218c4abc4, + 0x0ba0ce6fb2540a92, + 0xaecc5377ccd2b0ef, + 0xc11ad0f2a011c168, + 0x1c5a4c32d0bfe92a, + 0xed1f322b61d1d604, + 0x90af5d7174e78a83, + 0x506c29a0e666815f, + 0x5053d0102ff893d2, + 0x54f731b4085ac946, + 0x733749cfe21355ab, + 0x32f7a063d4ad43f8, + 0xe5343e587b5e5d1e, + 0xc0e6fe7ad162469b, + 0xed48d6a34a49a7f9, + 0x98a06d024b68cd97, + 0xab0a55475693e194, + 0xd5e032a70fd0ffa7, + 0x0b050793e64ae739, + 0xa09902e1816e7d29, + 0x2640b912a74d28d2, + 0x64da28ce2641f943, + 0xe82330c8f0c0be7c, + 0x9027ed1180802ac6, + 0x28e739f42d1482a0, + 0x5b9e95496cd50bc8, + 0xd29954dacf762a6f, + 0x5e59017da19f29ce, + 0xd3ad89fb4e26a058, + 0x36c079fad94cff14, + 0xcf1d594ee1bef76f, + 0x9108169a3df266c3, + 0xa281f807b6a03eb1, + 0x873adec4d65bd57e, + 0x1badb7afbbbc11e4, + 0xf31d82660adfe876, + 0x80bd5648, + ] + .span(), + array![ + 0xc4a440b1a01102f9, + 0x3969b0c09139282d, + 0x9cbfda9fe532254a, + 0x7ed5d8e8a424c1bf, + 0xffdabaa0da52e97b, + 0x52d7d98b6d93947a, + 0x2a393a8bd8bd2fbd, + 0xa1bdf99909348d33, + 0x1100a0467678105d, + 0xe369147638f178b5, + 0x71a9fb0f909f78bf, + 0x583d7a8a7649d3e8, + 0x17a0dd0aad1c5936, + 0x4d61315b3884d1fa, + 0x83be5a97959bbabd, + 0x8301009624ac80cc, + 0xa0ef97f2327d184d, + 0x149fccea7de6888a, + 0x9cef77c2f7fe18c2, + 0x65f45bf48b551057, + 0x595009e3e81946a2, + 0xc10af680de3f0ca0, + 0x20ba6ae86874c488, + 0x76dd0a762ec2f509, + 0x29b484e8f2a5008c, + 0x903ee2650407a0ea, + 0x6ef3e3b755f98730, + 0xb5dae74e7d54c91c, + 0x7b98cbc8a4ecb196, + 0x7a01843172a073fc, + 0xb442de1416bb6aa2, + 0xe1a4e0e43d2a6e88, + 0x4c7ae56697867af4, + 0xd1641177a0a4808d, + 0x83a2f11192df3d5d, + 0xa41bcc4ce0112d5c, + 0x46f9f649e94d4c70, + 0x7e8f38a0e7276078, + 0x6910d91ea9bb8194, + 0xbc1895b7a1e97e37, + 0x3f67491a0e148cf4, + 0x661aa0c79d08dbb0, + 0xbae7f8980692546d, + 0x948a9a4e0ea5465e, + 0xa87af3b0cef159de, + 0x17a0cc2f218b8da1, + 0x78a422b82e26a345, + 0xf02820f191d68219, + 0xf5b64741487e9990, + 0xa0f7320918aee5fb, + 0x76b832fd41ad2238, + 0x562698f5166c6659, + 0x8cc54eff977ee295, + 0x6788e130e1661af3, + 0x9d3847995a05f5a0, + 0x83f82b3df6042972, + 0x526a3f19c6aeaa5d, + 0x0c3fed0fef0e76c7, + 0xd57edf5f200ea08d, + 0x577455eb7dfb4feb, + 0x8515cfa916e3fb43, + 0x33b9d4cadf37b171, + 0x63a487f889a0ed11, + 0x93f7ac9e98299d0c, + 0x61f15f15dbf1d83b, + 0xcec095010ed87f1b, + 0x80a68778, + ] + .span(), + array![ + 0x9d99ee5ca01102f9, + 0x718cfd1f8d72c2de, + 0x2d2191cfecc39b56, + 0x1cc59360702b6245, + 0xbe3960a0c261e2f4, + 0xf41244c2977cb023, + 0x3ce5aab9c48812a0, + 0x3ddc8577ae4bb1df, + 0xa02da0e93fbb3f10, + 0xdf7a547c6557993f, + 0xffa317d9c6089ded, + 0xf203bccd7ddfa938, + 0x70a04826c6e60022, + 0x9619712d712cba4f, + 0x4c4dd96d022bd305, + 0x83af07d2a9934e29, + 0xa083be1dd409218f, + 0x9c3ab0c6f93b3d96, + 0xb593f5ea39517fd7, + 0x0c8b34c46e00a0b9, + 0x2145f5c0f0300f6c, + 0x75cb5fa55e3071a0, + 0x73e564b622b7d445, + 0x022ba70dc9155ec3, + 0x72eae3ac17cb92e7, + 0xad863d7e82f2a002, + 0x92766445ba5dd234, + 0xf150548095c1e96d, + 0x1526413a359e5ea2, + 0xfc8af56c91a0d9be, + 0xe414e346e31e5916, + 0xfb5eba4e2e7d7819, + 0x5872d97dab134060, + 0x615e0ce1a0bbd9ba, + 0x258b96575c22fd58, + 0xef7998285751eaae, + 0xbe6697b6c5af2f88, + 0x972be7a02e6d7b4a, + 0x124def6cc25837c7, + 0xc81e604f8e72e2e1, + 0xdaff2c0729010557, + 0x316fa0a1fa7cba5e, + 0x6a09a3a2831319fa, + 0xa04192f3fa0e26a0, + 0xb5032ea58874f3e7, + 0xf2a00340e23b6c08, + 0x7ef5f056401f3bb6, + 0x8136d6f15419c073, + 0x51e40d6613e4b0e4, + 0xa031928c18dc5e19, + 0x1010127ad8c00db9, + 0x8b0f20cab38db09c, + 0xf03736d6873bb3f9, + 0x0e34f847ce7ed8ee, + 0xc70b5d3c432046a0, + 0x2ab0bdf3cedfc54f, + 0x6120574252c3bc11, + 0x24c517bb7ee7eeb6, + 0xf877bad85ed3a0f1, + 0x153858932ff1f622, + 0x06b53db20c624055, + 0xf58414076214041f, + 0xf5b9dac3dda07386, + 0xe5a5b676d8b2710b, + 0x85b80c919b65ac5d, + 0xf561815eb587be0b, + 0x80efc780, + ] + .span(), + array![ + 0x68bdb97ea01102f9, + 0x8b1064346a7e703d, + 0x231d3944a343edfd, + 0xe5b1b3c993a4348e, + 0x759426a0493316a9, + 0xead237cc12b7c961, + 0xa8d030b48aaf334b, + 0x813079136c313ec8, + 0x7885a0e856317a41, + 0xdbdd4d9c1a897697, + 0x84d0f4f124c97010, + 0x8f5407402a859357, + 0x13a0628a0acf17a9, + 0x1a337078335bf480, + 0x3d3a0e71904efc66, + 0xa00c45e239c314a7, + 0xa012ccc781e8a1c3, + 0x472c2cf37cd19e25, + 0x89f736e6b2e6d1b0, + 0xbe5228aac7df21a0, + 0x7e77dd520f0563c7, + 0x34a6bdf7a46956a0, + 0x605ea0a95c8331ee, + 0x153164427ea10319, + 0x754b92c9873e571a, + 0x8bb30b7d9897a079, + 0x73399da4f82e29cc, + 0xd8abc14f1cf26e27, + 0xc09ed92452e02856, + 0x5022c5a784a0c4de, + 0x269fa9269a00e08a, + 0x09d747926f9640b5, + 0xa208597b2710f6c5, + 0x46a626c9a01d431c, + 0xbcb4c775d7165c7b, + 0x7937485ef08a5216, + 0x0fb70b713997e0a8, + 0x44b3c8a0a1ce0bc7, + 0x9875ec1bb38e4ff7, + 0x1db1dfe1b2f2d10f, + 0x3ffe5db701cd8d8d, + 0x5481a0f967e57604, + 0xcda26de3d706bc7b, + 0x8d7f78dcf4800483, + 0xbc7c842dbf9f085f, + 0x1aa06261fcc5fc09, + 0xc55c43be0d37637f, + 0xde3211f4ceec8b75, + 0x9a1e06de138f053b, + 0xa0252ee10ef1f096, + 0xab5c2c608bf54cc9, + 0xfdbac1b241ce0abb, + 0x2a90d9204f8c4627, + 0x46dc5020a562297f, + 0x16efe04ee931afa0, + 0x1072a0182c3dd6b0, + 0xd8e538aec461e353, + 0x38bd65aae5c1f928, + 0x4288940395fea00b, + 0x09a893417b081529, + 0x4d0a83649fd69ee0, + 0x795df895b0e225d7, + 0xc2178b7ea8a05160, + 0xd00b899083a8d9fc, + 0x7b419dfdda0aa3eb, + 0x7f5bbb883e1e7ee3, + 0x8027cda8, + ] + .span(), + array![ + 0x5581eda4a07101f9, + 0xd5111547ea5c54a2, + 0x4b1fec18db84d370, + 0x8537c1d7ae6f56fb, + 0x06a08080cbc9df22, + 0x10c6cd2c69ac990c, + 0x93dee1c6766af5b8, + 0xb348396b55e98bcd, + 0x80129b99bbe80276, + 0x0562ec2e3cc8bfa0, + 0x8b076699a733c392, + 0x78b5b82117ec50a0, + 0xa613011249cb30a0, + 0x7068b3979ff8a0c6, + 0xd8e2eef6399bf1b4, + 0x47d7f177febcc8cf, + 0x7ae242f5faedcc66, + 0x865ea57e93a0e94d, + 0xa0afbe49d5c9635f, + 0xff909d74f211bda8, + 0xed96bfe6e238286a, + 0x5ddba535a02caf0d, + 0xfd97460f36d94ffb, + 0x11ece487b149932a, + 0x86c9ded3a5fde827, + 0x18955da027329f87, + 0xfef67004b8a00b2d, + 0xc63b240909fda606, + 0x23899e7ff818e02d, + 0x10b9a0853c868a88, + 0xafc929a021b8c4c9, + 0x12639f697f9bb2d8, + 0x35accd64e12da7ab, + 0xa080e800b9941511, + 0x1021d1bfe372ee71, + 0x2fc2c9d8cebfca23, + 0xe8e313c3613bf42a, + 0x2d23c417040536e5, + 0xe7c108928484a080, + 0x61e9867c309be2c3, + 0x81b805d7196e1d2d, + 0xf5fe944b8f54cf8d, + 0x03d653f4afa0d485, + 0x516ca97707f93858, + 0x7b972d02f9acf612, + 0xa2ce82f3b1bba0a8, + 0x80d62f2a, + ] + .span(), + array![ + 0x598c57a0808051f8, + 0x400bb190e7c2eda0, + 0x616be10e250f50cb, + 0xf3501527389e5cea, + 0xbea0801ee74dd943, + 0xf62e5e6b8b43bcbd, + 0x2df96e397dc1b4dd, + 0x4506143bf6cf6ffc, + 0x80244912bcd14b94, + 0x8080808080808080, + 0x808080, + ] + .span(), + ]; + + let expected_res = array![]; + + let key = 0xa0772cc0ab5df2ddbf685663d9f2235f2022a1d9cd4594642d0db66171285358; + + let mpt = MPTTrait::new(0xB28377C58119C5D785423161829FC87C613F5CC284C14A130D9CCFE5F4A0E196); + let res = mpt.verify(key, 64, proof.span()).unwrap(); + assert(res == expected_res.span(), 'Result not matching'); +} diff --git a/src/encoding/rlp.cairo b/src/encoding/rlp.cairo index d47df7c..1655395 100644 --- a/src/encoding/rlp.cairo +++ b/src/encoding/rlp.cairo @@ -1,5 +1,6 @@ -use cairo_lib::utils::types::words64::{Words64, Words64Trait, reverse_endianness_u64}; +use cairo_lib::utils::types::words64::{Words64, Words64Trait, reverse_endianness_u64, pow2}; use cairo_lib::utils::types::byte::Byte; +use cairo_lib::utils::array::span_contains; // @notice Enum with all possible RLP types // For more info: https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/ @@ -36,17 +37,17 @@ impl RLPTypeImpl of RLPTypeTrait { } // @notice Represent a RLP item -#[derive(Drop)] +#[derive(Drop, PartialEq)] enum RLPItem { - Bytes: Words64, + Bytes: (Words64, usize), // Should be Span to allow for any depth/recursion, not yet supported by the compiler - List: Span + List: Span<(Words64, usize)> } // @notice RLP decodes a rlp encoded byte array // For more info: https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/ // @param input RLP encoded input, in little endian 64 bits words -// @return Result with RLPItem and size of the decoded item +// @return Result with RLPItem and size of the encoded item fn rlp_decode(input: Words64) -> Result<(RLPItem, usize), felt252> { // It's guaranteed to fid in 32 bits, as we are masking with 0xff let prefix: u32 = (*input.at(0) & 0xff).try_into().unwrap(); @@ -56,13 +57,13 @@ fn rlp_decode(input: Words64) -> Result<(RLPItem, usize), felt252> { match rlp_type { RLPType::String(()) => { let mut arr = array![prefix.into()]; - Result::Ok((RLPItem::Bytes(arr.span()), 1)) + Result::Ok((RLPItem::Bytes((arr.span(), 1)), 1)) }, RLPType::StringShort(()) => { let len = prefix.into() - 0x80; let res = input.slice_le(6, len); - Result::Ok((RLPItem::Bytes(res), 1 + len)) + Result::Ok((RLPItem::Bytes((res, len)), 1 + len)) }, RLPType::StringLong(()) => { let len_len = prefix - 0xb7; @@ -76,7 +77,7 @@ fn rlp_decode(input: Words64) -> Result<(RLPItem, usize), felt252> { .unwrap(); let res = input.slice_le(6 - len_len, len); - Result::Ok((RLPItem::Bytes(res), 1 + len_len + len)) + Result::Ok((RLPItem::Bytes((res, len)), 1 + len_len + len)) }, RLPType::ListShort(()) => { let mut len = prefix - 0xc0; @@ -105,8 +106,8 @@ fn rlp_decode(input: Words64) -> Result<(RLPItem, usize), felt252> { // @notice RLP decodes into RLPItem::List // @param input RLP encoded input, in little endian 64 bits words // @param len Length of the input -// @return Result with RLPItem::List -fn rlp_decode_list(ref input: Words64, len: usize) -> Result, felt252> { +// @return Result with a span of the decoded items and the decoded size of each +fn rlp_decode_list(ref input: Words64, len: usize) -> Result, felt252> { let mut i = 0; let mut output = ArrayTrait::new(); let mut total_len = len; @@ -118,9 +119,7 @@ fn rlp_decode_list(ref input: Words64, len: usize) -> Result, felt let (decoded, decoded_len) = match rlp_decode(input) { Result::Ok((d, dl)) => (d, dl), - Result::Err(e) => { - break Result::Err(e); - } + Result::Err(e) => { break Result::Err(e); } }; match decoded { RLPItem::Bytes(b) => { @@ -133,51 +132,97 @@ fn rlp_decode_list(ref input: Words64, len: usize) -> Result, felt } total_len -= decoded_len; }, - RLPItem::List(_) => { - panic_with_felt252('Recursive list not supported'); - } + RLPItem::List(_) => { panic_with_felt252('Recursive list not supported'); } } i += decoded_len; } } -impl RLPItemPartialEq of PartialEq { - fn eq(lhs: @RLPItem, rhs: @RLPItem) -> bool { - match lhs { - RLPItem::Bytes(b) => { - match rhs { - RLPItem::Bytes(b2) => { - b == b2 - }, - RLPItem::List(_) => false - } +fn rlp_decode_list_lazy(input: Words64, lazy: Span) -> Result<(RLPItem, usize), felt252> { + let mut output = ArrayTrait::new(); + let mut lazy_index = 0; + + let list_prefix: u32 = (*input.at(0) & 0xff).try_into().unwrap(); + let list_type = RLPTypeTrait::from_byte(list_prefix.try_into().unwrap()).unwrap(); + let (mut current_input_index, len) = match list_type { + RLPType::String(()) => { return Result::Err('Not a list'); }, + RLPType::StringShort(()) => { return Result::Err('Not a list'); }, + RLPType::StringLong(()) => { return Result::Err('Not a list'); }, + RLPType::ListShort(()) => (1, list_prefix - 0xc0), + RLPType::ListLong(()) => { + let len_len = list_prefix - 0xf7; + let len_span = input.slice_le(6, len_len); + // Enough to store 4.29 GB (fits in u32) + assert(len_span.len() == 1 && *len_span.at(0) <= 0xffffffff, 'Len of len too big'); + + // len fits in 32 bits, confirmed by previous assertion + let len = reverse_endianness_u64(*len_span.at(0), Option::Some(len_len.into())) + .try_into() + .unwrap(); + (1 + len_len, len) + } + }; + + let rlp_byte_len = current_input_index + len; + + loop { + if output.len() == lazy.len() { + break Result::Ok((RLPItem::List(output.span()), rlp_byte_len)); + } + + if current_input_index >= rlp_byte_len { + break Result::Err('Too many items to decode'); + } + + let current_word = current_input_index / 8; + + let pow2_shift = pow2((current_input_index % 8) * 8); + let prefix = (*input.at(current_word) / pow2_shift) & 0xff; + + let rlp_type = RLPTypeTrait::from_byte(prefix.try_into().unwrap()).unwrap(); + let (item_start_skip, item_len) = match rlp_type { + RLPType::String(()) => { (0, 1) }, + RLPType::StringShort(()) => { + let len = prefix - 0x80; + (1, len) }, - RLPItem::List(l) => { - match rhs { - RLPItem::Bytes(_) => false, - RLPItem::List(l2) => { - let len_l = (*l).len(); - if len_l != (*l2).len() { - return false; - } - let mut i: usize = 0; - loop { - if i >= len_l { - break true; - } - if (*l).at(i) != (*l2).at(i) { - break false; - } - i += 1; - } - } - } - } + RLPType::StringLong(()) => { + let len_len = prefix - 0xb7; + + let current_word = (current_input_index + 1) / 8; + let current_word_offset = 7 - ((current_input_index + 1) % 8); + + let len_span = input + .slice_le(current_word * 8 + current_word_offset, len_len.try_into().unwrap()); + // Enough to store 4.29 GB (fits in u32) + assert(len_span.len() == 1 && *len_span.at(0) <= 0xffffffff, 'Len of len too big'); + + // len fits in 32 bits, confirmed by previous assertion + let len: u32 = reverse_endianness_u64( + *len_span.at(0), Option::Some(len_len.try_into().unwrap()) + ) + .try_into() + .unwrap(); + + (1 + len_len, len.into()) + }, + RLPType::ListShort(()) => { panic_with_felt252('Recursive list not supported') }, + RLPType::ListLong(()) => { panic_with_felt252('Recursive list not supported') } + }; + + current_input_index += item_start_skip.try_into().unwrap(); + if span_contains(lazy, lazy_index) { + let current_word = current_input_index / 8; + let current_word_offset = 7 - (current_input_index % 8); + let start = current_word * 8 + current_word_offset; + + let item_len = item_len.try_into().unwrap(); + let decoded = input.slice_le(start, item_len); + output.append((decoded, item_len)); } - } - fn ne(lhs: @RLPItem, rhs: @RLPItem) -> bool { - !(lhs == rhs) + current_input_index += item_len.try_into().unwrap(); + + lazy_index += 1; } } - diff --git a/src/encoding/tests/test_rlp.cairo b/src/encoding/tests/test_rlp.cairo index 13afdac..e797c4f 100644 --- a/src/encoding/tests/test_rlp.cairo +++ b/src/encoding/tests/test_rlp.cairo @@ -1,4 +1,4 @@ -use cairo_lib::encoding::rlp::{RLPItem, rlp_decode}; +use cairo_lib::encoding::rlp::{RLPItem, rlp_decode, rlp_decode_list_lazy}; #[test] #[available_gas(99999999)] @@ -14,7 +14,7 @@ fn test_rlp_decode_string() { let (res, len) = rlp_decode(arr.span()).unwrap(); assert(len == 1, 'Wrong len'); - assert(res == RLPItem::Bytes(arr.span()), 'Wrong value'); + assert(res == RLPItem::Bytes((arr.span(), 1)), 'Wrong value'); i += 1; }; @@ -31,7 +31,7 @@ fn test_rlp_decode_short_string() { let mut expected_res = array![ 0x8d39c034f66c805a, 0xf31ea949fd892d8f, 0xf7bb9484cd74a43d, 0xa8da3b ]; - let expected_item = RLPItem::Bytes(expected_res.span()); + let expected_item = RLPItem::Bytes((expected_res.span(), 0x9b - 0x80)); assert(res == expected_item, 'Wrong value'); } @@ -62,7 +62,7 @@ fn test_rlp_decode_long_string_len_1() { 0x10eada53d0c6673c, 0xd97d1986 ]; - let expected_item = RLPItem::Bytes(expected_res.span()); + let expected_item = RLPItem::Bytes((expected_res.span(), 0x3c)); assert(res == expected_item, 'Wrong value'); } @@ -143,7 +143,7 @@ fn test_rlp_decode_long_string_len_2() { 0x9976686868876897, 0x6087 ]; - let expected_item = RLPItem::Bytes(expected_res.span()); + let expected_item = RLPItem::Bytes((expected_res.span(), 0x0102)); assert(res == expected_item, 'Wrong value'); } @@ -156,12 +156,46 @@ fn test_rlp_decode_short_list() { assert(len == 1 + (0xc9 - 0xc0), 'Wrong len'); let mut expected_res = array![ - array![0x893535].span(), array![0x42].span(), array![0x923845].span() + (array![0x893535].span(), 3), (array![0x42].span(), 1), (array![0x923845].span(), 3) ]; let expected_item = RLPItem::List(expected_res.span()); assert(res == expected_item, 'Wrong value'); } +#[test] +#[available_gas(99999999)] +fn test_rlp_lazy_decode_short_list() { + let mut arr = array![0x45834289353583c9, 0x9238]; + let rlp_byte_len = 10; + + let res = rlp_decode_list_lazy(arr.span(), array![].span()).unwrap(); + let expected_res = (RLPItem::List(array![].span()), rlp_byte_len); + assert(res == expected_res, 'Wrong value indexes: empty'); + + let res = rlp_decode_list_lazy(arr.span(), array![1].span()).unwrap(); + let expected_res = (RLPItem::List(array![(array![0x42].span(), 1)].span()), rlp_byte_len); + assert(res == expected_res, 'Wrong value indexes: 1'); + + let res = rlp_decode_list_lazy(arr.span(), array![0, 1, 2].span()).unwrap(); + let mut expected_res = ( + RLPItem::List( + array![ + (array![0x893535].span(), 3), (array![0x42].span(), 1), (array![0x923845].span(), 3) + ] + .span() + ), + rlp_byte_len + ); + assert(res == expected_res, 'Wrong value: indexes: 0, 1, 2'); + + let res = rlp_decode_list_lazy(arr.span(), array![0, 2].span()).unwrap(); + let mut expected_res = ( + RLPItem::List(array![(array![0x893535].span(), 3), (array![0x923845].span(), 3)].span()), + rlp_byte_len + ); + assert(res == expected_res, 'Wrong value: indexes: 0, 2'); +} + #[test] #[available_gas(99999999)] fn test_rlp_decode_long_list() { @@ -239,6 +273,167 @@ fn test_rlp_decode_long_list() { assert(len == 1 + (0xf9 - 0xf7) + 0x0211, 'Wrong len'); let mut expected_res = array![ + ( + array![0x1b7a06b509cf7077, 0x75818924a962df35, 0xb4cd681fadecaece, 0xf44ac1730c4044a8] + .span(), + 32 + ), + ( + array![0x4661b25ad085a31e, 0x344568fe87045c6d, 0xdc184b5c4b1a9fc1, 0xb47150026035361a] + .span(), + 32 + ), + ( + array![0xd1d34035ce044c2c, 0xe5a5533c30721846, 0xa8368d4f30c18366, 0xeecd3ffaf56a0c80] + .span(), + 32 + ), + ( + array![0xd37d4bc58d77dca9, 0xfe61d139e72282c4, 0x17d5dcb2ceeec0b0, 0x5138a6378e5bf037] + .span(), + 32 + ), + ( + array![0xdd62df56554d5fa9, 0x9b56ae97049962c2, 0x9307207bdafd8ecd, 0xd71897db4cded3f8] + .span(), + 32 + ), + ( + array![0x6e2238146d06d439, 0xa974a843e9c94aaf, 0x86b91dd8b05fc2a9, 0x4c03e2b336138c1d] + .span(), + 32 + ), + ( + array![0x18a46ab4637ccc7a, 0xcb6b25a141a0c9b3, 0x5ada7a396b316173, 0x300113bb1b496788] + .span(), + 32 + ), + ( + array![0x93c42e25818a3515, 0xb74680c736fe1371, 0x29bb913497a1fb11, 0xae52f85f78007a18] + .span(), + 32 + ), + ( + array![0xa7faab16d3429168, 0xdb1d2049dfce8b1c, 0xc490dc0a254e10b2, 0x58964a531f2256e8] + .span(), + 32 + ), + ( + array![0xa8fd3425995036dc, 0xa83baeb0dba714a3, 0x2ace690c55b59dc7, 0xa3c1c4ad07c06024] + .span(), + 32 + ), + ( + array![0x05b055663b68b020, 0x6b504b4ed003e19e, 0xdab792630039c1cb, 0xe7420366c27811b1] + .span(), + 32 + ), + ( + array![0x1c0f63fb45ebed8e, 0x17225718eb3697d9, 0xe21bb715f3d5c6cb, 0x14269bd9e83cb003] + .span(), + 32 + ), + ( + array![0x6f985af63da32379, 0x69b9c2e4e6f9e7d5, 0x3999be4e94086b73, 0xf309e62f6114864a] + .span(), + 32 + ), + ( + array![0x4a71201ad0d73465, 0x64ce46b9552afba4, 0xd1a22aadff2d22c3, 0xfdb12ac97334928a] + .span(), + 32 + ), + ( + array![0x2dbfb8fe8bc2f9bf, 0xe86fb0c3c818b6a9, 0xf7384714bdc0b10c, 0x2f50e229ff6121c4] + .span(), + 32 + ), + ( + array![0xa54e703c6961147f, 0x02c5725ea3bb1b02, 0x2e24988f459e43f6, 0x5fb3e28fea4837d0] + .span(), + 32 + ), + (array![].span(), 0) + ]; + let expected_item = RLPItem::List(expected_res.span()); + assert(res == expected_item, 'Wrong value'); +} + +#[test] +#[available_gas(99999999)] +fn test_rlp_lazy_decode_long_list() { + let mut arr = array![ + 0x09cf7077a01102f9, + 0xa962df351b7a06b5, + 0xadecaece75818924, + 0x0c4044a8b4cd681f, + 0x85a31ea0f44ac173, + 0x045c6d4661b25ad0, + 0x1a9fc1344568fe87, + 0x35361adc184b5c4b, + 0x4c2ca0b471500260, + 0x1846d1d34035ce04, + 0x8366e5a5533c3072, + 0x0c80a8368d4f30c1, + 0xa9a0eecd3ffaf56a, + 0xc4d37d4bc58d77dc, + 0xb0fe61d139e72282, + 0x3717d5dcb2ceeec0, + 0xa05138a6378e5bf0, + 0xdd62df56554d5fa9, + 0x9b56ae97049962c2, + 0x9307207bdafd8ecd, + 0xd71897db4cded3f8, + 0x2238146d06d439a0, + 0x74a843e9c94aaf6e, + 0xb91dd8b05fc2a9a9, + 0x03e2b336138c1d86, + 0x6ab4637ccc7aa04c, + 0x25a141a0c9b318a4, + 0x7a396b316173cb6b, + 0x13bb1b4967885ada, + 0x25818a3515a03001, + 0xc736fe137193c42e, + 0x3497a1fb11b74680, + 0x5f78007a1829bb91, + 0xd3429168a0ae52f8, + 0xdfce8b1ca7faab16, + 0x254e10b2db1d2049, + 0x1f2256e8c490dc0a, + 0x5036dca058964a53, + 0xa714a3a8fd342599, + 0xb59dc7a83baeb0db, + 0xc060242ace690c55, + 0xb020a0a3c1c4ad07, + 0xe19e05b055663b68, + 0xc1cb6b504b4ed003, + 0x11b1dab792630039, + 0x8ea0e7420366c278, + 0xd91c0f63fb45ebed, + 0xcb17225718eb3697, + 0x03e21bb715f3d5c6, + 0xa014269bd9e83cb0, + 0x6f985af63da32379, + 0x69b9c2e4e6f9e7d5, + 0x3999be4e94086b73, + 0xf309e62f6114864a, + 0x71201ad0d73465a0, + 0xce46b9552afba44a, + 0xa22aadff2d22c364, + 0xb12ac97334928ad1, + 0xb8fe8bc2f9bfa0fd, + 0xb0c3c818b6a92dbf, + 0x4714bdc0b10ce86f, + 0xe229ff6121c4f738, + 0x3c6961147fa02f50, + 0x5ea3bb1b02a54e70, + 0x8f459e43f602c572, + 0x8fea4837d02e2498, + 0x805fb3e2 + ]; + let rlp_byte_len = 66 * 8 + 4; + + let mut expected_res_full = array![ array![0x1b7a06b509cf7077, 0x75818924a962df35, 0xb4cd681fadecaece, 0xf44ac1730c4044a8] .span(), array![0x4661b25ad085a31e, 0x344568fe87045c6d, 0xdc184b5c4b1a9fc1, 0xb47150026035361a] @@ -274,6 +469,137 @@ fn test_rlp_decode_long_list() { .span(), array![].span() ]; + + let res = rlp_decode_list_lazy(arr.span(), array![].span()).unwrap(); + let expected_res = (RLPItem::List(array![].span()), rlp_byte_len); + assert(res == expected_res, 'Wrong value indexes: empty'); + + let res = rlp_decode_list_lazy(arr.span(), array![0].span()).unwrap(); + let expected_res = (RLPItem::List(array![(*expected_res_full.at(0), 32)].span()), rlp_byte_len); + assert(res == expected_res, 'Wrong value indexes: 0'); + + let res = rlp_decode_list_lazy(arr.span(), array![1].span()).unwrap(); + let expected_res = (RLPItem::List(array![(*expected_res_full.at(1), 32)].span()), rlp_byte_len); + assert(res == expected_res, 'Wrong value indexes: 1'); + + let res = rlp_decode_list_lazy(arr.span(), array![0xa].span()).unwrap(); + let expected_res = ( + RLPItem::List(array![(*expected_res_full.at(0xa), 32)].span()), rlp_byte_len + ); + assert(res == expected_res, 'Wrong value indexes: 10'); + + let res = rlp_decode_list_lazy(arr.span(), array![0x5, 0x9, 0xf].span()).unwrap(); + let expected_res = ( + RLPItem::List( + array![ + (*expected_res_full.at(0x5), 32), + (*expected_res_full.at(0x9), 32), + (*expected_res_full.at(0xf), 32) + ] + .span() + ), + rlp_byte_len + ); + assert(res == expected_res, 'Wrong value indexes: 5, 9, 15'); +} + +#[test] +#[available_gas(99999999)] +fn test_rlp_decode_list_long_string() { + let arr = array![ + 0x7235e356aca05bf8, + 0x7f0b03476f57b94f, + 0x4760f75aaf1d2720, + 0xa9c2173ae53aab1f, + 0xf338d438b8ed276f, + 0x27777eada3968dad, + 0x53189e661865fe38, + 0xc101f7b5d6dffd52, + 0x65454695474abcbb, + 0x4567644756674547, + 0x5663776535476567, + 0xfa77645733, + ]; + + let (res, len) = rlp_decode(arr.span()).unwrap(); + assert(len == 1 + (0xf8 - 0xf7) + 0x5b, 'Wrong len'); + + let expected_res = array![ + ( + array![0x57b94f7235e356ac, 0x1d27207f0b03476f, 0x3aab1f4760f75aaf, 0xed276fa9c2173ae5,] + .span(), + 32 + ), + ( + array![ + 0xada3968dadf338d4, + 0x661865fe3827777e, + 0xb5d6dffd5253189e, + 0x95474abcbbc101f7, + 0x4756674547654546, + 0x6535476567456764, + 0xfa77645733566377, + ] + .span(), + 56 + ) + ]; let expected_item = RLPItem::List(expected_res.span()); assert(res == expected_item, 'Wrong value'); } + +#[test] +#[available_gas(99999999)] +fn test_rlp_lazy_decode_list_long_string() { + let arr = array![ + 0x7235e356aca05bf8, + 0x7f0b03476f57b94f, + 0x4760f75aaf1d2720, + 0xa9c2173ae53aab1f, + 0xf338d438b8ed276f, + 0x27777eada3968dad, + 0x53189e661865fe38, + 0xc101f7b5d6dffd52, + 0x65454695474abcbb, + 0x4567644756674547, + 0x5663776535476567, + 0xfa77645733, + ]; + let rlp_byte_len = 11 * 8 + 5; + + let expected_res_full = array![ + array![0x57b94f7235e356ac, 0x1d27207f0b03476f, 0x3aab1f4760f75aaf, 0xed276fa9c2173ae5,] + .span(), + array![ + 0xada3968dadf338d4, + 0x661865fe3827777e, + 0xb5d6dffd5253189e, + 0x95474abcbbc101f7, + 0x4756674547654546, + 0x6535476567456764, + 0xfa77645733566377, + ] + .span(), + ]; + + let res = rlp_decode_list_lazy(arr.span(), array![].span()).unwrap(); + let expected_res = (RLPItem::List(array![].span()), rlp_byte_len); + assert(res == expected_res, 'Wrong value indexes: empty'); + + let res = rlp_decode_list_lazy(arr.span(), array![0].span()).unwrap(); + let expected_res = (RLPItem::List(array![(*expected_res_full.at(0), 32)].span()), rlp_byte_len); + assert(res == expected_res, 'Wrong value indexes: 0'); + + let res = rlp_decode_list_lazy(arr.span(), array![1].span()).unwrap(); + let expected_res = (RLPItem::List(array![(*expected_res_full.at(1), 56)].span()), rlp_byte_len); + assert(res == expected_res, 'Wrong value indexes: 1'); + + let res = rlp_decode_list_lazy(arr.span(), array![0, 1].span()).unwrap(); + let expected_res = ( + RLPItem::List( + array![(*expected_res_full.at(0), 32), (*expected_res_full.at(1), 56)].span() + ), + rlp_byte_len + ); + assert(res == expected_res, 'Wrong value indexes: 0, 1'); +} diff --git a/src/hashing/keccak.cairo b/src/hashing/keccak.cairo index 424cea1..d106bce 100644 --- a/src/hashing/keccak.cairo +++ b/src/hashing/keccak.cairo @@ -1,31 +1,33 @@ use cairo_lib::utils::types::words64::{Words64, bytes_used_u64}; use keccak::cairo_keccak; +const EMPTY_KECCAK: u256 = 0x70A4855D04D8FA7B3B2782CA53B600E5C003C7DCB27D7E923C23F7860146D2C5; + // @notice Wrapper arround cairo_keccak that format the input for compatibility with EVM // @param words The input data, as a list of 64-bit little-endian words -// @return The keccak hash of the input, matching the output of the EVM's keccak256 opcode -fn keccak_cairo_words64(words: Words64) -> u256 { - let n = words.len(); +// @param last_word_bytes Number of bytes in the last word +// @return The little endian keccak hash of the input, matching the output of the EVM's keccak256 opcode +fn keccak_cairo_words64(words: Words64, last_word_bytes: usize) -> u256 { + if words.is_empty() { + return EMPTY_KECCAK; + } + let n = words.len(); let mut keccak_input = ArrayTrait::new(); let mut i: usize = 0; - if n > 1 { - loop { - if i >= n - 1 { - break (); - } - keccak_input.append(*words.at(i)); - i += 1; - }; - } + loop { + if i >= n - 1 { + break (); + } + keccak_input.append(*words.at(i)); + i += 1; + }; let mut last = *words.at(n - 1); - let mut last_word_bytes = bytes_used_u64(last); if last_word_bytes == 8 { keccak_input.append(last); - last = 0; - last_word_bytes = 0; + cairo_keccak(ref keccak_input, 0, 0) + } else { + cairo_keccak(ref keccak_input, last, last_word_bytes) } - - cairo_keccak(ref keccak_input, last, last_word_bytes) } diff --git a/src/hashing/tests/test_keccak.cairo b/src/hashing/tests/test_keccak.cairo index f99cf8c..6baa154 100644 --- a/src/hashing/tests/test_keccak.cairo +++ b/src/hashing/tests/test_keccak.cairo @@ -5,7 +5,7 @@ use cairo_lib::hashing::keccak::keccak_cairo_words64; fn test_keccak_cairo_word64_full_byte() { let input = array![0xffffffffffffffff]; - let res = keccak_cairo_words64(input.span()); + let res = keccak_cairo_words64(input.span(), 8); assert( res == 0xAF7D4E460ACF8E540E682A9EE91EA1C08C1615C3889D75EB0A70660A4BFB0BAD, 'Keccak output not matching' @@ -17,7 +17,7 @@ fn test_keccak_cairo_word64_full_byte() { fn test_keccak_cairo_word64_remainder() { let mut input = array![0x23FDAE89F78C76AB, 0x45D2BC4A]; - let res = keccak_cairo_words64(input.span()); + let res = keccak_cairo_words64(input.span(), 4); assert( res == 0x82CBD5B00CD06A188C831D69CB9629C92A2D5E7A78CEA913C5F9AFF62E66BBB9, 'Keccak output not matching' diff --git a/src/utils/bitwise.cairo b/src/utils/bitwise.cairo index e5d9bc1..ab1f9c8 100644 --- a/src/utils/bitwise.cairo +++ b/src/utils/bitwise.cairo @@ -14,7 +14,11 @@ fn left_shift< impl TMul: Mul, impl TOneable: Oneable, impl TCopy: Copy, - impl TDrop: Drop + impl TDrop: Drop, + impl TDiv: Div, + impl TRem: Rem, + impl TPartialEq: PartialEq, + impl TPartialOrd: PartialOrd >( num: T, shift: T ) -> T { @@ -29,18 +33,26 @@ fn left_shift< fn right_shift< T, impl TZeroable: Zeroable, + impl TOneable: Oneable, impl TAdd: Add, impl TSub: Sub, - impl TMul: Mul, impl TDiv: Div, - impl TOneable: Oneable, impl TCopy: Copy, impl TDrop: Drop >( num: T, shift: T ) -> T { + let mut num = num; + let mut shift = shift; let two = TOneable::one() + TOneable::one(); - num / pow(two, shift) + + loop { + if shift.is_zero() { + break num; + } + num = num / two; + shift = shift - TOneable::one(); + } } // @notice Bit length of a number @@ -49,24 +61,24 @@ fn right_shift< fn bit_length< T, impl TZeroable: Zeroable, - impl TPartialOrd: PartialOrd, - impl TAddImpl: Add, - impl TSub: Sub, - impl TMul: Mul, impl TOneable: Oneable, + impl TAddImpl: Add, + impl TDiv: Div, impl TCopy: Copy, impl TDrop: Drop >( num: T ) -> T { let mut bit_position = TZeroable::zero(); - let mut cur_n = TOneable::one(); + let mut cur_n = num; + let two = TOneable::one() + TOneable::one(); + loop { - if cur_n > num { + if cur_n.is_zero() { break (); }; bit_position = bit_position + TOneable::one(); - cur_n = left_shift(cur_n, TOneable::one()); + cur_n = cur_n / two; }; bit_position } diff --git a/src/utils/math.cairo b/src/utils/math.cairo index fe25aa0..56f93fb 100644 --- a/src/utils/math.cairo +++ b/src/utils/math.cairo @@ -4,7 +4,34 @@ use math::Oneable; // @param base The base of the exponentiation // @param exp The exponent of the exponentiation // @return The exponentiation result + fn pow< + T, + impl TZeroable: Zeroable, + impl TOneable: Oneable, + impl TCopy: Copy, + impl TDrop: Drop, + impl TAdd: Add, + impl TSub: Sub, + impl TMul: Mul, + impl TDiv: Div, + impl TRem: Rem, + impl TPartialEq: PartialEq, + impl TPartialOrd: PartialOrd +>( + mut base: T, mut exp: T +) -> T { + let two = TOneable::one() + TOneable::one(); + let four = two + two; + let sixteen = four * four; + if exp < sixteen { + slow_pow(base, exp) + } else { + fast_pow(base, exp) + } +} + +fn slow_pow< T, impl TZeroable: Zeroable, impl TSub: Sub, @@ -18,6 +45,39 @@ fn pow< if exp.is_zero() { TOneable::one() } else { - base * pow(base, exp - TOneable::one()) + base * slow_pow(base, exp - TOneable::one()) } } + +fn fast_pow< + T, + impl TZeroable: Zeroable, + impl TOneable: Oneable, + impl TCopy: Copy, + impl TDrop: Drop, + impl TAdd: Add, + impl TSub: Sub, + impl TMul: Mul, + impl TDiv: Div, + impl TRem: Rem, + impl TPartialEq: PartialEq +>( + mut base: T, mut exp: T +) -> T { + let mut ans = TOneable::one(); + loop { + if exp.is_zero() { + break ans; + } + let two = TOneable::one() + TOneable::one(); + let mm = exp % two; + if mm == TOneable::one() { + ans = ans * base; + exp = exp - TOneable::one(); + } else { + base = base * base; + exp = exp / two; + }; + } +} + diff --git a/src/utils/tests/test_bitwise.cairo b/src/utils/tests/test_bitwise.cairo index 7dfcf4f..58e0665 100644 --- a/src/utils/tests/test_bitwise.cairo +++ b/src/utils/tests/test_bitwise.cairo @@ -1,4 +1,4 @@ -use cairo_lib::utils::bitwise::{left_shift, right_shift, bit_length}; +use cairo_lib::utils::bitwise::{left_shift, right_shift, bit_length, reverse_endianness_u256}; #[test] #[available_gas(999999)] @@ -20,6 +20,7 @@ fn test_right_shift() { assert(right_shift(256_u32, 8_u32) == 1, '256 >> 8'); assert(right_shift(512_u32, 8_u32) == 2, '512 >> 8'); assert(right_shift(65280_u32, 8_u32) == 255, '65280 >> 8'); + assert(right_shift(128392_u32, 33_u32) == 0, '128392 >> 33'); } #[test] @@ -33,3 +34,27 @@ fn test_bit_length() { assert(bit_length(8_u32) == 4, 'bit length of 8 is 4'); } +#[test] +#[available_gas(999999999)] +fn test_bit_length_most_significant_bit_one() { + assert(bit_length(4294967295_u32) == 32, 'bit length of 2^32-1 is 32'); +} + +#[test] +#[available_gas(999999)] +fn test_reverse_endianness_u256() { + assert(reverse_endianness_u256(0) == 0, 'reverse endianness of 0'); + assert( + reverse_endianness_u256( + 1 + ) == 0x0100000000000000000000000000000000000000000000000000000000000000, + 'reverse endianness of 1' + ); + assert( + reverse_endianness_u256( + 0x1307645868aee0028be496b378bfeee2bac59d1239549a8ef4bff9009af5ef + ) == 0xEFF59A00F9BFF48E9A5439129DC5BAE2EEBF78B396E48B02E0AE685864071300, + 'reverse endianness of 31 bytes' + ); +} + diff --git a/src/utils/types/tests/test_words64.cairo b/src/utils/types/tests/test_words64.cairo index a34abdf..ecd7f57 100644 --- a/src/utils/types/tests/test_words64.cairo +++ b/src/utils/types/tests/test_words64.cairo @@ -1,5 +1,5 @@ use cairo_lib::utils::types::words64::{ - Words64, Words64Trait, Words64TryIntoU256LE, reverse_endianness_u64, bytes_used_u64 + Words64, Words64Trait, reverse_endianness_u64, bytes_used_u64 }; #[test] @@ -19,7 +19,6 @@ fn test_slice_words64_le_multiple_words_not_full() { fn test_slice_words64_le_multiple_words_full() { let val: Words64 = array![0xabcdef1234567890, 0x7584934785943295, 0x48542576].span(); - let gas = testing::get_available_gas(); let res = val.slice_le(4, 16); assert(res.len() == 2, 'Wrong len'); assert(*res.at(0) == 0x943295abcdef1234, 'Wrong value at 0'); @@ -47,33 +46,81 @@ fn test_slice_words64_le_single_word_full() { } #[test] -#[should_panic] #[available_gas(99999999)] -fn test_into_u256_le_wrong_num_words() { - let words = array![0x83498349, 0x83479735927498, 0x12345623ff458695, 0xabcd344, 0xabcdef3345] +fn test_as_u256_be_full() { + let words = array![ + 0x2e8b632605e21673, 0x480829ebcee54bc4, 0xb6f239256ff310f9, 0x09898da43a5d35f4, + ] .span(); - let res: u256 = words.try_into().unwrap(); + + let expected = 0x7316e20526638b2ec44be5ceeb290848f910f36f2539f2b6f4355d3aa48d8909; + assert(words.as_u256_be(32).unwrap() == expected, 'Wrong value'); +} + +#[test] +#[available_gas(99999999)] +fn test_as_u256_be_not_full() { + let words = array![0x2e8b632605e21673, 0x480829ebcee54bc4, 0xb6f2392a].span(); + + let expected = 0x7316e20526638b2ec44be5ceeb2908482a39f2b6; + assert(words.as_u256_be(20).unwrap() == expected, 'Wrong value'); +} + +#[test] +#[available_gas(99999999)] +fn test_as_u256_be_not_full_start() { + let words = array![0x008b632605e20000, 0x480829ebcee54bc4, 0xb6f2392a].span(); + + let expected = 0xe20526638b00c44be5ceeb2908482a39f2b6; + assert(words.as_u256_be(20).unwrap() == expected, 'Wrong value'); +} + +#[test] +#[available_gas(99999999)] +fn test_as_u256_be_not_full_end() { + let words = array![0x2e8b632605e20000, 0x480829ebcee54bc4, 0xb6f2392a].span(); + + let expected = 0x0000e20526638b2ec44be5ceeb2908482a39f2b600; + assert(words.as_u256_be(21).unwrap() == expected, 'Wrong value'); } #[test] #[available_gas(99999999)] -fn test_into_u256_le() { +fn test_as_u256_le_full() { let words = array![ 0x2e8b632605e21673, 0x480829ebcee54bc4, 0xb6f239256ff310f9, 0x09898da43a5d35f4, ] .span(); let expected = 0x09898DA43A5D35F4B6F239256FF310F9480829EBCEE54BC42E8B632605E21673; - assert(words.try_into().unwrap() == expected, 'Wrong value'); + assert(words.as_u256_le(32).unwrap() == expected, 'Wrong value'); } #[test] #[available_gas(99999999)] -fn test_into_u256_le_not_full() { - let words = array![0x2e8b632605e21673, 0x4bc4, 0, 0,].span(); +fn test_as_u256_le_not_full() { + let words = array![0x2e8b632605e21673, 0x480829ebcee54bc4, 0xb6f2392a].span(); - let expected = 0x4BC42E8B632605E21673; - assert(words.try_into().unwrap() == expected, 'Wrong value'); + let expected: u256 = 0xB6F2392A480829EBCEE54BC42E8B632605E21673000000000000000000000000; + assert(words.as_u256_le(20).unwrap() == expected, 'Wrong value'); +} + +#[test] +#[available_gas(99999999)] +fn test_as_u256_le_not_full_start() { + let words = array![0x008b632605e20000, 0x480829ebcee54bc4, 0xb6f2392a].span(); + + let expected = 0xB6F2392A480829EBCEE54BC4008B632605E20000000000000000000000000000; + assert(words.as_u256_le(20).unwrap() == expected, 'Wrong value'); +} + +#[test] +#[available_gas(99999999)] +fn test_as_u256_le_not_full_end() { + let words = array![0x008b632605e20000, 0x480829ebcee54bc4, 0xb6f2392a].span(); + + let expected: u256 = 0x00B6F2392A480829EBCEE54BC4008B632605E200000000000000000000000000; + assert(words.as_u256_le(21).unwrap() == expected, 'Wrong value'); } #[test] @@ -92,6 +139,14 @@ fn test_reverse_endianness_not_full() { assert(reverse_endianness_u64(val, Option::Some(3)) == expected, 'Wrong value'); } +#[test] +#[available_gas(99999999)] +fn test_reverse_endianness_not_full_padding() { + let val = 0xabcdef; + let expected = 0xefcdab00; + assert(reverse_endianness_u64(val, Option::Some(4)) == expected, 'Wrong value'); +} + #[test] #[available_gas(99999999)] fn test_bytes_used() { diff --git a/src/utils/types/words64.cairo b/src/utils/types/words64.cairo index ce39da2..23ef8f1 100644 --- a/src/utils/types/words64.cairo +++ b/src/utils/types/words64.cairo @@ -1,44 +1,102 @@ -use cairo_lib::utils::bitwise::{left_shift, right_shift}; +use cairo_lib::utils::bitwise::left_shift; +use cairo_lib::utils::math::pow; // @notice Represents a span of 64 bit words // @dev In many cases it's expected that the words are in little endian, but the overall order is big endian // Example: 0x34957c6d8a83f9cff74578dea9 is represented as [0xcff9838a6d7c9534, 0xa9de7845f7] type Words64 = Span; -impl Words64TryIntoU256LE of TryInto { - // @notice Converts a span of 64 bit little endian words into a little endian u256 - fn try_into(self: Words64) -> Option { - if self.len() > 4 { - return Option::None(()); +#[generate_trait] +impl Words64Impl of Words64Trait { + // @notice Converts little endian 64 bit words to a big endian u256 + // @param bytes_used The number of bytes used + // @return The big endian u256 representation of the words + fn as_u256_be(self: Words64, bytes_used: usize) -> Result { + let len = self.len(); + + if len > 4 { + return Result::Err('Too many words'); } - if self.len() == 0 { - return Option::Some(0); + if len == 0 || bytes_used == 0 { + return Result::Ok(0); } - let pows = array![ - 0x10000000000000000, // 2 ** 64 - 0x100000000000000000000000000000000, // 2 ** 128 - 0x1000000000000000000000000000000000000000000000000 // 2 ** 192 - ]; + let mut len_last_word = bytes_used % 8; + if len_last_word == 0 { + len_last_word = 8; + } + + let mut output: u256 = reverse_endianness_u64( + (*self.at(len - 1)), Option::Some(len_last_word) + ) + .into(); + + let word_pow2 = 0x10000000000000000; // 2 ** 64 + let mut current_pow2: u256 = if len_last_word == 8 { + word_pow2 + } else { + pow2(len_last_word * 8).into() + }; - let mut output: u256 = (*self.at(0)).into(); - let mut i: usize = 1; + let mut i = 1; loop { - if i == self.len() { - break Option::Some(output); + if i == len { + break Result::Ok(output); } - // left shift and add - output = output | (*self.at(i)).into() * *pows.at(i - 1); + output = output + | (reverse_endianness_u64(*self.at(len - i - 1), Option::None(())).into() + * current_pow2); + + if i < len - 1 { + current_pow2 = current_pow2 * word_pow2; + } + + i += 1; + } + } + + // @notice Converts little endian 64 bit words to a little endian u256 + // @param bytes_used The number of bytes used + // @return The little endian u256 representation of the words + fn as_u256_le(self: Words64, bytes_used: usize) -> Result { + let len = self.len(); + + if len > 4 { + return Result::Err('Too many words'); + } + + if len == 0 || bytes_used == 0 { + return Result::Ok(0); + } + + let mut len_last_word = bytes_used % 8; + if len_last_word == 0 { + len_last_word = 8; + } + + let mut output: u256 = 0; + + let word_pow2 = 0x10000000000000000; // 2 ** 64 + let mut current_pow2: u256 = pow(2, (32 - bytes_used.into()) * 8); + + let mut i = 0; + loop { + if i == len { + break Result::Ok(output); + } + + output = output | ((*self.at(i)).into() * current_pow2); + + if i < len - 1 { + current_pow2 = current_pow2 * word_pow2; + } i += 1; } } -} -#[generate_trait] -impl Words64Impl of Words64Trait { // @notice Slices 64 bit little endian words from a starting byte and a length // @param start The starting byte // The starting byte is counted from the left. Example: 0xabcdef -> byte 0 is 0xab, byte 1 is 0xcd...