Verifying a Memory
This is the operation the whole protocol exists to support. Everything else is plumbing.
The goal: given a memory and a proof, establish that it was part of a specific wallet's committed log - without trusting toda's API, toda's database, or toda's word for what is on-chain.
What you need
| Input | From |
|---|---|
The memory: content and metadata | The owner, or an agent case |
| The inclusion proof | GET /memories/:id/proof |
| The owner's wallet public key | The owner |
| A Solana RPC endpoint | Yours. Not toda's |
Note what is not on that list: any credential, any cooperation from toda at verification time, and any network call to toda after you have the proof.
The three checks
Verification is three independent claims, and all three must hold.
1. The leaf is the memory. leaf == sha256(canonical(memory))
2. The proof reaches the root. fold(leaf, siblings, directions) == root
3. The root is on the chain. root == readUserCommit(owner).merkleRoot
Skipping check 1 means you verified a proof about some leaf, not about the memory in your hand. Skipping check 3 means you verified arithmetic. Both are common mistakes.
Doing it
import { Connection, PublicKey } from "@solana/web3.js";
import { leafHash, bytesToHex, hexToBytes, verifyProof } from "@toda/crypto";
import { readUserCommit } from "@toda/solana";
// The memory, as you received it.
const memory = {
content: "The user always deploys on Fridays.",
metadata: { kind: "preference" },
};
// The proof bundle, as you received it. Data - not to be trusted.
const bundle = await fetch(`${apiUrl}/memories/${id}/proof`, {
headers: { authorization: `Bearer ${token}` },
}).then((r) => r.json());
// ── 1. The leaf is the memory ────────────────────────────
const computed = leafHash(memory);
if (bytesToHex(computed) !== bundle.proof.leaf) {
throw new Error("this proof is not about this memory");
}
// ── 2. The proof reaches the root ────────────────────────
const root = hexToBytes(bundle.batchRoot);
if (!verifyProof(bundle.proof, root)) {
throw new Error("proof does not reconstruct the claimed root");
}
// ── 3. The root is on the chain ──────────────────────────
// YOUR connection. Not one Toda supplied.
const connection = new Connection("https://api.devnet.solana.com");
const tip = await readUserCommit(connection, new PublicKey(ownerPubkey));
if (!tip) throw new Error("this wallet has never anchored");
if (bytesToHex(tip.merkleRoot) !== bundle.batchRoot) {
throw new Error("the claimed root is not the wallet's current commitment");
}
// The memory was in the log this wallet committed to,
// as of block time tip.lastUpdated.
That is the whole thing. Three hashes, a fold, and one RPC read.
bundle.onchainRoot for check 3The bundle contains an onchainRoot field, supplied by toda's API. Using it to verify a root toda also supplied is circular: an adversarial API returns a consistent pair and you learn nothing.
toda.verify(id) does exactly this. It is a convenience for the case where toda is not your adversary - a health check, a dashboard badge. It is not a verification you can present to a third party.
Recomputing the leaf yourself
If you do not want to depend on @toda/crypto, the leaf encoding is small enough to reimplement. It must be byte-exact - see leaf encoding.
import hashlib, json
def canonical(value):
if isinstance(value, list):
return [canonical(v) for v in value]
if isinstance(value, dict):
return {k: canonical(value[k]) for k in sorted(value)}
return value
def leaf_hash(content, metadata=None):
body = json.dumps(
{"content": content, "metadata": canonical(metadata)},
separators=(",", ":"), # no whitespace - JSON.stringify's default
ensure_ascii=False,
).encode("utf-8")
return hashlib.sha256(b"toda:leaf:v1" + b"\x00" + body).digest()
Three ways to get this wrong, all of which produce a different hash and a failing proof:
- Whitespace.
JSON.stringifyemits none. Python'sjson.dumpsemits", "and": "by default - henceseparators. - Key order.
contentbeforemetadataat the top level; every nested object's keys sorted by code point. - Missing metadata. It must serialize as
null, not be omitted and not be{}.
Verifying the fold yourself
def verify_proof(leaf, siblings, directions, root):
acc = leaf
for sibling, sibling_on_right in zip(siblings, directions):
pair = (acc + sibling) if sibling_on_right else (sibling + acc)
acc = hashlib.sha256(b"toda:node:v1" + pair).digest()
return acc == root
directions[i] is True means the sibling at level i sits on the right, so the accumulator goes first. A single-leaf log yields an empty siblings list and the check reduces to leaf == root, which is correct.
Reading the chain yourself
You do not need Anchor. The commitment account is a fixed 129-byte layout at a PDA you can derive.
from solders.pubkey import Pubkey
program_id = Pubkey.from_string("5SwaBH3pXzEKhmL6pbqZ1JgeDiGaf6i5oVj8951RX9sm")
pda, _bump = Pubkey.find_program_address([b"user", bytes(owner)], program_id)
data = rpc.get_account_info(pda).value.data
# 8-byte Anchor discriminator, then:
owner_key = data[8:40]
authority = data[40:72]
merkle_root = data[72:104] # ← what you compare against
memory_count = int.from_bytes(data[104:112], "little")
nonce = int.from_bytes(data[112:120], "little")
last_updated = int.from_bytes(data[120:128], "little", signed=True)
merkle_root is the root over the wallet's leaves 0..memory_count-1. Compare it to the root your fold produced.
What you have proven
The memory, exactly as you hold it, was a member of the leaf set whose Merkle root this wallet's commitment account contained at block time
last_updated.
And, transitively: the memory existed before that block time, has not been altered since, and occupies leaf index leafIndex in that wallet's log.
What you have not proven
| Not proven | Why |
|---|---|
| The memory is true | Truth is not a cryptographic property. Somebody wrote this; the chain says when |
| The owner wrote it | A memory drop lets a delegate write into a log. toda proves what was written, not who wrote it |
| The memory is the latest on its subject | Logs are append-only. Superseding facts are new leaves. Resolution is your application's job |
| The log is complete | You proved inclusion, not exclusion. toda has no non-inclusion proof |
| The memory still exists in toda's database | The commitment survives whether or not the row does |
The memory was written at createdAt | createdAt is a database column. The block time is the only timestamp toda cannot choose |
That last row is worth internalizing. Anything in the proof bundle other than leaf, siblings, and directions is toda telling you something. The block time is the chain telling you something. Only one of those is a proof.
Anchored under an older root
toda.verify() requires batchRoot === onchainRoot, which holds only for memories anchored under the current tip. Older memories are still in the log the tip commits to - but establishing that requires the current leaf set, not the historical batch root.
To verify an older memory against the live tip, rebuild the proof against all of the owner's leaves and fold to the current root:
const leaves = allLeaves(owner); // ordered by leafIndex
const proof = getProof(leaves, memory.leafIndex);
const root = computeRoot(leaves);
verifyProof(proof, root) && bytesToHex(tip.merkleRoot) === bytesToHex(root);
This is what GET /memories/:id/proof already does - it builds the proof against the current leaf set - so the returned proof folds to the current root even when batchRoot records an older one. Fold the proof and compare that result to the tip, rather than comparing batchRoot to the tip, and older memories verify correctly.
An independent verifier who holds only one memory and its proof cannot do this, because they do not have the other leaves. They can verify against batchRoot and check that batchRoot was anchored by txSignature - a transaction they can fetch from the chain themselves, whose block time bounds the memory's existence. That is the strongest claim available from a single memory, and it is enough for the thing proofs are for.