Ballot TrailBorn Between 2 Generals

Section 7 · Section 8

Audit anchoring

A per-token hash chain protects one package's sequence. It does nothing about an event quietly deleted from the middle of the whole log. Batch anchoring is what closes that, and it is not a blockchain.

What anchoring is

Every hour of active operations — or every ten thousand events, whichever comes first — the log closes a batch, builds a Merkle tree over the event hashes, signs the root together with the batch metadata, and writes that signed anchor to two independently controlled durable stores.

An auditor can then be handed one event and a short proof path, and check for themselves that it was in the batch, without being shown any of the other events in it.

What anchoring is not

It is not a public ledger. No raw token, no postal reference, no voter-linked metadata and no event detail is published anywhere. Publishing even a bare root can only be considered after legal and privacy review, and never in a form that lets anyone enumerate voter status.

The specification is explicit: a public blockchain must not be the lifecycle record.

Run it

Build a batch, take a proof, break it

This builds a real tree over real event hashes in your browser, using the domain-separated construction below.

The construction

MBLTS-MERKLE-V1
leaf     = SHA-256( 0x00 || event_hash )
internal = SHA-256( 0x01 || left || right )
odd count at any level: the final node is duplicated
an empty batch has no root and MUST NOT be anchored

The 0x00 and 0x01 prefixes are domain separation, and they are not decoration. Without them a leaf and an internal node are the same shape, and an attacker can present an internal node as though it were a leaf — claiming inclusion for something that was never an event. The prefixes make the two structurally impossible to confuse.

The duplication rule has a known ambiguity, and this build does not paper over it.

Duplicating the final node means a batch of [A,B,C] and a batch of [A,B,C,C] compute the same root. So a root, on its own, does not pin how many events were in the batch. A verifier that checks only the root is accepting a weaker claim than it thinks it is.

The button above demonstrates the collision on real hashes. It is a genuine property of the rule the specification names, not a flaw introduced here.

What closes it: sign the metadata, not the root alone. The batch record already carries event_count and the first/last global sequence numbers. This engine puts all of it — root included — through canonical serialization and signs that. Two batches with different counts therefore produce different signature preimages even when their roots collide, and verifyBatch() reports COUNT_MISMATCH outright.

Bind the count, or version the padding rule to something unambiguous. Do not ship the root by itself and call it settled.

The verifier, in full

export async function verifyInclusion(eventHash, proof, expectedRoot) {
  let node = await merkleLeaf(eventHash);          // SHA-256(0x00 || h)
  for (const step of proof) {
    const sib = unhex(step.hash);
    node = step.position === 'LEFT'
      ? await merkleNode(sib, node)                // SHA-256(0x01 || sib || node)
      : await merkleNode(node, sib);
  }
  return timingSafeEqualHex(hex(node), expectedRoot);
}

That is the shipped function, copied out of /assets/mblts.mjs — the same bytes your browser just executed. On the verification page you can run it against deliberately corrupted input and watch it refuse.

Where the anchors live

One copy in a write-once, retention-locked repository the election authority controls. One copy in a repository it does not control — an auditor, an oversight body, or an escrow arrangement. A tamper-evidence scheme where the same party holds every copy of the evidence is a filing system with extra steps.

Paper remains authoritativeThe token never touches a voteAppend, never overwrite