# TACET — Verifiable Silence for AI Training Disclosure

**Version:** 0.1-draft · **Status:** Working draft · **License:** CC0 (text), Apache-2.0 (code)
**Editor:** Crovia Trust · `info@croviatrust.com`

> *tacet* (Latin, "it is silent"): the notation a composer writes on a part that
> does not play for a whole movement. The silence is deliberate, measured, and
> written into the score so that anyone reading it knows exactly where it
> begins and ends.

---

## 1. Purpose

Existing transparency logs prove **presence**: a certificate was logged, a
binary was published, a key was registered. TACET is a transparency log whose
primary product is a portable, offline-verifiable proof of **absence over
time**: that for a given AI model, no training-data disclosure satisfying a
public predicate existed in the log, and none was found on the model's
monitored public surfaces, across a contiguous range of anchored epochs.

TACET turns the sentence *"no contemporaneous disclosure was observed between
date A and date B"* from a signed opinion of an observer into an arithmetic
fact that anyone can check against Bitcoin, a public randomness beacon, and
the signatures of independent witnesses. It does not accuse. It records
auditability debt with a defined, reproducible unit.

## 2. Design constraints

1. **Uses only established primitives.** SHA-256, Ed25519 (RFC 8032), sparse
   Merkle trees, RFC 8785-style canonical JSON (CSC-1, as in Crovia Seal),
   drand public randomness, OpenTimestamps. Nothing here needs new
   cryptography; the contribution is the object and the protocol.
2. **Reuses Crovia Seal unchanged.** Every artifact that leaves TACET is
   wrapped in a `crovia.seal.v1` object as specified in `draft-crovia-seal-01`.
   TACET adds no field, no modality and no anchor kind to the Seal
   specification. Any conformant Seal verifier can verify the outer layer.
3. **Trust-minimised.** The log operator cannot back-date, forward-date, or
   silently rewrite an epoch without being caught by anyone holding a prior
   epoch sheet, a beacon value, or a Bitcoin anchor. Witnesses are optional
   and permissionless.
4. **Silence never accrues without observation.** The silence counter of a
   target is a function of anchored, negative observations only. When
   observation stops, the counter stops.
5. **Non-accusatory by construction.** A TACET proof states a property of
   the log and of committed surface snapshots. It carries no verdict field.

## 3. Terminology

| Term | Meaning |
|---|---|
| **Target** | An AI model identified by a canonical `target_id` (§4.1). |
| **Slot** | The position of a target in the verifiable map (§5). |
| **Value** | What a slot holds: nothing, a *commitment*, a *disclosure*, or a *retraction* (§5.3). |
| **Epoch** | A fixed observation period (default: 1 hour, aligned to UTC). Each epoch produces one *epoch sheet*. |
| **Epoch sheet** | The signed head of the map for one epoch, with its temporal bounds (§6). |
| **Snapshot** | A commitment to the bytes fetched from one public surface of a target during an epoch, with the result of the predicate (§7). |
| **Predicate** | A published, versioned, deterministic rule that decides whether fetched bytes constitute a disclosure (§7.3). |
| **Silence proof** | A proof that a slot was empty across a range of epochs, at one of three strength levels (§8). |
| **Witness** | An independent party that countersigns epoch sheets it has verified (§9). |
| **Operator** | The party running the map and issuing epoch sheets (initially Crovia Trust). |

Requirement words (MUST, SHOULD, MAY) are used as in RFC 2119.

## 4. Canonical encoding

### 4.1 Target identifiers

A `target_id` is a UTF-8 string of the form `<organization>/<model>` as used by
the hosting platform (e.g. `meta-llama/Llama-3.1-8B`). Normalisation: Unicode
NFC, trim ASCII whitespace, no case folding (platform ids are case-sensitive).
Identifiers that are not of the form `org/model`, or that denote internal
infrastructure (collectors, file paths, test ids), MUST NOT be inserted into
the map. An operator MUST publish its exclusion rules.

The slot **key** is `SHA-256(UTF-8(target_id))`, a 256-bit value, read
MSB-first as the path from the root of the map.

### 4.2 Canonical JSON

All TACET objects are canonicalised with **CSC-1**, the canonical JSON profile
of Crovia Seal (RFC 8785 subset: UTF-16 code-unit key ordering, no floats,
integers within ±(2⁵³−1), no insignificant whitespace). Hashes of objects are
`SHA-256(CSC-1(object))` and written as `sha256:<64 lowercase hex>`.

### 4.3 Domain separation

Every signature and every hash of a TACET structure is domain-separated by a
fixed ASCII prefix followed by a single `0x0A`:

| Purpose | Prefix bytes |
|---|---|
| Empty leaf | `TACET-EMPTY-v1\n` |
| Leaf hash | `TACET-LEAF-v1\n` |
| Inner node | `TACET-NODE-v1\n` |
| Epoch sheet signature | `TACET-EPOCH-v1\n` |
| Witness countersignature | `TACET-WITNESS-v1\n` |
| Snapshot hash | `TACET-SNAPSHOT-v1\n` |

## 5. The verifiable map

### 5.1 Structure

The map is a **sparse Merkle tree of depth 256**. A key's slot is the leaf
reached by following its bits from the root (bit 0 = left). Let
`H(x) = SHA-256(x)`.

```
EMPTY[0]      = H("TACET-EMPTY-v1\n")
EMPTY[d]      = H("TACET-NODE-v1\n" || EMPTY[d-1] || EMPTY[d-1])       for 1 ≤ d ≤ 256
leaf(key, v)  = H("TACET-LEAF-v1\n" || key || value_hash(v))
node(l, r)    = H("TACET-NODE-v1\n" || l || r)
```

`EMPTY[d]` is the hash of an empty subtree of height `d`. The root of an
empty map is `EMPTY[256]`. `value_hash(v)` is defined in §5.3.

### 5.2 Proofs

A **proof** for key `k` at root `R` is the list of the 256 sibling hashes on
the path from the leaf to the root, leaf-side first. A proof is **compact** if
siblings equal to the corresponding `EMPTY[d]` are omitted and replaced by a
256-bit bitmap (`bitmap[d] = 1` iff the sibling at height `d` is present).

- **Inclusion**: recomputing the root from `leaf(k, v)` and the siblings yields `R`.
- **Non-inclusion**: recomputing the root from `EMPTY[0]` and the siblings yields `R`.

For a map holding `n` keys, a compact non-inclusion proof contains at most
`⌈log₂ n⌉ + 1` hashes with overwhelming probability, plus 32 bytes of bitmap.

### 5.3 Slot values

A slot value is one of:

```json
{"kind": "commitment", "commit_hash": "sha256:…", "committer": {"alg":"ed25519","key_hex":"…"}, "seal_hash": "sha256:…"}
{"kind": "disclosure", "summary_hash": "sha256:…", "summary_url": "https://…", "snapshot_hash": "sha256:…", "seal_hash": "sha256:…", "reveals": "sha256:…" | null}
{"kind": "retraction", "of": "sha256:…", "seal_hash": "sha256:…"}
```

`value_hash(v) = H(CSC-1(v))`. `seal_hash` is the hash of the `crovia.seal.v1`
object by which the party asserting the value signed it; `commit_hash =
H(summary_bytes || salt)` with a 32-byte salt revealed at disclosure time in
`reveals`.

**Transitions.** A slot moves only forward: `∅ → commitment → disclosure`,
`∅ → disclosure`, `disclosure → retraction`. A slot MUST never return to `∅`.
Consequently a non-inclusion proof at epoch `e` implies non-inclusion at every
epoch `< e` for the same map, and silence is a monotone predicate on epochs.

## 6. Epoch sheets and the temporal sandwich

For each epoch the operator fetches the **drand** round scheduled at the epoch
start — the first round of the hour, from the pinned chain (chain hash in the
trust root); relays serve any past round, so this does not depend on when the
hourly run actually starts. At the end it computes the map root and signs an
epoch sheet:

```json
{
  "sheet_version": "crovia.tacet.epoch.v1",
  "map_id": "urn:crovia:tacet:map:disclosure",
  "epoch": 4123,
  "epoch_start": "2026-09-19T09:00:00Z",
  "epoch_end":   "2026-09-19T10:00:00Z",
  "root": "sha256:…",
  "prev_sheet_hash": "sha256:…",
  "size": 6867,
  "opened": {"kind": "drand", "chain_hash": "…", "round": 1234567, "randomness": "…", "signature": "…"},
  "snapshots_root": "sha256:…",
  "operator": {"id": "urn:crovia:tacet:operator:crovia-trust", "pubkey": {"alg": "ed25519", "key_hex": "…"}},
  "signature": {"alg": "ed25519", "domain": "TACET-EPOCH-v1", "sig_hex": "…"},
  "closed": {"kind": "ots", "status": "pending" | "bitcoin", "anchored_digest": "sha256:…", "block_height": 956737, "proof_ref": "…"}
}
```

- `sheet_hash = H(CSC-1(sheet \ {signature, closed}))`. The signature is
  Ed25519 over `"TACET-EPOCH-v1\n" || CSC-1(sheet \ {signature, closed})`.
- `prev_sheet_hash` chains sheets; the genesis sheet has `null`.
- `opened` binds the epoch to a randomness value that did not exist before
  `epoch_start`: nothing in the epoch (root, snapshots) could have been
  produced earlier. **Lower time bound.**
- `closed` is an OpenTimestamps attestation of `sheet_hash` confirmed in a
  Bitcoin block. The sheet existed before that block. **Upper time bound.**
  The `closed` field is filled in after confirmation and is outside the
  signed region; verifiers check it independently against the OTS proof
  (§8.6). `anchored_digest` MUST equal `sheet_hash`; `proof_ref` locates the
  `.ots` file; `block_height` is the Bitcoin block carrying the attestation.
- `snapshots_root` is the Merkle root (RFC 6962 tree) of all snapshot hashes
  taken during the epoch (§7), so that every observation is bound to the
  epoch's temporal sandwich.

An epoch whose `closed.status` is `bitcoin` is **anchored**. Only anchored
epochs contribute to silence at strength ≥ 2 (§8.3).

## 7. Surface snapshots and predicates

### 7.1 Surfaces

For each target the operator publishes a **surface list**: the public URLs
where the target's training-data disclosure is expected to appear (model card,
vendor documentation page, repository README, regulator-mandated summary
location). Surface lists are versioned objects in the map under a reserved
namespace key `H("surfaces/" || target_id)`, so that changes to what is
monitored are themselves logged.

### 7.2 Snapshot

```json
{
  "snapshot_version": "crovia.tacet.snapshot.v1",
  "target_id": "meta-llama/Llama-3.1-8B",
  "surface_url": "https://huggingface.co/meta-llama/Llama-3.1-8B",
  "fetched_at": "2026-09-19T09:17:02Z",
  "epoch": 4123,
  "beacon_round": 1234567,
  "http_status": 200,
  "body_sha256": "sha256:…",
  "body_len": 48213,
  "tls_cert_sha256": "sha256:…" | null,
  "resolved_ip": "…" | null,
  "predicate": {"id": "crovia.pred.art53-summary", "version": "1.0.0", "code_hash": "sha256:…"},
  "result": false,
  "observer": {"id": "urn:crovia:observer:hetzner-1", "pubkey": {"alg": "ed25519", "key_hex": "…"}},
  "signature": {"alg": "ed25519", "domain": "TACET-SNAPSHOT-v1", "sig_hex": "…"}
}
```

`snapshot_hash = H("TACET-SNAPSHOT-v1\n" || CSC-1(snapshot \ {signature}))`.
The `beacon_round` MUST equal the `opened.round` of the epoch the snapshot
belongs to. A snapshot with `result: false` is a **negative snapshot**.

### 7.3 Predicates

A predicate is a pure function `bytes → bool` published as source code with a
`code_hash`, a semantic version, and test vectors. Predicates decide whether
fetched bytes contain a disclosure of the kind the surface is expected to
carry (for example: a link to a document following the EU AI Office training
content summary template; a `crovia.seal.v1` object whose subject is the
summary; structured `training_data` metadata). Predicates MUST be
re-executable by third parties on archived copies of the surface (e.g. the
Internet Archive), so that a negative snapshot can be independently re-derived
from public data. The operator MUST NOT change a predicate's behaviour without
incrementing its version.

A predicate's documentation MUST state what it does not recognise. The
reference predicate `crovia.pred.hf-card-training-data` 1.0.0 recognises a
YAML `datasets:` block and English lead phrases only; a disclosure written
solely in another language is not recognised and yields a negative snapshot.
Verifiers weighing a proof SHOULD read the predicate's stated limits alongside
the surfaces it was run on.

## 8. Silence proofs

### 8.1 Object

```json
{
  "proof_version": "crovia.tacet.silence.v1",
  "map_id": "urn:crovia:tacet:map:disclosure",
  "target_id": "meta-llama/Llama-3.1-8B",
  "key": "<64 hex>",
  "from_epoch": 2000,
  "to_epoch": 4123,
  "strength": 1 | 2 | 3,
  "sheets": [ <epoch sheet>, … ],
  "paths": {
    "initial": {"bitmap": "<64 hex>", "siblings": ["<64 hex>", …]},
    "deltas": [ {"epoch": 2001, "changed": [[height, "<64 hex>"], …]}, … ]
  },
  "snapshots": [ <negative snapshot>, … ],
  "witnesses": { "<sheet_hash>": [ {"id": "…", "pubkey": {...}, "sig_hex": "…"}, … ] },
  "silence": {
    "map_epochs": 2124,
    "observed_epochs": 2117,
    "observed_from": "2026-06-24T10:00:00Z",
    "observed_to":   "2026-09-19T10:00:00Z",
    "silence_seconds": 7513200,
    "silence_days": "86.96"
  }
}
```

`silence_days` is a decimal string (CSC-1 has no floats); `silence_seconds`
is the normative integer. Level-3 proofs additionally carry
`"witness_set": {"k": 2, "n": 3, "ids": [...]}`.

### 8.2 Delta-encoded non-inclusion chains

A naïve proof over `N` epochs carries `N` compact paths. TACET encodes the
path for `from_epoch` once (`paths.initial`) and, for each subsequent epoch,
only the siblings that changed (`paths.deltas[i].changed`, a list of
`(height, new_hash)`; a sibling that becomes the default is encoded with
`new_hash = null`). The verifier reconstructs the path incrementally,
recomputes the root for every epoch from `EMPTY[0]`, and checks it against
`sheets[i].root`. Because a sibling changes only when a key sharing that
prefix with the target changed in that epoch, deltas are small and often
empty; a year of hourly epochs for a map of 10⁴ keys typically fits in a few
hundred kilobytes, while remaining fully verifiable without trusting any
intermediate summary.

The proof MUST include every sheet in `[from_epoch, to_epoch]`; sheets chain
via `prev_sheet_hash`, so a gap is detectable.

### 8.3 Strength levels

| Level | Name | What is proven | Required content |
|---|---|---|---|
| 1 | `map-silence` | The slot was empty in every epoch of the range. | sheets, paths |
| 2 | `surface-silence` | Level 1, and for every *observed* epoch at least one negative snapshot for the target exists, is included under `snapshots_root`, and carries the epoch's beacon round. | + snapshots with Merkle inclusion against `snapshots_root`, only anchored epochs counted |
| 3 | `witnessed-silence (k/n)` | Level 2, and every sheet in the range carries at least `k` valid witness countersignatures from the proof's declared witness set of size `n`. | + witnesses |

A level-1 proof states only that nobody put a disclosure *in this map*. It
MUST be displayed as "map silence", never as evidence about the world. Level 2
is the minimum for any public statement about a target's disclosure.

### 8.4 Monotonicity rule (normative)

```
observed_epochs      = #{ e ∈ [from, to] : sheet(e).closed.status == "bitcoin"
                           ∧ ∃ negative snapshot s for target with s.epoch == e }
silence_seconds      = Σ over observed epochs e of (epoch_end(e) − epoch_start(e))
silence_days         = floor(silence_seconds * 100 / 86400) / 100,
                       rendered with exactly two decimals ("0.12", "86.96")
```

`silence_days` is **truncated**, never rounded: a verifier that recomputes it
MUST produce the same string byte-for-byte, and a proof MUST NOT display more
silence than its anchored seconds support (10 800 s is `"0.12"`, not `"0.13"`).

Epochs without a negative snapshot (observer down, surface unreachable,
predicate error) contribute **nothing**. Unanchored epochs contribute nothing.
Time between two sheets that no epoch covers (operator downtime; sheets may be
separated in time, never in number) contributes nothing.
`silence_days` therefore cannot grow while observation is paused, and a
verifier can recompute it from the proof alone. `observed_to` MUST accompany
every displayed silence figure.

### 8.5 Verification algorithm

1. Verify each sheet's operator signature; verify `prev_sheet_hash` chaining
   over the range; verify `map_id` and monotonically increasing `epoch`.
2. For each sheet, verify `opened` (§8.5.1) and, if `closed.status ==
   bitcoin`, verify the OTS proof of `sheet_hash` (§8.6) and record the block
   time.
3. Reconstruct the path per epoch from `paths`; for each epoch recompute the
   root from `EMPTY[0]`; require equality with `sheets[i].root`.

#### 8.5.1 What "verify `opened`" means

`opened` is a claim with two parts, checked differently:

- **Chain and schedule** — from the sheet alone, offline: `opened.chain_hash`
  equals the pinned chain, and the round's scheduled time
  `genesis_time + (round − 1) · period` lies inside the hour the sheet opens:
  `epoch_start − period ≤ t < epoch_start + tolerance_seconds`, with
  `tolerance_seconds` from the trust root (3600, the epoch length). This says
  the round *number* fits the epoch. It says nothing about the bytes.
- **Round bytes** — that `randomness` and `signature` are the chain's output
  for that round. The complete check is the round's BLS12-381 signature under
  the chain public key in the trust root (drand `pedersen-bls-chained`:
  message `SHA-256(previous_signature ‖ round)`, round as a big-endian 64-bit
  integer). Equivalent in practice, and
  what both Crovia verifiers do: fetch the same round from a drand relay and
  require byte equality. Without one of the two, the lower time bound rests
  on the operator's word, because an operator can write a well-placed round
  number with invented bytes.

A verifier that checks only chain and schedule MUST say so (the reference
returns a warning naming the epochs; the browser page prints a note). It MUST
NOT report the bound as verified. A round whose bytes cannot be obtained is
*unchecked*, never failed. Neither Crovia verifier implements BLS12-381; the
relay comparison is what they run when the network is available.

**Correction, 2026-09-24.** `tolerance_seconds` was 1800 until this date.
Epochs 76, 87, 92 and 99 of `urn:crovia:tacet:map:disclosure` were opened
31–42 minutes into their hour because the operator's hourly run started late,
and their signed, anchored sheets cannot change. Their rounds lie inside their
hour and remain correct lower bounds for everything in it, so the bound is
now the hour; the reference verifier reports any opening later than 900 s as
*late* in its per-sheet transcript. The operator now opens every epoch with
the first round of its hour (§6) and refuses to sign a sheet that fails this
check, so lateness of the run no longer reaches the sheet.

### 8.6 Checking the Bitcoin anchor without a Bitcoin node

The `.ots` file at `closed.proof_ref` is a standard OpenTimestamps proof
(magic `\x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94`,
version 1). Its *file digest* is `SHA-256(raw_bytes(sheet_hash))`, the 32
sheet-hash bytes stamped as a file, so a verifier first checks

```
ots.file_hash_op == sha256  ∧  ots.file_digest == SHA-256(unhex(sheet_hash))
```

then walks the operation tree (`append`, `prepend`, `sha256`, `ripemd160`,
`sha1`, `reverse`, `hexlify`) from that digest. Every leaf that is a
`BitcoinBlockHeaderAttestation(height)` states that the 32-byte message at
that leaf **is the merkle root of block `height`** (internal byte order; block
explorers display it byte-reversed). The anchor verifies iff at least one such
leaf has `height == closed.block_height` and its message equals the merkle
root in that block's header, obtained from any header source the verifier
trusts — a full node, a headers file, or a public explorer. `PendingAttestation`
leaves (calendar URLs) are ignored. A verifier that cannot reach a header
source MUST report the anchor as *unchecked*, never as failed, and MUST print
the height and expected merkle root so a human can check them elsewhere.
4. Level ≥ 2: verify each snapshot's observer signature, `beacon_round`,
   `result == false`, `target_id`, and its inclusion in the epoch's
   `snapshots_root`. Recompute `silence` per §8.4 and require equality with
   the proof's `silence` block.
5. Level 3: verify witness signatures over each `sheet_hash`; require ≥ `k`.
6. Verify the outer Seal (§10).

## 9. Witnesses

A witness is any party that (a) fetches epoch sheets, (b) independently
verifies signature, chaining, `opened` and `closed`, and (c) publishes
countersignatures `Ed25519("TACET-WITNESS-v1\n" || sheet_hash)`. A witness MAY
additionally run its own observer and publish its own snapshots; a snapshot
from a second observer for the same target and epoch strengthens a level-2
proof and is included in the `snapshots` list. Witness identities and keys are
published in a witness list in the map under `H("witnesses")`. The protocol is
that of Crovia Countersign; TACET specifies only the signed bytes.

## 10. Seal wrapping (portable proof)

A silence proof is delivered as a `crovia.seal.v1` object, unmodified from
`draft-crovia-seal-01`:

| Seal field | Content |
|---|---|
| `subject.input_hash`, `input_len` | hash and length of `CSC-1(query)`, where `query = {"query_version":"crovia.tacet.query.v1","map_id","target_id","from_epoch","to_epoch","min_strength"}` |
| `subject.output_hash`, `output_len` | hash and length of `CSC-1(silence proof)` |
| `subject.modality` | `"text"` (the proof is canonical JSON text) |
| `generator.id` | `"crovia/tacet"` |
| `generator.version` | TACET reference version |
| `generator.weights_hash` | `null` |
| `generator.params` | `{"map_id": …, "strength": "2", "epochs": "2000-4123"}` (strings) |
| `chain` | previous silence proof Seal for the same target, so a target's proofs form a chain |
| `anchor` | `{"kind":"crovia-beacon", …}` with the drand round current at emission |
| `witnesses` | Seal-level countersignatures (optional) |
| `checks` | `{"tacet": {"strength": 2, "silence_days": 86.96, "last_negative_snapshot_at": …}}` — a human-readable summary; non-normative |

The proof file is `{"seal": <seal>, "query": <query>, "proof": <silence proof>}`.
Verification: `verify_seal(seal)` per the Seal specification; then check
`seal.subject.input_hash == H(CSC-1(query))` and `output_hash ==
H(CSC-1(proof))`; then §8.5. A verifier that only implements Crovia Seal can
still establish who issued the proof, when (lower-bounded by the beacon), and
that the bytes are intact.

## 11. Commit-then-reveal for vendors

A vendor MAY pre-register a model's training-data summary before release:

1. Vendor computes `commit_hash = H(summary_bytes || salt)` and emits a
   `crovia.seal.v1` with `output_hash = commit_hash`, `generator.id` = the
   model id, `modality = "text"`.
2. The operator (or any relay) inserts `{"kind":"commitment", …}` into the
   target's slot. The epoch sheet's temporal sandwich proves when.
3. At release the vendor publishes the summary at a surface URL and emits a
   Seal over the summary bytes; the operator inserts
   `{"kind":"disclosure", …, "reveals": salt}`. Verifiers check
   `H(summary || salt) == commit_hash`.

The vendor obtains a proof of timely, unchanged publication that does not
depend on the operator's honesty. A silence proof for a committed slot is
impossible by construction (the slot is non-empty), so the vendor also gains
a guarantee against false silence claims. The three observable states of a
target are therefore `never-committed`, `committed-unrevealed`, `revealed`,
and TACET reports which one applies.

## 12. Public data layout

The operator publishes, content-addressed and immutable once anchored:

```
/tacet/<map_id>/sheets/<epoch>.json          epoch sheet
/tacet/<map_id>/snapshots/<epoch>.jsonl      snapshots of the epoch
/tacet/<map_id>/values/<epoch>.jsonl         slot writes of the epoch
/tacet/<map_id>/witness/<epoch>.jsonl        countersignatures received
/tacet/<map_id>/HEAD.json                    latest sheet + pointers
```

Consumers fetch `HEAD.json` (small) and only the sheets they need. No page
loads a whole-history file.

## 13. Security considerations

- **Back-dating** an epoch is prevented by `opened` (beacon randomness is
  unpredictable); **forward-dating** by `closed` (Bitcoin block time).
- **Split view** (showing different sheets to different parties) is detected
  by witnesses and by any two consumers comparing `sheet_hash` for the same
  epoch; the OTS anchor commits to one hash per epoch.
- **Selective observation** (not fetching a surface to manufacture silence)
  cannot inflate silence: an epoch without a negative snapshot contributes
  zero (§8.4). It can only *reduce* the proven silence, which is the
  conservative failure mode.
- **Predicate gaming** by an operator is bounded by predicate publication and
  reproducibility on archived copies (§7.3).
- **Key compromise** of the operator: sheets remain chained and anchored;
  a compromised key can produce forged sheets only going forward, and only
  until witnesses refuse to countersign an unchained head. Key rotation is
  logged in the map under `H("operator")`.
- A silence proof is **not** a proof that a disclosure exists nowhere. It is
  a proof about the map and about the listed surfaces under the listed
  predicate. The proof carries the surface list and predicate so that its
  scope is explicit.

## 14. Conformance

An implementation is conformant if it passes the vectors in
`tacet/conformance/vectors/v1/`: empty-map root, inclusion and non-inclusion
proofs, delta-chain reconstruction, sheet signature and chaining, silence
computation under paused observation, rejection of a proof with a missing
sheet, rejection of a proof whose silence block overstates observed epochs,
round-trip of the Seal wrapper against the Crovia Seal reference verifier, and
the §8.6 anchor check over real OpenTimestamps proofs of live sheets (parse,
file-digest binding, Bitcoin attestation at the recorded height, merkle-root
match, and the three ways it must fail plus the one way it must stay unchecked).
Two implementations pass them today: `tacet/reference/python` and the browser
verifier in `site/registry/seal/verify/`.

---

*TACET is an open protocol stewarded by Crovia Trust. The text of this
specification is dedicated to the public domain (CC0).*
