Appendix: Specification

Modified

October 14, 2026

This appendix holds the byte-level rules that 4  How EventChain Works relies on.

Entry Specification

Lines and rejection. Each line ends in 0x0a, and a file that contains 0x0d anywhere counts as malformed. A verifier that finds a malformed line under any rule in this appendix rejects the whole AOF and reports the line.

Field encodings. Every member of the entry object is a JSON string. Binary members use base64url without padding (Josefsson 2006, sec. 5):

entry_id and payload_ref are opaque strings with no encoding requirement.

Event ID. The Event ID (Append-Only File) excludes the row’s terminating 0x0a. After the append, the Hub returns the new position and head hash and the number of lines written (receipt); for a one-line submission the head hash is that line’s Event ID. The Hub may record Event IDs in a separate lookup index. An application keeps evidence(row key, Event ID, salt) beside its existing data. The Hub answers lookups, and a holder checks each answer against the Event ID and its own copy of the chain.

entry.data(). The signed message entry.data() is the JSON Canonicalization Scheme (Rundgren et al. 2020) serialisation of the entry object without its signature member. The removal applies to exactly the member named signature; the signature covers every other member the declared revision permits. Because RFC 8785 orders members by name, producers may write members in any order on the line. An unknown member or an unsupported revision makes the entry malformed.

Chain bytes. prev_hash is the SHA-256 digest of the previous line exactly as written: every byte after the preceding line terminator (or the start of the file), up to and excluding the previous line’s own 0x0a. A verifier applies no canonicalisation, so reformatting a line into a form a JSON parser would read as equivalent changes its hash and breaks the next entry’s predecessor check. The final line has no successor; a retained head or a heartbeat at or beyond it binds its exact bytes.

Figure 1 separates the two byte paths; the rules above define them.

One stored JSON line branches into an exact-byte hash path excluding LF, producing Event ID and next prev_hash, and a validated-object path removing signature before JCS to produce the signed message. Malformed objects are rejected before canonicalisation.
Figure 1: The line hash uses exact written bytes before LF. The signed message uses the validated object without signature, serialised with JCS. These inputs are not interchangeable.

Genesis. The first entry carries a prev_hash of 32 zero bytes, encoded like any other hash, and a verifier rejects an AOF whose first entry carries any other value.

Duplicate members. An entry in which any member name appears more than once counts as malformed. Parsers resolve duplicate names differently, so relying on a parser’s default would let one line verify two ways.

Unicode. An escape that denotes an unpaired UTF-16 surrogate in a member name or value counts as malformed; a verifier never substitutes a replacement character.

Revisions and Mint entries. The string member v declares the revision. Without it, an entry has the six base members; "1" also admits the Mint members in Table 1, which record a person’s confirmation of an earlier entry by passkey (What a Signature Proves). A verifier compares revision labels as strings, never as numbers. A revision-1 entry without Mint members is an ordinary producer-signed entry.

Table 1: Mint envelope for revision 1
Mint member Value or encoding
kind "mint"
target_hash Target entry’s Event ID: unpadded base64url of its 32-byte line hash
attester_key Unpadded base64url of the 33-byte SEC1 compressed credential public key
assertion_sig Unpadded base64url of the 64-byte WebAuthn signature, raw r ‖ s
authenticator_data Exact authenticator-data bytes, unpadded base64url
client_data_json Exact client-data JSON bytes, unpadded base64url

A Mint entry declares v = "1" and carries all six Mint members beside the base members. Partial or misplaced Mint members, or an unknown kind, make the entry malformed. The target must be an earlier verified entry in the same chain.

WebAuthn Assertion Checks

For a Mint entry, the verifier checks three things, following the WebAuthn assertion format (Hodges et al. 2021). client_data_json must be valid UTF-8 JSON of type webauthn.get whose challenge equals the encoded target_hash. authenticator_data must hold at least 37 bytes, with the user-presence flag set and consistent backup-state flags. assertion_sig must verify under attester_key over authenticator_data ‖ SHA-256(client_data_json). These checks leave the relying party’s origin and RP ID policy, user verification and signature counter to the application’s accepted authentication context.

Heartbeat Encoding and Retrieval

Signed format. The Hub takes position and head_hash in Table 2 from the same chain state.

Table 2: Heartbeat fields
Member Meaning
v, kind Literal values "1" and "heartbeat"
topic_id Exact topic identifier from the agreed settings
position Number of entries in the committed prefix, at least one
head_hash SHA-256 of the exact final row in that prefix, excluding its terminating 0x0a
issued_at Hub-asserted issuance time, Unix seconds with an optional fractional part
signature Hub signature over the statement fields above

The encoding is a UTF-8 JSON object with string values. The Hub signs the JCS serialisation of the other members and then adds signature, using ECDSA-P256 and the binary encodings of the entry specification. A verifier rejects duplicate or unknown members, unknown revisions, unpaired surrogates and malformed values. position uses ASCII decimal digits without leading zeros. issued_at uses the same form, optionally followed by a decimal point and fractional digits without trailing zeros. The verifier takes the accepted Hub key and interval from the agreed publication settings.

Retrieval. The Hub serves these endpoints for hosts to pull, and each host serves them for holders. Relative to the server’s base URL for the topic, GET heartbeat/latest returns the latest heartbeat as one JSON object and GET heartbeat returns the full list as an array. GET heartbeat?from={timestamp}&until={timestamp} returns the heartbeats with from <= issued_at < until, and an omitted bound leaves that end open. If conflicting signed heartbeats arise, the host keeps both.

Submission, Receipt and Refusal

Submission. A submission is one or more lines, each the exact bytes of an entry, joined by 0x0a with no terminator after the last. The first line’s prev_hash is the expected head, and no other field carries it; each later line’s prev_hash names the hash of the line before it in the submission. A line is a JSON object and cannot contain a raw newline, so the separator is unambiguous; a body that ends in 0x0a has an empty last line and counts as malformed. The Hub appends the body and one 0x0a, whole or not at all. The submission hash is the SHA-256 digest of the body, so for a one-line submission it is the line’s Event ID.

Receipt and refusal. A receipt or refusal has the shape of a heartbeat, so a verifier that checks one checks all three: a flat JSON object of string members, signed under the Hub key over the JCS serialisation of the other members, with the encodings of the entry specification and the rejection rules of the heartbeat. Table 3 lists the members.

Table 3: Receipt and refusal fields
Member Receipt Refusal
v, kind "1", "receipt" "1", "refusal"
topic_id Exact topic identifier Exact topic identifier
submission_hash Hash of the submitted body Hash of the submitted body
issued_at Hub-asserted time, as in a heartbeat Hub-asserted time, as in a heartbeat
position, head_hash Chain state after the append; head_hash is the last appended line’s Event ID and binds every line before it Present on head_mismatch: the current state, so the submitter knows what extension to fetch
count Number of lines appended Absent
reason Absent One value from Table 4
line Absent On a verifier fault, the ordinal of the refused line within the body
signature Hub signature over the members above Hub signature over the members above

Table 4 lists the reasons a refusal can carry. The first five are the verifier’s own fault names, so a refusal and a verifier verdict on the same line agree; the last is the Hub’s.

Table 4: Refusal reasons
Reason Meaning
malformed The line is not an entry under the entry specification
chain_mismatch The line’s prev_hash is not the hash of the line before it in the submission
key_off_curve The public_key is 33 well-formed bytes that name no point on P-256
signature_invalid The signature is not this key’s over the line’s signed message
attestation_fault A Mint envelope failed one of the WebAuthn checks
head_mismatch The first line’s prev_hash is not the current head; another submission arrived first

A receipt is the Hub’s signed commitment that the submission is in the history at that position, so a history that later omits it contradicts the Hub’s own signature, and the producer who retained the receipt holds the proof. The Hub never retries a refused submission and never reuses a signature over an old head: the submitter fetches the extension, verifies it and signs again.

CBC and Correction References

Offer and acceptance. The offer is an ordinary entry whose payload names the subject of the agreement, the parties, the conditions and the time of the offer. The acceptance carries subject_hash, the offer’s Event ID, together with its acknowledgement of the terms. The reference binds the acceptance to that exact offer wherever the acceptance sits in the chain. When the parties require the acceptance to follow the offer immediately, its prev_hash also equals the offer’s Event ID. Whether subject_hash becomes an entry member or a payload field remains open, and it belongs to neither the base nor the Mint members above. Either way, the signature must cover it, directly or through the payload hash.

Corrections

A statement that addresses an earlier entry refers to it through subject_hash, exactly as an acceptance does, while prev_hash still names the immediately preceding line. The payload says which kind of statement the entry is and carries its substance. When the accepting party disputes an offer, it withholds acceptance and records a signed objection that refers to the offer, so the CBC stays pending and the objection says why; a resolution is a further signed entry that refers to the objection. When an author has lost its key and enrolled a new one (What a Signature Proves), application policy decides whether a correction signed with the new key counts as that author’s.

Key Registries

A registry (What a Signature Proves) is a topic whose entries are binding events and whose keeper accepts each binding. Each is an ordinary entry under the entry specification with no additional member. Four kinds of binding event exist, and the payload says which kind an entry is:

  • Register. The subject key signs the offer, so the entry’s public_key is the key the binding covers. A person’s passkey cannot sign an entry, so for a person the producing system signs the offer and the person confirms it in a Mint entry (WebAuthn checks), and the binding covers attester_key. The payload carries the subject type (person, device, service or registry root), an identity reference, a role and the hash of the enrolment evidence. The keeper signs the acceptance with subject_hash naming the offer (CBC references). The binding stands from the acceptance’s position; before that it is pending, and a refusal is a signed objection (corrections).
  • Rotate. The new key signs the offer, whose payload names the binding it replaces by Event ID, and the keeper accepts. A registry’s root key rotates the same way: the new root signs the offer and the current root accepts, and holders follow the new root from that position.
  • Retire. The keeper signs one entry that names the binding by Event ID. Signatures the key made before that position stand.
  • Compromise. The keeper signs one entry that names the binding and the time from which the key is suspect, which may precede the entry. The verifier reports each entry the key signed at or after that time and before the claim’s position as inside a claimed compromise window and leaves the decision to the application.

Under the payload rule, the binding payload travels with its salt, so a holder resolves keys offline; the enrolment evidence, including any personal details, stays with the keeper under its access policy. Each topic declares the registries it accepts, by registry topic and root key, directly or through a binding in a registry it already accepts, and a change to that declaration is a CBC that the topic’s other parties countersign. When two accepted registries bind one key differently, the verifier reports conflicting, keeps both bindings and leaves the decision to the application.

Three conventions remain open, each with the constraint an answer must meet:

  • Declaration encoding. How the first entry carries the accepted registries and the scope of each, and how a holder authenticates which topic a history belongs to through that first entry. Constraint: every holder can read it without the payload access policy.
  • Entry-to-binding reference. Whether a verifier resolves a key by lookup in the accepted registries at verification time, as this appendix assumes, or an entry carries a reference to its binding. Constraint: a carried reference needs a new entry revision, because an unknown member makes an entry malformed.
  • Attribution outcomes. The states the verifier reports for a key, modelled on the heartbeat outcomes: bound, pending, unbound, conflicting, retired before signing and inside a claimed compromise window are the candidates. Constraint: signature validity, binding evidence, compromise status and authorisation stay separate.

The foundation stewards these conventions (Protocol Stewardship). The questions it settles first:

  1. Who may keep a registry that others accept, and what enrolment evidence each must demand.
  2. How a keeper recovers when it loses its root key or a thief rotates the root first: a new first entry, and each accepting topic’s countersigned change to its declaration.
  3. Whether any rule beyond reporting decides between two accepted registries that disagree.
  4. How far back a compromise claim may reach, and who must countersign one.
  5. How long a keeper retains enrolment evidence, and how it deletes it.

Payload Access and Export

The application attaches the access policy to the payload, never to payload_ref. An authorised holder can export public payloads with their salts, keeping the exact bytes the hash covers and linking each to its payload_ref or Event ID, so others can verify them offline. Salt length and encoding, including the boundary between salt and payload, remain open in the specification. A public key or a payload hash can still be linkable to a person, so neither is automatically anonymous under GDPR.

Verifier Outcomes for Heartbeat Evidence

The verifier reports heartbeat evidence as missing when no authenticated heartbeat for the topic is retrievable, stale when the latest one falls outside the application’s freshness window, or conflicting when authenticated heartbeats or the supplied history disagree at a covered position. For that topic, the application pauses the actions that need verified shared evidence, keeps capturing and queuing new events locally, and escalates. A conflict keeps both heartbeats for reconciliation.