Verify an append-only note
An append-only note is a log or stream: every entry — the note's creation and each append — is SHA-256 hashed the moment we receive it and anchored to Bitcoin, so you can prove the whole log existed, in order, and hasn't changed. This is the hands-on version of the verification reference: copy-paste commands you can run yourself, no cryptography knowledge needed.
Verifying a different kind of note? See editable notes or encrypted notes.
What you'll need
- Your proof file. On the Provenance dashboard click Download next to a note and save it as
proof.json. (Or fetch it with your API token:curl -H "Authorization: Bearer YOUR_TOKEN" https://freshjots.com/api/v1/notes/<id>/proof > proof.json.) - Python 3 — already on macOS and most Linux; on Windows install it from python.org. Every check below is a short Python command, and nothing gets installed.
- The OpenTimestamps client, for the final Bitcoin step only:
pip install opentimestamps-client(this gives you theotscommand).
Run everything below in the same folder as proof.json.
1. Content integrity — these are your exact bytes
Each entry stores the exact bytes we received (base64-encoded) and their SHA-256 hash. Decode the bytes, hash them, and confirm the hash matches — proof that the recorded content is byte-for-byte what we stored.
python3 -c '
import json, base64, hashlib
e = json.load(open("proof.json"))["entries"][0]
print("your bytes:", base64.b64decode(e["content"]).decode(errors="replace"))
print("computed :", hashlib.sha256(base64.b64decode(e["content"])).hexdigest())
print("claimed :", e["content_hash"])'
The last two lines must be identical; the first shows you the exact bytes we stored — your note's text (or, for an encrypted note, the ciphertext you sent). (Change the [0] to check a different entry.)
2. Leaf value — bound to your note's identity
A bare hash proves nothing — anyone can hash anything. So each entry becomes a "leaf": SHA-256(0x00 ‖ note_token ‖ prev_hash ‖ content_hash), folding in the note's public token and its position in the chain. This leaf is the value that actually reaches Bitcoin, so the commitment is to your content, in your note, in order.
python3 -c '
import json, hashlib
p = json.load(open("proof.json")); e = p["entries"][0]
leaf = hashlib.sha256(b"\x00" + p["note_token"].encode() + bytes.fromhex(e["prev_hash"]) + bytes.fromhex(e["content_hash"])).hexdigest()
print("leaf value:", leaf)'
You'll fold this value up to the root in the next step. (The 0x00 prefix keeps a leaf from ever being mistaken for a tree node.)
3. Merkle branch — folds into the day's root
Every leaf from every account on a given day is combined into one tree, and only its single root_hash goes to Bitcoin. The entry's branch is the short path from your leaf up to that root: fold each step with SHA-256(0x01 ‖ left ‖ right), putting the sibling on its stated side. If it lands on the anchor's root, your entry really is inside that day's Bitcoin commitment.
python3 -c '
import json, hashlib
def sha(b): return hashlib.sha256(b).digest()
p = json.load(open("proof.json")); e = p["entries"][0]
acc = sha(b"\x00" + p["note_token"].encode() + bytes.fromhex(e["prev_hash"]) + bytes.fromhex(e["content_hash"]))
for s in e["branch"]:
sib = bytes.fromhex(s["hash"])
acc = sha(b"\x01" + (acc + sib if s["side"] == "right" else sib + acc))
print("folds to :", acc.hex())
print("anchor root:", p["anchors"][e["anchor_id"]]["root_hash"])'
The two lines must match. (A brand-new entry may not be anchored yet: if anchor_id is null it's hashed but not yet in a day's Bitcoin commitment — check back within a day.)
4. Order — nothing inserted, reordered, or dropped
When a note has more than one entry, each entry's prev_hash equals the previous entry's content_hash — an unbroken chain. For an append-only log that's the order entries were written; for an editable note it's the order of its dated snapshots. Either way, an unbroken chain proves nothing was inserted, reordered, or removed from the middle.
python3 -c '
import json
es = json.load(open("proof.json"))["entries"]
print("chain intact:", all(es[i]["prev_hash"] == es[i-1]["content_hash"] for i in range(1, len(es))))'
It should print True (a single-entry note trivially passes). (Honest limit: the chain proves nothing was changed in the middle, but not that nothing was withheld after the last entry you hold.)
5. Bitcoin — the root is committed to the blockchain
Finally, confirm the anchor's root_hash really is in Bitcoin. Each anchor ships its OpenTimestamps proof (ots). Write both to disk:
python3 -c '
import json, base64
a = json.load(open("proof.json"))["anchors"]; k = list(a)[0]
open("anchor", "wb").write(bytes.fromhex(a[k]["root_hash"]))
open("anchor.ots", "wb").write(base64.b64decode(a[k]["ots"]))
print("wrote anchor + anchor.ots for anchor", k)'
Then read the Bitcoin attestation out of it:
ots info anchor.ots # look for BitcoinBlockHeaderAttestation(<height>)
ots verify anchor.ots # if you run a Bitcoin node, prints the exact block time
ots info needs no Bitcoin node: it prints the block height the root was committed in, plus that block's Merkle root. Look the height up on any public block explorer and confirm the Merkle root matches — that block's timestamp is when your content provably existed. (ots verify automates this but needs access to a Bitcoin node. If ots info shows only PendingAttestation, the anchor isn't Bitcoin-confirmed yet — give it up to a day.)
Shortcut: check 1–4 in one go
Save this as verify.py and run python3 verify.py proof.json. It runs checks 1–4 over every entry and tells you exactly what passed; then finish with the Bitcoin step above.
import sys, json, base64, hashlib
def sha(b): return hashlib.sha256(b).digest()
proof = json.load(open(sys.argv[1] if len(sys.argv) > 1 else "proof.json"))
token = proof["note_token"].encode()
anchors = proof["anchors"]
prev = bytes(32) # the first entry's prev_hash is 32 zero bytes
ok = True
for i, e in enumerate(proof["entries"]):
content = base64.b64decode(e["content"])
ch, ph = bytes.fromhex(e["content_hash"]), bytes.fromhex(e["prev_hash"])
if sha(content) != ch: # 1. content integrity
print(i, "FAIL: content != content_hash"); ok = False; continue
if ph != prev: # 4. order
print(i, "FAIL: prev_hash out of order"); ok = False
prev = ch
leaf = sha(b"\x00" + token + ph + ch) # 2. leaf value
if e.get("anchor_id") is None:
print(i, "OK: hashed, not yet Bitcoin-anchored"); continue
acc = leaf # 3. Merkle branch
for s in e["branch"]:
sib = bytes.fromhex(s["hash"])
acc = sha(b"\x01" + (acc + sib if s["side"] == "right" else sib + acc))
if acc == bytes.fromhex(anchors[e["anchor_id"]]["root_hash"]):
print(i, "OK: folds to anchor", e["anchor_id"])
else:
print(i, "FAIL: does not fold to the anchor root"); ok = False
print("PASS - content, order and Merkle paths check out." if ok else "FAIL - see above.")
print("Now confirm each anchor is in Bitcoin with the `ots` step.")
When it all passes
You've shown — trusting no one, not even us — that every entry in this log existed, in the order it was written, by the time of a specific Bitcoin block, and hasn't changed since. That's the full guarantee for an append-only note. What it does not prove is that the content is true, that it wasn't altered before it reached us, or that nothing was withheld after the last entry you hold; see the verification reference and what notarization is for for the honest limits.