Skip to content

Instantly share code, notes, and snippets.

@ChristopherA
Last active August 22, 2026 18:05
Show Gist options
  • Select an option

  • Save ChristopherA/016ced1645da8bfdb576e5b8492c4c38 to your computer and use it in GitHub Desktop.

Select an option

Save ChristopherA/016ced1645da8bfdb576e5b8492c4c38 to your computer and use it in GitHub Desktop.
A redactable append-only log: an audit record separate from live state, with a digest chain, wrap-then-sign entries, and elision that preserves the digest so redacted entries still verify. A stack-agnostic extraction over Gordian Envelope and deterministic CBOR.

A Redactable Append-Only Log

A design for an append-only event log whose entries can be redacted, and disclosed to different readers at different depths, without breaking verification. It is described so that it can be implemented in any language on any platform.

The design rests on four practices: the log is a truth separate from the application's live state rather than the source that state is rebuilt from, redaction preserves the chain instead of breaking it, one committed entry serves every audience at a different depth, and the format's version moves when meaning moves rather than when fields are added.

Scope

The encoding beneath this design is neither new nor specific to it. Entries are Gordian Envelope structures serialized as deterministic CBOR, both published specifications with a reference implementation that this system's output is checked against. What is specified here is the profile written over that stack, together with the practices that came out of operating it.

Storage, rotation, and retention are out of scope. So is behaviour at scale: the claims here are correctness claims about mechanisms.

Provenance and evidence

This design is extracted from eOS-ChatTheatre, a public streaming server whose event log is its audit and content record.

Section 5 classifies every claim by the evidence that carries it: a passing test vector, an acceptance gate that verifies the running server's own bytes with an independent decoder, or neither. The third class is short and it is not empty. The most significant item in it is the signing posture, which is stated here rather than deferred: entries are appended unsigned, and the signature slot is proven by vectors rather than by production traffic.


Part I — Practices

Each practice states what to do, the failure it prevents, and what adopting it costs. The failure is the portable part, because implementations differ and failure modes recur.

BP-1: Keep the log as the audit truth, separate from the state it describes

Practice. Live state is authoritative for what is true now. The log is authoritative for what happened. Neither is derived from the other, and reads divide accordingly: present state answers questions about now, and the log answers questions about the past.

Rationale. The failure has two versions, and the second resembles good architecture.

The first version is having no log. Current state is the only record, so what happened must be inferred from what is true now. History is only as good as the last write, the question of who changed something and when requires an audit table that the same code can rewrite, and a bug that corrupts state corrupts the history of that state in the same instant, because they are the same bytes.

The second version makes the log the source of truth that state is replayed from. Once the present is a function of the log, every entry is load-bearing for correctness and removing one changes what replay produces. Redaction becomes a correctness question rather than a policy question, which produces tombstones, compensating entries, and eventually a rule that nothing is ever really removed. A system that promised an audit trail has then committed to permanent retention of everything anyone put into it.

Cost. Two records exist that can disagree, so the design requires a single writer path that appends as it mutates, and a discipline about which record answers a given question.

Consequence. The log's content is not load-bearing for correctness, which is the property that makes BP-2 affordable. Content can be destroyed permanently without anything downstream becoming incorrect.

Limit. This practice does not by itself make a log tamper-proof, and that is the most commonly overclaimed property in this area. A hash chain held end to end by one party is tamper-evident, and only to a reader who already holds a digest from earlier. A server that rewrites its own log from the beginning produces a chain that is perfectly self-consistent. Two mechanisms address this: a signature over an entry makes it attributable independently of the server, and a digest published outside the server, in a permalink a reader retains or an attestation a third party holds, makes a rewrite detectable. An implementation should be explicit about which of the two it has enabled.

BP-2: Build entries as digest trees so redaction preserves verification

Practice. An entry is a tree of digests rather than an opaque record. The content, each field, and each field's value are separately addressable by their own hash, and the entry's identity is the root of that tree, computed from its parts. Redaction replaces an element with its own digest.

Rationale. A log built as a hash chain over opaque serialized records has one way to remove content, which is to delete or overwrite the record. That changes its digest, breaks the chain from that point forward, and voids any signature over it. Systems facing this either retain everything permanently, and so cannot honour a removal request or keep restricted content out of an export, or they accept that redaction voids verification, after which the chain is ornamental because nobody verifies anything.

Replacing an element with its digest leaves the tree's shape intact. The root is therefore unchanged, the entry's identity is unchanged, and both its chain position and any signature over it survive. A verifier who never held the removed content, and never will, can still check that entry and every entry after it.

Consequence. Whether content can be deleted stops being a question about whether the audit trail survives.

BP-3: Commit one entry and project it per reader

Practice. Commit a single entry and produce per-reader views at read time. A reader without clearance for an element receives the same entry with that element elided: same identity, same chain position, and explicitly marked as withheld.

Rationale. Storing one copy per audience means two records that can drift apart, two paths where an access check can be wrong, and no way to demonstrate that the two describe the same event.

The subtler failure affects readers who may not see something. If a restricted entry is omitted from their history, the omission is itself a disclosure: either they observe a gap they cannot account for, or they observe nothing and are confidently wrong about what happened. Absence and withholding are different facts and should not render identically. Because projection preserves identity, two readers at different depths can compare notes and establish that they hold the same event.

Consequence. How deep to elide, for whom, and for which kind of event is a policy choice that lives outside the format. Changing what is withheld from a class of reader changes no committed byte and requires no format change. This matters because disclosure policy is the part of such a system most likely to be revised once real people use it.

Distinguish two acts. A projection is a read: temporary, per audience, and the full entry remains. A redaction is a write: permanent, applied to the stored entry, and the content is destroyed for everyone including the operator who ran it. Building both on elision means destructive removal is not a separate mechanism that must be got right independently, and an implementation can make it irreversible by construction.

BP-4: Move the version when meaning moves

Practice. Within a version, a reader accepts fields and vocabulary values it does not know, verifies such entries normally, renders them generically, and fails closed by keeping any entry it cannot classify out of disclosure-sensitive views. An explicit list bounds what that tolerance covers.

Rationale. The failure runs in both directions and most systems choose one.

Versioning on every addition means an optional field that no existing reader needed invalidates deployed readers for logs they could have read. The predictable adaptation is that people stop adding fields and begin overloading existing ones, which is the same change without the announcement.

Never versioning means that eventually somebody changes what an existing field means while its name and type stay put. This outcome is worse: every committed entry still decodes cleanly, now says something false, and nothing signals it.

The bounded list is the load-bearing part. Tolerance never covers the entry form, the chain rule, the set of facts the root entry declares, any change to what an existing field means, or new values in the vocabularies that decide disclosure. Each of those mints a new version. The root entry declares which version the log opened under, and a reader meeting a version it does not know refuses rather than guessing. Committed logs are never rewritten to match a later version.

One decision to make before the first production entry. Field names can be written as plain strings, or as short codes assigned by a registry that both sides consult. Codes are smaller on the wire. Strings cost bytes and carry no registry dependency, which matters when the vocabulary belongs to one application rather than being of general interest. Because digests commit to the choice, switching later changes every digest in the log, which makes it a new version of the entire format applied to a corpus that cannot be recomputed. Decide it while the log is empty.


Part II — Normative requirements

Requirements use MUST, MUST NOT, SHOULD, and MAY in the usual sense. This part is what a second implementation is checked against.

The sections below carry a worked example: a two-entry deployment log for a fictional service, assembled with the reference command-line implementation of the encoding layer and shown in envelope notation, the format's own textual rendering. Every fragment is the verbatim output of the command beside it, executed while this document was written. The example signs its entries with a throwaway key minted for the purpose, because the fragments demonstrate signature properties. That matches the posture of the vectors rather than of the production log, as section 4.4 records.

3. Two layers, and where to cut

Two layers with independent lifetimes are present, and only the upper one belongs to the implementer.

The encoding layer is published, specified, and has implementations available: Gordian Envelope for structure, over deterministic CBOR for bytes. It provides canonical serialization, the digest tree, and the elision mechanics. BP-2's argument is a property of this layer rather than an invention of the profile above it, since eliding an element without invalidating the digest tree is what the format was designed for. Its version moves when its specifications move.

The profile layer belongs to the implementer. It defines what an entry means: which fields exist, what their values range over, what the chain rule is, what the root entry declares, and what the disclosure policy withholds from whom. Its version moves when that meaning moves.

Cutting between them allows reuse of work that is easy to get subtly wrong: canonicalization, digest-tree construction, and elision. Merging them produces two recognizable symptoms: a format version bumped because an optional field was added, and a question about whether a redaction is legal that has an answer at the byte level. Neither question should be able to reach the encoding layer.

A test for the seam: the profile should be fully describable without reference to how bytes are laid out, and the encoding layer should be describable without a single noun from the application.

3.1 Terms

  • Entry — the unit appended to the log, chained, and identified. It is not the same object as the event it carries.
  • Event — the content and its fields, before being wrapped into an entry.
  • Subject — the thing an envelope is about. For an event, the content itself.
  • Assertion — a predicate and object pair attached to a subject, equivalent to a field in other formats.
  • Digest — the root of an element's digest tree, computed over its canonical bytes.
  • Elision — replacing an element with its digest, leaving every digest above it unchanged.
  • Projection — a per-reader view of an entry, produced by elision at read time.
  • Root entry — the first entry of a chain, declaring what the chain covers and which profile version it opened under. Also called genesis.

4.1 The encoding layer

Serialization MUST be deterministic, because digests are computed over the serialized bytes and any encoder freedom forks them between implementations. Deterministic CBOR is used here, specified as an application profile over CBOR's own determinism rules.

An entry's structure is a subject plus a set of assertions, where every element is addressable by its own digest and the whole forms a Merkle-like tree. This structure is what makes elision possible.

The tree for the worked example's first event, in the reference implementation's tree view, with every element carrying its own digest:

$ envelope format --type tree "$EVENT"
c45e0bd0 NODE
    6c639e25 subj "Deployed build 4112 to production."
    067f6722 ASSERTION
        f35a9745 pred "prev"
        589d50d4 obj Digest(20daa128)
    81d4d5bd ASSERTION
        29c09059 pred "author"
        c90eb8af obj "alice"
    9f867236 ASSERTION
        7df580be pred "time"
        3c17341c obj 2026-08-21T09:14:00Z

Assertion ordering MUST be by raw assertion digest, ascending, and that ordering MUST be used for both the serialized form and the digest computation. Two orderings, or an ordering by predicate name, produce entries that verify locally and fail against any other decoder.

The tree above shows that ordering: its assertions run 067f, 81d4, 9f86, ascending by assertion digest. Notation fragments elsewhere in this document order the same assertions differently for display, and the tree shows the committed order.

An implementation that uses an existing encoding-layer library does not need the remainder of this section. For an implementation writing its own encoder, which is reasonable on a platform without a library, three digest-image asymmetries are where it will diverge, and each produces digests that look plausible and match nothing.

  • A leaf's digest is computed over the content's bytes without the tag its serialized form carries.
  • A registry-assigned constant's digest is computed with a tag its serialized form omits.
  • An elided element's digest is its own digest verbatim, hashed no further, and its serialized form is a plain byte string rather than a tagged digest value.

A reimplementation SHOULD be checked byte for byte against a reference implementation before any log is committed, because digests commit every one of these choices into every entry already written.

4.2 Entry structure

The event's subject is the content. Everything else is an assertion on it.

Five predicates carry across applications and form the portable core: author, the principal the event is attributed to; time; prev, the chain link described in section 4.5; inReplyTo, an optional digest naming an earlier entry; and audience, zero or more principals for whom a restricted entry is intended. Every other predicate belongs to the application's vocabulary, which the profile enumerates.

Assembled with the reference implementation and rendered in notation, with $PREV holding the root entry's digest:

$ EVENT=$(envelope subject type string "Deployed build 4112 to production.")
$ EVENT=$(envelope assertion add pred-obj string author string "alice" "$EVENT")
$ EVENT=$(envelope assertion add pred-obj string time date "2026-08-21T09:14:00Z" "$EVENT")
$ EVENT=$(envelope assertion add pred-obj string prev digest "$PREV" "$EVENT")
$ envelope format "$EVENT"
"Deployed build 4112 to production." [
    "author": "alice"
    "prev": Digest(20daa128)
    "time": 2026-08-21T09:14:00Z
]

An implementation MUST treat the predicate set as open in the direction the version rules allow, and MUST NOT assume the five above are the only ones present.

Whether predicates are written as strings or as registry-assigned constants is decided before the first production entry, as BP-4 describes. Strings are used here, because the vocabulary belongs to one application and a registry dependency buys compactness at a cost paid permanently.

4.3 Entry form: wrap, then sign

The entry is the event wrapped: the whole event becomes the subject of an outer envelope, and that wrapper carries a signature assertion. Entries MUST be wrapped.

A signature over an unwrapped event covers its subject alone, which allows assertions to be added or removed while the signature still verifies. Wrapping places the complete event, subject and all assertions together, under the signature. Wrapping also keeps elision and signing independent: eliding an element inside the event leaves the wrapper's digest unchanged, so the signature over it still verifies.

Signing the bare event demonstrates the failure, with the event's fields outside the signature:

$ LOOSE=$(envelope sign -s "$PRVKEYS" "$EVENT")
$ LOOSE=$(envelope assertion add pred-obj string severity string "routine" "$LOOSE")
$ envelope verify -v "$PUBKEYS" "$LOOSE" > /dev/null && echo "verifies with a field added under the signature"
verifies with a field added under the signature
$ LOOSE=$(envelope assertion remove pred-obj string author string "alice" "$LOOSE")
$ envelope verify -v "$PUBKEYS" "$LOOSE" > /dev/null && echo "verifies with the author gone"
verifies with the author gone

An attribution that detaches while the signature continues to verify is the failure in full. Wrapping first places everything under the signature:

$ ENTRY1=$(envelope sign -s "$PRVKEYS" "$(envelope subject type wrapped "$EVENT")")
$ envelope format "$ENTRY1"
{
    "Deployed build 4112 to production." [
        "author": "alice"
        "prev": Digest(20daa128)
        "time": 2026-08-21T09:14:00Z
    ]
} [
    'signed': Signature
]

The same edit is then fatal. Re-attributing the event inside the wrapper and carrying the original signature over causes verification to refuse, because the signature covers the wrapper's digest and the wrapper's digest covers everything within:

$ SIG=$(envelope assertion find predicate known signed "$ENTRY1")
$ INNER=$(envelope extract wrapped "$ENTRY1")
$ TAMPERED=$(envelope assertion remove pred-obj string author string "alice" "$INNER")
$ TAMPERED=$(envelope assertion add pred-obj string author string "mallory" "$TAMPERED")
$ TAMPERED=$(envelope assertion add envelope "$SIG" "$(envelope subject type wrapped "$TAMPERED")")
$ envelope verify -v "$PUBKEYS" "$TAMPERED" > /dev/null 2>&1 || echo "verification fails"
verification fails

4.4 Signing posture

The log described here appends entries unsigned. The server deliberately holds no signing secret belonging to the people whose events it records, and per-entry signing depends on a key design that has not shipped.

The signature slot is proven by the vectors and by the acceptance gate, both of which sign with a test key, and every property claimed for it holds on those. An implementer with signing keys available SHOULD sign at append time. An implementer building this architecture should treat this half as a proven mechanism rather than a running one.

4.5 Identity and the chain

An entry's identity is the digest of the entry as appended, wrapper included. This identifier MUST serve every purpose that names an entry: the chain link, the reply target, the sequence anchor, and any external permalink. A second identifier space is a liability, because the two can disagree about which entry they name.

The signature is inside that digest. Under a randomized signature scheme this has a consequence worth knowing before anything is cached: signing the same event twice produces two different identities. The identity exists once the entry is appended.

The first entry's identity, as every entry after it will carry it:

$ envelope digest --hex "$ENTRY1"
111a53b4789e15794be49c09feb4bef51875175fad7f71886c23ce97d180c4dc

Every entry MUST carry prev, naming the previous entry's identity. There is no exception for the first ordinary entry: it names the root entry, so every entry has a predecessor and the chain has exactly one origin.

The worked example's second entry is assembled like the first, carrying that digest as its prev, which the notation abbreviates to the first four bytes. It also carries an audience, used in section 4.6:

$ ENTRY2=$(envelope sign -s "$PRVKEYS" "$(envelope subject type wrapped "$EVENT2")")
$ envelope format "$ENTRY2"
{
    "Rolled back 4112: the crash traces to the payments module." [
        "audience": "carol"
        "author": "bob"
        "prev": Digest(111a53b4)
        "time": 2026-08-21T09:41:00Z
    ]
} [
    'signed': Signature
]

The chain rule is a checkable equality rather than a convention:

$ PREV_OF_2=$(envelope extract digest "$(envelope extract object "$(envelope assertion find predicate string prev "$(envelope extract wrapped "$ENTRY2")")")")
$ test "$PREV_OF_2" = "$(envelope digest "$ENTRY1")" && echo "entry 2's prev equals entry 1's digest"
entry 2's prev equals entry 1's digest

The root entry MUST declare what the chain covers and which profile version it opened under. It is itself an entry, formed and chained like any other. A reader meeting a profile version it does not know MUST refuse the log rather than interpret it. Committed logs are never rewritten to a later version.

A log MAY be partitioned, with one chain per scope, each chain rooting independently. Where it is, entry identity remains globally unique because it is a digest, so an identifier resolves without knowing which partition holds it. Chains SHOULD root lazily, on first append rather than on creation, so an empty scope costs nothing.

4.6 Elision

Two distinct operations share this mechanism and MUST be distinguished in the implementation, because one is reversible and the other must not be.

Projection is a read. The stored entry is untouched, and a reader without clearance receives a form of it with elements elided. Projection MUST be enforced on every read path, including live delivery, replay after a disconnect, and history queries. Enforcing it on some paths is equivalent to enforcing it on none.

Redaction is a write. The stored entry becomes the elided form permanently and the content is destroyed for every reader. A redaction MUST leave no undestroyed copy anywhere the application controls, including the projection metadata described below. It is the one operation here that cannot be undone, and an implementation SHOULD make that structural rather than a matter of policy.

Both MUST preserve the entry's identity. An elided entry's digest equals the full entry's digest, its position in the chain is unchanged, and a signature over it still verifies.

In the worked example, the second entry's content is restricted to its audience, so any other reader receives it withheld. Eliding addresses the content by its digest:

$ WITHHELD=$(envelope digest "$(envelope subject type string "Rolled back 4112: the crash traces to the payments module.")")
$ PUBLIC=$(envelope elide removing "$WITHHELD" "$ENTRY2")
$ envelope format "$PUBLIC"
{
    ELIDED [
        "audience": "carol"
        "author": "bob"
        "prev": Digest(111a53b4)
        "time": 2026-08-21T09:41:00Z
    ]
} [
    'signed': Signature
]

That is the shallower of two withholding depths: content withheld, attribution and chain position public. The deeper form elides more elements by the same operation. Identity, chain position, and signature survive it, and two commands check both halves:

$ test "$(envelope digest "$PUBLIC")" = "$(envelope digest "$ENTRY2")" && echo "digest unchanged"
digest unchanged
$ envelope verify -v "$PUBKEYS" "$PUBLIC" > /dev/null && echo "signature verifies on the elided form"
signature verifies on the elided form

A restricted entry MUST be visibly withheld rather than silently omitted from a reader's history. What is withheld MAY differ by kind of event, and this is where disclosure policy lives: one class of restricted event might withhold only its content while remaining attributable, and another might present as nothing but its slot in the chain. Both are the same entry, and neither needs a format change to become the other.

Implementations SHOULD hold the fields a projection reasons over as ordinary application state beside the entry, rather than parsing the committed bytes on every read. This permits a further discipline worth adopting deliberately: the elided form is rebuilt from that state rather than edited out of the stored bytes, and the rebuilt entry's digest is compared against the committed one before anything is served or destroyed. A divergence aborts with nothing mutated. Every export and every redaction becomes a self-test of the elision implementation at the cost of one comparison, which catches a failure that otherwise surfaces years later as an entry nobody can verify.

That discipline creates an invariant for whoever adds the next field: every assertion an append path can produce MUST have a matching arm in the rebuild path, or the first redaction of an entry carrying it fails the digest check.

4.7 Versioning and tolerance

Within a major profile version, a reader MUST accept entries carrying predicates and vocabulary values it does not know. It MUST verify them normally, since chain and signature mechanics do not depend on understanding any field. It SHOULD render them generically. It MUST fail closed: an entry it cannot classify MUST NOT enter any projection whose purpose is deciding who may see what.

Tolerance MUST NOT extend to the entry form, the chain rule, the root entry's declared set, a change in what an existing predicate means, or new values in any vocabulary that decides disclosure. Each of those is a new profile version.

4.8 Invariants

The following MUST NOT hold in a conforming implementation. The list is the fastest way to check one.

  • No entry exists without a prev, and no chain has two origins.
  • No entry's identity is anything other than the digest of the entry as appended.
  • No elided form has a different digest from the entry it came from.
  • No restricted entry is silently absent from a reader's history rather than visibly withheld.
  • No redacted content survives anywhere the application controls.
  • No read path serves an unprojected entry.
  • No unclassifiable entry reaches a disclosure-sensitive view.
  • No committed log is rewritten to satisfy a later profile version.

Part III — Evidence

This part records what carries each claim. Readers deciding how much of this design to trust should read it first.

The balance is uneven in an unusual direction and is stated before the detail. The format claims are the strongest material in this document, verifiable by a stranger with one command-line tool and no access to anything belonging to this project. The signing claims are the weakest, and one of them is not running.

Everything cited below is public, at github.com/eOSContinuum/eOS-ChatTheatre: the profile in docs/design/envelope-profile.md, the vectors in docs/design/envelope-vectors/, and the verification script in scripts/.

The command fragments in Part II are none of these. They are illustrations, generated against the reference implementation while this document was written and signed with a throwaway key. The committed vector set is separate and unchanged by them.

5.1 Carried by test vectors

A committed vector set verifies offline against the reference implementation of the encoding layer, with no network, no server, and nothing regenerated. Running it requires one tool and a checkout: 50 checks pass and none fail, 13 over the ancestor profile and 37 over the current one. That run was repeated while this document was written rather than quoted from a previous one.

The checks establish the following, ordered by significance to a reimplementer.

Elision is digest-stable and signature-stable across four independently constructed shapes: the withheld-content form of a restricted entry, a single field withheld while its neighbours remain visible, a structural view that keeps some fields and drops others, and a maximally elided entry with everything gone but the chain link. In each of the four, the elided entry's digest equals the full entry's digest and the signature over it still verifies.

The fourth shape is the one to study when building this. It hides even the field naming what kind of event it was, so a reader without clearance cannot distinguish it from any other withheld entry, and the chain still verifies through it. A format whose withheld entries are self-identifying leaks the shape of what it is hiding.

The remaining checks establish that every entry's prev equals the prior entry's digest, across a three-entry log and an eight-entry one; that every entry verifies against the test signer, root entry included; that the full form of a restricted entry shows its content while the withheld form does not and is explicitly marked elided; that a reply reference resolves to an earlier entry's identity; and that the root entry pins the profile version string.

5.2 Carried by the running system, checked by an independent decoder

Vectors are fixtures, and a fixture proves only what its author thought to write down. The stronger evidence is an acceptance gate that drives real sessions against a running server over HTTP and live streams, restarts the server mid-run, exports the server's own log bytes, and hands them to the reference command-line implementation. The bytes are checked by something other than the code that produced them. That suite was run while this document was written: every gate passed, including the format gate.

Over those live bytes, the gate establishes the digest chain across the exported span, and establishes that the server's own withheld export and the full entry share one digest, with the content present in one and absent-and-marked in the other.

The gate also establishes the elision property on a live entry, with an asymmetry that requires stating. Because entries are appended unsigned, the gate signs an exported live entry with the test key, then elides the content, then checks that the digest is unchanged and the signature still verifies. The entry is real and the signature is synthetic.

The independence is what makes this stronger than the vectors. The encoder here was written from scratch, on a platform with no library for this format, from the specifications and the three digest-image asymmetries named in section 4.1. An implementation that nobody involved in writing it controls reads its output and agrees, which is the evidence that the encoding layer is portable in practice rather than in principle.

5.3 Carried by running code

The separation of the two truths is enforced where it would be easy to fudge. A reset that clears what the shared view shows its readers moves a presentation floor and does not touch the log: replay, the chain, and every other read still see the entries that reset skipped. The record is not what the interface currently displays, and the code reflects that at the point where a shortcut would be tempting.

Projection is applied on every read path without exception. Live delivery, replay after a disconnect, recent history, and a span read all route through one projection function, and no read path reaches the stored entries around it.

Two withholding depths are chosen by kind of event, which makes BP-3's point about policy living outside the format concrete. One class of restricted event withholds its content and remains attributable, because the fact that it happened is public. Another presents as nothing but its slot in the chain.

The rebuild-and-check discipline runs on both export and destructive redaction: the elided form is rebuilt from application state rather than edited out of the committed bytes, its digest is compared against the committed entry, and a divergence aborts before anything is written or served. Redaction destroys the content in place and leaves no undestroyed copy.

Chains root lazily, one per partition, and entry identity remains globally unique across partitions because it is a digest.

5.4 Carried by neither

Production entries are unsigned. The server holds no signing secret belonging to the people whose events it records, by design, and per-entry signing depends on a key architecture that has not shipped. Everything claimed for the signature slot holds on vectors and on test-signed live entries. Nothing claimed for it is running. An implementer with signing keys available should sign at append time.

No digest is anchored outside the server. BP-1 states that a chain held end to end by one party is tamper-evident only against a digest that left the building, and that an implementation should be explicit about which mechanism it has enabled. Neither is enabled here. The chain is internally consistent and externally unattested. That bounds which interactions the log can support. In the progressive trust lifecycle it sustains the phases through Fulfillment and forecloses the two that require a third party: Escalation, in which an independent party inspects the record, and Dispute, in which an arbitrator resolves a conflict from it. Both need a digest the server did not supply.

Nothing here is proven at scale. Log growth, read latency under load, and concurrent append behaviour were not measured.

Storage, rotation, and retention are absent. They are not described, not tested, and not implied by anything above.

5.5 Adoption priority

Implementers adopting part of this design should take the separation in BP-1 first, because it is the one that cannot be added later. A log that state is replayed from has already foreclosed redaction, and no amount of digest work reopens it. The choice is made on the day the log's purpose is decided, usually without being recognized as a choice.

The second priority is wrapping before signing. Signing an event directly, without the wrapper, is the intuitive design, and it leaves assertions addable and removable under a signature that still verifies. It costs one extra layer to close and remains invisible until someone looks for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment