diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..cafc36b6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Syntax highlighting of sway files as rust +*.sw linguist-language=Rust diff --git a/README.md b/README.md index 34d35769..b373d9f8 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,29 @@ These libraries contain helper functions, generalized standards, and other tools ### Libraries - +- [Binary Merkle Proof](./sway_libs/src/merkle_proof/) is used to verify Binary Merkle Trees computed off-chain. ## Using a library -If you wish to use any of the libraries then clone this repository and go through the general [installation](https://fuellabs.github.io/sway/latest/introduction/installation.html) steps required to use our tools, and import the library into your contract. +To import the Merkle Proof library the following should be added to the project's `Forc.toml` file under `[dependencies]`: + + +```rust +sway_libs = { git = "https://github.com/FuelLabs/sway-libs", branch = "master" } +``` + +You may then import your desired library in your Sway Smart Contract as so: + +```rust +use sway_libs::::; +``` + +For example, to import the Merkle Proof library use the following statement: + +```rust +sway_libs::binary_merkle_proof::verify_proof; +``` ## Contributing -Check [CONTRIBUTING.md](./CONTRIBUTING.md) for more info! \ No newline at end of file +Check [CONTRIBUTING.md](./CONTRIBUTING.md) for more info! diff --git a/sway_libs/.gitignore b/sway_libs/.gitignore new file mode 100644 index 00000000..5e7f2c02 --- /dev/null +++ b/sway_libs/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/sway_libs/src/lib.sw b/sway_libs/src/lib.sw index 468c7b7e..21838a91 100644 --- a/sway_libs/src/lib.sw +++ b/sway_libs/src/lib.sw @@ -1 +1,3 @@ library sway_libs; + +dep merkle_proof/binary_merkle_proof; diff --git a/sway_libs/src/merkle_proof/README.md b/sway_libs/src/merkle_proof/README.md new file mode 100644 index 00000000..f27e3fc6 --- /dev/null +++ b/sway_libs/src/merkle_proof/README.md @@ -0,0 +1,73 @@ +# Overview + +Merkle trees allow for on-chain verification of off-chain data. With the merkle root posted on-chain, the generation of proofs off-chain can provide verifibly true data. + +More information can be found in the [specification](./SPECIFICATION.md). + +## Known Issues + +Due to the Fuel VM padding of copy types smaller than one word, the current hashing of the leaf and node values do not match RFC-6962. The present implementation is the equavalent of hashing a `u64` as opposed to the standard `u8`. As a result, hashing of smaller sized copy types may result in unexpected behavior. For example, the `sha-256(0u8)` will result in the same hash as `sha-256(0u64)`. This will cause an incompatability with the [Fuel-Merkle](https://github.com/FuelLabs/fuel-merkle) repository. The issue regarding this can be tracked [here](https://github.com/FuelLabs/sway/issues/2594). + +# Using the Library + +## Using the Merkle Proof Library In Sway + +Once imported, using the Merkle Proof library is as simple as calling the desired function. Here is a list of function definitions that you may use. For more information please see the [specification](./SPECIFICATION.md). + +- `leaf_digest(data: b256) -> b256` +- `node_digest(left: b256, right: b256) -> b256` +- `process_proof(key: u64, merkle_leaf: b256, num_leaves: u64, proof: [b256; 2]) -> b256` +- `verify_proof(key: u64, merkle_leaf: b256, merkle_root: b256, num_leaves: u64, proof: [b256; 2]) -> bool` + +> **Note** Fuels-rs does not currently does not support the Sway standard library's `vec` as a function argument. As a result, the current implementation uses an array. + +## Using the Merkle Proof Library in Fuels-rs + +To generate a Merkle Tree and corresponding proof for your Sway Smart Contract, use the [Fuel-Merkle](https://github.com/FuelLabs/fuel-merkle) crate. + +### Importing Into Your Project + +The import the Fuel-Merkle crate, the following should be added to the project's `Cargo.toml` file under `[dependencies]`: + +``` +fuel-merkle = { version = "0.3" } +``` + +### Importing Into Your Rust File + +The following should be added to your Rust file to use the Fuel-Merkle crate. + +```rust +use fuel_merkle::binary::in_memory::MerkleTree; +``` + +### Using Fuel-Merkle + +#### Generating A Tree + +To create a merkle tree using Fuel-Merkle is as simple as pushing your leaves in increasing order. + +```rust +let mut tree = MerkleTree::new(); +let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()].to_vec(); +for datum in leaves.iter() { + tree.push(datum); +} +``` + +#### Generating And Verifying A Proof + +To generate a proof for a specific leaf, you must have the index or key of the leaf. Simply call the prove function: + +```rust +let mut proof = tree.prove(key).unwrap(); +``` + +Once the proof has been generated, you may call the Sway Smart Contract's `verify_proof` function: + +```rust +let merkle_root = proof.0; +let merkle_leaf = proof.1[0]; +proof.1.remove(0); +contract_instance.verify_proof(key, merkle_leaf, merkle_root, num_leaves, proof.1).call().await; +``` diff --git a/sway_libs/src/merkle_proof/SPECIFICATION.md b/sway_libs/src/merkle_proof/SPECIFICATION.md new file mode 100644 index 00000000..6025ebce --- /dev/null +++ b/sway_libs/src/merkle_proof/SPECIFICATION.md @@ -0,0 +1,47 @@ +# Overview + +This document provides an overview of the Merkle Proof library. + +It outlines the use cases, i.e. specification, and describes how to implement the library. + +# Use Cases + +A common use case for Merkle Tree verification is airdrops. An airdrop is a method of distribution a set amount of tokens to a specified number of users. These often include a list of addresses and amounts. By posting the root hash, users can provide a proof and claim their airdrop. + +## Public Functions + +### `leaf_digest` + +The `leaf_digest` function is used to compute leaf hash of a Merkle Tree. Given the data provided, it returns the leaf hash following [RFC-6962](https://tools.ietf.org/html/rfc6962) as described by `MTH({d(0)}) = SHA-256(0x00 || d(0))`. + +### `node_digest` + +The `node_digest` function is used to complute a node within a Merkle Tree. Given a left and right node, it returns the node hash following [RFC-6962](https://tools.ietf.org/html/rfc6962) as described by `MTH(D[n]) = SHA-256(0x01 || MTH(D[0:k]) || MTH(D[k:n]))`. + +### `process_proof` + +The `process_proof` function will compute the Merkle root from a Merkle Proof. Given a leaf, the key for the leaf, the corresponding proof, and number of leaves in the Merkle Tree, the root of the Merkle Tree will be returned. + +### `verify_proof` + +The `verify_proof` function will verify a Merkle Proof against a Merkle root. Given a Merkle root, a leaf, the key for the leaf, the corresponding proof, and the number of leaves in the Merkle Tree, a `bool` will be returned as to whether the proof is valid. + +# Specification + +All cryptographic primitives follow the [Fuel Specs](https://github.com/FuelLabs/fuel-specs/blob/master/specs/protocol/cryptographic_primitives.md). + +## Hashing + +All hashing is done with SHA-2-256 (also known as SHA-256), defined in [FIPS 180-4](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf). + +## Merkle Trees + +Currently only one tree structure is supported, the Binary Merkle Tree. Implementation for a Binary Merkle Sum Tree and Sparse Merkle Tree found in the [fuel-merkle](https://github.com/FuelLabs/fuel-merkle) repository will be added soon. + +A Sparse Merkle Tree proof can be tracked [here](https://github.com/FuelLabs/sway-libs/issues/18) and Sum Merkle Tree proof can be tracked [here](https://github.com/FuelLabs/sway-libs/issues/17). + +### Binary Merkle Tree + +Binary Merkle trees are constructed in the same fashion as described in [Certificate Transparency (RFC-6962)](https://tools.ietf.org/html/rfc6962). Leaves are hashed once to get leaf node values and internal node values are the hash of the concatenation of their children (either leaf nodes or other internal nodes). + +For more information please check out the offical [Fuel Specs](https://github.com/FuelLabs/fuel-specs/blob/master/specs/protocol/cryptographic_primitives.md#binary-merkle-tree). diff --git a/sway_libs/src/merkle_proof/binary_merkle_proof.sw b/sway_libs/src/merkle_proof/binary_merkle_proof.sw new file mode 100644 index 00000000..e4aa8489 --- /dev/null +++ b/sway_libs/src/merkle_proof/binary_merkle_proof.sw @@ -0,0 +1,179 @@ +// TODO: Function definitions that use arrays should be updated to Vecs once +// https://github.com/FuelLabs/fuels-rs/issues/353 is resolved + +library binary_merkle_proof; + +use std::{hash::sha256, revert::require}; + +pub enum ProofError { + InvalidKey: (), + InvalidProofLength: (), +} + +/// Concatenated to leaf hash input as described by +/// "MTH({d(0)}) = SHA-256(0x00 || d(0))" +pub const LEAF = 0u8; +/// Concatenated to node hash input as described by +/// "MTH(D[n]) = SHA-256(0x01 || MTH(D[0:k]) || MTH(D[k:n]))" +pub const NODE = 1u8; + +/// Returns the computed leaf hash of "MTH({d(0)}) = SHA-256(0x00 || d(0))". +/// +/// # Arguments +/// +/// * 'data' - The hash of the leaf data. +pub fn leaf_digest(data: b256) -> b256 { + sha256((LEAF, data)) +} + +/// Returns the computed node hash of "MTH(D[n]) = SHA-256(0x01 || MTH(D[0:k]) || MTH(D[k:n]))". +/// +/// # Arguments +/// +/// * 'left' - The hash of the left node. +/// * 'right' - The hash of the right node. +pub fn node_digest(left: b256, right: b256) -> b256 { + sha256((NODE, left, right)) +} + +/// Calculates the length of the path to a leaf +/// +/// # Arguments +/// +/// * `key` - The key or index of the leaf. +/// * `num_leaves` - The total number of leaves in the Merkle Tree. +fn path_length_from_key(key: u64, num_leaves: u64) -> u64 { + let mut total_length = 0; + let mut num_leaves = num_leaves; + let mut key = key; + + while true { + // The height of the left subtree is equal to the offset of the starting bit of the path + let path_length = starting_bit(num_leaves); + // Determine the number of leaves in the left subtree + let num_leaves_left_sub_tree = (1 <<(path_length - 1)); + + if key <= (num_leaves_left_sub_tree - 1) { + // If the leaf is in the left subtreee, path length is full height of the left subtree + total_length = total_length + path_length; + break; + } else if num_leaves_left_sub_tree == 1 { + // If the left sub tree has only one leaf, path has one additional step + total_length = total_length + 1; + break; + } else if (num_leaves - num_leaves_left_sub_tree) <= 1 { + // If the right sub tree only has one leaf, path has one additonal step + total_length = total_length + 1; + break; + } else { + // Otherwise add 1 to height and loop + total_length = total_length + 1; + key = key - num_leaves_left_sub_tree; + num_leaves = num_leaves - num_leaves_left_sub_tree; + } + } + + total_length +} + +/// This function will compute and return a Merkle root given a leaf and corresponding proof. +/// +/// # Arguments +/// +/// * 'key' - The key or index of the leaf to prove. +/// * `merkle_leaf` - The hash of a leaf on the Merkle Tree. +/// * 'num_leaves' - The number of leaves in the Merkle Tree. +/// * `proof` - The Merkle proof that will be used to traverse the Merkle Tree and compute a root. +/// +/// # Reverts +/// +/// * When an incorrect proof length is provided. +/// * When there is one or no leaves and a proof is provided. +/// * When the key is greater than or equal to the number of leaves. +/// * When the computed height gets larger than the proof. +pub fn process_proof(key: u64, merkle_leaf: b256, num_leaves: u64, proof: [b256; +2]) -> b256 { + // let proof_length = proof.len(); + let proof_length = 2; + require((num_leaves > 1 && proof_length == path_length_from_key(key, num_leaves)) || (num_leaves <= 1 && proof_length == 0), ProofError::InvalidProofLength); + require(key < num_leaves, ProofError::InvalidKey); + + let mut digest = merkle_leaf; + // If proof length is zero then the leaf is the root + if proof_length == 0 { + return digest + } + + let mut height = 1; + let mut stable_end = key; + + // While the current subtree is complete, determine the position of the next + // sibling using the complete subtree algorithm. + while true { + // Determine if the subtree is complete. + let sub_tree_start_index = (key / (1 << height)) * (1 << height); + let sub_tree_end_index = sub_tree_start_index + (1 << height) - 1; + + // If the Merkle Tree does not have a leaf at the `sub_tree_end_index`, we deem that the + // subtree is not complete. + if sub_tree_end_index >= num_leaves { + break; + } + stable_end = sub_tree_end_index; + require(proof_length > height - 1, ProofError::InvalidProofLength); + + // Determine if the key is in the first or the second half of the subtree. + if (key - sub_tree_start_index) <(1 <<(height - 1)) { + digest = node_digest(digest, proof[height - 1]); + } else { + digest = node_digest(proof[height - 1], digest); + } + + height = height + 1; + } + + // Determine if the next hash belongs to an orphan that was elevated. + if stable_end != (num_leaves - 1) { + require(proof_length > height - 1, ProofError::InvalidProofLength); + digest = node_digest(digest, proof[height - 1]); + height = height + 1; + } + + // All remaining elements in the proof set will belong to the left sibling. + while(height - 1) < proof_length { + digest = node_digest(proof[height - 1], digest); + height = height + 1; + } + + digest +} + +/// Calculates the starting bit of the path to a leaf +/// +/// # Arguments +/// +/// * `num_leaves` - The number of leaves in the Merkle Tree. +fn starting_bit(num_leaves: u64) -> u64 { + let mut starting_bit = 0; + + while(1 << starting_bit) < num_leaves { + starting_bit = starting_bit + 1; + } + + starting_bit +} + +/// This function will take a Merkle leaf and proof and return whether the corresponding root +/// matches the root given. +/// +/// # Arguments +/// +/// * 'key' - The key or index of the leaf to verify. +/// * `merkle_leaf` - The hash of a leaf on the Merkle Tree. +/// * `merkle_root` - The pre-computed Merkle root that will be used to verify the leaf and proof. +/// * 'num_leaves' - The number of leaves in the Merkle Tree. +/// * `proof` - The Merkle proof that will be used to traverse the Merkle Tree and compute a root. +pub fn verify_proof(key: u64, merkle_leaf: b256, merkle_root: b256, num_leaves: u64, proof: [b256; +2]) -> bool { + process_proof(key, merkle_leaf, num_leaves, proof) == merkle_root +} diff --git a/tests/src/Cargo.toml b/tests/src/Cargo.toml index 7156de66..6fc77075 100644 --- a/tests/src/Cargo.toml +++ b/tests/src/Cargo.toml @@ -6,8 +6,11 @@ edition = "2021" license = "Apache-2.0" [dependencies] -forc = { version = "0.19" } -fuels = { version = "0.20" } +forc = { version = "0.19.1" } +fuel-core = { version = "0.9.6 "} +fuel-merkle = { version = "0.3" } +fuels = { version = "0.19" } +sha2 = { version = "0.10" } tokio = { version = "1.12", features = ["rt", "macros"] } [[test]] diff --git a/tests/src/test_projects/harness.rs b/tests/src/test_projects/harness.rs index fc513856..71f7ef77 100644 --- a/tests/src/test_projects/harness.rs +++ b/tests/src/test_projects/harness.rs @@ -1 +1,3 @@ -// Add test modules here: +// Add test modules here: + +mod merkle_proof; diff --git a/tests/src/test_projects/merkle_proof/.gitignore b/tests/src/test_projects/merkle_proof/.gitignore new file mode 100644 index 00000000..5e7f2c02 --- /dev/null +++ b/tests/src/test_projects/merkle_proof/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/tests/src/test_projects/merkle_proof/Forc.toml b/tests/src/test_projects/merkle_proof/Forc.toml new file mode 100644 index 00000000..660c62a2 --- /dev/null +++ b/tests/src/test_projects/merkle_proof/Forc.toml @@ -0,0 +1,8 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "merkle_proof" + +[dependencies] +sway_libs = { path = "../../../../sway_libs" } diff --git a/tests/src/test_projects/merkle_proof/mod.rs b/tests/src/test_projects/merkle_proof/mod.rs new file mode 100644 index 00000000..dc39c4cc --- /dev/null +++ b/tests/src/test_projects/merkle_proof/mod.rs @@ -0,0 +1 @@ +mod tests; diff --git a/tests/src/test_projects/merkle_proof/src/main.sw b/tests/src/test_projects/merkle_proof/src/main.sw new file mode 100644 index 00000000..12dc78e4 --- /dev/null +++ b/tests/src/test_projects/merkle_proof/src/main.sw @@ -0,0 +1,37 @@ +contract; + +use sway_libs::binary_merkle_proof::{ + leaf_digest, + node_digest, + process_proof, + verify_proof, +}; + +abi MerkleProofTest { + fn leaf_digest(data: b256) -> b256; + fn node_digest(left: b256, right: b256) -> b256; + fn process_proof(key: u64, merkle_leaf: b256, num_leaves: u64, proof: [b256; + 2]) -> b256; + fn verify_proof(key: u64, merkle_leaf: b256, merkle_root: b256, num_leaves: u64, proof: [b256; + 2]) -> bool; +} + +impl MerkleProofTest for Contract { + fn leaf_digest(data: b256) -> b256 { + leaf_digest(data) + } + + fn node_digest(left: b256, right: b256) -> b256 { + node_digest(left, right) + } + + fn process_proof(key: u64, merkle_leaf: b256, num_leaves: u64, proof: [b256; + 2]) -> b256 { + process_proof(key, merkle_leaf, num_leaves, proof) + } + + fn verify_proof(key: u64, merkle_leaf: b256, merkle_root: b256, num_leaves: u64, proof: [b256; + 2]) -> bool { + verify_proof(key, merkle_leaf, merkle_root, num_leaves, proof) + } +} diff --git a/tests/src/test_projects/merkle_proof/tests/functions/leaf_digest.rs b/tests/src/test_projects/merkle_proof/tests/functions/leaf_digest.rs new file mode 100644 index 00000000..7a13046f --- /dev/null +++ b/tests/src/test_projects/merkle_proof/tests/functions/leaf_digest.rs @@ -0,0 +1,24 @@ +use crate::merkle_proof::tests::utils::{ + abi_calls::leaf_digest, + test_helpers::{build_tree, merkle_proof_instance}, +}; + +mod success { + + use super::*; + + #[ignore] + #[tokio::test] + async fn computes_leaf() { + let instance = merkle_proof_instance().await; + + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()].to_vec(); + let key = 0; + let (_tree, _root, leaf, _proof) = build_tree(leaves, key).await; + + assert_eq!( + leaf_digest(&instance, "A".as_bytes().try_into().unwrap()).await, + leaf + ); + } +} diff --git a/tests/src/test_projects/merkle_proof/tests/functions/mod.rs b/tests/src/test_projects/merkle_proof/tests/functions/mod.rs new file mode 100644 index 00000000..0f1cda5b --- /dev/null +++ b/tests/src/test_projects/merkle_proof/tests/functions/mod.rs @@ -0,0 +1,4 @@ +mod leaf_digest; +mod node_digest; +mod process_proof; +mod verify_proof; diff --git a/tests/src/test_projects/merkle_proof/tests/functions/node_digest.rs b/tests/src/test_projects/merkle_proof/tests/functions/node_digest.rs new file mode 100644 index 00000000..c02abfc5 --- /dev/null +++ b/tests/src/test_projects/merkle_proof/tests/functions/node_digest.rs @@ -0,0 +1,87 @@ +use crate::merkle_proof::tests::utils::{ + abi_calls::node_digest, + test_helpers::{build_tree, merkle_proof_instance}, +}; +use fuel_merkle::common::{Bytes32, LEAF}; +use sha2::{Digest, Sha256}; + +mod success { + + use super::*; + + #[ignore] + #[tokio::test] + async fn computes_node() { + let instance = merkle_proof_instance().await; + + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()].to_vec(); + let key = 2; + let (_tree, root, leaf, proof) = build_tree(leaves, key).await; + + assert_eq!(node_digest(&instance, proof[0], leaf).await, root); + } + + // NOTE: This test does not use the Fuel-Merkle library but replicates it's functionality + // without the use of a `u8`. Due to a `u8` being padded to a full word in Sway, the Fuel-Merkle + // repository sha-256 hashes and the Sway sha-256 hashes do not produce the same result. This + // test uses a `u64` instead for node concatentation and passes as expected. Once this is resolved + // this test will be modified to use a `u8` again. + // The issue can be tracked here: https://github.com/FuelLabs/sway/issues/2594 + #[tokio::test] + async fn computes_node_manual() { + let instance = merkle_proof_instance().await; + + // Data as bytes + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()]; + + // ABC + // / \ + // AB C + // / \ + // A B + + // Leaf A hash + let mut merkle_leaf_a = Sha256::new(); + merkle_leaf_a.update(&[LEAF]); + merkle_leaf_a.update(&leaves[0]); + let leaf_a_hash: Bytes32 = merkle_leaf_a.finalize().try_into().unwrap(); + + // Leaf B hash + let mut merkle_leaf_b = Sha256::new(); + merkle_leaf_b.update(&[LEAF]); + merkle_leaf_b.update(&leaves[1]); + let leaf_b_hash: Bytes32 = merkle_leaf_b.finalize().try_into().unwrap(); + + // leaf C hash + let mut merkle_leaf_c = Sha256::new(); + merkle_leaf_c.update(&[LEAF]); + merkle_leaf_c.update(&leaves[2]); + let leaf_c_hash: Bytes32 = merkle_leaf_c.finalize().try_into().unwrap(); + + // Node AB hash + let node_u64: u64 = 1; + let mut node_ab = Sha256::new(); + node_ab.update(node_u64.to_be_bytes()); + node_ab.update(&leaf_a_hash); + node_ab.update(&leaf_b_hash); + let node_ab_hash: Bytes32 = node_ab.finalize().try_into().unwrap(); + + // Root hash + let mut node_abc = Sha256::new(); + node_abc.update(node_u64.to_be_bytes()); + node_abc.update(&node_ab_hash); + node_abc.update(&leaf_c_hash); + let node_abc_hash: Bytes32 = node_abc.finalize().try_into().unwrap(); + + // This passes due to use of u64 for node concatenation + assert_eq!( + node_digest(&instance, leaf_a_hash, leaf_b_hash).await, + node_ab_hash + ); + + assert_eq!( + node_digest(&instance, node_ab_hash, leaf_c_hash).await, + node_abc_hash + ); + } +} diff --git a/tests/src/test_projects/merkle_proof/tests/functions/process_proof.rs b/tests/src/test_projects/merkle_proof/tests/functions/process_proof.rs new file mode 100644 index 00000000..f64a6775 --- /dev/null +++ b/tests/src/test_projects/merkle_proof/tests/functions/process_proof.rs @@ -0,0 +1,220 @@ +// TODO: More extensive testing using different proof lengths should be added when https://github.com/FuelLabs/fuels-rs/issues/353 is revolved. +// TODO: Using the fuel-merkle repository will currently fail all tests due to https://github.com/FuelLabs/sway/issues/2594 +use crate::merkle_proof::tests::utils::{ + abi_calls::process_proof, + test_helpers::{build_tree, merkle_proof_instance}, +}; +use fuel_merkle::common::{Bytes32, LEAF}; +use sha2::{Digest, Sha256}; + +mod success { + + use super::*; + + #[ignore] + #[tokio::test] + async fn fails_to_process_merkle_proof() { + let instance = merkle_proof_instance().await; + + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()].to_vec(); + let num_leaves = 3; + let key = 0; + + let (_tree, root, leaf, proof) = build_tree(leaves, key).await; + + assert_ne!( + process_proof( + &instance, + key, + leaf, + num_leaves, + [proof[1], proof[0]].to_vec() + ) + .await, + root + ); + } + + #[ignore] + #[tokio::test] + async fn processes_merkle_proof() { + let instance = merkle_proof_instance().await; + + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()].to_vec(); + let num_leaves = 3; + let key = 0; + + let (_tree, root, leaf, proof) = build_tree(leaves, key).await; + + assert_eq!( + process_proof(&instance, key, leaf, num_leaves, proof).await, + root + ); + } + + // NOTE: This test does not use the Fuel-Merkle library but replicates it's functionality + // without the use of a `u8`. Due to a `u8` being padded to a full word in Sway, the Fuel-Merkle + // repository sha-256 hashes and the Sway sha-256 hashes do not produce the same result. This + // test uses a `u64` instead for node concatentation and passes as expected. Once this is resolved + // this test will be modified to use a `u8` again. + // The issue can be tracked here: https://github.com/FuelLabs/sway/issues/2594 + #[tokio::test] + async fn processes_merkle_proof_manual() { + let instance = merkle_proof_instance().await; + + // Data as bytes + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()]; + let key = 0; + let num_leaves = 3; + + // ABC + // / \ + // AB C + // / \ + // A B + + // Leaf A hash + let mut merkle_leaf_a = Sha256::new(); + merkle_leaf_a.update(&[LEAF]); + merkle_leaf_a.update(&leaves[0]); + let leaf_a_hash: Bytes32 = merkle_leaf_a.finalize().try_into().unwrap(); + + // Leaf B hash + let mut merkle_leaf_b = Sha256::new(); + merkle_leaf_b.update(&[LEAF]); + merkle_leaf_b.update(&leaves[1]); + let leaf_b_hash: Bytes32 = merkle_leaf_b.finalize().try_into().unwrap(); + + // leaf B hash + let mut merkle_leaf_c = Sha256::new(); + merkle_leaf_c.update(&[LEAF]); + merkle_leaf_c.update(&leaves[2]); + let leaf_c_hash: Bytes32 = merkle_leaf_c.finalize().try_into().unwrap(); + + // Node AB hash + let node_u64: u64 = 1; + let mut node_ab = Sha256::new(); + node_ab.update(node_u64.to_be_bytes()); + node_ab.update(&leaf_a_hash); + node_ab.update(&leaf_b_hash); + let node_ab_hash: Bytes32 = node_ab.finalize().try_into().unwrap(); + + // Root hash + let mut node_abc = Sha256::new(); + node_abc.update(node_u64.to_be_bytes()); + node_abc.update(&node_ab_hash); + node_abc.update(&leaf_c_hash); + let node_abc_hash: Bytes32 = node_abc.finalize().try_into().unwrap(); + + // This passes due to use of u64 for node concatenation + assert_eq!( + process_proof( + &instance, + key, + leaf_a_hash, + num_leaves, + [leaf_b_hash, leaf_c_hash].to_vec() + ) + .await, + node_abc_hash + ); + } +} + +mod revert { + + use super::*; + + #[ignore] + #[tokio::test] + #[should_panic(expected = "Revert(42)")] + async fn panics_when_invalid_proof_length_given() { + let instance = merkle_proof_instance().await; + + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()].to_vec(); + let num_leaves = 3; + let key = 0; + + let (_tree, root, leaf, _proof) = build_tree(leaves, key).await; + + assert_eq!( + process_proof(&instance, key, leaf, num_leaves, [].to_vec()).await, + root + ); + } + + #[tokio::test] + #[should_panic(expected = "Revert(42)")] + async fn panics_when_invalid_num_leaves_given() { + let instance = merkle_proof_instance().await; + + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()].to_vec(); + let num_leaves = 1; + let key = 0; + + let (_tree, root, leaf, proof) = build_tree(leaves, key).await; + + assert_eq!( + process_proof(&instance, key, leaf, num_leaves, proof).await, + root + ); + } + + #[tokio::test] + #[should_panic(expected = "Revert(42)")] + async fn panics_when_key_greater_or_equal_to_num_leaves() { + let instance = merkle_proof_instance().await; + + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()].to_vec(); + let num_leaves = 3; + let mut key = 0; + + let (_tree, _root, leaf, proof) = build_tree(leaves, key).await; + key = num_leaves; + + process_proof(&instance, key, leaf, num_leaves, proof).await; + } + + #[tokio::test] + #[should_panic(expected = "Revert(42)")] + async fn panics_with_key_equal_num_leaves_length() { + let instance = merkle_proof_instance().await; + + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()].to_vec(); + let num_leaves = 4; + let key = 0; + + let (_tree, _root, leaf, proof) = build_tree(leaves, key).await; + + process_proof(&instance, num_leaves, leaf, num_leaves, proof).await; + } + + #[tokio::test] + #[should_panic(expected = "Revert(42)")] + async fn panics_with_key_greater_than_num_leaves_length() { + let instance = merkle_proof_instance().await; + + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()].to_vec(); + let num_leaves = 4; + let mut key = 0; + + let (_tree, _root, leaf, proof) = build_tree(leaves, key).await; + key = num_leaves + 1; + + process_proof(&instance, key, leaf, num_leaves, proof).await; + } + + #[tokio::test] + #[should_panic(expected = "Revert(42)")] + async fn panics_with_num_leaves_zero() { + let instance = merkle_proof_instance().await; + + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()].to_vec(); + let num_leaves = 0; + let key = 0; + + let (_tree, _root, leaf, proof) = build_tree(leaves, key).await; + + process_proof(&instance, key, leaf, num_leaves, proof).await; + } +} diff --git a/tests/src/test_projects/merkle_proof/tests/functions/verify_proof.rs b/tests/src/test_projects/merkle_proof/tests/functions/verify_proof.rs new file mode 100644 index 00000000..38b522ec --- /dev/null +++ b/tests/src/test_projects/merkle_proof/tests/functions/verify_proof.rs @@ -0,0 +1,51 @@ +use crate::merkle_proof::tests::utils::{ + abi_calls::verify_proof, + test_helpers::{build_tree, merkle_proof_instance}, +}; + +mod success { + + use super::*; + + #[ignore] + #[tokio::test] + async fn fails_merkle_proof_verification() { + let instance = merkle_proof_instance().await; + + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()].to_vec(); + let num_leaves = 3; + let key = 0; + + let (_tree, root, leaf, proof) = build_tree(leaves, key).await; + + assert_eq!( + verify_proof( + &instance, + key, + leaf, + root, + num_leaves, + [proof[1], proof[0]].to_vec() + ) + .await, + false + ); + } + + #[ignore] + #[tokio::test] + async fn verifies_merkle_proof() { + let instance = merkle_proof_instance().await; + + let leaves = ["A".as_bytes(), "B".as_bytes(), "C".as_bytes()].to_vec(); + let num_leaves = 3; + let key = 0; + + let (_tree, root, leaf, proof) = build_tree(leaves, key).await; + + assert_eq!( + verify_proof(&instance, key, leaf, root, num_leaves, proof).await, + true + ); + } +} diff --git a/tests/src/test_projects/merkle_proof/tests/mod.rs b/tests/src/test_projects/merkle_proof/tests/mod.rs new file mode 100644 index 00000000..39c93631 --- /dev/null +++ b/tests/src/test_projects/merkle_proof/tests/mod.rs @@ -0,0 +1,2 @@ +mod functions; +mod utils; diff --git a/tests/src/test_projects/merkle_proof/tests/utils/mod.rs b/tests/src/test_projects/merkle_proof/tests/utils/mod.rs new file mode 100644 index 00000000..d44ff49b --- /dev/null +++ b/tests/src/test_projects/merkle_proof/tests/utils/mod.rs @@ -0,0 +1,104 @@ +use fuel_merkle::{ + binary::in_memory::MerkleTree, + common::{Bytes32, ProofSet}, +}; +use fuels::prelude::*; + +abigen!( + TestMerkleProofLib, + "test_projects/merkle_proof/out/debug/merkle_proof-abi.json" +); + +pub mod abi_calls { + + use super::*; + + pub async fn leaf_digest(contract: &TestMerkleProofLib, data: [u8; 32]) -> [u8; 32] { + contract.leaf_digest(data).call().await.unwrap().value + } + + pub async fn node_digest( + contract: &TestMerkleProofLib, + left: [u8; 32], + right: [u8; 32], + ) -> [u8; 32] { + contract + .node_digest(left, right) + .call() + .await + .unwrap() + .value + } + + pub async fn process_proof( + contract: &TestMerkleProofLib, + key: u64, + leaf: [u8; 32], + num_leaves: u64, + proof: Vec<[u8; 32]>, + ) -> [u8; 32] { + contract + .process_proof(key, leaf, num_leaves, proof) + .call() + .await + .unwrap() + .value + } + + pub async fn verify_proof( + contract: &TestMerkleProofLib, + key: u64, + leaf: [u8; 32], + root: [u8; 32], + num_leaves: u64, + proof: Vec<[u8; 32]>, + ) -> bool { + contract + .verify_proof(key, root, leaf, num_leaves, proof) + .call() + .await + .unwrap() + .value + } +} + +pub mod test_helpers { + + use super::*; + + pub async fn build_tree( + leaves: Vec<&[u8]>, + key: u64, + ) -> (MerkleTree, Bytes32, Bytes32, ProofSet) { + let mut tree = MerkleTree::new(); + + for datum in leaves.iter() { + let _ = tree.push(datum); + } + + let merkle_root = tree.root(); + let mut proof = tree.prove(key).unwrap(); + let merkle_leaf = proof.1[0]; + proof.1.remove(0); + + (tree, merkle_root, merkle_leaf, proof.1) + } + + pub async fn merkle_proof_instance() -> TestMerkleProofLib { + let wallet = launch_provider_and_get_wallet().await; + + let contract_id = Contract::deploy( + "./test_projects/merkle_proof/out/debug/merkle_proof.bin", + &wallet, + TxParameters::default(), + StorageConfiguration::default(), + ) + .await + .unwrap(); + + let instance = + TestMerkleProofLibBuilder::new(contract_id.to_string(), wallet.clone()).build(); + + instance + } +}