This document specifies tzun-anchor/1. It defines how the position of a DEO record in its
evidence log is committed, through a second log, to a 32-byte value published somewhere the
operator of the log does not control, and how a verifier checks that commitment from the record
alone.
It relies on the other parts of this specification:
- Tree: tree hashing, inclusion and consistency proofs, the hash encoding, and the evidence log of each chain;
- Record: the record, its
canonicalDigestand itspayload.sequenceNumber; - Verification: the four row outcomes, the report and the exit codes;
- Bundle: the export bundle, which carries the material of §7.2 rule 4 and of §10.8.
Tag. A record artifact that follows this document says so with
"anchorSpec": "tzun-anchor/1" (§8.1).
Requirement keywords (MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, RECOMMENDED, MAY) have their BCP 14 meaning only when written in capitals; Specs sets out the conventions shared by every part.
Notation.
SHA-256is FIPS 180-4 SHA-256.keccak256is the Keccak-256 hash Ethereum uses, which is not FIPS 202 SHA3-256.‖joins raw bytes.LFis the byte 0x0A.- A "hash" with no further qualification is 32 bytes written as Tree §4 requires:
64 hex digits, no prefix. Wherever a
0xprefix is used, the text says so. - "base64" is RFC 4648 §4: the standard alphabet, padded with
=. - An "integer" is a JSON number read under the integer rule of Tree §4: an integral value from 0 to 2^53 − 1.
1. Overview
evidence log of one chain leaf i: the canonicalDigest of the record whose
│ sequenceNumber is i + 1 (Tree §5.2)
│ taken at size n
▼
checkpoint body "<origin>\n<n>\n<root in base64>\n" §4
│ checkpointDigest = SHA-256(body)
▼
anchor log of the domain one leaf per source log that grew in the epoch,
│ in byte order of origin §5
│ taken at the epoch's size N
▼
P = SHA-256(anchor checkpoint body) the one value every publication of the epoch carries §6
├── evm-calldata/1 an EVM transaction whose calldata is exactly P §7.2
└── rfc3161/1 an RFC 3161 timestamp token whose message imprint is P §7.3
| Term | In this document |
|---|---|
| Domain | One anchoring authority: a single store, or a single hosted deployment, or one tenant domain within a hosted deployment (§4.3.3). A domain owns exactly one anchor log, and on each chain it publishes to it uses one dedicated publisher address. |
| Store | An evidence store that a deployment runs for itself. |
| Source log | A log whose checkpoints the domain's anchor log records. Every evidence log is a source log. |
| Checkpoint body | The three-line statement of a log's origin, size and root at one size (§4). |
| Anchor log | The domain's second-level log, whose leaves are checkpoint digests of its source logs (§5). |
| Epoch | One append of source-log checkpoint digests to the anchor log, together with the publications of the anchor checkpoint that append produces. |
| P | What an epoch publishes: the checkpoint digest of the anchor log's body at the size that epoch reached (§6). |
| Publication | One external commitment to P, of a named kind (§7). |
| Publisher manifest | The verifier's own statement of which addresses may publish for which anchor origin (§9). |
| Header source | The verifier's own view of an EVM chain, trusted only to say which blocks are canonical and which are final (§10.4). |
A record carries everything it needs in nonRepudiation.anchor (§8): both checkpoint bodies, both
inclusion paths, and the publications of its epoch. An anchored record can therefore be checked
without the store that wrote it.
2. Versions and test origins
- Every format this document defines is named by a tag:
anchorSpec(tzun-anchor/1), each publication kind (evm-calldata/1,rfc3161/1), each log kind (evidence,anchors), andmerkleSpecfor the tree profile (Tree §0). - Specs lists each version and says whether it is frozen. A frozen version is never
edited. A change to the bytes it defines, or to the outcome a verifier reaches under it, is made
under a new tag instead:
tzun-anchor/2,evm-calldata/2, a new log kind. - A verifier that meets a tag it does not implement abstains on what that tag governs, and on nothing else (§10.3 steps 1 and 6). An abstention is never a failure.
- An anchor under a test origin (§4.3.4) is a trial. It makes no production claim and never fixes
a version, and a verifier reports its namespace as
testwhatever else it finds (§10.6).
3. Source logs
- The kinds defined here.
evidence: one log per evidence chain, whose leaf rule is Tree §5.2.anchors: the domain's anchor log (§5). - Other source logs. A domain MAY anchor further source logs. Each is a growing log under Tree §5.1, with its own origin under the domain prefix (§4.3.5). This version defines no kind or leaf rule for them beyond the preimage rule of Tree §5.2, and verifying a record never needs one: a verifier checks that a source log's checkpoint is in the anchor log, not what the source log's leaves mean. A writer anchors only source logs whose leaves its store binds to stored data (§11), and computes each of their leaves so that it can never be a record's digest (Tree §5.2).
- What a source log contributes. In each epoch in which a source log grew, and in which its new checkpoint is consistent with the one last anchored for it (§11), it contributes exactly one leaf to the anchor log. Otherwise it contributes none.
4. Checkpoint bodies
4.1 Format
A checkpoint body is the note body of a C2SP tlog-checkpoint (c2sp.org/tlog-checkpoint) with
further restrictions. Its bytes are exactly:
<origin> LF <tree size> LF <root> LF
| Line | Content |
|---|---|
| origin | The log's name (§4.3). |
| tree size | The log's size in ASCII decimal: no sign, no leading zero, at least 1, at most 2^53 − 1 (9007199254740991). |
| root | MTH(D[size]) of the log (Tree §3.1), 32 bytes in canonical base64: 44 characters, the last of them =, the 43rd carrying zero in its two unused low bits. |
- The body has three lines and no more. Each ends with LF, including the last. No CR appears anywhere, nothing comes before the origin, and nothing follows the final LF.
- C2SP allows extension lines after the root. Tzun bodies have none, and a reader refuses them.
- C2SP allows a size of 0. A Tzun log is never checkpointed while empty, and a reader refuses a body of size 0.
- The origin has a stricter grammar than C2SP asks for (§4.3.1). A reader applies it when it binds two origins (§10.3 step 2b), not while it parses a body.
Inside JSON, a body is a JSON string, and the body's bytes are the ASCII bytes of that string's value.
4.2 Parsing
A reader accepts a body when all three of these hold, and refuses it with malformed_checkpoint
otherwise:
-
every byte is either LF or in the range 0x21 to 0x7E;
-
the entire string, from its first byte to its last, matches
([\x21-\x2A\x2C-\x7E]+)\n([1-9][0-9]{0,15})\n([A-Za-z0-9+/]{42}[AEIMQUYcgkosw048]=)\n(a partial match is not enough; in Python, use
re.fullmatch); -
the size, read as a decimal integer, does not exceed 2^53 − 1.
The class [AEIMQUYcgkosw048] holds exactly the base64 characters whose two low bits are zero. It
makes the encoding canonical: each 32-byte root has one spelling and no other string decodes to it.
A JSON value that is not a string is not a body, and is refused the same way.
Parsing asks only that the origin be non-empty printable ASCII without space or +, the set C2SP
recommends, so any C2SP body that meets the other rules parses. Whether the origin follows Tzun's
scheme is decided later, when the two origins of an anchor are bound (§10.3 step 2b). A reader MAY
give a more detailed diagnostic, such as "extension line" or "size out of range", but the reason
it reports is always malformed_checkpoint.
4.3 Origins
4.3.1 The Tzun origin scheme (readers and writers)
- Segments. A Tzun origin has three or four segments, separated by
/. No segment is empty, and each consists of bytes from 0x21 to 0x7E other than/and+. A Tzun origin therefore contains no space, no control character, no byte outside ASCII and no+. (C2SP requires a non-empty origin and recommends a schema-less URL without Unicode spaces or+.) - Domain prefix. The first two segments form the domain prefix:
<ns>/<storeId>for a store (§4.3.2),log.tzun.ai/<deploymentId>for a hosted deployment (§4.3.3). The first segment alone is the origin's namespace. - The rest. After the prefix comes exactly one of:
- Everything else is not a Tzun origin. That includes a three-segment origin whose last
segment is
evidence, and a four-segment origin whose third segment is anything butevidence. - Comparison. Origins are compared as byte strings; two origins are equal only when their bytes are.
- Per-chain kinds.
evidenceis the only kind that takes a chain segment intzun-anchor/1, and step 4 of §10.3 applies theevidenceleaf rule. A per-chain log with a different leaf rule therefore needs a newanchorSpec, not only a new kind.
4.3.2 Store origins (writers)
| Log | Origin |
|---|---|
The evidence log of chain c | <ns>/<storeId>/evidence/<hex(UTF-8(c))> |
A source log of kind k | <ns>/<storeId>/<k> |
| The anchor log | <ns>/<storeId>/anchors |
nsis a lowercase DNS-style name: one or more labels separated by., each label made ofa–zand0–9with-allowed only inside it, each label at most 63 characters and the whole name at most 253. A store that is not configured otherwise useslocal.invalid; the.invalidtop-level name is reserved (RFC 6761), so that default can never be mistaken for a routable name.storeIdis 128 bits from a cryptographically secure random source, written as 32 lowercase hex characters.nsandstoreIdare fixed when a store is created and never change afterwards. A store whose data is lost and recreated takes a newstoreId, and so begins new logs under new origins rather than continuing an old origin at a size that contradicts what that origin already published. The new anchor origin needs its own manifest entry, and the old entry is closed by setting itsvalidUntil(§9).- The chain segment is the lowercase hex of the UTF-8 bytes of the chain id, two characters
per byte: the chain
defaultgives64656661756c74. The segment has one spelling and never contains/or+, and every writer computes the same segment for the same chain id. - Chain ids. A chain id MUST NOT be empty, MUST consist of Unicode scalar values only, and MUST NOT contain U+0000. A lone UTF-16 surrogate has no UTF-8 encoding, and replacing it with U+FFFD would give one chain the bytes of another; a store refuses such a chain id, and refuses one containing U+0000.
kconsists of lowercase ASCII letters and digits and starts with a letter.evidenceandanchorsare reserved and are never ak.
The chain segment rule (readers). A chain segment is one or more pairs of lowercase hex digits. The bytes they spell contain no 0x00, are well-formed UTF-8 (no overlong form, no encoded surrogate; a leading U+FEFF is part of the chain id and is kept), and encode back to exactly the same segment. An origin whose chain segment breaks any of this, for instance by using uppercase hex, is not a Tzun origin, and step 2b of §10.3 refuses it.
4.3.3 Hosted-platform origins (writers)
| Log | Origin |
|---|---|
| Any source log | log.tzun.ai/<deploymentId>/<logId> |
| The anchor log | log.tzun.ai/<deploymentId>/anchors |
deploymentIdis 128 random bits written as 32 lowercase hex characters. It is fixed when the deployment's storage is created. If that storage is ever rebuilt, the deployment either restores its logs with the rest of its data, or takes a newdeploymentId, lists the new anchor origin in the manifest (§9) and closes the old entry withvalidUntil. It never restarts a log under an origin that has already published.logIdis 128 random bits written as 32 lowercase hex characters. The deployment maps it privately to the account, environment, kind and chain it serves. Hosted chain ids can contain account identifiers, which do not belong in a public name, so hosted source-log origins are opaque. A verifier therefore cannot check the kind or the chain of a hosted source log (§10.3 step 2b).- Leaves of other kinds. Since a verifier cannot tell a hosted log's kind from its origin,
nothing but the leaf itself keeps a leaf of another kind from verifying as a record's leaf. A
hosted deployment therefore computes every leaf of a log whose kind is not
evidenceunder the leaf preimage rule of Tree §5.2: as the SHA-256 of a preimage that can never be a record's canonical bytes. - Tenant domains. A hosted deployment MAY run further anchoring domains. Each is named by its
own 128-bit id, written as 32 lowercase hex characters, in the second segment in place of
deploymentId. Its anchor origin islog.tzun.ai/<domain id>/anchors, it has its own key in the manifest, and the rebuild rule above applies to it too. This uses the grammar of §4.3.1 unchanged.
4.3.4 Test origins
- An origin is a test origin exactly when its first five bytes are
test.. For a store, that means annswhose first label istest. A hosted deployment that anchors for testing usestest.log.tzun.aias its namespace. - A verifier MAY accept an origin's own claim to be a test origin without any authentication:
calling oneself a test is never an escalation. A verifier MUST NOT accept an unauthenticated claim
to be production (§10.6,
anchorNamespace).
4.3.5 Origin binding
- A domain's anchor origin is its domain prefix followed by
/anchors. - Every source log the domain anchors has a Tzun origin under the same domain prefix, and is not itself an anchor origin.
- Step 2b of §10.3 checks both points and, when a chain id is supplied, that the log is that
chain's evidence log, apart from an opaque hosted log (§4.3.3). These rules hold for any source
log, and the
domaincases of the conformance vectors pin them. For a record's artifact, step 2b also requires an evidence log when no chain id is supplied, since a record has a leaf in no other log.
4.4 Checkpoint digest
checkpointDigest(body) = SHA-256(the bytes of body)
The result is 32 bytes, written as a hash wherever it is carried. A signature is never part of a body, so signing a checkpoint cannot change its digest.
4.5 Signatures
- A
tzun-anchor/1checkpoint is the body alone. This version defines no log key, and a verifier of this version verifies no checkpoint signature. - In C2SP a checkpoint is by definition a signed note, so a stock signed-note reader rejects an unsigned body, and a C2SP witness cosigns only checkpoints signed by a log key it knows. Nothing in this document claims otherwise.
- The signed form is reserved. It is a C2SP signed note (c2sp.org/signed-note): the body, an empty
line, then one or more signature lines
— <key name> <base64(key ID ‖ signature)>, each ending with LF. A log key is Ed25519 (signature type 0x01); a witness cosignature iscosignature/v1(type 0x04). - Signatures are additive. Adding one changes nothing that is already anchored. The record artifact (§8) carries bodies only; signatures travel beside bodies in export bundles (Bundle §7.2).
- Log and witness keys reach a verifier through its own configuration (§9, reserved members), never through the artifact.
- A log MUST NOT sign, and MUST NOT publish, a checkpoint inconsistent with one it has already signed or published (§11).
5. The anchor log
- One per domain. Its origin is the domain prefix followed by
/anchors. - Leaves. A leaf is
d = checkpointDigest(body)of one source-log checkpoint: 32 raw bytes, whose leaf hash under Tree §3.1 isSHA-256(0x00 ‖ d). - Epochs. In an epoch, the writer takes a checkpoint of every source log of the domain that grew since its last anchored checkpoint, at that log's current size, and appends the digest of each one that is consistent with the log's last anchored checkpoint (§11). An epoch appends at least one leaf. There are no empty epochs.
- Order within an epoch. The digests of one epoch are appended in ascending byte order of their origins: bytes compared as unsigned values, and an origin that is a proper prefix of another sorts first. Anyone who holds the set of checkpoints of an epoch can therefore rebuild exactly the same tree.
- Uniqueness. A pair of origin and size is appended at most once. Distinct pairs give distinct bodies, and so distinct leaves (Tree §5.4).
- Consistency of source logs. Each anchored checkpoint of a source log, after the first, is
joined to the previous anchored one by the consistency proof
PROOF(m_prev, D[n])(Tree §3.5), and that proof is verified before the new checkpoint is stored (§11). - The anchor log's own checkpoints. At each size
Nit publishes, the anchor log has a checkpoint body whose origin is the anchor origin. Successive published sizes are joined byPROOF(N_prev, A[N]). - Not every size is published. An epoch whose publication never took effect can be replaced by a later epoch, so the published sizes are a subsequence of the anchor log's sizes. Consistency proofs of the anchor log join successive published sizes.
6. The published value P
P = checkpointDigest(anchor checkpoint body at the epoch's size)
= SHA-256(anchor checkpoint body)
- Every publication of an epoch commits to the same 32 bytes.
- In EVM contexts P is written as
0xfollowed by 64 lowercase hex digits. Elsewhere it is a hash. - P is the digest of a body, not a bare root, because the body states the origin, the size and the root together. No inclusion or consistency proof fixes the size it was made for (Tree §3.6), so a verifier has to take the size from something the publication authenticates. P still has 32 bytes, so publishing it costs what publishing a root would, and checking it on chain is still a single comparison.
7. Publications
7.1 Common rules
- A publication kind is a string
<name>/<version>. This version definesevm-calldata/1(§7.2) andrfc3161/1(§7.3), and reserveswitness/1(§7.4). - A publication entry is a JSON object whose
kindmember names its kind (§8.2). - A domain publishes each epoch through one kind or several. A verifier checks every publication on
its own against P (§10.3 step 6, §10.4, §10.5) and tells publications apart by their index in
publications. - A verifier that does not implement a kind abstains on that publication alone, with
unsupported_publication_kind, and still decides the rest of the anchor.
7.2 evm-calldata/1
Writer rules.
- Chain. The chain is named in CAIP-2 form,
eip155:<chain id>, with the chain id in decimal and no leading zero. Ethereum mainnet iseip155:1; the Sepolia test network iseip155:11155111; the Hoodi test network iseip155:560048. A domain names its chain in its configuration; this version fixes none. - Sender. The transaction is sent from the domain's dedicated publisher address for that chain. Its key signs publications and the domain's own accounting transactions and nothing else, so the address's history can be enumerated and every transaction in it accounted for.
- Transaction.
valueis 0 anddatais exactly the 32 bytes of P: the calldata is0x ‖ hex(P), with no selector, prefix, magic number or other byte. The signature MUST cover the chain id (EIP-155). An EIP-1559 (type 2) transaction SHOULD be used; an EIP-2930 (type 1) or an EIP-155 legacy transaction MAY be. The writer conventionally sends the transaction to the publisher's own address, but a verifier MUST NOT rely ontoand §10.4 never reads it: anyone can send any transaction to any address, so the recipient says nothing about who published. - Material for offline checking. When the publication is confirmed, the writer keeps the raw
signed transaction, the RLP of the block header, the position of the transaction within the
block, and the proof of the transaction in the block's transactions trie. Export bundles carry
them (Bundle §7.4). They are kept rather than fetched again later, because the
endpoint they came from may not last. The header is kept as the chain encodes it, with every
field the block's fork appends after the fifteen that every header has: the Glamsterdam
fork, for instance, appends
block_access_list_hashandslot_number. A writer that rebuilds a header from its fields MUST include every field the block has, or its hash will not be the block's; a verifier reads the header by position and MUST NOT require any number of items beyond fifteen (§10.4 item 6).
What it proves. From the bundle alone, with no network access, only this: a transaction for that
chain id, carrying P, bears the publisher key's signature. When, in addition, a header source of the
verifier's own choosing confirms that the block is canonical and final, it proves as well that P was
on that public ledger by the block's time; and, since the manifest names that sender as a publisher
for the anchor origin, that the domain named by the origin published it. A test network such as
Sepolia or Hoodi has no economic security and is reported as testnet (§9).
7.3 rfc3161/1
Writer rules.
- The writer sends an RFC 3161
TimeStampReqwhosemessageImprintis{ hashAlgorithm: id-sha256, hashedMessage: P }to each timestamp authority the deployment uses, withcertReqset to true. RFC 3161 §2.4.1 then requires the authority to include its signing certificate in the token, without which the token cannot be verified offline (§10.5 step 1). - The writer keeps the DER
TimeStampRespand carries it in base64. Each authority's token is a publication entry of its own. - The timestamp authorities are third parties the deployment chooses. Tzun does not act as one.
What it proves. That P existed by the token's genTime, as attested by an authority whose
certificate is a timestamping certificate under a root the verifier trusts (§10.5). It proves
nothing more:
- No origin. A token authenticates the authority, not whoever asked for it. Anyone can build a
one-leaf log under any origin, obtain a token over its P, and hold an artifact whose publication
verifies. The origin, the position and the namespace are then the requester's own claims
(§10.6:
anchorOriginAuthenticatedisfalse). - No enumeration. A token has no nonce to account for. Whoever controls the database of a
deployment that publishes only through RFC 3161 can delete old tokens and anchor a rewritten
log again, and nothing inside the deployment will notice. The replacement tokens carry later
genTimevalues, which only the holder of an earlier export or checkpoint can see. - Consequence. A deployment whose only publication kind is
rfc3161/1, and that claims to detect rewrites by whoever controls its database, MUST copy every anchor checkpoint and token to a write-once store that party cannot rewrite. Without such a copy, it claims existence by a time and nothing else.
7.4 witness/1 (reserved)
The name is reserved for a C2SP cosignature/v1 over a domain's checkpoint by an independent
witness, which cosigns only after verifying consistency with the last checkpoint it saw. It
depends on signed checkpoints (§4.5). This version defines no entry format for it, and a verifier
of this version abstains on it (unsupported_publication_kind).
7.5 What each kind establishes
evm-calldata/1, confirmed by the verifier's header source | evm-calldata/1, offline | rfc3161/1 | |
|---|---|---|---|
| P existed by a time | Yes, the block's time | No | Yes, the token's genTime |
| Who published (the anchor origin) | Yes, through the manifest | No | No |
| Every publication can be enumerated | Yes | Yes | No |
| Publicly visible | Yes | Yes | No: the operator holds the only copy |
Makes anchorValid true | Yes | No (the publication stays null) | Yes, meaning existence by a time only |
8. The record artifact: nonRepudiation.anchor
8.1 Members
The artifact is a JSON object with exactly these nine members:
| Member | Type | Content |
|---|---|---|
anchorSpec | string | "tzun-anchor/1" |
merkleSpec | string | "rfc6962" (Tree) |
leafIndex | integer | Where the record sits in its evidence log; it MUST equal payload.sequenceNumber − 1 |
logCheckpoint | string | A checkpoint body (§4) for the evidence log holding the record, taken at some size n > leafIndex |
logPath | array of hashes | PATH(leafIndex, D[n]), leaf end first (Tree §3.3) |
anchorLeafIndex | integer | The index of checkpointDigest(logCheckpoint) in the anchor log |
anchorCheckpoint | string | A checkpoint body for the domain's anchor log, taken at the size N its epoch published, with N > anchorLeafIndex |
anchorPath | array of hashes | PATH(anchorLeafIndex, A[N]), leaf end first |
publications | array | One or more publication entries of the epoch (§8.2), in any order |
A writer MUST NOT write any other member. Changing the set of members requires a new anchorSpec.
8.2 Publication entries
evm-calldata/1:
| Member | Type | Content |
|---|---|---|
kind | string | "evm-calldata/1" |
chain | string | CAIP-2 chain. Shown to people only; verification uses the chain the manifest names. |
transactionId | string | 0x and 64 lowercase hex digits: keccak256 of the raw signed transaction |
blockNumber | integer | The number of the block that includes the transaction |
blockHash | string | 0x and 64 lowercase hex digits |
publisher | string | 0x and 40 hex digits. Shown to people only; verification uses the address recovered from the signature. |
A writer MUST write all six members and no others. The material of §7.2 rule 4 is not part of the
envelope. An export bundle carries it and joins it to this entry as the members rawTransaction,
blockHeader, txIndex and txProof (Bundle §7.4). A bundle's own copy of the entry
MAY leave out transactionId; a verifier that holds both uses the envelope's. Because of that
join, a verifier MUST NOT fail an entry for carrying members it does not read.
rfc3161/1:
| Member | Type | Content |
|---|---|---|
kind | string | "rfc3161/1" |
token | string | The DER TimeStampResp, in base64 |
8.3 Example (informative)
"anchor": {
"anchorSpec": "tzun-anchor/1",
"merkleSpec": "rfc6962",
"leafIndex": 41, // payload.sequenceNumber is 42
"logCheckpoint": "local.invalid/<32 hex>/evidence/64656661756c74\n1024\n<44 base64>\n",
"logPath": ["<hash>", "…"], // 10 hashes: PATH(41, D[1024])
"anchorLeafIndex": 377,
"anchorCheckpoint": "local.invalid/<32 hex>/anchors\n412\n<44 base64>\n",
"anchorPath": ["<hash>", "…"],
"publications": [
{ "kind": "evm-calldata/1", "chain": "eip155:11155111", "transactionId": "0x…",
"blockNumber": 7123456, "blockHash": "0x…", "publisher": "0x…" },
{ "kind": "rfc3161/1", "token": "<base64 TimeStampResp>" }
]
}
Here the chain is default, whose chain segment is 64656661756c74 (§4.3.2).
8.4 Rules
- Placement. The artifact is a member of
nonRepudiation, and nothing undernonRepudiationis hashed into the record's digest (Record §2.4). Adding it changes nocanonicalDigest, and the envelope stays atenvelopeVersione1. - Only once anchored. A record that is not yet anchored has no
anchormember. No placeholder or pending marker is ever written in its place; an export bundle states the pending state in a slot of its own (Bundle §7.7). - Written once. For an EVM publication, the artifact is written only once the chain's finalized height has reached the publication's block. For a deployment that publishes only through RFC 3161, it is written once the token is in hand. It lists every publication of the epoch that was complete when it was written, and it is never rewritten: later evidence (log signatures, cosignatures, newer consistency proofs) travels outside the envelope.
- Epoch members repeat.
anchorCheckpointandpublicationsare the same in every record of an epoch, andanchorLeafIndexandanchorPathare the same in every record anchored through one source-log checkpoint; each record carries its own copy so that it verifies alone. The cost is two paths of about log₂(n) hashes each, plus the size of each timestamp token. blockchainAnchoris not part of this format.nonRepudiation.blockchainAnchoris a legacy member that no writer populates, and a writer MUST NOT write it. A verifier that finds it without ananchorabstains (§10.3 step 1).
9. The publisher manifest
A verifier learns who may publish from its own input, never from the artifact.
{
"publishers": {
"<anchor origin>": [
{ "address": "0x…", "chain": "eip155:1", "namespace": "production",
"validFrom": "2026-10-01T00:00:00Z", "validUntil": null }
]
}
}
-
publishersmaps each anchor origin, compared as bytes, to its list of entries. -
Entry members.
-
address:0xfollowed by 40 hex digits, compared without regard to case. An EIP-55 checksum is not required. -
chain: a CAIP-2 chain. For EVM,eip155:followed by 1 to 31 decimal digits with no leading zero. An EVM verifier skips entries whose chain belongs to another CAIP-2 namespace. -
namespace:"production"or"test". -
validFromandvalidUntil: absent ornullfor no bound. Otherwise a string of exactly this form, a narrowing of RFC 3339 §5.6:YYYY-MM-DD "T" hh:mm:ss [ "." 1*DIGIT ] ( "Z" / ( "+" / "-" ) hh:mm )TandZare uppercase only. Every field is checked against its range: the day exists in its month and year, the hour is 00 to 23, the minute 00 to 59, and the second 00 to 59, so a leap second60is refused; an offset's hour is 00 to 23 and its minute 00 to 59. A value outside this, such as2026-09-31orT24:00:00, makes the entry malformed. It is never carried over into the next day or month. -
Both bounds are inclusive and compared at full precision. A block time is a whole number of seconds, so the effect is that of rounding
validFromup andvalidUntildown to a whole second. AvalidFromof2026-10-01T00:00:00.0005Zadmits a block at00:00:01and not one at00:00:00. An implementation MUST NOT convert the value to a type with millisecond precision before comparing, since.0005would then read as.000. -
Any other member of an entry is ignored.
-
-
What a verifier reads for an anchor origin, deciding in this order:
- no manifest, a manifest that is not a JSON object, a
publishersmember that is missing or not an object, or no key for the anchor origin: no entries (no_expectation:publisher, §10.4 item 5); - a value for the origin that is not an array, or an element of it that is not an object or
whose
chainis not a string: unreadable (input_not_supplied:publisher_manifest); - entries whose
chaindoes not begin witheip155:are skipped by an EVM verifier; - an EVM entry that breaks any rule above: unreadable, reported as such and never skipped;
- no EVM entry left: no entries.
- no manifest, a manifest that is not a JSON object, a
-
Where manifests come from.
- Tzun lists its own publishers in one public manifest, served at
https://tzun.ai/anchors/publishers.jsonbeside a page for people athttps://tzun.ai/anchors/. Eachtzun-verifyrelease pins a copy byte for byte. The copy on the site is never trusted by itself. - A customer that publishes with a key of its own is not in the public manifest; it is given a manifest of its own.
- A store's own verifier takes its manifest from its configuration.
- Tzun lists its own publishers in one public manifest, served at
-
Primary and supplementary manifests. A verifier reads exactly one primary manifest and any number of supplementary ones, including none.
- The primary is the copy pinned in the verifier's release, unless the auditor supplies one of their own, which then replaces the pinned copy; the two are never merged. A store's configured manifest is its primary.
- A supplementary manifest counts only for anchor origins that are not keys of the primary's
publishers. It can never add, remove or change an entry for an origin the primary lists, so a customer's manifest cannot add a signer for an origin that Tzun's manifest governs. - Overlap is decided by the presence of keys alone, before any value is read. An anchor origin
that is a key of
publishersin the primary and in a supplementary manifest, or in two supplementary manifests, is unreadable (input_not_supplied:publisher_manifest), whatever the values are and whether or not they are valid. Entries from two manifests are therefore never combined for one origin. - For an origin the primary lists, the primary's value is read as described above. For any other origin, the single supplementary manifest that lists it is read the same way.
- A supplementary manifest that is not a JSON object, or whose
publishersmember is missing or not an object, makes every origin the primary does not list unreadable, since what it was meant to list cannot be known. - A primary that is missing, not a JSON object, or without a
publishersobject gives no entries for an origin, unless a supplementary manifest lists that origin; the origin is then unreadable, since the verifier cannot tell whether the primary would have listed it. - A report names the manifests it read as Verification §6.2 states.
tzun-verify-report/1names exactly one, so a verifier that writes it reads the primary alone (next bullet). - A verifier MAY offer no way to supply a supplementary manifest, and then reads the primary alone.
-
Networks come from the verifier's own registry, never from the artifact. A registry names at least these chains:
Chain Network Name eip155:1mainnetEthereum mainnet eip155:11155111testnetSepolia eip155:560048testnetHoodi Any other chain is
nullunless the verifier's registry names it. A verifier released before a chain was added to this table reports that chain's network asnull, which claims less, never more. -
Rotation. Rotating a key adds an entry for the new address and sets
validUntilon the old one; the nexttzun-verifyrelease pins both. A publication by the old address after itsvalidUntilfails (evm_sender_mismatch); one made before it still holds. -
Reserved members.
frameOriginsandvkeys(log and witness keys, §4.5) are reserved at the top level of a manifest. Atzun-anchor/1verifier ignores every top-level member exceptpublishers.
10. Verification
Given identical inputs, all conforming verifiers MUST agree, check by check, on the outcome and on the reason this section assigns.
10.1 Outcomes
The four outcomes are those of every row of a report (Verification §2.1). An implementation that exposes an anchor verdict as a value maps them as follows:
| Outcome | Meaning here | Verdict value |
|---|---|---|
checked-correct | Decided, and it holds | true |
checked-wrong | Decided, and it fails | false |
not-checked | This verifier implements the check but could not decide it | null |
abstain | The artifact uses a construction or version outside what this verifier implements | null, with an unsupported_* or legacy_* reason |
Rules.
- An abstention is never a failure. Meeting an unknown tag, a verifier abstains on the part that tag governs and on nothing more. An abstention is not a pass either (Verification §6.6).
- A failure is reported once, by the check that decided it.
- A check whose prerequisite was not decided (it failed, was not checked, or abstained) is
not-checkedwith the reasonblocked_by:<id of that prerequisite>. - Only a missing artifact is absent. An artifact that is present but unreadable is never reported as absent or pending.
- There is no overall "valid". An auditor who needs a check decided says so in their policy, and an undecided check then counts against the record (Verification §3.2).
- Every fact is derived again from the material, never read from a stored summary.
- Verification never throws. Any input may be hostile JSON.
Reasons. The lists below are closed, and a verifier MUST NOT use any reason not in them.
checked-wrong:
| Reason | Decided by |
|---|---|
malformed_anchor | step 2: the artifact's shape |
malformed_checkpoint | step 2: a checkpoint body |
malformed_merkle_path | step 2: an inclusion path |
origin_not_in_domain | step 2b |
chain_id_mismatch | step 2b |
leaf_index_mismatch | step 3 |
merkle_proof_invalid | step 4 |
anchor_proof_invalid | step 5 |
malformed_publication | step 6 dispatch; §10.4 items 1 and 2; §10.5 step 1 |
evm_tx_proof_invalid | §10.4 items 3 and 6 |
evm_calldata_mismatch | §10.4 item 4 |
evm_sender_mismatch | §10.4 items 5, 6 and 13 |
evm_chain_mismatch | §10.4 item 5 |
anchor_not_found_on_chain | §10.4 items 11 and 12 |
imprint_mismatch | §10.5 step 2 |
verification_failed | §10.5 steps 3 and 4 |
split_view | §10.8 |
log_inconsistent_with_anchor | §10.8 |
abstain: unsupported_anchor_spec and unsupported_merkle_spec (step 1),
legacy_blockchain_anchor (step 1), unsupported_publication_kind (step 6 dispatch; §10.4 item 1),
unsupported_evm_tx_type (§10.4 item 2).
not-checked:
| Reason | When |
|---|---|
absent | The record has no artifact (§10.9) |
not_yet_published | A bundle says the record is waiting for its epoch (§10.9) |
not_applicable | The check does not apply |
blocked_by:<id> | A prerequisite check was not decided |
input_not_supplied:sequence_number | Step 3 without payload.sequenceNumber |
input_not_supplied:p | §10.4 item 1, when P is not 32 bytes |
input_not_supplied:publisher_manifest | §10.4 item 5: the manifest is unreadable for the origin |
input_not_supplied:evm_header_source | §10.4 item 7 |
not_in_bundle:raw_transaction, not_in_bundle:block_header, not_in_bundle:tx_proof | §10.4 items 1, 6 and 12 |
not_in_bundle:consistency_proof | §10.8 |
no_expectation:publisher | §10.4 item 5: no manifest entry for the origin |
evm_header_source_timeout, evm_header_source_unavailable, evm_header_source_chain_mismatch, evm_block_not_finalized, evm_header_source_inconsistent | The header source's states, §10.4 items 8 to 12 |
no_matching_trust_anchor | §10.5 step 4 |
missing_dependency | §10.5 step 5 |
evm_verifier_error, anchor_verifier_error | The verifier failed (below) |
A publication's own outcome, and anchorReason (§10.6), can also carry publication_not_checked.
A reader that consults the store which wrote a record can also report anchor_not_yet_published
for a record whose epoch is not yet published; a verifier that holds only the record or a bundle
never does.
Publication level. Each publication has one outcome of its own, taken from its checks:
truewhen every one of its checks ischecked-correct;falsewhen one of its checks ischecked-wrong, with that check's reason;nullwith that check's reason when one of its checks abstains;- otherwise
nullwith the reasonpublication_not_checked, while its checks carry the detailednot-checkedreason. A token that chains to no supplied root is thereforenull,publication_not_checkedat the level of the publication, andno_matching_trust_anchoron itsanchor.tsa.trust[i]check (§10.5).
Failures of the verifier. evm_verifier_error, anchor_verifier_error and
missing_dependency report that the verifier failed, never that the artifact did. Each is a bug
or a missing library to report, not a verdict. evm_verifier_error is an unforeseen failure
inside the EVM publication checks (§10.4 item 15). anchor_verifier_error is any other
unforeseen failure while verifying an anchor: in steps 1 to 5, in the dispatch of step 6, on the
RFC 3161 path, or while the verdict is put together. It is reported as anchorReason and on every
step check.
10.2 Inputs
| Input | Source | Needed |
|---|---|---|
The anchor object and the record's digest | The record. The digest is canonicalDigest as recomputed from the payload (Record §4.2). | Yes. A verifier that cannot establish the digest reports the anchor and each step check not-checked, blocked by the check that could not (Bundle §7.4). |
sequenceNumber | payload.sequenceNumber | No. Without it, step 3 is not checked. |
chainId | The caller, or the bundle's chain.chainId | No. Without it, step 2b does not check the chain. |
| Publisher manifest (§9): the primary, and any supplementary ones a verifier accepts; the network registry | The verifier's own configuration | For EVM publications |
| Header sources, by CAIP-2 chain | The verifier's own configuration | For an EVM publication to be true |
timeoutMs | The caller | No. Default 30,000 ms (§10.4 item 8). |
| RFC 3161 trust roots, and any intermediate certificates | The verifier's own configuration | For an RFC 3161 publication to be true |
| Log and witness keys | The verifier's own configuration | Reserved (§4.5) |
A verifier holding an export bundle joins the bundle's material to each EVM entry of the envelope before checking it (§8.2; Bundle §7.4).
10.3 Steps 1 to 6
The steps run in order. A step that fails ends the anchor as false, and the step checks after it
report blocked_by. When any of steps 1 to 5 does not hold, step 6 does not run and no
publication check is reported.
| # | Check | Rule | Outcome when it does not hold |
|---|---|---|---|
| 1 | anchor.spec | Tags. anchorSpec is the string tzun-anchor/1 and merkleSpec is the string rfc6962. A missing tag counts as unknown, and so does an anchor value that is not a JSON object. When anchor is missing or null and blockchainAnchor is present and not null, the record is legacy. When neither is present the artifact is absent (§10.9). When both are present, anchor is verified and blockchainAnchor is not read here (Verification §4.7 gives it a row of its own). | Abstain: unsupported_anchor_spec, then unsupported_merkle_spec; legacy_blockchain_anchor |
| 2 | anchor.checkpoint | Shape and parse, in this order. The shape: the object has exactly the nine members of §8.1, leafIndex and anchorLeafIndex are integers, and publications is a non-empty array (each element is examined in step 6). Then both bodies parse under §4.2; a body that is not a string does not parse. Then both paths are arrays whose every element is a hash under Tree §4; null is not an empty path. | false: malformed_anchor, malformed_checkpoint, malformed_merkle_path |
| 2b | anchor.origin | Origin binding (§4.3.5). Both origins are Tzun origins (§4.3.1, with the chain segment rule of §4.3.2); the rest of the anchor origin is anchors; the rest of the log origin is not; and the two share a domain prefix. The kind. A log origin whose rest is not evidence/<chain segment> never holds a record, since a record has a leaf in its own chain's evidence log and nowhere else, and is chain_id_mismatch whether or not chainId is supplied, with one exception: a log whose namespace is exactly log.tzun.ai or exactly test.log.tzun.ai and whose kind is not evidence is an opaque hosted log (§4.3.3, §4.3.4). Its kind cannot be read and its chain is left unchecked; the leaf preimage rule keeps its leaves out of the record domain (Tree §5.2). A namespace that extends one of those names (test.log.tzun.ai.example) or sits beneath it (staging.log.tzun.ai) is not a hosted namespace, and an evidence origin under a hosted namespace still names its chain and is checked. The chain, when chainId is supplied: the evidence log origin's chain segment spells chainId (a chainId with no UTF-8 encoding spells nothing), and chainChecked is then true. With no chainId, or for an opaque hosted log, chainChecked is false. | false: origin_not_in_domain, chain_id_mismatch |
| 3 | anchor.leafIndex | Position. When sequenceNumber is supplied, leafIndex equals sequenceNumber − 1 (a sequenceNumber that is not a number never does), and leafIndexChecked is true. Without it, the check is not-checked, input_not_supplied:sequence_number, leafIndexChecked is false, and nothing after it is blocked, since no later step depends on step 3. | false: leaf_index_mismatch |
| 4 | anchor.inclusion | The record in its log. Tree §3.4 with the record's digest as the leaf, leafIndex as its index, the size and root of logCheckpoint, and logPath. | false: merkle_proof_invalid |
| 5 | anchor.anchorInclusion | The log in the anchor log. Tree §3.4 with checkpointDigest(logCheckpoint) as the leaf, anchorLeafIndex as its index, the size and root of anchorCheckpoint, and anchorPath. | false: anchor_proof_invalid |
| 6 | anchor.publication[i] | Each publication, separately, against P = checkpointDigest(anchorCheckpoint): the dispatch below, then §10.4 or §10.5. | Per publication |
Steps 1 to 5 need nothing beyond the record and the caller's sequenceNumber and chainId.
Step 6: dispatch. The elements of publications are verified one at a time, in array order.
For the element at index i, counting from 0, check anchor.publication[i] decides:
- an element that is not a JSON object (
null, an array, a string, a number or a boolean) isfalse,malformed_publication; - a
kindthat is missing, is not a string, or names a kind this verifier does not implement makes the check abstain,unsupported_publication_kind; a missingkindcounts as unknown, as a missing tag does in step 1; - otherwise the check is
checked-correct, and the kind's own checks follow: §10.4 forevm-calldata/1, §10.5 forrfc3161/1.
When anchor.publication[i] is not checked-correct, no other check is reported for that
element, and its publication-level outcome follows §10.1.
Check ids per publication. Every check of the publication at index i carries the suffix
[i]: anchor.publication[0], anchor.evm.tx[0], anchor.evm.canonical[0],
anchor.tsa.token[1], anchor.tsa.trust[1]. A blocked_by reason names the suffixed id. Two
publications of one kind, such as the tokens of two authorities (§7.3), are told apart by index.
10.4 evm-calldata/1
An EVM publication has two checks, whose ids end in the publication's index [i] (step 6):
anchor.evm.tx, the facts that can be established offline, and anchor.evm.canonical, the
header source's confirmation. The publication is true only when both are checked-correct. The
checks read the joined entry (§8.2), P, the anchor origin, the manifests, the header sources and
timeoutMs.
When anchor.evm.tx fails, abstains, or is not checked for any reason other than
not_in_bundle:block_header (item 6), anchor.evm.canonical is not-checked,
blocked_by:anchor.evm.tx[i].
Offline facts (anchor.evm.tx), in order.
-
Shape. Under step 6, the dispatch has already refused an element that is not an object and matched the kind. A verifier that checks one publication on its own applies those two rules itself and reports them on
anchor.evm.tx: a non-object element givesfalse,malformed_publication, and akindother thanevm-calldata/1abstains,unsupported_publication_kind. A P that is not 32 bytes isnot-checked,input_not_supplied:p; this can only happen when a publication is checked on its own. A missing ornullrawTransactionisnot-checked,not_in_bundle:raw_transaction. For every optional member,nullreads as missing. Otherwise the entry isfalse,malformed_publication, unless every member is well formed:rawTransaction:0xfollowed by a non-empty, even number of hex digits;blockHash, required, andtransactionId, optional:0xfollowed by 64 hex digits;blockNumber, required: an integer;blockHeader, optional:0xfollowed by a non-empty, even number of hex digits;txIndexandtxProof: both missing, or both present,txIndexan integer andtxProofa non-empty array whose every element is0xfollowed by a non-empty, even number of hex digits.
Hex digits may be of either case.
-
Transaction type, from the first byte of
rawTransaction(EIP-2718). A byte of0xc0or above is a legacy transaction;0x01and0x02are the typed transactions this version reads; a byte from0x80to0xbfisfalse,malformed_publication; any other byte abstains,unsupported_evm_tx_type. The transaction MUST decode, and its signature MUST yield a sender, or the entry isfalse,malformed_publication. -
Transaction id. When
transactionIdis present, it equalskeccak256(rawTransaction), or the check isfalse,evm_tx_proof_invalid. When it is missing, this item does not apply. -
Calldata. The transaction's data is exactly the 32 bytes of P, or the check is
false,evm_calldata_mismatch. -
Manifest. From the entries for the anchor origin, read from the primary and any supplementary manifests as §9 describes:
- unreadable:
not-checked,input_not_supplied:publisher_manifest; - no entries (no manifest, no
publishersmember, or no key for the origin among them):not-checked,no_expectation:publisher; - no entry whose address is the recovered sender:
false,evm_sender_mismatch; - no entry for the sender whose chain id equals the chain id the transaction signed:
false,evm_chain_mismatch. A pre-EIP-155 legacy transaction covers no chain id with its signature, so it can never pass this item.
The entries that name the sender on the signed chain are the matching entries.
- unreadable:
-
Header and trie proof, if carried.
- The header is the RLP of an Ethereum block header: one RLP list, with nothing after it, of at
least 15 items. Later forks append items (§7.2 rule 4), and a verifier accepts any number from
15 up. A verifier reads three of them, by position counting from 0:
transactionsRootat 4, a 32-byte string;numberat 8 andtimestampat 11, each a canonical RLP integer (no leading zero byte) no greater than 2^53 − 1. The other items are read only for the depth limit below;keccak256over the whole header binds them. A header that does not read this way isfalse,evm_tx_proof_invalid. keccak256(blockHeader)equalsblockHash, and the header'snumberequalsblockNumber, or the check isfalse,evm_tx_proof_invalid.- A header or trie node containing a list nested more than 64 lists deep, counting a top-level
list as depth 1, or a list whose content is not a sequence of RLP items, does not decode:
false,evm_tx_proof_invalid. Every list is walked for this rule, the items no other rule reads included. No real header or trie node comes near that depth; the limit keeps the verdict from depending on how far a decoder can recurse. - When
txIndexandtxProofare also present, walkingtxProoffrom the header'stransactionsRootalong the keyrlp(txIndex)reaches exactlyrawTransaction, or the check isfalse,evm_tx_proof_invalid.txProofis the path itself, root first: its first node hashes underkeccak256totransactionsRoot, each later node is the one its predecessor references by hash at the next step of the key, a node shorter than 32 bytes is read where it is embedded rather than looked up, and the walk ends at the last node with every node used once. A reordered, repeated, foreign or surplus node, a path that stops early, and a path that shows the key absent all fail. A trie proof carried without a header is not walked. - When the header is present, at least one matching entry is valid at the header's timestamp
(§9), or the check is
false,evm_sender_mismatch. The header is the artifact's own claim: it can make the publication fail, never make it pass. - When the header is missing and any matching entry has a bounded validity window,
anchor.evm.txstaysnot-checked,not_in_bundle:block_header, until the header source confirms a block time (item 13). This state does not blockanchor.evm.canonical.
- The header is the RLP of an Ethereum block header: one RLP list, with nothing after it, of at
least 15 items. Later forks append items (§7.2 rule 4), and a verifier accepts any number from
15 up. A verifier reads three of them, by position counting from 0:
The recipient is never read. No item reads the transaction's to. A publication sent to any
address verifies in the same way; sending to oneself is a writer's convention (§7.2 rule 3).
When items 1 to 6 hold, anchor.evm.tx is checked-correct. These facts alone never make the
publication true. Except for the sender's signature, all of them come from the bundle, including
the header, the block time and the chain. They show that a transaction carrying P bears the
publisher key's signature. They do not show that it was mined, when, or on which network: a key
holder could sign a transaction, never broadcast it, and fabricate a header with an earlier date.
The header source. The auditor supplies it; the artifact never does. It answers four
questions: which chain it serves; the height of the newest block it treats as final (the chain's
finalized tag, or a confirmation depth the auditor chooses); which block, with its hash and
timestamp, is canonical at a given height, if any; and, optionally, where a transaction with a
given id is, with its block and sender, if anywhere. The verifier trusts it for canonicality and
finality and for nothing else. A verifier MAY hold one header source per CAIP-2 chain, or one for
whatever chain a publication names, whose chain id item 9 then checks.
Its finality mode. A header source declares what it counts as final: finalized (the chain's
own tag), or a depth of confirmations. A verifier MUST report that mode with the result (the fact
finality of item 14, and anchorFinality, §10.6), so that a reader knows what "final" meant. On
Ethereum a depth below 64 blocks is not finality: 64 blocks are two epochs, the least distance by
which the finalized block can trail the head, and that distance has been measured at 64 to 96
blocks on Sepolia, Hoodi and mainnet. A header source that a verifier builds from the auditor's
settings SHOULD refuse any depth that is not a positive integer, and a depth below 64 unless the
auditor explicitly accepts blocks that are not final; the mode it reports then says so. A header
source handed to a verifier ready-made is taken at its word, and the verdict reports whatever
finality it declares.
Canonicality (anchor.evm.canonical), in order.
- Source. The header source for the chain of the matching entries. With none:
not-checked,input_not_supplied:evm_header_source. - One deadline. All header-source calls made while verifying one record share one deadline,
timeoutMsafter verification starts (30,000 ms unless the caller sets it). A verifier MUST enforce it. A call still waiting at the deadline isnot-checked,evm_header_source_timeout. A call that fails, or answers with something unusable, isnot-checked,evm_header_source_unavailable. - Chain. The source's chain id equals the chain id the transaction signed, or the check is
not-checked,evm_header_source_chain_mismatch. - Finality. A
blockNumberabove the source's final height isnot-checked,evm_block_not_finalized. - Block. The source's canonical block at
blockNumber:- none:
not-checked,evm_header_source_inconsistent, since a source that calls a height final must have a block there; - a hash other than
blockHash:false,anchor_not_found_on_chain; - when the bundle carried a header, a timestamp other than the header's:
not-checked,evm_header_source_inconsistent.
- none:
- Inclusion. Something has to place the transaction in that block: a trie proof verified in
item 6, or the source's own lookup of the transaction by its id. The lookup is optional.
- With a verified trie proof, a lookup that fails, runs out of time or finds nothing contradicts nothing.
- Without one, a lookup that fails or runs out of time is
evm_header_source_unavailableorevm_header_source_timeout, and a lookup that finds nothing places nothing. - A lookup that finds the transaction but gives no block hash or no sender, as for a pending
transaction, is
not-checked,evm_header_source_inconsistent. - A lookup that places the transaction in a different block: with a verified trie proof,
not-checked,evm_header_source_inconsistent. Without one,false,anchor_not_found_on_chain, but only when the source also reports that other block as canonical at its own height; otherwisenot-checked,evm_header_source_inconsistent. - A lookup that names the same block but a different sender:
not-checked,evm_header_source_inconsistent. - A lookup that names the same block and the same sender places the transaction; the fact
inclusionis thenheader_source, unless a trie proof has already made ittx_proof. - When nothing placed the transaction:
not-checked,not_in_bundle:tx_proof, ornot_in_bundle:block_headerwhen a trie proof was carried without a header.
- The validity window at the confirmed block time. When no matching entry is valid at the
block time the source confirmed, the publication is
false,evm_sender_mismatch. That failure is reported onanchor.evm.tx;anchor.evm.canonicalhas decided what it checks and stayschecked-correct. When a matching entry is valid andanchor.evm.txwas waiting on item 6'snot_in_bundle:block_header,anchor.evm.txbecomeschecked-correct. - Otherwise
anchor.evm.canonicalischecked-correct, with the factschain(the matching entries' chain),namespace(the namespace of the matching entries valid at the block time, ornullwhen they disagree or none is valid),network(from the verifier's registry),blockTime, andinclusion. Of the facts ofanchor.evm.canonical, onlyfinalityis given before the check is decided: from the moment the source's chain id has matched (item 9), it appears whatever the outcome,evm_block_not_finalizedincluded, and it is missing only when the source declares no mode. The facts ofanchor.evm.tx(§10.7) are reported whatever its outcome, once the transaction has decoded. - An unforeseen failure inside these checks is
not-checked,evm_verifier_error, onanchor.evm.tx, withanchor.evm.canonicalblocked by it. It signals a defect in the verifier, to be reported; it says nothing about the publication.
Silence is never an answer. A header source that returns nothing for a transaction has not said
that the transaction is off the chain: nodes expire their transaction indexes (a default node keeps
about a year), and endpoints behind a load balancer lag. Only an affirmative answer, a different
canonical block at the claimed height or the transaction in another block that the source calls
canonical, makes the publication false.
Inclusion by the header source. The header source is already trusted for canonicality, so
accepting its own lookup as evidence of inclusion adds no trust. A verifier with RPC access and no
trie proof can therefore reach true. It reports inclusion: "header_source", so the difference
stays visible.
10.5 rfc3161/1
An RFC 3161 publication has two checks, whose ids end in the publication's index [i] (step 6):
anchor.tsa.token, steps 1 to 3: whether the token is an RFC 3161 timestamp over P, signed by a timestamping certificate. It needs nothing from the verifier's configuration.anchor.tsa.trust, step 4: whether that certificate chains to a root the verifier's configuration supplies.
Order matters. The steps are taken one after another, the earliest that fails settles the
outcome, and the other check then reports blocked_by. A token over another value is therefore
false, whether or not it chains to a trusted root.
-
Parse (
anchor.tsa.token). All of the following hold, or the publication isfalse,malformed_publication:tokenis a non-empty string of base64 in canonical form: RFC 4648 §4, with its padding, pad bits zero (RFC 4648 §3.5), and no character outside the alphabet and=(so no whitespace and no URL-safe alphabet). It decodes to the DER of aTimeStampResp, with nothing after it, whosestatusisgranted(0) orgrantedWithMods(1) and which carries atimeStampToken;- that token is a CMS
SignedDatawhoseeContentTypeisid-ct-TSTInfo(RFC 3161 §2.4.2), with exactly oneSignerInfoand aTSTInfothat decodes; - its
certificatesinclude the certificate named by thesidof theSignerInfo, which the steps below call the signer certificate (§7.3 requirescertReq), and every element ofcertificatesdecodes as an X.509 certificate (RFC 5280), its public key included. A certificate whose public key does not decode under an algorithm and a curve the verifier implements, such as an EC point that is not on its named curve, is not a certificate, whether or not it is the signer's. A public key whose algorithm, named curve or curve parameters the verifier does not implement is not a decoding failure: its certificate is read with the key left unread, and step 5 applies to any step that needs that key. A curve OID assigned to no curve is read the same way, since no verifier can tell it from a curve it lacks.
-
Imprint (
anchor.tsa.token). ThemessageImprintof theTSTInfohas the hash algorithmid-sha256, with its parameters absent orNULL, and ahashedMessageequal to P. Otherwisefalse,imprint_mismatch. -
Signature and signer (
anchor.tsa.token). All of the following hold, or the publication isfalse,verification_failed:- The signature. The signed attributes contain exactly one
content-typeattribute, whose value isid-ct-TSTInfo, and exactly onemessage-digestattribute, whose value equals the digest of the encapsulatedTSTInfo. The signature over the signed attributes verifies under the public key of the signer certificate. - The signer binding. The signed attributes contain a
signingCertificateattribute (RFC 2634ESSCertID, which RFC 3161 §2.4.2 requires) or asigningCertificateV2attribute (RFC 5816ESSCertIDv2), or both, and neither more than once. In each one present, the first certificate identifier matches the signer certificate: itscertHashis the hash of the certificate's DER (SHA-1 forESSCertID; forESSCertIDv2the stated algorithm, SHA-256 when none is stated), and itsissuerSerial, when present, names the certificate's issuer, by a firstdirectoryNamewhose DER encoding is byte for byte the certificate's issuer Name, and its serial number. - A timestamping certificate. The signer certificate has an
extendedKeyUsageextension, marked critical, whose only purpose isid-kp-timeStamping(1.3.6.1.5.5.7.3.8), as RFC 3161 §2.3 requires. A certificate issued for another purpose, such as a TLS server or code-signing certificate, never yields a timestamp, even under a root the verifier trusts.
- The signature. The signed attributes contain exactly one
-
Trust (
anchor.tsa.trust). The verifier considers chains that lead from the signer certificate to a root its configuration supplies, in which the DER encoding of every certificate's issuer Name is byte for byte that of the subject Name of the certificate after it. Names are compared as bytes, with no RFC 5280 §7.1 string preparation: an issuer written as a UTF8String does not name a subject written as a PrintableString. Intermediate certificates come from the token's other certificates and from any the configuration supplies. A path is valid when:- each certificate's signature verifies under the public key of the next;
- every certificate strictly between the signer and the root is a CA:
basicConstraintswithcAtrue,keyUsageincludingkeyCertSignwhen that extension is present, and nopathLenConstraintexceeded; - every certificate on the path, the signer and the root included, is valid at
genTime(notBefore ≤ genTime ≤ notAfter).
A signer certificate that is itself a supplied root is a path of one certificate. The outcome is:
- some path is valid:
checked-correct, with the factsgenTimeandauthority.authorityis taken from the subject Name of the signer certificate alone: the value of its firstcommonNameattribute (2.5.4.3), taking the RDNs in the order they are encoded and the attributes of each RDN in the order they are encoded, written as the characters its string holds (a TeletexString read as ISO 8859-1, a BMPString as UTF-16), with no escaping, no trimming and no normalization, even when it is empty. When the subject has nocommonName,authorityis the empty string. Achecked-correcttrust check always carriesauthority; - some path reaches a supplied root, but none is valid:
false,verification_failed; - no path reaches a supplied root:
not-checked,no_matching_trust_anchor. "It does not chain to a root I trust" is not evidence of forgery.
A token chooses its own certificates, so a verifier MAY bound its search, for example by the length of a path and by the number of certificates and signatures it examines. A search that stops at such a bound has shown no path to a supplied root and says nothing against the token: it is
not-checked,no_matching_trust_anchor. The bounds MUST lie far above any real authority's path, which is a handful of certificates. -
Missing dependency. A step the verifier cannot carry out for want of a cryptographic dependency (a library, or an algorithm or encoding the token uses that the verifier's library does not support) is
not-checked,missing_dependency, on that step's check; any later check then reportsblocked_by.A key whose algorithm, named curve or curve parameters the verifier does not implement (step 1) is such an algorithm wherever a step needs that key: the signer's key at step 3, and at step 4 the key of each certificate that issues another on a path. A certificate in
certificatesthat no step needs is not examined further. At step 4, a valid path giveschecked-correctwhatever other candidates hold. Otherwise, when some path to a supplied root meets every rule of step 4 but for signatures the verifier cannot check, because the issuing key or the signature's algorithm is one it does not implement, the check isnot-checked,missing_dependency. Otherwise the outcomes of step 4 apply.A verifier MUST implement ECDSA on P-256, P-384 and P-521, and RSA PKCS #1 v1.5, each with SHA-256, SHA-384 and SHA-512, for the token's signature and for the signatures of step 4.
The publication is true when both checks are checked-correct. When it is undecided, its
publication-level reason is publication_not_checked (§10.1). An anchor whose only publication
chains to no supplied root is therefore anchorValid: null, anchorReason: publication_not_checked (§10.6).
10.6 The anchor verdict
An implementation that exposes an anchor verdict gives these fields. Which of them appear in a
tzun-verify report is stated in Verification §6.4.
| Field | Value |
|---|---|
anchorValid | false if any of the steps numbered 2 through 5 fails, or if any publication is false, since one false publication already shows the artifact to be untrue. Otherwise true if some publication is true, and null if none is. |
anchorReason | With false: the reason of the earliest failure, steps before publications and publications in array order. With null: step 1's abstention, if step 1 abstained; else publication_not_checked, if some publication is undecided; else the abstention reason of the first publication. If the digest could not be established (§10.2), the blocked_by reason. Left out with true. |
merkleProofValid | Steps 4 and 5 together: true when both hold, false when either fails, null when they did not run. |
anchorPublication | "evm-calldata", "rfc3161" or null: the strongest kind with a true publication, EVM first, so the rung an anchor stands on is always visible. null whenever anchorValid is not true. |
anchorOriginAuthenticated | true if and only if some evm-calldata/1 publication is true. |
anchorPosition | { origin, leafIndex, treeSize, anchorOrigin, anchorLeafIndex, anchorSize, leafIndexChecked, chainChecked, authenticated } when steps 2–5 all hold and none of the publications is false; null otherwise. It is a fact only as far as its flags say. |
anchorNetwork | "mainnet", "testnet" or null, from the matching entries' chain through the verifier's registry (§9), taken from the first true EVM publication in array order, and only when an EVM publication is true. Never from the artifact. |
anchorFinality | The finality mode (§10.4) of the header sources behind the true EVM publications: "finalized", or the declared depth, marked when the auditor accepted blocks that are not final. null when no EVM publication is true, when those sources declare different modes, and when a source declares none. |
anchorNamespace | "test" once step 2's shape check holds and anchorCheckpoint parses with a test origin (§4.3.4). Neither the manifest nor any later outcome changes that, because calling oneself a test is never an escalation. Before that point (an abstention in step 1, malformed_anchor, or anchor_verifier_error) it is null. Otherwise, when the origin is authenticated, the namespace of the manifest entries (null when the valid matching entries disagree); otherwise null. "production" therefore needs both an authenticated origin and an origin that is not a test origin. |
existedBy | The earliest proven time, with its source: the genTime of a true RFC 3161 publication or the block time of a true EVM publication, whichever is earlier (on a tie, an EVM publication first, since it also authenticates the origin; then the first in array order). Times are compared to the millisecond, the digits beyond it dropped, as §10.7 writes them, so two that differ only below the millisecond are a tie. A block time from a header no source confirmed never counts. |
A lying artifact yields no facts. When anchorValid is false, none of the facts of this table
is given: anchorPublication, anchorPosition, anchorNetwork, anchorFinality and existedBy
are null, anchorOriginAuthenticated is false, and anchorNamespace is null unless the
origin is a test origin.
An anchor that is true through RFC 3161 alone proves existence by a time and nothing more, and
every presentation of it MUST say so (§7.3).
10.7 Check catalogue
These ids are the same in every conforming verifier. Each check has one of the outcomes of §10.1.
The ids marked [i] carry the index of their publication (§10.3 step 6).
| Check id | Covers | Facts |
|---|---|---|
anchor | The anchor as a whole (§10.6), mirroring anchorValid: true gives checked-correct and false gives checked-wrong; null gives abstain if anchorReason is an abstention and not-checked if it is not. Its reason is anchorReason. §10.8 depends on it. | |
anchor.spec | Step 1 | |
anchor.checkpoint | Step 2 | |
anchor.origin | Step 2b | Whenever it is checked-correct, including when a later step fails: originAuthenticated, namespace (the value of anchorNamespace), chainChecked |
anchor.leafIndex | Step 3 | leafIndexChecked, on every outcome step 3 reaches |
anchor.inclusion | Step 4 | |
anchor.anchorInclusion | Step 5 | |
anchor.publication[i] | The dispatch of step 6 | |
anchor.evm.tx[i] | §10.4 items 1 to 6, and item 13 | Once the transaction decodes: transactionId, txType, sender, signedChainId |
anchor.evm.canonical[i] | §10.4 items 7 to 14 | When checked-correct: chain, namespace, network, blockTime, inclusion. Whatever the outcome, from the point where the source's chain id has matched: finality (§10.4 item 14) |
anchor.tsa.token[i] | §10.5 steps 1 to 3 | |
anchor.tsa.trust[i] | §10.5 step 4 | When checked-correct: genTime, authority |
anchor.newer | §10.8: the anchor of another checkpoint body of the record's log | When checked-correct: treeSize and anchorSize of the two bodies it authenticated |
anchor.consistency[<origin>] | §10.8, one check per origin | When checked-correct: fromSize, toSize |
anchor.pending | §10.9 |
Order. A verification of one anchor lists anchor, followed by the six step checks
(anchor.spec through anchor.anchorInclusion), followed by the checks of the publications,
publication by publication in array order and each opening with its anchor.publication[i]. A
reader looks a check up by its id, never by its position. A verifier holding a bundle adds
anchor.newer, the anchor.consistency[<origin>] checks and anchor.pending (Bundle
§7.5 to §7.7). A verifier that checks one EVM publication on its own names its two checks
anchor.evm.tx and anchor.evm.canonical, with no suffix.
Fact values.
| Fact | Value |
|---|---|
originAuthenticated, chainChecked, leafIndexChecked | boolean |
namespace | "production", "test" or null |
transactionId | 0x and 64 lowercase hex digits: keccak256 of the raw transaction |
txType | integer: the EIP-2718 type, 0 for a legacy transaction |
sender | 0x and 40 lowercase hex digits |
signedChainId | the signed chain id as a decimal string, or null for a legacy transaction signed before EIP-155 |
chain | the CAIP-2 chain of the matching entries |
network | "mainnet", "testnet" or null |
blockTime, genTime | a UTC time written YYYY-MM-DDThh:mm:ss.sssZ, with exactly three fraction digits; digits beyond the millisecond are dropped |
inclusion | "tx_proof" or "header_source" |
finality | "finalized", or { "confirmations": <integer> }, which also carries "acceptNonFinal": true when the depth is below 64 and the auditor accepted it |
authority | string (§10.5 step 4) |
treeSize, anchorSize, fromSize, toSize | integer |
10.8 Consistency between carried checkpoints
A verifier may hold two checkpoint bodies of one origin: an export bundle carries the record's own
logCheckpoint together with the newest one anchored when it was written, and a monitor fetches
bodies over time.
Both bodies are authenticated before they are compared. A body that no one published proves nothing about the log, so it can neither pass nor fail this check.
- The record's own two bodies are authenticated when the record's anchor is
true(checkanchor). - Any other body is authenticated only by an anchor of its own, carried with it or joined to it:
an anchor checkpoint of the same domain, the body's inclusion path in it, and that epoch's
publications. That anchor is verified by steps 2, 2b, 5 and 6 of §10.3, with the body in the
place of
logCheckpoint(steps 3 and 4 concern a record and are not required), and it authenticates the body, and its anchor checkpoint, when the result istrueunder §10.6. That verification is the checkanchor.newer, with the outcomes of theanchorrow of §10.7. A verifier that also holds the record's path in the other body MAY run every step, as a b1 bundle verifier does (Bundle §7.5). - The comparison is made per origin, as
anchor.consistency[<origin>], for every origin whose bodies the verifier carries or an authenticated anchor names. Bodies are compared only within one origin, and the origin, size and root always come from the parsed bodies. - A carried body of the origin that neither
anchornoranchor.newerauthenticated leaves the checknot-checked: blocked byanchoras long as the record's own anchor is nottrue, and byanchor.newerafter that. An origin whose carried bodies are all authenticated, but which has fewer than two authenticated bodies, has nothing to compare and reports no check.
Between two authenticated bodies:
- the same size and the same root:
checked-correct, withfromSizeandtoSizeboth that size; - the same size and different roots:
false,split_view. That is a finding only when both bodies are authenticated; - sizes
m < nwith a consistency proof carried between exactly those sizes: Tree §3.5, with both sizes and both roots taken from the parsed bodies, never from separate fields. A proof that is not an array of hashes under Tree §4, or that does not verify, isfalse,log_inconsistent_with_anchor; - sizes
m < nwith no proof carried:not-checked,not_in_bundle:consistency_proof.
10.9 Unanchored records
A record with no anchor member (§8.4) and no blockchainAnchor is unanchored. A verifier that
holds only the record reports anchor and each of the six step checks as not-checked, absent.
That is the only case reported as absent: an artifact that is present, however unreadable, is
never absent (§10.1).
An export bundle can state that its record is waiting for its epoch. That state is reported as
anchor.pending, not-checked, not_yet_published: never an abstention and never absent
(Bundle §7.7).
A reader that also consults the store which wrote a record can tell more, such as whether the record's epoch is published or is waiting for finality. That is outside this specification.
10.10 Consumers that monitor
A monitoring consumer, such as a scanner that re-verifies stored records, MUST treat
unsupported_anchor_spec, unsupported_merkle_spec and legacy_blockchain_anchor as findings,
although each verdict is an abstention: stored evidence that can no longer be verified is worth
raising. It MUST NOT raise publication_not_checked, not_yet_published or
anchor_not_yet_published, which are the ordinary state of a deployment with no header source and
of a record waiting for its epoch (Tree §6.1).
11. Writer obligations a verifier relies on
The checks of §10 detect a writer that breaks these obligations. A domain's anchoring writer MUST:
- Never publish or sign a checkpoint inconsistent with one it has already published or
signed. Before storing a new checkpoint of a log with an anchored predecessor, it generates
PROOF(m_prev, n)and verifies it with a verifier implemented separately from its generator (Tree §5.3). When that fails, it does not anchor that log, reportslog_inconsistent_with_anchor, and carries on with the other logs.split_viewandlog_inconsistent_with_anchor(§10.8) are findings because an honest writer never produces them. - Write each record artifact once, after finality for EVM, or once the token is in hand when publishing only through RFC 3161 (§8.4).
- Keep the material of §7.2 rule 4 for every confirmed EVM publication, so that exports can carry it.
- Publish only what it re-derives. Before it signs or requests a publication of P, the writer rebuilds the anchor checkpoint from the anchor log's leaves. It explains every leaf above the last size it published as the digest of a checkpoint of one of its source logs, whose root it rebuilds from that log's leaves, starting from a predecessor checkpoint whose body it has verified under its last published root. It anchors only source logs whose leaves its store binds to stored data: for an evidence log, a leaf only as a copy of the record stored at that position (Tree §5.2).
- Authenticate its own history from outside its store. The writer accepts an earlier publication as its own only by the publication's signed transaction and a sender that its configuration or its manifest names, inside that sender's validity window at a block time a header source confirms. It treats a publication as final only when a header source confirms its block. A row in its own database that says a publication was made, or was final, is not enough: whoever can write that database could have written the row.
12. What this specification does not claim
- Compliance with RFC 9162, or being a Certificate Transparency log (Tree §7.1).
- That a checkpoint is a C2SP signed note. A
tzun-anchor/1checkpoint is the note body alone (§4.5). - That an anchor published only through RFC 3161 authenticates its origin, its publisher or its position. It proves existence by a time only (§7.3).
- Economic security from a test network such as Sepolia or Hoodi.
- That an anchor dates a signature. It dates a record's digest at its position in its log.
- That an anchor alone shows a digest in a log is what was captured. An anchor proves that a digest held a position by a time; that the digest is the one first recorded at that position rests on how the store wrote the leaf (Tree §5.2).
- That Tzun is a timestamp authority. It is not (§7.3).
- That an operator holding both the database and the publisher key cannot anchor a forged history. It can, though only with later dates, since a block time cannot be set in the past. It can be detected only through the accounting of the publisher's transactions, an earlier export or checkpoint, a write-once copy (§7.3), a monitor the customer runs, or an independent witness. This specification does not claim that gap is closed.
- That a party able to write a domain's anchoring state cannot stop that domain's anchoring. It can: a writer that finds a leaf or a publication it cannot explain stops rather than publish past it (§11). What such a party cannot do, without the publisher key, is make the domain publish what its writer did not derive.
13. Conformance vectors
The files below, in the conformance vectors (Vectors), pin this document (Specs
§5). Each file's source member, generator in evidence-log-origin-vectors.json, says how it
was generated.
| File | Pins |
|---|---|
checkpoint-vectors.json | §4 to §6 and steps 1 to 6 of §10.3: bodies that parse, with their digests; bodies refused as malformed_checkpoint; chain segments both ways, and those refused; the origin binding with and without a chain id, hosted namespaces and their lookalikes included; an epoch's anchor-log leaves in origin order, the anchor root, the anchor checkpoint body and P; and artifact cases, each with the §10.6 fields and the exact §10.7 checks, in report order. No case carries a raw transaction or a token, so no publication in it is true. |
evidence-log-origin-vectors.json | Origins of evidence logs (§4.3.2) for ASCII and multi-byte chain ids, chain ids a store refuses, and ns names that are valid and invalid. |
anchor-e2e-vectors.json | Records anchored end to end on a local EVM chain: each epoch's anchor checkpoint, P and consistency proofs, each publication with its raw transaction, header, transaction index and trie proof, the manifest, and the header source's answers, with the verdict each record is given with and without the header source. |
merkle-rfc6962-vectors.json, merkle-consistency-vectors.json | The tree profile (Tree §9). |
bundle-b1/ | Whole bundles (Bundle §13). They exercise §10.4 with signed transactions, fabricated headers, missing header sources, blocks that are not final, and manifests that do and do not name the sender; §10.8 with newer anchors, split views and consistency proofs that are tampered or missing; and the steps of §10.5 through the record's own timestamp token. |
An implementation conforms to this document when it gives every case of these files its stated
outcome, reason and check rows. A test of missing_dependency (§10.5 step 5) cannot be a vector,
since a file cannot take a dependency away; each implementation tests it on its own.