Skip to main content
Two clients that disagree on identity do not share a brain at all. So identity is the part of the protocol this SDK implements rather than delegates, and the part the paper leaves open that an SDK cannot.

Canonical serialization

The canonical form is JCS (RFC 8785), tagged in every envelope so it stays versionable:
Chosen over a binary encoding because a block is a small record and the protocol targets several languages: a canonical form a human can read and grep beats compactness here. Canonicalization erases the order a mapping was built in, which is what makes block_id a function of the content and nothing else:

Values a payload refuses

Floats and unsafe integers are refused inside a payload. A float fails at construction, not at commit.
JCS defines float serialization through ECMAScript rules that are hard to reproduce identically across languages, and integers outside the IEEE-754 safe range lose precision in any double-backed JSON parser. Either divergence would mean two conforming clients computing different block_id values for the same knowledge — which is the one failure the protocol cannot tolerate. If you need a number with a fraction, store it as a string or as a scaled integer and say which in the payload.

Three levels of hashes are three types

None is a str, and none is interchangeable with another.
Offering one level where another is expected is refused, not coerced:
That distinction matters in practice. Two clients that packed the same blocks with different gzip settings have different layer digests and the same Merkle root: the OciDigest says the bytes differ, the MerkleRoot says the knowledge does not.

Hashing primitives

The domain-separating prefixes are what stop a leaf hash from being replayed as an internal node — see Merkle DAGs.

Timestamps

One function, always UTC, so two clients recording the same event do not disagree about when it happened because of a local timezone.

Actor identifiers

A provenance record names who performed an operation, and a provenance record is a block: the identifier enters the payload, the payload enters the envelope, and the envelope is what block_id is computed over. So an identifier two parties spell differently is two names for one fact — the same silent divergence canonical serialization exists to prevent, arriving through a field nobody had canonicalized. Two forms, and no third:
The namespace carries whoever made or vouches for the name, so nothing repeats it as a separate field. No scheme prefix is stored: mailto: is ceremony around a value people already write correctly, and a URL invites exactly the questions a canonical form must leave closed — a trailing slash, a default port, a percent-encoded octet. An implementation exporting to a format that requires URIs derives one at that boundary.
Refused, never normalized. Alex@Example.org raises ActorIdError rather than being lowered. Rewriting it would mint a block_id the caller neither asked for nor can predict — and therefore one they cannot search for either.
The check is asymmetric on purpose, and the second half is the easy one to lose:
Every provenance record ever written decodes through Actor, so a validator on the type would make every brain that predates this rule unreadable — punishing readers for a writer’s old habit. Enforcement attaches where an identifier is being chosen: opening a brain, and the request models. What is already published still reads.