Docs menu

Prove on Traza Cloud

The engine’s ledger guide defines the hash chain and evidence packs, and is explicit about what they prove and what they do not. Traza Cloud adds a notary: a signer outside the store, whose signatures you can check without trusting us.

Evidence packs

Any read key can fetch your project’s pack. since and until are Unix nanoseconds; they default to the epoch and now.

curl -s "https://api.trazadb.com/v1/evidence?since=1790812800000000000" \
  -H "Authorization: Bearer $TRAZA_API_KEY" -o pack.json

The pack covers your project’s tenant only: its activity, retention, usage and erasures, the chain entries in the window and the chain’s verification. Other tenants’ erasure entries on the same cell appear as stubs that keep the chain followable but say nothing about whose they were. A window too large to fold within the engine’s query budget answers 400; narrow it.

Check a pack’s digest

The digest is SHA-256 over the canonical JSON of every other field. Decimal numbers must be kept exactly as written, which this script, from the engine’s guide, does with the Python standard library:

import hashlib, json, sys
class Raw(str): pass  # a non-integer number, kept exactly as the server wrote it
def canon(v):
    if isinstance(v, dict):
        return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + canon(v[k]) for k in sorted(v)) + "}"
    if isinstance(v, list):
        return "[" + ",".join(canon(x) for x in v) + "]"
    return str(v) if isinstance(v, Raw) else json.dumps(v, ensure_ascii=False)
pack = json.load(open(sys.argv[1], encoding="utf-8"), parse_float=Raw)
claimed = pack.pop("digest")
ok = hashlib.sha256(canon(pack).encode()).hexdigest() == claimed
print("digest ok" if ok else "digest MISMATCH")
sys.exit(0 if ok else 2)
$ python3 check_pack.py pack.json
digest ok

Do not round-trip a pack through a parser that reads numbers as doubles, such as JavaScript’s JSON.parse: timestamps are above 253 and the recomputed digest will not match.

The notary

The notary signs with Ed25519. Its keys, current and retired, are published:

curl -s https://app.trazadb.com/.well-known/traza-notary.json
{"authority": "notary.trazadb.com", "algorithm": "ed25519",
 "keys": [{"id": "ntk_…", "public_key_hex": "…", "created_at": …, "retired_at": null}],
 "log": "https://app.trazadb.com/notary/log"}

It does two things:

  • Anchors. Every hour, for each cell whose ledger head has moved, it signs a canonical statement of the head (authority, cell, store_id, seq, head, anchored_at), records the anchor back into that cell’s ledger and appends the statement to its public transparency log. Anchor entries appear whole in every evidence pack from that cell, on every plan.
  • Attestations (Team and above). From a project’s Evidence tab, choose a window. The console fetches the engine’s pack, re-verifies its digest over the exact bytes, and has the notary sign a statement naming your org, project, tenant, window, the pack’s digest and the ledger head. Download gives a bundle with the statement, the signature, the key and the pack, byte for byte.

Verify an attestation

You need jq, xxd, Python 3 and OpenSSL 3 (openssl pkeyutl -rawin is not in LibreSSL or OpenSSL 1.1). From a downloaded bundle.json:

# The statement exactly as signed (-j: no trailing newline), and the signature.
jq -j .attestation.statement_json bundle.json > statement.json
jq -r .attestation.signature_hex bundle.json | xxd -r -p > sig.bin

# The raw Ed25519 key as a PEM public key: the fixed SPKI prefix, then the key.
{ printf '302a300506032b6570032100'; jq -r .attestation.public_key_hex bundle.json; } | xxd -r -p > key.der
openssl pkey -pubin -inform DER -in key.der -out notary.pem

openssl pkeyutl -verify -pubin -inkey notary.pem -rawin -in statement.json -sigfile sig.bin
Signature Verified Successfully

Then tie the signature to the notary and to your pack:

  1. The bundle’s key_id and public_key_hex must match a key published at /.well-known/traza-notary.json (pin it the first time you see it).
  2. jq -r .pack_digest statement.json must equal jq -r .pack.digest bundle.json.
  3. The pack’s own digest must recompute. Run the script above with json.load(...)["pack"] in place of the pack file; it keeps decimals exact either way.

Verify an anchor

An anchor entry in your pack carries target_seq, target_head, authority, anchored_at_ns, signature and public_key. To check it:

  1. Confirm target_head is the head the chain has at target_seq (the engine’s ledger script recomputes every head).

  2. Find the transparency log entry with the same store_id and ledger_seq:

    curl -s "https://app.trazadb.com/notary/log?limit=1000" | jq '.entries[] | select(.ledger_seq == 4)'
  3. Verify its signature_hex over its statement_json with the recipe above, and check the statement’s head is your target_head.

On Pro and up, any run can become a proof link: in the console, open the run and create one. The link is a read-only certificate page at app.trazadb.com/p/<token> with the run’s tape summary, the ledger head when the link was created, and the notary’s Ed25519 signature over the run, its tape digest, that ledger head and the time of issue. The page shows how to verify the signature with openssl against the published notary key, so the person you send it to needs neither an account nor trust in the page. Links expire, and can be revoked at any time.

The transparency log

The log is append-only and hash-chained, like the engine’s ledger:

hash = sha256("traza-notary/1\n" + prev_hash + "\n" + statement_json)

Entry 0’s prev_hash is 64 zeros. Entries come oldest first; page through them with after (the last seq you read) and limit (up to 1,000). It carries store ids, ledger heads and signatures, and no tenant data.

import hashlib, json, sys, urllib.request

base, after, prev, ok = sys.argv[1], None, "0" * 64, True
while True:
    url = f"{base}/notary/log?limit=1000" + ("" if after is None else f"&after={after}")
    page = json.load(urllib.request.urlopen(url, timeout=30))
    for e in page["entries"]:
        want = hashlib.sha256(f"traza-notary/1\n{prev}\n{e['statement_json']}".encode()).hexdigest()
        good = e["prev_hash"] == prev and e["hash"] == want
        ok &= good
        print(e["seq"], e["hash"], "ok" if good else "BROKEN")
        prev, after = e["hash"], e["seq"]
    if not page["entries"]:
        break
sys.exit(0 if ok else 2)
python3 check_notary_log.py https://app.trazadb.com