Verify a cycle
Fetch your records for any cycle, recompute the Merkle root with SHA-256, and check it against the root published on the Canton ledger.
This page gives the exact rules, so anyone can check Titan with a few lines of code and no trust in the exchange. If you only want the idea, read Commitments first.
What you need
- Your records:
GET /v1/me/batches/{batchNum}/proof, signed with your session. It only ever answers about your own account. - The published root: read the
BatchManifestfor that batch straight from the Canton ledger through the public observer party, which can see manifests and nothing else. The same data is also served atGET /v1/manifests/{batchNum}for convenience; the ledger is the source you do not have to trust. - SHA-256. Nothing else.
The hashing rules
leafHash(bytes) = SHA-256( 0x00 ‖ bytes )
nodeHash(left, right) = SHA-256( 0x01 ‖ left ‖ right )- A level with an odd number of nodes duplicates its last node.
- A tree with one leaf has that leaf's hash as its root.
- An empty tree has a root of 32 zero bytes.
- The users tree is built over payloads sorted ascending by leaf hash, so your position in it says nothing about you.
- Your event tree keeps your leaves in the order they happened.
All hashes are shown as lowercase hex.
The four checks
The proof response contains your payload, its leafHash, your event leaves, your state, the
manifest and a sibling path. The hex fields are hex strings: decode them to bytes before hashing.
The payload is yours
leafHash(payload.hex) must equal leafHash.
The payload was published
Start from leafHash and walk path from the bottom up. Each entry is { hash, side }:
side = "left" → acc = nodeHash(hash, acc)
side = "right" → acc = nodeHash(acc, hash)The final acc must equal the usersRoot in the manifest you read from the ledger. This is the
step that ties your data to what the exchange published. An empty path means you were the only
active user that cycle, and leafHash is itself the users root. If path is empty and verification.errors
is not, no proof could be built. There is nothing to fold.
Your events are the ones committed
Build the Merkle root of leaves in the order given. It must equal the event root inside
payload.hex.
Your account state is the one committed
leafHash(state.hex) must equal the state root inside payload.hex.
The response also carries parsed fields and the exchange's own verification.status, for
convenience. Read the roots from the hex bytes rather than the parsed fields, and do not rely on
the status. The point is that you do the arithmetic. Where the roots sit inside payload.hex is in
Byte layouts.
If something does not match
503 proof_not_readynames its reason.manifest_pendingandusers_incompleteclear with time; retry a few times, then report it. You will never be handed a partial proof.404means no batch with that number includes you.- A real mismatch: keep the full response and the manifest you read. Together with the orders you signed, that is proof the records and the published root disagree. Contact the Titan team and share it; the auditor can check the same root independently.
The rules above are complete: any SHA-256 library is enough to run every check.