Contents

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 canonicalDigest and its payload.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-256 is FIPS 180-4 SHA-256. keccak256 is the Keccak-256 hash Ethereum uses, which is not FIPS 202 SHA3-256.
  • ‖ joins raw bytes. LF is the byte 0x0A.
  • A "hash" with no further qualification is 32 bytes written as Tree §4 requires: 64 hex digits, no prefix. Wherever a 0x prefix 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
TermIn this document
DomainOne 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.
StoreAn evidence store that a deployment runs for itself.
Source logA log whose checkpoints the domain's anchor log records. Every evidence log is a source log.
Checkpoint bodyThe three-line statement of a log's origin, size and root at one size (§4).
Anchor logThe domain's second-level log, whose leaves are checkpoint digests of its source logs (§5).
EpochOne append of source-log checkpoint digests to the anchor log, together with the publications of the anchor checkpoint that append produces.
PWhat an epoch publishes: the checkpoint digest of the anchor log's body at the size that epoch reached (§6).
PublicationOne external commitment to P, of a named kind (§7).
Publisher manifestThe verifier's own statement of which addresses may publish for which anchor origin (§9).
Header sourceThe 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), and merkleSpec for 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 test whatever else it finds (§10.6).

3. Source logs

  1. The kinds defined here. evidence: one log per evidence chain, whose leaf rule is Tree §5.2. anchors: the domain's anchor log (§5).
  2. 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).
  3. 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
LineContent
originThe log's name (§4.3).
tree sizeThe log's size in ASCII decimal: no sign, no leading zero, at least 1, at most 2^53 − 1 (9007199254740991).
rootMTH(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:

  1. every byte is either LF or in the range 0x21 to 0x7E;

  2. 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);

  3. 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:
    • anchors, which names the domain's anchor log (§5);
    • evidence/<chain segment>, which names a store's evidence log for one chain (§4.3.2);
    • one other segment that is neither anchors nor evidence, which names a store's source log by its kind or a hosted deployment's log by an opaque id.
  • 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 but evidence.
  • Comparison. Origins are compared as byte strings; two origins are equal only when their bytes are.
  • Per-chain kinds. evidence is the only kind that takes a chain segment in tzun-anchor/1, and step 4 of §10.3 applies the evidence leaf rule. A per-chain log with a different leaf rule therefore needs a new anchorSpec, not only a new kind.

4.3.2 Store origins (writers)

LogOrigin
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
  • ns is a lowercase DNS-style name: one or more labels separated by ., each label made of a–z and 0–9 with - allowed only inside it, each label at most 63 characters and the whole name at most 253. A store that is not configured otherwise uses local.invalid; the .invalid top-level name is reserved (RFC 6761), so that default can never be mistaken for a routable name.
  • storeId is 128 bits from a cryptographically secure random source, written as 32 lowercase hex characters.
  • ns and storeId are fixed when a store is created and never change afterwards. A store whose data is lost and recreated takes a new storeId, 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 its validUntil (§9).
  • The chain segment is the lowercase hex of the UTF-8 bytes of the chain id, two characters per byte: the chain default gives 64656661756c74. 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.
  • k consists of lowercase ASCII letters and digits and starts with a letter. evidence and anchors are reserved and are never a k.

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)

LogOrigin
Any source loglog.tzun.ai/<deploymentId>/<logId>
The anchor loglog.tzun.ai/<deploymentId>/anchors
  • deploymentId is 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 new deploymentId, lists the new anchor origin in the manifest (§9) and closes the old entry with validUntil. It never restarts a log under an origin that has already published.
  • logId is 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 evidence under 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 is log.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 an ns whose first label is test. A hosted deployment that anchors for testing uses test.log.tzun.ai as 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 domain cases 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/1 checkpoint 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 is cosignature/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 is SHA-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 N it publishes, the anchor log has a checkpoint body whose origin is the anchor origin. Successive published sizes are joined by PROOF(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 0x followed 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 defines evm-calldata/1 (§7.2) and rfc3161/1 (§7.3), and reserves witness/1 (§7.4).
  • A publication entry is a JSON object whose kind member 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.

  1. Chain. The chain is named in CAIP-2 form, eip155:<chain id>, with the chain id in decimal and no leading zero. Ethereum mainnet is eip155:1; the Sepolia test network is eip155:11155111; the Hoodi test network is eip155:560048. A domain names its chain in its configuration; this version fixes none.
  2. 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.
  3. Transaction. value is 0 and data is exactly the 32 bytes of P: the calldata is 0x ‖ 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 on to and §10.4 never reads it: anyone can send any transaction to any address, so the recipient says nothing about who published.
  4. 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_hash and slot_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.

  1. The writer sends an RFC 3161 TimeStampReq whose messageImprint is { hashAlgorithm: id-sha256, hashedMessage: P } to each timestamp authority the deployment uses, with certReq set 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).
  2. The writer keeps the DER TimeStampResp and carries it in base64. Each authority's token is a publication entry of its own.
  3. 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: anchorOriginAuthenticated is false).
  • 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 genTime values, 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 sourceevm-calldata/1, offlinerfc3161/1
P existed by a timeYes, the block's timeNoYes, the token's genTime
Who published (the anchor origin)Yes, through the manifestNoNo
Every publication can be enumeratedYesYesNo
Publicly visibleYesYesNo: the operator holds the only copy
Makes anchorValid trueYesNo (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:

MemberTypeContent
anchorSpecstring"tzun-anchor/1"
merkleSpecstring"rfc6962" (Tree)
leafIndexintegerWhere the record sits in its evidence log; it MUST equal payload.sequenceNumber − 1
logCheckpointstringA checkpoint body (§4) for the evidence log holding the record, taken at some size n > leafIndex
logPatharray of hashesPATH(leafIndex, D[n]), leaf end first (Tree §3.3)
anchorLeafIndexintegerThe index of checkpointDigest(logCheckpoint) in the anchor log
anchorCheckpointstringA checkpoint body for the domain's anchor log, taken at the size N its epoch published, with N > anchorLeafIndex
anchorPatharray of hashesPATH(anchorLeafIndex, A[N]), leaf end first
publicationsarrayOne 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:

MemberTypeContent
kindstring"evm-calldata/1"
chainstringCAIP-2 chain. Shown to people only; verification uses the chain the manifest names.
transactionIdstring0x and 64 lowercase hex digits: keccak256 of the raw signed transaction
blockNumberintegerThe number of the block that includes the transaction
blockHashstring0x and 64 lowercase hex digits
publisherstring0x 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:

MemberTypeContent
kindstring"rfc3161/1"
tokenstringThe 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 under nonRepudiation is hashed into the record's digest (Record §2.4). Adding it changes no canonicalDigest, and the envelope stays at envelopeVersion e1.
  • Only once anchored. A record that is not yet anchored has no anchor member. 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. anchorCheckpoint and publications are the same in every record of an epoch, and anchorLeafIndex and anchorPath are 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.
  • blockchainAnchor is not part of this format. nonRepudiation.blockchainAnchor is a legacy member that no writer populates, and a writer MUST NOT write it. A verifier that finds it without an anchor abstains (§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 }
    ]
  }
}
  • publishers maps each anchor origin, compared as bytes, to its list of entries.

  • Entry members.

    • address: 0x followed 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".

    • validFrom and validUntil: absent or null for 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 )
      

      T and Z are 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 second 60 is refused; an offset's hour is 00 to 23 and its minute 00 to 59. A value outside this, such as 2026-09-31 or T24: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 validFrom up and validUntil down to a whole second. A validFrom of 2026-10-01T00:00:00.0005Z admits a block at 00:00:01 and not one at 00:00:00. An implementation MUST NOT convert the value to a type with millisecond precision before comparing, since .0005 would 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 publishers member 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 chain is not a string: unreadable (input_not_supplied:publisher_manifest);
    • entries whose chain does not begin with eip155: 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.
  • Where manifests come from.

    • Tzun lists its own publishers in one public manifest, served at https://tzun.ai/anchors/publishers.json beside a page for people at https://tzun.ai/anchors/. Each tzun-verify release 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.
  • 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 publishers in 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 publishers member 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 publishers object 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/1 names 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:

    ChainNetworkName
    eip155:1mainnetEthereum mainnet
    eip155:11155111testnetSepolia
    eip155:560048testnetHoodi

    Any other chain is null unless the verifier's registry names it. A verifier released before a chain was added to this table reports that chain's network as null, which claims less, never more.

  • Rotation. Rotating a key adds an entry for the new address and sets validUntil on the old one; the next tzun-verify release pins both. A publication by the old address after its validUntil fails (evm_sender_mismatch); one made before it still holds.

  • Reserved members. frameOrigins and vkeys (log and witness keys, §4.5) are reserved at the top level of a manifest. A tzun-anchor/1 verifier ignores every top-level member except publishers.

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:

OutcomeMeaning hereVerdict value
checked-correctDecided, and it holdstrue
checked-wrongDecided, and it failsfalse
not-checkedThis verifier implements the check but could not decide itnull
abstainThe artifact uses a construction or version outside what this verifier implementsnull, 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-checked with the reason blocked_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:

ReasonDecided by
malformed_anchorstep 2: the artifact's shape
malformed_checkpointstep 2: a checkpoint body
malformed_merkle_pathstep 2: an inclusion path
origin_not_in_domainstep 2b
chain_id_mismatchstep 2b
leaf_index_mismatchstep 3
merkle_proof_invalidstep 4
anchor_proof_invalidstep 5
malformed_publicationstep 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:

ReasonWhen
absentThe record has no artifact (§10.9)
not_yet_publishedA bundle says the record is waiting for its epoch (§10.9)
not_applicableThe check does not apply
blocked_by:<id>A prerequisite check was not decided
input_not_supplied:sequence_numberStep 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_inconsistentThe 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_errorThe 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:

  • true when every one of its checks is checked-correct;
  • false when one of its checks is checked-wrong, with that check's reason;
  • null with that check's reason when one of its checks abstains;
  • otherwise null with the reason publication_not_checked, while its checks carry the detailed not-checked reason. A token that chains to no supplied root is therefore null, publication_not_checked at the level of the publication, and no_matching_trust_anchor on its anchor.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

InputSourceNeeded
The anchor object and the record's digestThe 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).
sequenceNumberpayload.sequenceNumberNo. Without it, step 3 is not checked.
chainIdThe caller, or the bundle's chain.chainIdNo. Without it, step 2b does not check the chain.
Publisher manifest (§9): the primary, and any supplementary ones a verifier accepts; the network registryThe verifier's own configurationFor EVM publications
Header sources, by CAIP-2 chainThe verifier's own configurationFor an EVM publication to be true
timeoutMsThe callerNo. Default 30,000 ms (§10.4 item 8).
RFC 3161 trust roots, and any intermediate certificatesThe verifier's own configurationFor an RFC 3161 publication to be true
Log and witness keysThe verifier's own configurationReserved (§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.

#CheckRuleOutcome when it does not hold
1anchor.specTags. 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
2anchor.checkpointShape 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
2banchor.originOrigin 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
3anchor.leafIndexPosition. 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
4anchor.inclusionThe 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
5anchor.anchorInclusionThe 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
6anchor.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:

  1. an element that is not a JSON object (null, an array, a string, a number or a boolean) is false, malformed_publication;
  2. a kind that is missing, is not a string, or names a kind this verifier does not implement makes the check abstain, unsupported_publication_kind; a missing kind counts as unknown, as a missing tag does in step 1;
  3. otherwise the check is checked-correct, and the kind's own checks follow: §10.4 for evm-calldata/1, §10.5 for rfc3161/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.

  1. 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 gives false, malformed_publication, and a kind other than evm-calldata/1 abstains, unsupported_publication_kind. A P that is not 32 bytes is not-checked, input_not_supplied:p; this can only happen when a publication is checked on its own. A missing or null rawTransaction is not-checked, not_in_bundle:raw_transaction. For every optional member, null reads as missing. Otherwise the entry is false, malformed_publication, unless every member is well formed:

    • rawTransaction: 0x followed by a non-empty, even number of hex digits;
    • blockHash, required, and transactionId, optional: 0x followed by 64 hex digits;
    • blockNumber, required: an integer;
    • blockHeader, optional: 0x followed by a non-empty, even number of hex digits;
    • txIndex and txProof: both missing, or both present, txIndex an integer and txProof a non-empty array whose every element is 0x followed by a non-empty, even number of hex digits.

    Hex digits may be of either case.

  2. Transaction type, from the first byte of rawTransaction (EIP-2718). A byte of 0xc0 or above is a legacy transaction; 0x01 and 0x02 are the typed transactions this version reads; a byte from 0x80 to 0xbf is false, malformed_publication; any other byte abstains, unsupported_evm_tx_type. The transaction MUST decode, and its signature MUST yield a sender, or the entry is false, malformed_publication.

  3. Transaction id. When transactionId is present, it equals keccak256(rawTransaction), or the check is false, evm_tx_proof_invalid. When it is missing, this item does not apply.

  4. Calldata. The transaction's data is exactly the 32 bytes of P, or the check is false, evm_calldata_mismatch.

  5. 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 publishers member, 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.

  6. 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: transactionsRoot at 4, a 32-byte string; number at 8 and timestamp at 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; keccak256 over the whole header binds them. A header that does not read this way is false, evm_tx_proof_invalid.
    • keccak256(blockHeader) equals blockHash, and the header's number equals blockNumber, or the check is false, 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 txIndex and txProof are also present, walking txProof from the header's transactionsRoot along the key rlp(txIndex) reaches exactly rawTransaction, or the check is false, evm_tx_proof_invalid. txProof is the path itself, root first: its first node hashes under keccak256 to transactionsRoot, 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.tx stays not-checked, not_in_bundle:block_header, until the header source confirms a block time (item 13). This state does not block anchor.evm.canonical.

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.

  1. Source. The header source for the chain of the matching entries. With none: not-checked, input_not_supplied:evm_header_source.
  2. One deadline. All header-source calls made while verifying one record share one deadline, timeoutMs after verification starts (30,000 ms unless the caller sets it). A verifier MUST enforce it. A call still waiting at the deadline is not-checked, evm_header_source_timeout. A call that fails, or answers with something unusable, is not-checked, evm_header_source_unavailable.
  3. Chain. The source's chain id equals the chain id the transaction signed, or the check is not-checked, evm_header_source_chain_mismatch.
  4. Finality. A blockNumber above the source's final height is not-checked, evm_block_not_finalized.
  5. 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.
  6. 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_unavailable or evm_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; otherwise not-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 inclusion is then header_source, unless a trie proof has already made it tx_proof.
    • When nothing placed the transaction: not-checked, not_in_bundle:tx_proof, or not_in_bundle:block_header when a trie proof was carried without a header.
  7. 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 on anchor.evm.tx; anchor.evm.canonical has decided what it checks and stays checked-correct. When a matching entry is valid and anchor.evm.tx was waiting on item 6's not_in_bundle:block_header, anchor.evm.tx becomes checked-correct.
  8. Otherwise anchor.evm.canonical is checked-correct, with the facts chain (the matching entries' chain), namespace (the namespace of the matching entries valid at the block time, or null when they disagree or none is valid), network (from the verifier's registry), blockTime, and inclusion. Of the facts of anchor.evm.canonical, only finality is 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_finalized included, and it is missing only when the source declares no mode. The facts of anchor.evm.tx (§10.7) are reported whatever its outcome, once the transaction has decoded.
  9. An unforeseen failure inside these checks is not-checked, evm_verifier_error, on anchor.evm.tx, with anchor.evm.canonical blocked 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.

  1. Parse (anchor.tsa.token). All of the following hold, or the publication is false, malformed_publication:

    • token is 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 a TimeStampResp, with nothing after it, whose status is granted (0) or grantedWithMods (1) and which carries a timeStampToken;
    • that token is a CMS SignedData whose eContentType is id-ct-TSTInfo (RFC 3161 §2.4.2), with exactly one SignerInfo and a TSTInfo that decodes;
    • its certificates include the certificate named by the sid of the SignerInfo, which the steps below call the signer certificate (§7.3 requires certReq), and every element of certificates decodes 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.
  2. Imprint (anchor.tsa.token). The messageImprint of the TSTInfo has the hash algorithm id-sha256, with its parameters absent or NULL, and a hashedMessage equal to P. Otherwise false, imprint_mismatch.

  3. Signature and signer (anchor.tsa.token). All of the following hold, or the publication is false, verification_failed:

    • The signature. The signed attributes contain exactly one content-type attribute, whose value is id-ct-TSTInfo, and exactly one message-digest attribute, whose value equals the digest of the encapsulated TSTInfo. The signature over the signed attributes verifies under the public key of the signer certificate.
    • The signer binding. The signed attributes contain a signingCertificate attribute (RFC 2634 ESSCertID, which RFC 3161 §2.4.2 requires) or a signingCertificateV2 attribute (RFC 5816 ESSCertIDv2), or both, and neither more than once. In each one present, the first certificate identifier matches the signer certificate: its certHash is the hash of the certificate's DER (SHA-1 for ESSCertID; for ESSCertIDv2 the stated algorithm, SHA-256 when none is stated), and its issuerSerial, when present, names the certificate's issuer, by a first directoryName whose DER encoding is byte for byte the certificate's issuer Name, and its serial number.
    • A timestamping certificate. The signer certificate has an extendedKeyUsage extension, marked critical, whose only purpose is id-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.
  4. 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: basicConstraints with cA true, keyUsage including keyCertSign when that extension is present, and no pathLenConstraint exceeded;
    • 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 facts genTime and authority. authority is taken from the subject Name of the signer certificate alone: the value of its first commonName attribute (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 no commonName, authority is the empty string. A checked-correct trust check always carries authority;
    • 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.

  5. 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 reports blocked_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 certificates that no step needs is not examined further. At step 4, a valid path gives checked-correct whatever 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 is not-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.

FieldValue
anchorValidfalse 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.
anchorReasonWith 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.
merkleProofValidSteps 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.
anchorOriginAuthenticatedtrue 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.
anchorFinalityThe 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.
existedByThe 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 idCoversFacts
anchorThe 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.specStep 1
anchor.checkpointStep 2
anchor.originStep 2bWhenever it is checked-correct, including when a later step fails: originAuthenticated, namespace (the value of anchorNamespace), chainChecked
anchor.leafIndexStep 3leafIndexChecked, on every outcome step 3 reaches
anchor.inclusionStep 4
anchor.anchorInclusionStep 5
anchor.publication[i]The dispatch of step 6
anchor.evm.tx[i]§10.4 items 1 to 6, and item 13Once the transaction decodes: transactionId, txType, sender, signedChainId
anchor.evm.canonical[i]§10.4 items 7 to 14When 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 4When checked-correct: genTime, authority
anchor.newer§10.8: the anchor of another checkpoint body of the record's logWhen checked-correct: treeSize and anchorSize of the two bodies it authenticated
anchor.consistency[<origin>]§10.8, one check per originWhen 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.

FactValue
originAuthenticated, chainChecked, leafIndexCheckedboolean
namespace"production", "test" or null
transactionId0x and 64 lowercase hex digits: keccak256 of the raw transaction
txTypeinteger: the EIP-2718 type, 0 for a legacy transaction
sender0x and 40 lowercase hex digits
signedChainIdthe signed chain id as a decimal string, or null for a legacy transaction signed before EIP-155
chainthe CAIP-2 chain of the matching entries
network"mainnet", "testnet" or null
blockTime, genTimea 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
authoritystring (§10.5 step 4)
treeSize, anchorSize, fromSize, toSizeinteger

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 (check anchor).
  • 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 is true under §10.6. That verification is the check anchor.newer, with the outcomes of the anchor row 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 anchor nor anchor.newer authenticated leaves the check not-checked: blocked by anchor as long as the record's own anchor is not true, and by anchor.newer after 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, with fromSize and toSize both that size;
  • the same size and different roots: false, split_view. That is a finding only when both bodies are authenticated;
  • sizes m < n with 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, is false, log_inconsistent_with_anchor;
  • sizes m < n with 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:

  1. 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, reports log_inconsistent_with_anchor, and carries on with the other logs. split_view and log_inconsistent_with_anchor (§10.8) are findings because an honest writer never produces them.
  2. Write each record artifact once, after finality for EVM, or once the token is in hand when publishing only through RFC 3161 (§8.4).
  3. Keep the material of §7.2 rule 4 for every confirmed EVM publication, so that exports can carry it.
  4. 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).
  5. 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/1 checkpoint 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.

FilePins
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.jsonOrigins 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.jsonRecords 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.jsonThe 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.