Skip to content

Latest commit

 

History

History

merkle_tree

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Merkle Trees

A Merkle tree is a binary tree data structure where each leaf node contains the hash of a data block, and each non-leaf node contains the hash of its two child nodes. This structure allows for efficient and secure verification of content in large data structures.

What is a Merkle Tree?

Merkle trees provide a way to efficiently verify the integrity of data. Here's how they work:

  1. Leaf Nodes: Start by hashing each piece of data (e.g., files, transactions) to create the leaf nodes
  2. Internal Nodes: Each internal node is created by hashing the concatenation of its two child nodes
  3. Root Hash: The hash at the top of the tree (root) represents a cryptographic summary of all the data

For example, with 8 files (f₁, f₂, ..., f₈):

  • First, hash each file: h₁ = H(f₁), h₂ = H(f₂), ..., h₈ = H(f₈)
  • Then hash pairs: h₁₂ = H(h₁, h₂), h₃₄ = H(h₃, h₄), etc.
  • Continue until you reach the root: h₁₋₈ = H(h₁₋₄, h₅₋₈)

The power of Merkle trees comes from their ability to generate compact proofs. A Merkle proof for a specific piece of data consists of the minimal set of hashes needed to recompute the root hash. This allows verification that a piece of data belongs to the original set without needing all the data.

Overview

The Merkle tree implementation in Lambdaworks provides:

  • A generic MerkleTree structure that can work with different hash functions and data types
  • Support for generating and verifying inclusion proofs
  • Multiple backend implementations for different use cases
  • Serialization and deserialization of proofs
  • Optional parallel processing for improved performance

Implementation

The implementation in this codebase includes:

  • MerkleTree: The main Merkle tree data structure
  • Proof: Represents a Merkle proof for verifying inclusion of data
  • IsMerkleTreeBackend: A trait for implementing different backend strategies
  • Several backend implementations:
    • FieldElementBackend: For hashing field elements using various hash functions
    • FieldElementVectorBackend: For hashing vectors of field elements
    • BatchPoseidonTree: For batch hashing with Poseidon

API Usage

Creating a Merkle Tree

Here's a basic example of creating a Merkle tree with field elements:

use lambdaworks_crypto::merkle_tree::{
    merkle::MerkleTree,
    backends::field_element::FieldElementBackend,
};
use lambdaworks_math::field::{
    element::FieldElement,
    fields::fft_friendly::stark_252_prime_field::Stark252PrimeField,
};
use sha3::Keccak256;

// Define the types we'll use
type F = Stark252PrimeField;
type FE = FieldElement<F>;

// Create some data
let values: Vec<FE> = (1..6).map(FE::from).collect();

// Build the Merkle tree using Keccak256 as the hash function
let merkle_tree = MerkleTree::<FieldElementBackend<F, Keccak256, 32>>::build(&values).unwrap();

Using BatchPoseidonTree for Efficient Hashing

The BatchPoseidonTree backend is specifically designed for efficient batch hashing using the Poseidon hash function, which is particularly useful in zero-knowledge proof systems. This backend provides optimized performance for vectors of field elements.

use lambdaworks_crypto::merkle_tree::{
    merkle::MerkleTree,
    backends::field_element_vector::BatchPoseidonTree,
};
use lambdaworks_crypto::hash::poseidon::starknet::PoseidonCairoStark252;
use lambdaworks_math::field::{
    element::FieldElement,
    fields::fft_friendly::stark_252_prime_field::Stark252PrimeField,
};

// Define the types we'll use
type F = Stark252PrimeField;
type FE = FieldElement<F>;

// Create some data (vectors of field elements)
let values: Vec<Vec<FE>> = vec![
    vec![FE::from(1), FE::from(2)],
    vec![FE::from(3), FE::from(4)],
    vec![FE::from(5), FE::from(6)],
    vec![FE::from(7), FE::from(8)],
];

// Build the Merkle tree using Poseidon hash function
let merkle_tree = MerkleTree::<BatchPoseidonTree<PoseidonCairoStark252>>::build(&values).unwrap();

// Generate a proof for a specific element
let proof = merkle_tree.get_proof_by_pos(1).unwrap();

// Verify the proof
let is_valid = proof.verify::<BatchPoseidonTree<PoseidonCairoStark252>>(
    &merkle_tree.root,
    1,
    &values[1]
);

assert!(is_valid, "Proof verification failed");

Key features of BatchPoseidonTree:

  1. Optimized for ZK Systems: Poseidon is designed to be efficient in zero-knowledge proof systems, making this backend ideal for ZK applications.

  2. Batch Hashing: The hash_many function efficiently processes multiple field elements at once.

  3. Field Element Compatibility: Works with vectors of field elements, which is common in cryptographic protocols.

  4. Performance: Poseidon offers better performance than traditional hash functions when working with field elements in ZK contexts.

Generating Proofs

To generate a proof for a specific leaf:

// Generate a proof for the first element (index 0)
let proof = merkle_tree.get_proof_by_pos(0).unwrap();

Verifying Proofs

To verify that a value is included in the tree:

// Verify the proof
let is_valid = proof.verify::<FieldElementBackend<F, Keccak256, 32>>(
    &merkle_tree.root,  // The Merkle root
    0,                  // The position of the leaf
    &values[0]          // The value to verify
);

assert!(is_valid, "Proof verification failed");

Working with Vectors of Field Elements

If you need to hash vectors of field elements:

use lambdaworks_crypto::merkle_tree::{
    merkle::MerkleTree,
    backends::field_element_vector::FieldElementVectorBackend,
};
use lambdaworks_math::field::{
    element::FieldElement,
    fields::fft_friendly::stark_252_prime_field::Stark252PrimeField,
};
use sha3::Keccak256;

// Define the types we'll use
type F = Stark252PrimeField;
type FE = FieldElement<F>;

// Create some data (vectors of field elements)
let values: Vec<Vec<FE>> = vec![
    vec![FE::from(1), FE::from(2)],
    vec![FE::from(3), FE::from(4)],
    vec![FE::from(5), FE::from(6)],
];

// Build the Merkle tree
let merkle_tree = MerkleTree::<FieldElementVectorBackend<F, Keccak256, 32>>::build(&values).unwrap();

// Generate a proof
let proof = merkle_tree.get_proof_by_pos(0).unwrap();

// Verify the proof
let is_valid = proof.verify::<FieldElementVectorBackend<F, Keccak256, 32>>(
    &merkle_tree.root,
    0,
    &values[0]
);

assert!(is_valid, "Proof verification failed");

Serialization and Deserialization

Proofs can be serialized and deserialized for storage or transmission. Note that serialization requires the alloc feature to be enabled:

// This requires the 'alloc' feature to be enabled
use lambdaworks_crypto::merkle_tree::{
    merkle::MerkleTree,
    proof::Proof,
    // For testing, you might use a simpler backend like TestBackend
};
use lambdaworks_math::traits::{Deserializable, Serializable};

// Serialize the proof
let serialized_proof = proof.serialize();

// Deserialize the proof
let deserialized_proof = Proof::deserialize(&serialized_proof).unwrap();

// Verify the deserialized proof
let is_valid = deserialized_proof.verify(
    &merkle_tree.root,
    0,
    &values[0]
);

assert!(is_valid, "Deserialized proof verification failed");

Note: The serialization example assumes that the type used for the Merkle tree nodes implements both Serializable and Deserializable traits.

Performance Considerations

  • The Merkle tree implementation automatically pads the input data to the next power of 2, which is required for a balanced binary tree
  • For large datasets, enable the parallel feature to use parallel processing for improved performance
  • Choose an appropriate backend based on your security and performance requirements:
    • Standard cryptographic hash functions (SHA-3, Keccak) provide strong security guarantees

References