FR EN

The access log

The access log is your company's tamper-evident register. Every check decision and every consent lifecycle event is recorded there, hash-chained, and folded periodically into an epoch whose root is anchored on-chain. It is the piece that answers "can you demonstrate what you did, and prove the register was not touched up afterwards". It equips the accountability obligation (P-39.1 art. 3.1; for a public body, A-2.1 art. 52.2).

A verifiable register, not proof of compliance

The log demonstrates that the recorded sequence of events is intact and complete up to the last anchored epoch. It does not certify that your practices are compliant, and it says nothing about what your other systems did with the information. Agreely documents and demonstrates; it does not certify.

What is recorded

One entry is written for each of these events, on your own subjects:

Event What it records
check A check decision, allow or deny, with its status.
grant A consent granted.
revoke A consent withdrawn.
erase An erasure (crypto-shred).
refuse An offer refused by the person.
view A read of a stored personal-information value (for example decrypting a customer's email for one of the permitted surfaces). It carries the reference, never the value.

The subject is identified by your own customer reference, never by a citizen DID: the log creates no linkable tie to the citizen layer. See unlinkability.

The write is off the hot path. The synchronous check drops the event in an inbox and answers; a single-writer worker per company (a Postgres advisory lock) then drains the inbox into the chain. A logging failure never slows or breaks a check.

The hash chain

Every row commits to the one before it:

row_hash = keccak256( prev_hash || canonicalJson(payload) )
prev_hash of the first record = 32 zero bytes

payload = {
  "chain_seq":   1024,
  "event_type":  "check",
  "decision":    "deny",
  "status":      "revoked",
  "occurred_at": "2026-06-26T15:04:05Z",
  "consentRef":  "0x…" | null,
  "commitment":  "0x…"
}

canonicalJson is RFC 8785 canonical serialization: keys sorted, no whitespace, / unescaped, UTF-8 unescaped. Note that chain_seq travels here as a JSON number; the "no numbers" rule described in canonicalization and hashing applies to the consent claim, not to this payload.

chain_seq is contiguous with no gap: a deletion in the middle of the chain leaves a gap, and a deletion at the tip breaks the seam of the anchored epoch that covered it.

The chain hashes the commitment, never the readable text

The commitment is the same keccak256( JCS(claim) || salt ) as the rest of the protocol. The category, the purpose and the customer reference are bound into history only through it. That is what lets a crypto-shred null the readable columns and the salt while leaving the row_hash, the epoch root and the on-chain anchor fully verifiable. An erasure never punches a hole in the register.

Epochs and anchoring

Periodically (on the first of two thresholds, an entry count or an elapsed delay), the not-yet-anchored rows are folded into an epoch:

  1. a Merkle tree is built over the range's row_hash values, in chain_seq order;
  2. the root and the head_hash (the row_hash at seq_to) are stored;
  3. the root, one opaque bytes32, is queued for on-chain anchoring (anchorAuditRoot, event AuditRootAnchored).

The on-chain payload carries no cleartext company identifier, no consentRef, no personal information, no DID. See the on-chain registry.

Each epoch root is additionally signed at seal time with your company's registered DID key, and that key can be authorized by your own root passkey committed in your DNS. It is an additive layer, and it never gates integrity: see epoch signing and delegation.

The Merkle convention is part of the recipe

The tree is OpenZeppelin's SimpleMerkleTree, and its exact shape is published inside the export itself, because an external auditor once implemented the obvious reading (pair adjacent, keccak(a||b), duplicate the odd tail) and got a different root:

  • Leaf: the row_hash as-is, never re-hashed.
  • Pair: keccak256(a || b) where a and b are the two 32-byte children, ordered ascending as byte strings.
  • Layout: the OpenZeppelin 2N-1 node array, the N leaves written at the end of the array in reverse order (leaf i at index 2N-2-i), node i = the pairing of nodes 2i+1 and 2i+2, root at index 0. An odd node is promoted, never duplicated.
  • N = 1 is not special-cased: the root is the single leaf.

This layout is not equivalent to level-by-level pairing with promotion: past four leaves, the two produce different roots.

Verification, and its honest boundary

The log screen shows a verification state. Two paths exist.

The fast path, run on every page render, costs O(open tail + number of epochs), not O(all rows). It proves the contiguity of chain_seq, the integrity of the open tail and its link onto the last anchored head_hash, the integrity of each anchored epoch seam, and a truncation guard.

The deep path, on demand, replays the whole chain: every row_hash is recomputed and compared, and for every non-erased row the commitment is recomputed from the claim and the decrypted salt. It is that content check that catches a rewrite of the readable columns, which would otherwise leave the chain replaying cleanly.

The open tail is not chain-proven

Rows past the last anchored seq_to are only as trustworthy as the live database, until their root lands on-chain. This is declared as such (openTail) on the screen and in the export, and it is never presented as chain-proven.

The export

The register exports as JSON or CSV, in the agreely.access-log.v1 format. The export is self-sufficient: beyond the entries, it carries everything needed to re-verify it offline, without Agreely and without reading our code.

  • the row_hash recipe and the genesis;
  • the full Merkle convention, spelled out;
  • the epochs with their seq_from, seq_to, root, head_hash and anchoring transaction;
  • the AgreelyRegistry contract address, the chainId and the chain name, because a transaction carrying a root proves only that someone wrote that bytes32 somewhere; without the contract and the chain, an auditor cannot confirm the registry is ours. The chain name is also what would disclose an anchor on a test network as such;
  • the epoch signature, its public key and its algorithm, when present;
  • the verification state (chainOk, contentOk, epochsOk, anchoredOk, openTail) and the caveats.

An export filtered to one customer is a projection: each entry still carries its own prev_hash and row_hash, but a contiguous chain replay needs the unfiltered company export. The scope is declared in the artifact so a detached slice is never mistaken for the whole register.

Next