TACET — Verifiable Silence for AI Training Disclosure
Version: 0.1-draft · Status: Working draft · License: CC0 (text), Apache-2.0 (code)
Editor: Crovia Trust · [email protected]
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
- 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.
- Reuses Crovia Seal unchanged. Every artifact that leaves TACET is
wrapped in a
crovia.seal.v1object as specified indraft-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. - 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.
- 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.
- 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 yieldsR. - Non-inclusion: recomputing the root from
EMPTY[0]and the siblings yieldsR.
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:
{"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
At the start of each epoch the operator fetches the current drand round (chain hash pinned in the trust root). At the end it computes the map root and signs an epoch sheet:
{
"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_hashchains sheets; the genesis sheet hasnull.openedbinds the epoch to a randomness value that did not exist beforeepoch_start: nothing in the epoch (root, snapshots) could have been produced earlier. Lower time bound.closedis an OpenTimestamps attestation ofsheet_hashconfirmed in a Bitcoin block. The sheet existed before that block. Upper time bound. Theclosedfield is filled in after confirmation and is outside the signed region; verifiers check it independently against the OTS proof (§8.6).anchored_digestMUST equalsheet_hash;proof_reflocates the.otsfile;block_heightis the Bitcoin block carrying the attestation.snapshots_rootis 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
{
"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
{
"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
- Verify each sheet's operator signature; verify
prev_sheet_hashchaining over the range; verifymap_idand monotonically increasingepoch. - For each sheet, verify
openedis a valid drand round for the pinned chain whose time is ≤epoch_start+ tolerance, and (ifclosed.status == bitcoin) verify the OTS proof ofsheet_hashand record the block time. - Reconstruct the path per epoch from
paths; for each epoch recompute the root fromEMPTY[0]; require equality withsheets[i].root.
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:
- Vendor computes
commit_hash = H(summary_bytes || salt)and emits acrovia.seal.v1withoutput_hash = commit_hash,generator.id= the model id,modality = "text". - The operator (or any relay) inserts
{"kind":"commitment", …}into the target's slot. The epoch sheet's temporal sandwich proves when. - 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 checkH(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 byclosed(Bitcoin block time). - Split view (showing different sheets to different parties) is detected
by witnesses and by any two consumers comparing
sheet_hashfor 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).