Ballot TrailBorn Between 2 Generals

Section 6 · Section 8 · Section 9

The event ledger

Append-only, per-token chained, signed on every write. Nothing below is a recording — the engine is running in this page, and the keys were generated when you loaded it.

Generating an Ed25519 signing key…

What each event carries

Every record is sealed the same way. The hash covers the whole canonical payload including the previous hash for that token, which is what makes a later edit detectable rather than merely discouraged.

// the preimage, in MBLTS-CANONICAL-V1: sorted keys, no whitespace,
// no undefined, timestamps as strings so float formatting never enters a hash
event_hash = SHA-256( canonicalJSON({ …event fields…, previous_event_hash }) )
signature  = Ed25519( event_signing_key, event_hash )

The signature covers the hash rather than the payload directly, so an auditor holding archived public keys can verify a decade-old event without needing the serialization rules to still be in living memory — they only need the hash to recompute, which the canonical form guarantees.

The twelve-step write

Section 6.3 of the specification is an ordered algorithm, and the engine keeps the step numbers in the code so the two can be read side by side. In order: authenticate the caller, validate the schema and idempotency key, resolve the token inside the trusted boundary by digest, reject unknown or terminal tokens, read the current state and validate the transition, authorise the role and any dual-control requirement, canonicalise, chain, sign, persist atomically, queue for anchoring, and alert.

A refusal is evidence. When any of those steps fails, the write does not simply return an error — the ledger appends a signed EVENT_REJECTED or DUPLICATE_OR_REPLAY_DETECTED record. A system that logs only its successes cannot show you an attack.

Two clocks, and why both are kept

Every event carries event_time — when the device says it happened — and recorded_time, stamped by the server, independent of any client. The log is ordered by the server's clock, and global_sequence counts recorded order.

An offline scanner that syncs six hours later is legitimate and common, so a stale timestamp is not an error. But a device claiming an earlier hour than the server observed is also exactly how somebody would try to slip an event in before a deadline. So the engine does neither of the two tempting things:

It does not silently correct the supplied time — that destroys the evidence that the claim was wrong. And it does not silently trust it — that lets a device reorder history. The supplied time is kept verbatim, the ordering follows the server, and the event is flagged BACKDATED or FUTURE_DATED with the measured skew for review.

A backdated event is not rejected. It is visible, which is the useful property — a rejected event tells an attacker exactly where the boundary is.

Idempotency, and why it is not just a retry convenience

A scanner on a bad network sends the same receipt twice. Without idempotency you get two RECEIVED events and a reconciliation that no longer balances. With it, the second call returns the first result and writes nothing. But the same key presented with a different payload is a 409 IDEMPOTENCY_CONFLICT — because that is no longer a retry, it is a rewrite wearing a retry's clothes.

Paper remains authoritativeThe token never touches a voteAppend, never overwrite