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:
Hash Result2 → HASH-B
Combine HASH-A + HASH-B → HASH-AB
Combine HASH-AB + HASH-CD → ROOT
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:
Merkle root validation ❌
Block hash validation ❌
Digital signature validation ❌
Efficiency
Operation Without Merkle Tree With Merkle Tree Verify all data O(n) - check every result O(n) - same Verify one result O(n) - check all results O(log n) - only proof path Data transfer Send all results Send only proof hashes Storage Store all results Store 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^256 ≈ 1 / 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:
Changes the leaf hash
Propagates up to parent nodes
Changes the Merkle root
Invalidates the block hash
Breaks the digital signature
Benchmarks
Typical performance on modern hardware:
Operation Time Results Build tree 5ms 100 results Build tree 50ms 1,000 results Build tree 500ms 10,000 results Generate proof <1ms Any size tree Verify proof <1ms Any 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