Back

Independent verification guide

Technical reference for auditors and journalists who want to independently verify any anchored vote on Base.

Version: 1.0 ยท Last updated: 2026-05-09 ยท Contract: MerkleAnchor.sol


TL;DR โ€” pick your language

๐Ÿ‡ฌ๐Ÿ‡ง English

What this is. IMDifferent anchors a cryptographic summary (Merkle root) of every anonymous vote to the Base blockchain daily. This document explains exactly how the anchoring works and provides a script you can run to verify any vote independently.

Why you should care. We don't ask you to trust us. We give you the math and the tools to verify for yourself that your vote was included in the on-chain commitment โ€” no account, no login, no trust required.

How to verify in 60 seconds:

  1. Get your vote ID from your ZK receipt (shown after voting)
  2. Run: node verify-merkle.mjs --vote-id <your-vote-id>
  3. The script tells you PASS or FAIL
  4. If it says PASS, that vote is provably anchored on the Base blockchain

For deeper verification, see the full technical reference below.

๐Ÿ‡ธ๐Ÿ‡ฆ ุงู„ุนุฑุจูŠุฉ

(machine-translated; native review pending)

ู…ุง ู‡ุฐุง. ูŠู‚ูˆู… IMDifferent ุจุชุซุจูŠุช ู…ู„ุฎุต ุชุดููŠุฑูŠ (ุฌุฐุฑ ู…ูŠุฑูƒู„) ู„ูƒู„ ุชุตูˆูŠุช ู…ุฌู‡ูˆู„ ุนู„ู‰ ุจู„ูˆูƒุชุดูŠู† Base ูŠูˆู…ูŠุงู‹. ูŠุดุฑุญ ู‡ุฐุง ุงู„ู…ุณุชู†ุฏ ุจุงู„ุถุจุท ูƒูŠู ูŠุนู…ู„ ุงู„ุชุซุจูŠุช ูˆูŠูˆูุฑ ุฃุฏุงุฉ ูŠู…ูƒู†ูƒ ุชุดุบูŠู„ู‡ุง ู„ู„ุชุญู‚ู‚ ู…ู† ุฃูŠ ุชุตูˆูŠุช ุจุดูƒู„ ู…ุณุชู‚ู„.

ู„ู…ุงุฐุง ูŠุฌุจ ุฃู† ุชู‡ุชู…. ู„ุง ู†ุทู„ุจ ู…ู†ูƒ ุฃู† ุชุซู‚ ุจู†ุง. ู†ู‚ุฏู… ู„ูƒ ุงู„ุฑูŠุงุถูŠุงุช ูˆุงู„ุฃุฏูˆุงุช ู„ู„ุชุญู‚ู‚ ุจู†ูุณูƒ ู…ู† ุฃู† ุชุตูˆูŠุชูƒ ุชู… ุชุถู…ูŠู†ู‡ ููŠ ุงู„ุงู„ุชุฒุงู… ุงู„ู…ุณุฌู„ ุนู„ู‰ ุงู„ุณู„ุณู„ุฉ โ€” ุจุฏูˆู† ุญุณุงุจุŒ ุจุฏูˆู† ุชุณุฌูŠู„ ุฏุฎูˆู„ุŒ ุจุฏูˆู† ุซู‚ุฉ ู…ุทู„ูˆุจุฉ.

ูƒูŠู ุชุชุญู‚ู‚ ููŠ 60 ุซุงู†ูŠุฉ:

  1. ุงุญุตู„ ุนู„ู‰ ู…ุนุฑู‘ู ุงู„ุชุตูˆูŠุช ู…ู† ุฅูŠุตุงู„ ZK ุงู„ุฎุงุต ุจูƒ (ูŠุธู‡ุฑ ุจุนุฏ ุงู„ุชุตูˆูŠุช)
  2. ุดุบู‘ู„: node verify-merkle.mjs --vote-id <ู…ุนุฑู‘ู-ุงู„ุชุตูˆูŠุช>
  3. ุงู„ุฃุฏุงุฉ ุชุฎุจุฑูƒ PASS (ู†ุฌุญ) ุฃูˆ FAIL (ูุดู„)
  4. ุฅุฐุง ู‚ุงู„ุช PASSุŒ ูุฅู† ุชุตูˆูŠุชูƒ ู…ุซุจุช ุจุดูƒู„ ู‚ุงุจู„ ู„ู„ุฅุซุจุงุช ุนู„ู‰ ุจู„ูˆูƒุชุดูŠู† Base

ู„ู„ุชุญู‚ู‚ ุงู„ุฃุนู…ู‚ุŒ ุงู†ุธุฑ ุงู„ู…ุฑุฌุน ุงู„ุชู‚ู†ูŠ ุงู„ูƒุงู…ู„ ุฃุฏู†ุงู‡.

๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol

(machine-translated; native review pending)

Quรฉ es esto. IMDifferent ancla un resumen criptogrรกfico (raรญz de Merkle) de cada voto anรณnimo en la blockchain Base diariamente. Este documento explica exactamente cรณmo funciona el anclaje y proporciona un script que puedes ejecutar para verificar cualquier voto de forma independiente.

Por quรฉ deberรญa importarte. No te pedimos que confรญes en nosotros. Te damos las matemรกticas y las herramientas para que verifiques por ti mismo que tu voto fue incluido en el compromiso registrado en la cadena โ€” sin cuenta, sin inicio de sesiรณn, sin confianza requerida.

Cรณmo verificar en 60 segundos:

  1. Obtรฉn tu ID de voto de tu recibo ZK (mostrado despuรฉs de votar)
  2. Ejecuta: node verify-merkle.mjs --vote-id <tu-id-de-voto>
  3. El script te dice PASS (aprobado) o FAIL (fallido)
  4. Si dice PASS, ese voto estรก demostrablemente anclado en la blockchain Base

Para una verificaciรณn mรกs profunda, consulta la referencia tรฉcnica completa a continuaciรณn.

๐Ÿ‡ซ๐Ÿ‡ท Franรงais

(machine-translated; native review pending)

Ce que c'est. IMDifferent ancre un rรฉsumรฉ cryptographique (racine de Merkle) de chaque vote anonyme sur la blockchain Base quotidiennement. Ce document explique exactement comment l'ancrage fonctionne et fournit un script que vous pouvez exรฉcuter pour vรฉrifier n'importe quel vote de maniรจre indรฉpendante.

Pourquoi vous devriez vous en soucier. Nous ne vous demandons pas de nous faire confiance. Nous vous donnons les mathรฉmatiques et les outils pour vรฉrifier par vous-mรชme que votre vote a รฉtรฉ inclus dans l'engagement enregistrรฉ sur la chaรฎne โ€” pas de compte, pas de connexion, pas de confiance requise.

Comment vรฉrifier en 60 secondes :

  1. Obtenez votre ID de vote depuis votre reรงu ZK (affichรฉ aprรจs le vote)
  2. Exรฉcutez : node verify-merkle.mjs --vote-id <votre-id-de-vote>
  3. Le script vous indique PASS (rรฉussi) ou FAIL (รฉchouรฉ)
  4. S'il indique PASS, ce vote est prouvablement ancrรฉ sur la blockchain Base

Pour une vรฉrification plus approfondie, consultez la rรฉfรฉrence technique complรจte ci-dessous.

๐Ÿ‡จ๐Ÿ‡ณ ไธญๆ–‡

(machine-translated; native review pending)

่ฟ™ๆ˜ฏไป€ไนˆใ€‚ IMDifferent ๆฏๅคฉๅฐ†ๆฏไธ€ๆฌกๅŒฟๅๆŠ•็ฅจ็š„ๅŠ ๅฏ†ๆ‘˜่ฆ๏ผˆMerkle ๆ น๏ผ‰้”šๅฎšๅˆฐ Base ๅŒบๅ—้“พไธŠใ€‚ๆœฌๆ–‡ๆกฃ่ฏฆ็ป†่งฃ้‡Šไบ†้”šๅฎš็š„ๅทฅไฝœๅŽŸ็†๏ผŒๅนถๆไพ›ไบ†ไธ€ไธช่„šๆœฌ๏ผŒๆ‚จๅฏไปฅ่ฟ่กŒๅฎƒๆฅ็‹ฌ็ซ‹้ชŒ่ฏไปปไฝ•ๆŠ•็ฅจใ€‚

ไธบไป€ไนˆๆ‚จๅบ”่ฏฅๅ…ณๆณจใ€‚ ๆˆ‘ไปฌไธ่ฆๆฑ‚ๆ‚จไฟกไปปๆˆ‘ไปฌใ€‚ๆˆ‘ไปฌไธบๆ‚จๆไพ›ๆ•ฐๅญฆๅŽŸ็†ๅ’Œๅทฅๅ…ท๏ผŒ่ฎฉๆ‚จ่‡ชๅทฑ้ชŒ่ฏๆ‚จ็š„ๆŠ•็ฅจๆ˜ฏๅฆ่ขซๅŒ…ๅซๅœจ้“พไธŠๆ‰ฟ่ฏบไธญโ€”โ€”ๆ— ้œ€่ดฆๆˆทใ€ๆ— ้œ€็™ปๅฝ•ใ€ๆ— ้œ€ไฟกไปปใ€‚

ๅฆ‚ไฝ•ๅœจ 60 ็ง’ๅ†…้ชŒ่ฏ๏ผš

  1. ไปŽๆ‚จ็š„ ZK ๆ”ถๆฎไธญ่Žทๅ–ๆŠ•็ฅจ ID๏ผˆๆŠ•็ฅจๅŽๆ˜พ็คบ๏ผ‰
  2. ่ฟ่กŒ๏ผšnode verify-merkle.mjs --vote-id <ๆ‚จ็š„ๆŠ•็ฅจID>
  3. ่„šๆœฌไผšๅ‘Š่ฏ‰ๆ‚จ PASS๏ผˆ้€š่ฟ‡๏ผ‰ๆˆ– FAIL๏ผˆๅคฑ่ดฅ๏ผ‰
  4. ๅฆ‚ๆžœๆ˜พ็คบ PASS๏ผŒ่ฏฅๆŠ•็ฅจๅทฒๅฏ่ฏๆ˜Žๅœฐ้”šๅฎšๅœจ Base ๅŒบๅ—้“พไธŠ

ๅฆ‚้œ€ๆ›ดๆทฑๅ…ฅ็š„้ชŒ่ฏ๏ผŒ่ฏทๅ‚้˜…ไธ‹ๆ–น็š„ๅฎŒๆ•ดๆŠ€ๆœฏๅ‚่€ƒใ€‚


Full Technical Reference (English)

ยง1 โ€” What we anchor and why

IMDifferent is an anonymous polling platform where votes carry no user identity โ€” no email, no wallet, no login is required to vote. This creates a trust problem: how can voters be confident that their vote was actually counted and not silently dropped, altered, or fabricated?

Our answer is daily Merkle anchoring. Once per day at 03:30 UTC, an automated process collects all votes cast in the previous UTC day, groups them by verification tier (World ID, identity-verified, mixed, anonymous), builds a binary Merkle tree for each tier, and commits the root hash of each tree to the Base blockchain via a smart contract event (one event per non-empty tier, all batched into a single transaction).

Once a root is on-chain, it cannot be altered without detection. Anyone with a vote's inclusion proof can independently recompute the root and check it against the on-chain record. The vote data itself never goes on-chain โ€” only the 32-byte cryptographic commitment. Privacy is preserved; integrity is provable.

The public API at /api/merkle/roots lists all anchored roots. The proof endpoint at /api/merkle/proof/[voteId] returns the Merkle inclusion proof for any specific vote.

ยง2 โ€” Threat model and honest limits

We believe transparency requires stating what we cannot guarantee, not just what we can.

What Merkle anchoring proves:

What Merkle anchoring does NOT prove:

Our commitment: We will never modify anchored data. If we discover an error in a past anchor, we will publish a correction notice โ€” not silently re-anchor.

ยง2.1 โ€” Privacy boundary

Merkle anchoring is designed to provide provability without adding identifiability. The leaf string contains the data needed to deterministically reproduce the hash, but the public API never returns the leaf string itself โ€” only its SHA-256 (leafHash).

The table below summarises what an external observer sees when they call the public proof endpoint:

FieldPresent in leaf string (hashed)Returned by /api/merkle/proof/[voteId]
voteIdyesyes
tieryesyes
batchDate (day-level)derived from createdAtyes
position in treederivedyes
leafHash (SHA-256, one-way)outputyes
Proof siblings, expected root, anchor metadataderivedyes
pollIdyesno โ€” never returned
pollOptionIdyesno โ€” never returned
voterIpHashyesno โ€” never returned
Verification methods usedyesno โ€” never returned
ledgerEntryId (if any)yesno โ€” never returned
Vote createdAt (precise)yesno โ€” only day-level batchDate

Key implications:

  1. No vote-to-poll linkage via the API. Given only a vote ID, the public proof endpoint does not reveal which poll the vote belongs to or which option was selected. The mapping exists inside the leaf (so the hash is unique to that vote), but the leaf string is never exposed.
  2. No voter-to-voter linkage via the API. No identity field โ€” hashed or otherwise โ€” is returned. Two votes from the same voter appear unrelated in the public surface.
  3. One-way hashing is the privacy ceiling, not a floor. The leafHash is a SHA-256 of the eight-field leaf string. Recovering the inputs from the hash would require brute-forcing across the unknown fields โ€” computationally infeasible for a 256-bit digest with high-entropy inputs.
  4. Day-level granularity for timing. The public response carries batchDate (the day of the anchor batch), not a precise vote timestamp. This is informationally weaker than what a concurrent visitor to the poll page already observes during an active poll's voting window.

What this does not eliminate. Pre-existing exposures unrelated to Merkle remain: poll-result aggregates are public by design (the product's purpose), and a poll's pollId is necessarily known to anyone with the poll URL. The Merkle layer adds neither of these; it adds only the ability to prove a vote was anchored.

In one line: the leaf string in ยง3 is the recipe fed into a one-way hash. Only the hash output (leafHash) leaves the server. If you can hash, you cannot un-hash โ€” that is the entire privacy guarantee.

ยง3 โ€” Leaf string format (canonical, byte-exact)

โš ๏ธ Read this first. The fields below describe the pre-image of a one-way SHA-256 hash โ€” i.e. what goes into the hash function on the server before a single byte is ever sent over the wire. They are documented here as the canonical recipe so anyone who already holds the underlying values (notably, the voter themselves) can reproduce the resulting leafHash and verify the on-chain commitment matches. None of the fields marked "no โ€” never returned" in ยง2.1 are exposed by the public API. External auditors see only the resulting 32-byte hash, never its inputs. See ยง2.1 for the full table of what is and is not on the wire.

Each vote produces a leaf string โ€” a pipe-delimited UTF-8 string with exactly 8 fields:

${tier}|${voteId}|${pollId}|${pollOptionId}|${voterIpHash}|${methodsSorted}|${ledgerEntryIdOrNull}|${createdAtIsoUtc}
#FieldTypeRules
1tierstringLowercase snake_case: world_id_only, identity_verified, mixed, anonymous
2voteIdUUIDThe vote's unique identifier (stripped of any composite suffix)
3pollIdUUIDThe poll this vote belongs to
4pollOptionIdUUIDThe option the voter selected
5voterIpHashhex stringSHA-256 hash of the voter's IP (salted) โ€” 64 lowercase hex chars
6methodsSortedstringVerification methods sorted alphabetically, joined with ,. Example: COINBASE,WORLD_ID
7ledgerEntryIdOrNullstringThe ledger entry ID if present, or the literal string null (not empty string)
8createdAtIsoUtcISO 8601Date.toISOString() format: YYYY-MM-DDTHH:mm:ss.sssZ (always 3-digit ms, capital T and Z)

Leaf hash = "0x" + SHA-256(UTF-8 bytes of the leaf string) โ€” 66 characters total (0x + 64 hex).

Worked example

Given a synthetic vote:

tier:                mixed
voteId:              a1b2c3d4-e5f6-7890-abcd-ef1234567890
pollId:              poll-001
pollOptionId:        opt-A
voterIpHash:         9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
verificationMethods: ["WORLD_ID", "COINBASE"]  (sorted โ†’ "COINBASE,WORLD_ID")
ledgerEntryId:       null  (โ†’ literal string "null")
createdAt:           2026-05-08T14:30:00.000Z

Leaf string:

mixed|a1b2c3d4-e5f6-7890-abcd-ef1234567890|poll-001|opt-A|9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08|COINBASE,WORLD_ID|null|2026-05-08T14:30:00.000Z

Leaf hash:

0x605e61483bd7d829c8717a5cf228e6ce0d6c0d46d4496805e9ecb3ead9a69152

ยง4 โ€” Tree construction (SHA-256, padding rules)

IMDifferent uses a standard binary Merkle tree with SHA-256 hashing.

Internal node hash algorithm:

  1. Strip the 0x prefix from both children's hex strings
  2. Concatenate as ASCII text: leftHex + rightHex (128 characters total)
  3. Compute SHA-256 of the UTF-8 bytes of that 128-character string
  4. Output: "0x" + 64 lowercase hex characters

Padding rules (Bitcoin-style):

Worked example: 3-leaf tree

Given leaf hashes from strings "merkle-test-alpha", "merkle-test-beta", "merkle-test-gamma":

L0 = SHA256("merkle-test-alpha")  = 0x12996b159e2631026727b2a44f14d055150b0fb86e50e073f4d3d9757ac31230
L1 = SHA256("merkle-test-beta")   = 0x562a803285d7d9376884b96c0bdbb621481e086044aa0a31386e456ae5fe8a63
L2 = SHA256("merkle-test-gamma")  = 0x3bacff573c572314e6c19d5f20c7e7abb37e5ce97759412bef1599d1bd37ab96

Padding: L2 is duplicated โ†’ [L0, L1, L2, L2]

Level 1:
  N01 = SHA256(strip0x(L0) + strip0x(L1)) = 0xb3f72b2102fadc19ada670a5154a83aa7ff0d3254f49b3f17c5f7ffd3631a3a7
  N23 = SHA256(strip0x(L2) + strip0x(L2)) = 0xc0957193f03645b8e935033663a508c6ba6ebfc7d37e68febb0da3e9b9314b9c

Level 2 (root):
  ROOT = SHA256(strip0x(N01) + strip0x(N23)) = 0x7b9ed5ee279b3f123e65685d3c6667ba7c92b71bca0422e5fed3b46a44762409

Tree diagram:

              ROOT
            0x7b9e...2409
           /              \
        N01                N23
     0xb3f7...a3a7      0xc095...4b9c
      /       \           /       \
    L0        L1        L2        L2 (dup)
  0x1299..  0x562a..  0x3bac..  0x3bac..

Proof walk: To verify L2's inclusion, the proof provides two siblings:

  1. { siblingHash: L2, side: "left" } โ†’ hash(L2, L2) = N23
  2. { siblingHash: N01, side: "left" } โ†’ hash(N01, N23) = ROOT โœ“

Side interpretation: side: "left" means the sibling goes on the LEFT; the current node goes on the RIGHT. side: "right" means the sibling goes on the RIGHT; the current node goes on the LEFT.

ยง5 โ€” Root โ†’ on-chain encoding (contract event ABI)

The on-chain anchor uses the MerkleAnchor smart contract deployed on Base (Sepolia for testnet, mainnet for production).

Contract event:

event Anchor(
    bytes32 indexed root,        // topic1 โ€” the Merkle root
    uint8   indexed tier,        // topic2 โ€” tier integer
    uint64          timestamp,   // data โ€” Unix seconds at anchor time
    uint32          leafCount,   // data โ€” number of leaves in tree
    bytes16         batchDate    // data โ€” ISO date as ASCII bytes
);

Important: Only root and tier are indexed (appear as topics). timestamp, leafCount, and batchDate are in the event data field โ€” they must be ABI-decoded, not read from topics.

Event topic0 (keccak256 of the event signature):

keccak256("Anchor(bytes32,uint8,uint64,uint32,bytes16)")
= 0x72571895b4d4c7decf1eb14520c5637e75b1f4280e445d1bc6277f6504d82fec

Tier integer mapping:

IntegerTier
0world_id_only
1identity_verified
2mixed
3anonymous

Gas cost: Each anchorBatch transaction costs approximately 50,000โ€“80,000 gas depending on the number of tiers anchored. The operator pays all gas costs โ€” voters never pay anything.

ยง6 โ€” The verifier script (full source, copy-paste)

The complete source code of scripts/verify-merkle.mjs is embedded below. Save it as verify-merkle.mjs and run with Node.js 18+. Zero npm dependencies required.

#!/usr/bin/env node
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// IMDifferent Merkle Verifier v1.0
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Single-file, zero-dependency Node.js script (18+) to independently verify
// that a vote is anchored in an IMDifferent Merkle tree committed on-chain.
//
// Modes:
//   A) --vote-id <uuid>                Online: fetch proof, verify, check chain
//   B) --paste                         Offline: read proof JSON from stdin
//   C) --vote-id <uuid> --rpc-url <u>  Custom RPC for on-chain check
//
// Exit codes: 0=pass, 1=leaf-fail, 2=proof-fail, 3=not-on-chain, 4=network, 5=bad-args
//
// License: MIT
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

import { createHash } from 'node:crypto';
import { createInterface } from 'node:readline';

// โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

const VERSION = '1.0';
const DEFAULT_BASE_URL = 'https://imdifferent.id';
const DEFAULT_RPC_MAINNET = 'https://mainnet.base.org';
const DEFAULT_RPC_TESTNET = 'https://sepolia.base.org';

// keccak256("Anchor(bytes32,uint8,uint64,uint32,bytes16)")
const ANCHOR_TOPIC0 = '0x72571895b4d4c7decf1eb14520c5637e75b1f4280e445d1bc6277f6504d82fec';

const TIER_INT_TO_STRING = {
  0: 'world_id_only',
  1: 'identity_verified',
  2: 'mixed',
  3: 'anonymous',
};

const TIER_STRING_TO_INT = Object.fromEntries(
  Object.entries(TIER_INT_TO_STRING).map(([k, v]) => [v, Number(k)])
);

// โ”€โ”€ Hashing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function sha256Hex(input) {
  return '0x' + createHash('sha256').update(input, 'utf8').digest('hex');
}

function hashLeafString(leafString) {
  return sha256Hex(leafString);
}

function hashInternalNode(left, right) {
  const leftHex = left.startsWith('0x') ? left.slice(2) : left;
  const rightHex = right.startsWith('0x') ? right.slice(2) : right;
  const combined = leftHex + rightHex; // 128 ASCII chars
  return sha256Hex(combined);
}

// โ”€โ”€ Proof walk โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function walkProof(leafHash, siblings, verbose) {
  let current = leafHash;
  for (let i = 0; i < siblings.length; i++) {
    const step = siblings[i];
    const prev = current;
    if (step.side === 'left') {
      current = hashInternalNode(step.siblingHash, current);
    } else {
      current = hashInternalNode(current, step.siblingHash);
    }
    if (verbose) {
      console.log(`    step ${i}: side=${step.side} sibling=${trunc(step.siblingHash)} โ†’ ${trunc(current)}`);
    }
  }
  return current;
}

function verifyProof(leafHash, siblings, expectedRoot, verbose) {
  const computed = walkProof(leafHash, siblings, verbose);
  return { pass: computed === expectedRoot, computed };
}

// โ”€โ”€ On-chain check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

async function checkOnChain(root, tierInt, contractAddress, chainId, rpcUrl) {
  const rpc = rpcUrl || (chainId === 8453 ? DEFAULT_RPC_MAINNET : DEFAULT_RPC_TESTNET);

  // eth_getLogs: filter by topic0 (Anchor event) and topic1 (root)
  const rootBytes32 = root.startsWith('0x') ? root : '0x' + root;
  const tierTopic = '0x' + tierInt.toString(16).padStart(64, '0');

  const body = JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'eth_getLogs',
    params: [{
      address: contractAddress,
      topics: [ANCHOR_TOPIC0, rootBytes32, tierTopic],
      fromBlock: '0x0',
      toBlock: 'latest',
    }],
  });

  const resp = await fetch(rpc, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body,
  });

  if (!resp.ok) {
    throw new Error(`RPC returned HTTP ${resp.status}`);
  }

  const json = await resp.json();
  if (json.error) {
    throw new Error(`RPC error: ${json.error.message || JSON.stringify(json.error)}`);
  }

  const logs = json.result || [];
  if (logs.length === 0) return null;

  return {
    txHash: logs[0].transactionHash,
    blockNumber: parseInt(logs[0].blockNumber, 16),
    logIndex: parseInt(logs[0].logIndex, 16),
  };
}

// โ”€โ”€ Proof validation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function validateProofShape(proof) {
  const required = ['voteId', 'tier', 'leafHash', 'siblings', 'expectedRoot'];
  for (const key of required) {
    if (!(key in proof)) {
      throw new Error(`Missing required field: "${key}"`);
    }
  }
  if (!Array.isArray(proof.siblings)) {
    throw new Error('"siblings" must be an array');
  }
  for (let i = 0; i < proof.siblings.length; i++) {
    const s = proof.siblings[i];
    if (!s.siblingHash || !s.side) {
      throw new Error(`siblings[${i}] missing siblingHash or side`);
    }
    if (s.side !== 'left' && s.side !== 'right') {
      throw new Error(`siblings[${i}].side must be "left" or "right", got "${s.side}"`);
    }
  }
  if (typeof TIER_STRING_TO_INT[proof.tier] !== 'number') {
    throw new Error(`Unknown tier: "${proof.tier}". Expected one of: ${Object.keys(TIER_STRING_TO_INT).join(', ')}`);
  }
}

// โ”€โ”€ CLI helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function trunc(hash) {
  if (!hash || hash.length < 12) return hash || '(none)';
  return hash.slice(0, 6) + '...' + hash.slice(-4);
}

function basescanUrl(chainId, txHash) {
  const prefix = chainId === 8453 ? 'basescan.org' : 'sepolia.basescan.org';
  return `https://${prefix}/tx/${txHash}`;
}

function printUsage() {
  console.log(`
โ•โ•โ• IMDifferent Merkle Verifier v${VERSION} โ•โ•โ•

Usage:
  node verify-merkle.mjs --vote-id <uuid>                    Mode A: online verification
  node verify-merkle.mjs --vote-id <uuid> --rpc-url <url>    Mode C: custom RPC
  node verify-merkle.mjs --paste                              Mode B: offline (JSON from stdin)

Options:
  --vote-id <uuid>    Vote ID to verify
  --paste             Read proof JSON from stdin (offline mode)
  --base-url <url>    Override API base URL (default: ${DEFAULT_BASE_URL})
  --rpc-url <url>     Use custom Base RPC endpoint for on-chain check
  --no-onchain        Skip on-chain verification
  --verbose           Print intermediate hashes
  --help              Show this help message

Exit codes:
  0  All checks pass
  1  Leaf reconstruction failed
  2  Proof walk produced wrong root
  3  Root not found on-chain
  4  Network / API error
  5  Bad CLI arguments

Examples:
  node verify-merkle.mjs --vote-id e42d5ad5-dd5a-4b74-b04b-83828b987a55
  node verify-merkle.mjs --vote-id e42d5ad5-... --base-url https://stg.imdifferent.id
  echo '{"voteId":"...","tier":"mixed",...}' | node verify-merkle.mjs --paste
  node verify-merkle.mjs --vote-id e42d5ad5-... --no-onchain
`);
}

function parseArgs(argv) {
  const args = {
    voteId: null,
    paste: false,
    baseUrl: DEFAULT_BASE_URL,
    rpcUrl: null,
    noOnchain: false,
    verbose: false,
    help: false,
  };

  for (let i = 2; i < argv.length; i++) {
    switch (argv[i]) {
      case '--vote-id':
        args.voteId = argv[++i];
        break;
      case '--paste':
        args.paste = true;
        break;
      case '--base-url':
        args.baseUrl = argv[++i];
        break;
      case '--rpc-url':
        args.rpcUrl = argv[++i];
        break;
      case '--no-onchain':
        args.noOnchain = true;
        break;
      case '--verbose':
        args.verbose = true;
        break;
      case '--help':
      case '-h':
        args.help = true;
        break;
      default:
        console.error(`Unknown argument: ${argv[i]}`);
        process.exit(5);
    }
  }

  return args;
}

function readStdin() {
  return new Promise((resolve, reject) => {
    const rl = createInterface({ input: process.stdin });
    const lines = [];
    rl.on('line', (line) => lines.push(line));
    rl.on('close', () => {
      try {
        resolve(JSON.parse(lines.join('\n')));
      } catch (err) {
        reject(new Error(`Failed to parse stdin JSON: ${err.message}`));
      }
    });
    rl.on('error', reject);
  });
}

// โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

async function main() {
  const args = parseArgs(process.argv);

  if (args.help) {
    printUsage();
    process.exit(0);
  }

  if (!args.voteId && !args.paste) {
    console.error('Error: specify --vote-id <uuid> or --paste');
    printUsage();
    process.exit(5);
  }

  if (args.voteId && args.paste) {
    console.error('Error: --vote-id and --paste are mutually exclusive');
    process.exit(5);
  }

  console.log(`\nโ•โ•โ• IMDifferent Merkle Verifier v${VERSION} โ•โ•โ•\n`);

  // โ”€โ”€ Step 1: Get proof data โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  let proof;

  if (args.paste) {
    process.stdout.write('[1/4] Reading proof from stdin...              ');
    try {
      proof = await readStdin();
      console.log('OK');
    } catch (err) {
      console.log('FAIL');
      console.error(`\nโœ— FAIL (exit 4) โ€” ${err.message}`);
      process.exit(4);
    }
  } else {
    const url = `${args.baseUrl}/api/merkle/proof/${args.voteId}`;
    process.stdout.write(`[1/4] Fetching proof from ${new URL(url).hostname}...    `);
    try {
      const resp = await fetch(url);
      if (!resp.ok) {
        const body = await resp.text().catch(() => '');
        console.log('FAIL');
        console.error(`\nโœ— FAIL (exit 4) โ€” API returned HTTP ${resp.status}: ${body.slice(0, 200)}`);
        process.exit(4);
      }
      proof = await resp.json();
      console.log('OK');
    } catch (err) {
      console.log('FAIL');
      console.error(`\nโœ— FAIL (exit 4) โ€” ${err.message}`);
      process.exit(4);
    }
  }

  // โ”€โ”€ Validate shape โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  try {
    validateProofShape(proof);
  } catch (err) {
    console.error(`\nโœ— FAIL (exit 1) โ€” ${err.message}`);
    process.exit(1);
  }

  const tierInt = TIER_STRING_TO_INT[proof.tier];
  const totalLeaves = proof.siblings.length > 0 ? Math.pow(2, proof.siblings.length) : 1;

  console.log(`Vote ID:        ${proof.voteId}`);
  console.log(`Tier:           ${proof.tier}`);
  if (proof.batchDate) console.log(`Batch Date:     ${proof.batchDate}`);
  if (typeof proof.position === 'number') console.log(`Position:       ${proof.position} / ~${totalLeaves}`);
  console.log('');

  // โ”€โ”€ Step 2: Verify leaf hash โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  process.stdout.write('[2/4] Verifying leaf hash...                   ');
  if (!proof.leafHash || typeof proof.leafHash !== 'string') {
    console.log('FAIL');
    console.error('\nโœ— FAIL (exit 1) โ€” leafHash missing from proof response');
    process.exit(1);
  }
  console.log(`OK (${trunc(proof.leafHash)})`);

  // โ”€โ”€ Step 3: Walk proof to root โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  process.stdout.write('[3/4] Walking proof to root...                 ');
  const result = verifyProof(proof.leafHash, proof.siblings, proof.expectedRoot, args.verbose);

  if (!result.pass) {
    console.log('FAIL');
    console.error(`  Expected: ${proof.expectedRoot}`);
    console.error(`  Computed: ${result.computed}`);
    console.error(`\nโœ— FAIL (exit 2) โ€” proof did not reconstruct the expected root`);
    console.error('  This usually means the proof endpoint returned a stale or');
    console.error('  corrupted response. Re-fetch and retry; if persistent,');
    console.error('  report the issue to admin@imdifferent.id');
    process.exit(2);
  }
  console.log(`OK (${trunc(proof.expectedRoot)})`);

  // โ”€โ”€ Step 4: On-chain check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  if (args.noOnchain || args.paste) {
    const reason = args.paste ? 'paste mode' : '--no-onchain';
    console.log(`[4/4] On-chain check...                        skipped (${reason})`);
    console.log(`\nโœ“ VERIFIED โ€” proof is mathematically valid (on-chain check skipped)`);
    process.exit(0);
  }

  if (!proof.anchor || !proof.anchor.contractAddress || !proof.anchor.chainId) {
    console.log('[4/4] On-chain check...                        skipped (no anchor data)');
    console.log('\nโœ“ VERIFIED โ€” proof is mathematically valid (vote may be pending anchoring)');
    process.exit(0);
  }

  const chainId = proof.anchor.chainId;
  const contractAddress = proof.anchor.contractAddress;
  process.stdout.write(`[4/4] Verifying root on-chain (Base ${chainId})...  `);

  try {
    const onChainResult = await checkOnChain(
      proof.expectedRoot,
      tierInt,
      contractAddress,
      chainId,
      args.rpcUrl,
    );

    if (!onChainResult) {
      console.log('FAIL');
      console.error(`\nโœ— FAIL (exit 3) โ€” root not found on-chain at contract ${trunc(contractAddress)}`);
      console.error('  The Merkle proof is mathematically valid, but no matching');
      console.error('  Anchor event was found on-chain. This could mean:');
      console.error('  - The anchor transaction is still pending');
      console.error('  - The RPC node is behind');
      console.error('  - The contract address is wrong');
      process.exit(3);
    }

    const txHash = onChainResult.txHash;
    console.log(`OK (tx ${trunc(txHash)})`);
    console.log(`\nโœ“ VERIFIED โ€” vote anchored on-chain`);
    console.log(`  Basescan: ${basescanUrl(chainId, txHash)}`);
    process.exit(0);
  } catch (err) {
    console.log('FAIL');
    console.error(`\nโœ— FAIL (exit 4) โ€” on-chain check failed: ${err.message}`);
    process.exit(4);
  }
}

main().catch((err) => {
  console.error('FATAL:', err);
  process.exit(99);
});

ยง6.1 โ€” Obtaining the script + verifying its integrity

The bytes embedded in ยง6 above are the canonical source. To make download and tamper-detection easy, IMDifferent publishes both the script and this document at stable, machine-fetchable URLs, plus a signed manifest binding their SHA-256 fingerprints to the same wallet that anchors Merkle roots on Base.

Public download URLs:

URLReturnsFilename hint
https://imdifferent.id/api/audit/verifier-scriptraw verify-merkle.mjsContent-Disposition: attachment sets the filename
https://imdifferent.id/api/audit/verification-docraw MERKLE_VERIFICATION.mdsame
https://imdifferent.id/api/audit/contract-sourceraw MerkleAnchor.solsame
https://imdifferent.id/api/audit/manifestsigned JSON manifestโ€”

Manifest shape:

{
  "type": "imdifferent-audit-manifest",
  "version": "v1",
  "files": [
    { "name": "MERKLE_VERIFICATION.md", "url": "/api/audit/verification-doc", "sha256": "0xโ€ฆ", "bytes": โ€ฆ },
    { "name": "MerkleAnchor.sol",       "url": "/api/audit/contract-source",  "sha256": "0xโ€ฆ", "bytes": โ€ฆ },
    { "name": "verify-merkle.mjs",      "url": "/api/audit/verifier-script",  "sha256": "0xโ€ฆ", "bytes": โ€ฆ }
  ],
  "signedAt": "2026-05-09T11:00:00.000Z",
  "signerAddress": "0xโ€ฆ",
  "message": "imdifferent.id audit-manifest v1 | <signedAt> | MERKLE_VERIFICATION.md:0xโ€ฆ | MerkleAnchor.sol:0xโ€ฆ | verify-merkle.mjs:0xโ€ฆ",
  "signature": "0xโ€ฆ",
  "verificationRecipe": "ECDSA-recover(message, signature) === signerAddress"
}

The message field is the exact byte sequence that was signed. Files are sorted alphabetically by name before concatenation so the message is deterministic for a given (signedAt, file bytes).

Three-step integrity check:

# 1. Download the artifacts.
curl -O https://imdifferent.id/api/audit/verifier-script
curl -O https://imdifferent.id/api/audit/verification-doc
curl -O https://imdifferent.id/api/audit/contract-source
curl https://imdifferent.id/api/audit/manifest > manifest.json

# 2. Confirm the SHA-256 hashes match what the manifest claims.
shasum -a 256 verify-merkle.mjs MERKLE_VERIFICATION.md MerkleAnchor.sol
cat manifest.json | jq -r '.files[] | "\(.sha256)  \(.name)"'

# 3. Verify the signature with viem (zero-trust in imdifferent.id).
node -e "
  const { verifyMessage } = require('viem');
  const m = require('./manifest.json');
  verifyMessage({
    address: m.signerAddress,
    message: m.message,
    signature: m.signature
  }).then(ok => console.log(ok ? 'PASS โ€” signature valid' : 'FAIL'));
"

Closing the trust loop. Open Basescan and look at the wallet that calls anchorBatch on the MerkleAnchor contract daily. That address must equal manifest.signerAddress. If they match, the bytes you just downloaded came from the same operator that anchors votes โ€” you no longer need to trust the imdifferent.id HTTPS endpoint at all. The script's behaviour is then verifiable on its own merits using the leaf format and tree rules in ยง3 and ยง4.

Why this matters. Without the manifest, an attacker who compromised our web tier could ship a doctored script that quietly accepts forged proofs. With the manifest, that attacker would also have to compromise the anchor wallet's private key โ€” which is the same key whose signing record is observable on-chain across every prior anchor. A successful attack against the verifier is therefore also a public, on-chain visible compromise of the anchor wallet.

ยง7 โ€” Step-by-step verification walkthrough

If you prefer to verify by hand (or want to understand what the script does), follow these steps using only curl and a SHA-256 tool.

Step 1: Fetch the proof

curl -s https://imdifferent.id/api/merkle/proof/YOUR_VOTE_ID | python3 -m json.tool

This returns a JSON object with leafHash, siblings, expectedRoot, and anchor fields.

Step 2: Verify the leaf hash

The API returns a pre-computed leafHash. If you have the raw vote fields (from your receipt), you can independently reconstruct the leaf string using the format from ยง3 and compute SHA-256(leafString).

Step 3: Walk the proof

Starting with current = leafHash, process each sibling in order:

for each step in siblings:
  if step.side == "left":
    current = SHA256(strip0x(step.siblingHash) + strip0x(current))
  else:
    current = SHA256(strip0x(current) + strip0x(step.siblingHash))

After all steps, current should equal expectedRoot.

Step 4: Check on-chain

Query the Base RPC for Anchor events matching the root:

curl -s -X POST https://sepolia.base.org \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0","id":1,"method":"eth_getLogs",
    "params":[{
      "address":"CONTRACT_ADDRESS_FROM_PROOF",
      "topics":["0x72571895b4d4c7decf1eb14520c5637e75b1f4280e445d1bc6277f6504d82fec","EXPECTED_ROOT","TIER_TOPIC"],
      "fromBlock":"0x0","toBlock":"latest"
    }]
  }'

If the result contains at least one log entry, the root is on-chain. The transactionHash field links to the anchor transaction on Basescan.

ยง8 โ€” Optional: enhanced mode (viem-based)

For developers who prefer type-safe contract interaction, you can replace the raw JSON-RPC call in Step 4 with viem:

import { createPublicClient, http, parseAbiItem } from 'viem';
import { baseSepolia } from 'viem/chains';

const client = createPublicClient({ chain: baseSepolia, transport: http() });

const logs = await client.getLogs({
  address: contractAddress,
  event: parseAbiItem('event Anchor(bytes32 indexed root, uint8 indexed tier, uint64 timestamp, uint32 leafCount, bytes16 batchDate)'),
  args: { root: expectedRoot, tier: tierInt },
  fromBlock: 0n,
});

console.log(logs.length > 0 ? 'VERIFIED' : 'NOT FOUND');

This is not required โ€” the default zero-deps script works without viem. This snippet is provided for power users who want richer error handling and ABI decoding.

ยง9 โ€” Operator key-rotation history

The anchoring wallet is an immutable parameter of the MerkleAnchor contract. If the wallet is ever compromised, a new contract is deployed โ€” the old contract's history remains permanently verifiable.

DateOld AddressNew AddressReasonBasescan Link
(no rotations yet โ€” this table is populated when a rotation occurs)

ยง10 โ€” EAS attestation

Note: EAS (Ethereum Attestation Service) attestation is forthcoming at <UID-TBD>. This document will link to the attestation post-4a.8 mainnet cutover. The EAS attestation will cryptographically bind the imdifferent.id domain to the anchoring wallet address, providing a third-party-verifiable proof that the entity controlling the domain also controls the wallet.

ยง11 โ€” FAQ

Q: What if a vote is deleted (GDPR)?

A vote's data row may be erased from our database upon a lawful deletion request. However, the leaf hash remains in the Merkle tree permanently โ€” it is on-chain and cannot be removed. An auditor will see that a leaf was anchored at time T, but the underlying vote data no longer exists. This is an expected audit signal (see ยง2), not a bug or a cover-up.

Q: What if you change the leaf format in the future?

The leaf string format documented in ยง3 is frozen for the current contract. If we ever need to change the format (e.g., adding new fields), we will deploy a new MerkleAnchor contract with a new address. The old contract and its anchored history remain permanently verifiable with the old format. Both formats will be documented.

Q: How do I verify without trusting your domain?

Two orthogonal trust dependencies and how to remove each:

  1. Trusting the verifier script itself. Download it from https://imdifferent.id/api/audit/verifier-script (or copy from ยง6 above), then use the signed manifest at https://imdifferent.id/api/audit/manifest to confirm the bytes were signed by the wallet that anchors votes on Base. See ยง6.1 for the three-step integrity check. After this step, you no longer trust imdifferent.id's HTTPS surface โ€” you trust an ECDSA signature over a hash you computed locally.

  2. Trusting our API for the proof JSON. Use the script's --paste mode: fetch the proof JSON from any vantage point (e.g. via Tor, via a peer, via a cached snapshot), then pipe it into the script. The script performs all verification locally โ€” no network calls. For the on-chain check, use --rpc-url with your own Base RPC endpoint (Alchemy, Infura, or a local node). At no point does the script need to talk to imdifferent.id.

After both steps, the only thing you trust is your local Node runtime, your local SHA-256 implementation, and the public Base blockchain.

Q: What's the gas cost? Who pays?

The operator (IMDifferent) pays all gas costs. Each anchorBatch transaction costs approximately 50,000โ€“80,000 gas. At typical Base gas prices (~0.01 gwei), this is fractions of a cent per day. Voters never pay anything.

Q: I found a discrepancy โ€” what do I do?

Please report it responsibly:

We take discrepancy reports seriously. Every report will be investigated and a public response published within 48 hours.

โ† Back to Anchor browser