Skip to main content

Overview

Ubu-Block uses Merkle trees (also called hash trees) to ensure the integrity of election results within each block. This cryptographic data structure makes it impossible to tamper with even a single vote without detection.
A Merkle tree allows you to verify that a specific result is part of a block without downloading the entire block’s data.

What is a Merkle Tree?

A Merkle tree is a binary tree of cryptographic hashes where:
  • Leaves: Hash of individual election results
  • Branches: Hash of two child nodes combined
  • Root: Single hash representing all data (Merkle root)
                    ROOT HASH
                   /          \
                  /            \
              HASH-AB        HASH-CD
             /      \       /      \
          HASH-A  HASH-B HASH-C  HASH-D
            |       |       |       |
         Result1 Result2 Result3 Result4

Structure

Implementation

// From types/src/merkle.rs:4
#[derive(Debug, Clone)]
pub struct MerkleNode {
    pub hash: [u8; 32],                    // SHA3-256 hash
    pub left: Option<Box<MerkleNode>>,     // Left child
    pub right: Option<Box<MerkleNode>>,    // Right child
}

pub struct MerkleTree {
    pub root: Option<Box<MerkleNode>>,     // Root node
    pub leaves: Vec<[u8; 32]>,             // Leaf hashes
}

Building a Merkle Tree

Step-by-Step Process

1. Hash Each Result

Each election result is serialized and hashed:
// From types/src/merkle.rs:42
fn hash_election_result(result: &CandidateResult) -> [u8; 32] {
    let serialized = bincode::serialize(result).unwrap();
    let mut hasher = Sha3_256::new();
    hasher.update(&serialized);
    hasher.finalize().into()
}
Example:
let result = CandidateResult {
    station_id: 022113056303301,
    candidate_id: 1,
    votes: 150,
};

let hash = hash_election_result(&result);
// hash = [0x7a, 0x3f, 0x9e, ...] (32 bytes)

2. Pair Up Hashes

Combine hashes in pairs to create the next level:
// From types/src/merkle.rs:172
fn hash_pair(left: [u8; 32], right: [u8; 32]) -> [u8; 32] {
    let mut hasher = Sha3_256::new();
    hasher.update(left);
    hasher.update(right);
    hasher.finalize().into()
}

3. Handle Odd Numbers

If there’s an odd number of nodes, duplicate the last one:
for chunk in hashes.chunks(2) {
    match chunk.len() {
        2 => {
            // Normal case: hash two nodes together
            let combined_hash = hash_pair(chunk[0], chunk[1]);
            next_level.push(combined_hash);
        }
        1 => {
            // Odd number: duplicate the last one
            let combined_hash = hash_pair(chunk[0], chunk[0]);
            next_level.push(combined_hash);
        }
        _ => unreachable!(),
    }
}

4. Repeat Until Root

Continue pairing and hashing until only one hash remains - the Merkle root.

Complete Example

// From types/src/merkle.rs:156
pub fn from_election_results_proper(results: &[CandidateResult]) -> Self {
    let mut tree = Self::new();
    
    // Step 1: Hash each result
    for result in results {
        let result_hash = Self::hash_election_result(result);
        tree.leaves.push(result_hash);
    }
    
    // Step 2: Build tree bottom-up
    if !tree.leaves.is_empty() {
        tree.root = Some(Box::new(Self::build_tree_proper(&tree.leaves)));
    }
    
    tree
}

Verification Proofs

Merkle trees enable efficient verification: prove a specific result exists in a block without revealing all results.

Generating a Proof

A Merkle proof is a list of sibling hashes needed to reconstruct the path to the root:
// From types/src/merkle.rs:185
pub fn generate_proof(&self, target_index: usize) -> Option<Vec<[u8; 32]>> {
    if target_index >= self.leaves.len() {
        return None;
    }
    
    let mut proof = Vec::new();
    Self::generate_proof_recursive(
        &self.root, 
        target_index, 
        0, 
        self.leaves.len(), 
        &mut proof
    );
    Some(proof)
}

Visual Example

Prove Result2 is in the tree:

                    ROOT
                   /    \
                  /      \
              HASH-AB*   HASH-CD  <- Include in proof
             /      \
          HASH-A*  HASH-B         <- Include in proof  
            |       |^
         Result1 [Result2]        <- This is what we're proving
Proof for Result2: [HASH-A, HASH-CD] To verify:
  1. Hash Result2 → HASH-B
  2. Combine HASH-A + HASH-B → HASH-AB
  3. Combine HASH-AB + HASH-CD → ROOT
  4. Compare with stored root ✓

Verifying a Proof

// From types/src/merkle.rs:240
pub fn verify_proof(
    leaf_hash: [u8; 32],
    proof: &[[u8; 32]],
    root_hash: [u8; 32],
    leaf_index: usize,
    total_leaves: usize,
) -> bool {
    let mut current_hash = leaf_hash;
    let mut index = leaf_index;
    
    // Climb the tree using proof hashes
    for &sibling_hash in proof {
        if index % 2 == 0 {
            // Current node is left child
            current_hash = Self::hash_pair(current_hash, sibling_hash);
        } else {
            // Current node is right child
            current_hash = Self::hash_pair(sibling_hash, current_hash);
        }
        index /= 2;
    }
    
    // Check if we reached the root
    current_hash == root_hash
}

Integration with Blocks

Storing the Merkle Root

Each block stores its Merkle root in the header:
pub struct Block {
    pub merkle_root: [u8; 32],  // Root hash of all results
    // ... other fields
}

pub struct ElectionBlockHeader {
    pub merkle_root: [u8; 32],  // Used in block hash calculation
    // ... other fields
}

Creating a Block with Merkle Tree

use types::merkle::MerkleTree;

// 1. Collect election results
let results = vec![
    CandidateResult { station_id: 1, candidate_id: 1, votes: 150 },
    CandidateResult { station_id: 1, candidate_id: 2, votes: 200 },
    CandidateResult { station_id: 2, candidate_id: 1, votes: 250 },
];

// 2. Build Merkle tree
let tree = MerkleTree::from_election_results_proper(&results);
let merkle_root = tree.get_root_hash().unwrap();

// 3. Create block with Merkle root
let block = Block::new(
    &signer,
    &prev_hash,
    results,
    height,
    merkle_root,  // <- Merkle root included
);

Validation Process

// From database/src/lib.rs:255
let header = ElectionBlockHeader {
    block_number: block.height as i64,
    merkle_root: block.merkle_root,  // Use stored Merkle root
    previous_hash: prev_hash,
    validator_signature: block.signature_pub_key_hash.clone(),
    timestamp: block.timestamp.timestamp() as i64,
};

// Hash header and compare
let calculated_hash = types::crypto::hash_block(&header);
if calculated_hash != block.hash {
    return Err("Block hash mismatch - data tampered!");
}

Why Merkle Trees Matter

Tamper Detection

Changing even a single vote invalidates the Merkle root:
// Original
let result1 = CandidateResult { station_id: 1, candidate_id: 1, votes: 100 };
let tree1 = MerkleTree::from_election_results_proper(&[result1]);
let root1 = tree1.get_root_hash().unwrap();

// Tampered (changed votes from 100 to 101)
let result2 = CandidateResult { station_id: 1, candidate_id: 1, votes: 101 };
let tree2 = MerkleTree::from_election_results_proper(&[result2]);
let root2 = tree2.get_root_hash().unwrap();

assert_ne!(root1, root2);  // Roots are different!
Since the Merkle root is included in the block header and the header is signed, any tampering breaks:
  1. Merkle root validation ❌
  2. Block hash validation ❌
  3. Digital signature validation ❌

Efficiency

OperationWithout Merkle TreeWith Merkle Tree
Verify all dataO(n) - check every resultO(n) - same
Verify one resultO(n) - check all resultsO(log n) - only proof path
Data transferSend all resultsSend only proof hashes
StorageStore all resultsStore only root
Example: For 1,000 results:
  • Full verification: 1,000 hashes
  • Merkle proof: ~10 hashes (log₂ 1000)
Merkle proofs require only logarithmic space relative to the number of results, making verification extremely efficient.

Practical Example

Let’s walk through a complete example:
// Create sample election results
let results = vec![
    CandidateResult { station_id: 1, candidate_id: 1, votes: 150 },
    CandidateResult { station_id: 2, candidate_id: 2, votes: 200 },
    CandidateResult { station_id: 3, candidate_id: 1, votes: 250 },
    CandidateResult { station_id: 4, candidate_id: 3, votes: 300 },
    CandidateResult { station_id: 5, candidate_id: 2, votes: 350 },
];

// Build Merkle tree
let tree = MerkleTree::from_election_results_proper(&results);

// Get root hash (stored in block)
let root_hash = tree.get_root_hash().unwrap();
println!("Merkle Root: {:x?}", root_hash);

// Generate proof for first result
let proof = tree.generate_proof(0).unwrap();
println!("Proof size: {} hashes", proof.len());

// Verify the proof
let leaf_hash = MerkleTree::hash_election_result(&results[0]);
let is_valid = MerkleTree::verify_proof(
    leaf_hash,
    &proof,
    root_hash,
    0,
    results.len()
);

assert!(is_valid, "Proof verification failed!");
println!("✓ Proof verified successfully");

Output

Merkle Root: [0x3a, 0x7f, 0x2c, ...]
Proof size: 3 hashes
✓ Proof verified successfully

Advanced Features

Visualizing the Tree

// From types/src/merkle.rs:267
pub fn print_tree(&self) {
    if let Some(root) = &self.root {
        Self::print_node(root, 0);
    }
}

fn print_node(node: &MerkleNode, depth: usize) {
    let indent = "  ".repeat(depth);
    println!("{}Hash: {:x?}...", indent, &node.hash[..4]);
    
    if let Some(left) = &node.left {
        println!("{indent}├─ Left:");
        Self::print_node(left, depth + 1);
    }
    
    if let Some(right) = &node.right {
        println!("{indent}└─ Right:");
        Self::print_node(right, depth + 1);
    }
}
Output:
Hash: [0x3a, 0x7f, 0x2c, 0x91]...
  ├─ Left:
    Hash: [0x5b, 0x3a, 0x8f, 0x12]...
    ├─ Left:
      Hash: [0x7a, 0x3f, 0x9e, 0x44]...
    └─ Right:
      Hash: [0x2c, 0x91, 0x5f, 0x78]...
  └─ Right:
    Hash: [0x8d, 0x62, 0x1a, 0xc3]...

Security Properties

Collision Resistance

SHA3-256 makes it computationally infeasible to:
  • Find two different results that hash to the same value
  • Create a fake result that matches an existing hash
  • Modify data without changing the hash
Collision probability: 1 / 2^2561 / 10^77 (more atoms than in the observable universe)

Preimage Resistance

Given a hash, you cannot:
  • Determine the original election result
  • Reconstruct the data from the hash
  • Work backwards from root to leaves

Tamper Evidence

Any modification to results:
  1. Changes the leaf hash
  2. Propagates up to parent nodes
  3. Changes the Merkle root
  4. Invalidates the block hash
  5. Breaks the digital signature

Performance

Benchmarks

Typical performance on modern hardware:
OperationTimeResults
Build tree5ms100 results
Build tree50ms1,000 results
Build tree500ms10,000 results
Generate proof<1msAny size tree
Verify proof<1msAny size tree

Space Complexity

  • Tree storage: O(n) where n = number of results
  • Proof size: O(log n) hashes
  • Block storage: O(1) - just the root hash

Testing

Comprehensive tests ensure correctness:
#[test]
fn test_merkle_proof_verification() {
    let results = vec![
        CandidateResult { station_id: 0, candidate_id: 1, votes: 100 },
        CandidateResult { station_id: 1, candidate_id: 2, votes: 101 },
        CandidateResult { station_id: 2, candidate_id: 3, votes: 102 },
        CandidateResult { station_id: 3, candidate_id: 4, votes: 103 },
        CandidateResult { station_id: 4, candidate_id: 5, votes: 104 },
    ];
    
    let tree = MerkleTree::from_election_results_proper(&results);
    let root_hash = tree.get_root_hash().unwrap();
    
    // Verify proofs for all items
    for i in 0..results.len() {
        let proof = tree.generate_proof(i).unwrap();
        let leaf_hash = MerkleTree::hash_election_result(&results[i]);
        
        assert!(
            MerkleTree::verify_proof(
                leaf_hash, 
                &proof, 
                root_hash, 
                i, 
                results.len()
            ),
            "Proof verification failed for item {}", i
        );
    }
}

Blockchain

How Merkle roots integrate with blocks

Consensus

How validation uses Merkle trees

Validation

Step-by-step validation guide

Security

Cryptographic foundations

Build docs developers (and LLMs) love