Skip to main content

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

InputFrom
The memory: content and metadataThe owner, or an agent case
The inclusion proofGET /memories/:id/proof
The owner's wallet public keyThe owner
A Solana RPC endpointYours. 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.

Do not use bundle.onchainRoot for check 3

The 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:

  1. Whitespace. JSON.stringify emits none. Python's json.dumps emits ", " and ": " by default - hence separators.
  2. Key order. content before metadata at the top level; every nested object's keys sorted by code point.
  3. 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 provenWhy
The memory is trueTruth is not a cryptographic property. Somebody wrote this; the chain says when
The owner wrote itA 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 subjectLogs are append-only. Superseding facts are new leaves. Resolution is your application's job
The log is completeYou proved inclusion, not exclusion. toda has no non-inclusion proof
The memory still exists in toda's databaseThe commitment survives whether or not the row does
The memory was written at createdAtcreatedAt 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.