TILLNOTARY · LOGS & ENTRIES

Named, append-only, forever.

GA

A log is a named append-only sequence of leaves. Entries are only ever added at the end — nothing is edited, nothing is deleted. Each entry has a permanent index, and each value appears in a log at most once.

01Anatomy

What a log is made of

FieldMeaning
slugThe log’s name inside your org — short, stable, and part of its identity.
originThe globally unique name signed into every checkpoint, shaped host/slug (e.g. notary.tilldev.dev/acme-releases). Verifiers check it, so a proof from one log can never be replayed against another.
suiteThe signature suite — hash function plus hybrid signature algorithms. Chosen at creation, fixed for the log’s lifetime. See the suite table.
witness_thresholdHow many witness cosignatures a checkpoint needs before it counts as complete. List a log’s witnesses with GET /v1/logs/{slug}/witnesses or tilldev notary witness <log>.
public_readWhen true, anyone can read leaves, proofs, and checkpoints with no token. Submitting always requires a token.
max_leaves_per_dayNew entries the log accepts per rolling 24 hours: 1 to 50,000, default 50,000. Change it any time in the log’s settings, with tilldev notary logs update <slug> --daily-cap <n>, or with PATCH /v1/logs/{slug}. Past it, submissions get a 429.
statusactive or frozen. A frozen log rejects new leaves (409) but keeps serving reads and proofs — history stays verifiable forever.
bash
# Dashboard: /<org>/notary/logs → "New log". Or the API (admin scope):
curl -s https://notary.tilldev.dev/v1/logs \
  -H "authorization: Bearer $TILLDEV_NOTARY_TOKEN" \
  -H "content-type: application/json" \
  -d '{
    "slug": "acme-releases",
    "description": "Release artifact provenance",
    "suite": "sig-ed25519-mldsa65-sha256.v1",
    "witness_threshold": 1,
    "public_read": true,
    "max_leaves_per_day": 5000
  }'
# 201 → { "log": { "slug": "acme-releases", "max_leaves_per_day": 5000, … },
#         "public_key": "…", "witnesses": ["tillnotary-witness/w1"] }
#   Pin that key now: tilldev notary trust-init acme-releases

# CLI equivalent:
tilldev notary logs create acme-releases --description "Release artifact provenance" --daily-cap 5000
02Submitting

Two submission modes

Hash-only is the default and the right choice almost always. You send the digest — exactly 64 hex characters on a SHA-256 log, 96 on a SHA-384 log — and nothing else. The log commits to the value without learning anything about your content:

bash
# Default mode: hash-only. Send exactly 64 hex characters on a SHA-256
# log (96 on a SHA-384 log). The server never sees your content.
curl -s https://notary.tilldev.dev/v1/logs/acme-releases/leaves \
  -H "authorization: Bearer $TILLDEV_NOTARY_TOKEN" \
  -H "content-type: application/json" \
  -d '{"leaf":"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"}'
# 201 → { "leaf_index": 1042, "leaf": "2cf2…", "duplicate": false,
#         "inclusion_proof": { … }, "checkpoint": { … }, "complete": true }
# Sending the same value again → 200 with the original receipt and "duplicate": true.

# CLI equivalents:
tilldev notary submit acme-releases --hash 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
tilldev notary submit acme-releases --file ./release.tar.gz   # hashes locally

Public attestation is for content that is meant to be published — a signed release statement, a transparency notice. You send the raw bytes (base64, at most 4096 bytes) and must also say content_class: "public-attestation":

bash
# Public-attestation mode: raw bytes (base64, ≤ 4096 bytes) AND the
# explicit content class. Both are required — a deliberate double opt-in,
# because these bytes become PUBLICLY READABLE.
curl -s https://notary.tilldev.dev/v1/logs/acme-transparency/leaves \
  -H "authorization: Bearer $TILLDEV_NOTARY_TOKEN" \
  -H "content-type: application/json" \
  -d '{
    "bytes": "eyJhcnRpZmFjdCI6InJlbGVhc2UtMi4zLjAiLCJzaGEyNTYiOiIyY2YyNGRiYS4uLiJ9",
    "content_class": "public-attestation"
  }'

# CLI equivalent — --attest is the opt-in flag:
tilldev notary submit acme-transparency --file ./attestation.json --attest
The double opt-in is deliberate
Sending bytes alone is rejected, and so is the content class alone. Both together are required precisely because the consequence is irreversible: those bytes become publicly readable in an append-only log, so nothing lands there by accident — no “I meant to send the hash.” If in doubt, submit the hash.
03Identity

Each value is logged once

An entry is identified by its position, (log, index), and each value appears in a log at most once. Submitting a value that is already there does not add a second entry: you get the original receipt back — same index, same submission time — with "duplicate": true. That makes submitting safe to retry after a timeout, and it works even after the log is frozen.

To record the same content as a separate event — a document re-signed, an artifact promoted to production — make the event part of what you hash: log the digest of a small statement that names the content’s digest together with what happened and when.

Keep the evidence bundle from tilldev notary submit --out when you can: it verifies with no network at all. When you only have the content, find its entry by value — see Reading entries back.

04Limits

Caps, freezing, and errors

Because a log is append-only, junk in a log is junk forever — so two brakes exist:

  • Daily cap → 429. Each log accepts at most max_leaves_per_day new entries in any rolling 24 hours; past it, submissions fail with RATE_LIMITED and a retry-after header saying when the oldest entry in the window ages out. Set the cap near your real volume, so a leaked submit token cannot flood your permanent record. Resubmitting a value already in the log does not count. Reads are not capped this way; request rate limits are covered under Tokens & limits.
  • Frozen → 409. Freezing a log ends its growth: new values fail with CONFLICT, while every read, proof, checkpoint and existing receipt keeps working. Freeze a log when the thing it tracked is finished — the history remains verifiable indefinitely.
json
# Every error, every endpoint — one envelope, plus an x-request-id header.
# HTTP/1.1 429, retry-after: 5123
{
  "error": "Daily submission quota reached for this log (5,000 per 24 hours). Retry after 5123 seconds.",
  "code": "RATE_LIMITED",
  "request_id": "0f5c2d1a-8b7e-4f3a-9c61-2e4d7a9b3c10"
}

# Codes: UNAUTHORIZED · FORBIDDEN · NOT_FOUND · ENTRY_NOT_FOUND · INVALID_INPUT
#        CONFLICT · RATE_LIMITED · PAYLOAD_TOO_LARGE · METHOD_NOT_ALLOWED
#        UPSTREAM_UNAVAILABLE · INTERNAL
05Reading

Reading entries back

Reading requires the read scope — or no token at all on a public_read log, which is what makes independent, third-party verification of your log possible.

bash
# The newest entries (limit up to 200; before=<index> pages back):
curl -s "https://notary.tilldev.dev/v1/logs/acme-releases/leaves?limit=20"
# → { "tree_size": 2210, "leaves": [ { "leaf_index": 2209, … }, … ] }

# One entry, with a fresh inclusion proof and the checkpoint it is proven against:
curl -s https://notary.tilldev.dev/v1/logs/acme-releases/leaves/1042
# → { "leaf": { "leaf_index": 1042, "leaf": "2cf2…", "bytes": null,
#               "content_class": "hash-only", "submitted_at": "…" },
#     "inclusion_proof": { … }, "checkpoint": { … } }

# On a public_read log, all of these work with no token at all.

Find an entry by its value. Hash your content and ask for the entry that holds it. The answer carries the same inclusion proof as a read by index, so the SDK’s findLeaf and the CLI verify it before reporting it; the dashboard’s entry list has the same search, and hashes a file in the browser without uploading it.

bash
# The entry holding a value, with the same proof and checkpoint as above:
curl -s https://notary.tilldev.dev/v1/logs/acme-releases/leaves/by-value/2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
# 404, code ENTRY_NOT_FOUND → no entry in this log holds that value

# CLI: hashes the file with the log's suite, verifies the proof, exits 1 if absent.
tilldev notary lookup acme-releases --file ./release.tar.gz
tilldev notary lookup acme-releases --out release.bundle.json \
  --hash 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

A 404 with ENTRY_NOT_FOUND means the log has no such entry. A plain NOT_FOUND means the log itself does not exist or your token cannot read it, so never read it as “not logged”.

Fetching an entry gives you its inclusion proof and the current checkpoint alongside it — what those actually establish, and how to check them, is the subject of Inclusion & consistency.