Verification¶
Verification is the operation the whole design exists to make cheap and total. Call it on an open ledger —
result = ledger.verify()
— or on a file by path, with no ledger object needed:
result = axiomere.verify_file("study.axmr")
Both return the same result object, with the same eight fields on every surface (this package, the file check, the core's command-line tool):
Verification(ok=True, events=3, break_seq=None, reason=None,
signatures_checked=3, signatures_failed=0,
unsigned_events=(), untrusted_actors=())
| Field | Meaning |
|---|---|
ok |
Did the ledger pass every check? |
events |
How many events were read. |
break_seq |
On failure, the sequence number where verification stopped. |
reason |
On failure, a short name for what failed (for example hash-mismatch). |
signatures_checked |
How many events carried a signature that was checked. |
signatures_failed |
How many of those signatures failed their check. |
unsigned_events |
The sequence numbers of events that carry no signature. Reported in every mode, pass or fail — see below. |
untrusted_actors |
Actors a trusted-mode check refused to vouch for — see below. |
What is checked¶
Verification re-walks the file and checks:
- The magic and the exact bytes — line 1 must be
AXMR1, and every event line must be byte-for-byte the canonical encoding of the event it parses to. A line that merely parses to the right event — respelled numbers, reordered keys, an extra field, a duplicated key, Windows line endings — fails withnon-canonical-line. The file a person inspects and the file the verifier checked are the same bytes. - The hash chain — every event's
hashrecomputed asSHA-256(prev_hash ‖ canonical(event body)), and everyprev_hashmatched against its predecessor. Alteration, insertion, deletion, or reordering of events breaks verification at the first affectedseq, with a named reason. - Checkpoint roots, recomputed — a checkpoint claims a Merkle
root over the events it closes. Verification never takes the
claim's word for it: it recomputes the root from the actual event
hashes and fails with
checkpoint-root-mismatchif they differ. (Before this check existed, a forged history could carry the real root copied in from the honest ledger and pass — including against the externally anchored value.) - Snapshot lineage and withdrawal rules — every snapshot's id
is recomputed from its content and parent, parents must be real
earlier snapshots of the same record, a
deadsnapshot must carry a non-empty reason and a score of exactly 0.0, and no live snapshot may follow a dead one for the same record. - Signature presence — next section.
- Signatures — each signed event's Ed25519 signature, checked according to the mode below.
Signature presence: missing is reported, and often fatal¶
A signature that is absent cannot fail a signature check — so presence is checked separately, and the findings are always on the face of the result:
unsigned_eventslists every event that carries no signature, in every mode, whether verification passes or fails. An honestly unsigned ledger verifies in bare mode withsignatures_checked0 and every sequence number listed — a visible state, never a silent one.- If a ledger carries signatures at all, then every event must carry
one. Stripping the signature from even one event of a signed
ledger fails verification with
unsigned-event— the downgrade attack (strip the signatures, keep the content) no longer works. - In trusted mode (below), an unsigned event fails regardless.
Two modes: bare and trusted¶
verify() and verify_file() both accept a trusted argument,
and it selects what signatures are checked against:
trusted=None(the default) — bare mode. Signatures are checked against the public keys embedded in the events themselves. This establishes self-consistency: the file is exactly the history its chain commits to, and each signed event was signed by the holder of the key it carries. It does not establish who anybody is — a key proves itself.trusted={"alice": public_key_hex, ...}— trusted mode. This is the identity check. Every event's actor must appear in the map, and every event must be signed with that actor's mapped key. An event by an actor the map does not name fails verification withuntrusted-actor, and the actor is reported inuntrusted_actors; there is no fallback to trusting the key embedded in the event. A mapped actor signing with the wrong key fails withsigner-key-mismatch.trusted={}is a real choice, not a missing argument. An empty map trusts nobody, so every event fails as untrusted.Noneand{}mean different things, and the difference is deliberate: onlyNoneasks for self-consistency alone.
seed, public = axiomere.generate_keypair()
ledger = axiomere.Database.create("study.axmr", seed_hex=seed)
# … appends, signed by the key above …
result = ledger.verify(trusted={"researcher": public})
Pinning the head: the honest limit of bare verification¶
A chain proves its own consistency — which means a shortened copy of a ledger is also a consistent chain. If events are dropped from the end of a file, what remains is a prefix of a valid history, and bare verification passes it. No amount of re-walking the file can change that; the file cannot know it used to be longer.
The detection is a pin. Pass expected_head — a dict with any of
head_hash (the last event's hash), min_events (a minimum event
count), and checkpoint_root (the latest checkpoint's Merkle root,
compared against the root verification recomputes, never the root
the file merely claims):
result = axiomere.verify_file(
"study.axmr",
expected_head={"head_hash": "…", "min_events": 42,
"checkpoint_root": "…"},
)
A shortfall fails with shortfall, a wrong final hash with
head-mismatch, a wrong checkpoint root with root-mismatch. The
values to pin come from outside the file — your own records, or an
anchored checkpoint root (below). A verification routine that checks
ledgers it cares about should pin the head, or compare the
recomputed checkpoint root against an anchored one; a bare ok on
its own does not rule out truncation.
Malformed files are verdicts, not exceptions¶
verify_file never raises because a file is malformed or unreadable:
it returns ok=False with a named reason, so a checking routine
can treat a corrupt file and a failed verification the same way —
as results. Opening a ledger for appends is a different act and
keeps the stricter behaviour: a ledger that fails verification
refuses to open, with the failure as the exception.
What you write must be exactly storable¶
Appending is the mirror image of the same exactness. A payload value
that cannot be stored exactly — a non-finite float (NaN, positive
or negative infinity) or an integer outside the exactly
representable range — raises ValueError at append time and appends
nothing; the file is byte-identical afterwards. (Earlier versions of
the bindings silently stored NaN as null and rounded huge integers
through floating point; that is gone.) Integers up to
2**64 - 1 and down to -2**63 are stored and read back exactly,
and finite floats round-trip bit-for-bit.
What verification does and does not establish¶
- Verification establishes that this file is exactly the history its chain commits to — nothing was quietly altered after the fact, no signature is missing where signatures are the norm, and every checkpoint root is the root of the events actually present.
- Signatures checked against embedded keys establish self-consistency. Checking them against a caller-supplied trusted-key map is what establishes identity.
- Verification alone does not protect against a sole-copy operator
recomputing a chain from the very first event (from genesis). That
is what layer 5 — external anchoring of Merkle checkpoint
roots — is for: deposit the root where the operator does not
control it (a DOI-minting repository, a timestamp authority, a
transparency log), and tampering becomes evident to anyone.
Anchoring only protects a reader who compares the anchored root
against the root verification recomputes from the file in hand
(or pins it via
expected_head) — never against a root read out of the file itself, which a forger can copy as easily as the honest writer wrote it. The core produces the roots and receipt data; anchor transport belongs to the embedding system, and a deployment that skips anchoring must say so.