Skip to main content
Ubu-Block’s security model ensures that any attempt to tamper with election results will be immediately detected. This page explains the multi-layered tamper detection mechanisms.

How Tampering is Detected

The blockchain validation process checks four critical properties for every block:

1. Block Hash Integrity

Each block’s hash is recalculated and compared to the stored hash:
let header = ElectionBlockHeader {
    block_number: block.height as i64,
    merkle_root: block.merkle_root,
    previous_hash: prev_hash,
    validator_signature: block.signature_pub_key_hash.clone(),
    timestamp: block.timestamp.timestamp() as i64,
};

let calculated_hash = types::crypto::hash_block(&header);
if calculated_hash != block.hash {
    return Err(sqlx::Error::Protocol(
        format!("Block hash mismatch at index {}", index).into(),
    ));
}
See crates/database/src/lib.rs:256-270 for the full implementation.
The block hash is calculated from the header, which includes the Merkle root of all election results. Any change to results changes the Merkle root, which changes the block hash.

2. Signature Verification

After confirming the hash matches, the system verifies the cryptographic signature:
let pub_key = self.get_public_key(&block.signature_pub_key_hash).await?;
let verifier: VerifyingKey = deserialize(&pub_key.bytes).unwrap();
let signature_bytes = hex::decode(&block.hash_signature).unwrap();
let signature: Signature = deserialize(&signature_bytes).unwrap();

verifier.verify(calculated_hash.as_bytes(), &signature).unwrap();
This ensures the block was created by someone with the corresponding private key.

3. Chain Linkage Verification

Each block must correctly reference its predecessor:
if block.prev_hash != hex::encode(prev_hash) {
    return Err(sqlx::Error::Protocol(
        format!("Previous hash mismatch at index {}", index).into(),
    ));
}
This creates an unbreakable chain where tampering with any block invalidates all subsequent blocks.

4. Merkle Root Verification

The stored Merkle root ensures the integrity of all election results in the block. Any modification to results data will produce a different Merkle root.
These four checks work together. An attacker would need to:
  1. Modify the data
  2. Recalculate the Merkle root
  3. Recalculate the block hash
  4. Forge a valid signature (impossible without the private key)
  5. Update all subsequent blocks in the chain

Real-World Tampering Example

The README demonstrates an actual tampering attempt. Let’s walk through what happens:

Initial State

First, we submit some valid election results through a submission node:
# Assuming you have a submission node running on localhost:8080
cargo run --bin ubu-block-cli -- -c config.toml submit http://localhost:8080 022113056303301 1 66
cargo run --bin ubu-block-cli -- -c config.toml submit http://localhost:8080 022113056303301 2 21
Querying the results shows:
+--------+--------------+----------+-----------+-------+-------+
| county | constituency |   ward   | candidate | party | votes |
+--------+--------------+----------+-----------+-------+-------+
| Kiambu |     Juja     | Kalimoni |   Omosh   |  ODM  |  21   |
| Kiambu |     Juja     | Kalimoni |   Mwas    |  PNU  |  66   |
+--------+--------------+----------+-----------+-------+-------+
Validation passes:
cargo run validate
# INFO ubu_block] Blockchain is valid!

Tampering Attempt

Now let’s try to manipulate Omosh’s votes directly in the SQLite database:
UPDATE "results" SET "votes" = 71 WHERE _rowid_ = 1
Querying again shows the tampered data:
+--------+--------------+----------+-----------+-------+-------+
| county | constituency |   ward   | candidate | party | votes |
+--------+--------------+----------+-----------+-------+-------+
| Kiambu |     Juja     | Kalimoni |   Omosh   |  ODM  |  71   |
| Kiambu |     Juja     | Kalimoni |   Mwas    |  PNU  |  66   |
+--------+--------------+----------+-----------+-------+-------+

Detection

But when we validate the blockchain:
cargo run validate

thread 'main' panicked at 'Could not verify block, found 0e70cebe0ab3bd8c3606a08d26483d092534eea4ccdb7816fc2692aee5ed3109, block: Block {... CandidateResult { station_id: 22113056303301, candidate_id: 1, votes: 71 }]......', src/db.rs:189:17
The tampering is immediately detected!

Why Tampering Fails

Here’s what happens when someone tries to tamper with the data:
  1. Merkle Root Mismatch: Changing vote counts changes the Merkle tree structure, producing a different Merkle root
  2. Hash Mismatch: The stored block hash was calculated with the original Merkle root. The new Merkle root produces a different hash
  3. Signature Invalid: Even if the attacker recalculates the hash, the stored signature was created with the original hash. Without the private key, they can’t create a new valid signature
  4. Chain Break: If they somehow modified multiple blocks, the prev_hash linkage would be broken
The validation process checks the entire blockchain from genesis to the current block, ensuring complete integrity.

No “Fungua Server”

The phrase “fungua server” (Swahili for “open the server”) refers to a common tactic in Kenya where election officials would:
  1. Take servers offline
  2. Modify results
  3. Bring servers back online with altered data
Ubu-Block makes this impossible because:
  • Distributed: Multiple nodes have copies of the blockchain
  • Cryptographically signed: Changes require private keys
  • Publicly verifiable: Anyone can run validation
  • Append-only: Old blocks cannot be modified without detection
Even with physical access to the database files, an attacker cannot successfully tamper with results without:
  • The private key of the block creator
  • The ability to compromise all distributed nodes
  • Going undetected during the validation process

Validation Command

The validation process runs automatically, checking all blocks:
pub async fn is_valid(&self) -> Result<bool, sqlx::Error> {
    let height = self.get_height().await?;
    
    for index in 0..height {
        let block = self.get_block_by_height(index).await?;
        // ... validation checks ...
    }
    
    Ok(true)
}
See crates/database/src/lib.rs:237-290 for the complete implementation.

Audit Trail

All submissions (accepted or rejected) are recorded, providing a complete audit trail:
  • Block height: Sequential numbering prevents gaps
  • Timestamps: Every block includes when it was created
  • Creator identification: Public key hash identifies who created each block
  • Signature chain: Each block signs the previous block’s hash

Best Practices

For Node Operators:
  • Regularly run validate to check blockchain integrity
  • Monitor for unexpected validation failures
  • Keep your private key secure and backed up
  • Run on multiple independent nodes for redundancy
For Observers:
  • You can independently verify any block
  • Download the blockchain and run validation yourself
  • Compare results across multiple nodes
  • Report any discrepancies immediately

What Tampering Looks Like

If validation fails, you’ll see errors indicating:
  • Block hash mismatch: Data was modified after the block was created
  • Signature verification failed: The signature doesn’t match the hash
  • Previous hash mismatch: Chain linkage was broken
  • Merkle root invalid: Results data was tampered with
Any of these errors means the blockchain has been compromised and should be restored from a trusted backup or synchronized from honest nodes.

Build docs developers (and LLMs) love