Checkpoints
A checkpoint is the transparency log's size and root, signed by the sequencer as a C2SP signed note, and it is the one thing you need to keep to catch a rewritten history later.
Get the newest checkpoint
https://mlx.app/api/log/checkpointReturns the newest checkpoint as plain text (text/plain; charset=utf-8), not cached. No parameters. Before the first block is logged it answers 404 with no checkpoint yet.
curl https://mlx.app/api/log/checkpointmlx.app/ledger/mlx-1
398
y5euvUWfr1GfTZZ9W2qY+v/y3w9qaB9L+J00VgslPX4=
— mlx.app/ledger/mlx-1 dAoLiBOyZ5xVXeU7JgBkvkJ4wV/8/ZvBbicOP4TsnQnNzvq3yWezFEx/fF10adlfmUJfQ4EmTZLa9gDek7gKluD3oAk=The same checkpoint, with its parts split out, is checkpoint in GET /api/log.
The format
The text follows the C2SP tlog-checkpoint format inside a C2SP signed note:
| Line | Value |
|---|---|
| 1 | The origin, the log's name: mlx.app/ledger/<chain>. |
| 2 | The log size: how many blocks it holds. Blocks are numbered from 0, so a size of 398 covers blocks 0 to 397. |
| 3 | The root hash of the log at that size, base64 (32 bytes). |
| 4 | Empty: the end of the note body. |
| 5 | The signature line: — (an em dash), a space, the origin, a space, then base64 of the 4-byte key ID followed by the 64-byte Ed25519 signature. |
The body is lines 1 to 3, each ending in \n (checkpointBody in lib/chain/tlog.ts). The signature is Ed25519 over the UTF-8 bytes of the body, with the sequencer's key, the same key that signs the blocks.
The key ID is the first 4 bytes of SHA-256(origin ‖ "\n" ‖ 0x01 ‖ public key), where 0x01 marks an Ed25519 key, as C2SP defines it. It lets a verifier pick the right signature line when a note carries several.
A new checkpoint is signed whenever the log grows. The server keeps every one: GET /api/log/checkpoint returns the newest, and ?size=N returns the one signed for a log of N blocks (404 if none was).
Verify a checkpoint
import { ed25519 } from "@noble/curves/ed25519";
import { sha256 } from "@noble/hashes/sha2";
import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from "@noble/hashes/utils";
const unb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
/** Returns { origin, size, root } if `note` carries a valid signature by `pubHex`, else null. */
function verifyCheckpoint(note, pubHex) {
const split = note.indexOf("\n\n");
if (split < 0) return null;
const body = note.slice(0, split + 1); // origin, size and root, each line ending in \n
const [origin, size, rootB64] = body.split("\n");
const keyId = sha256(concatBytes(utf8ToBytes(origin + "\n"), Uint8Array.of(0x01), hexToBytes(pubHex))).slice(0, 4);
for (const line of note.slice(split + 2).split("\n")) {
const m = /^— (\S+) (\S+)$/.exec(line);
if (!m || m[1] !== origin) continue;
const raw = unb64(m[2]); // 4-byte key ID ‖ 64-byte Ed25519 signature
if (raw.length !== 68 || bytesToHex(raw.slice(0, 4)) !== bytesToHex(keyId)) continue;
if (ed25519.verify(raw.slice(4), utf8ToBytes(body), hexToBytes(pubHex))) {
return { origin, size: Number(size), root: bytesToHex(unb64(rootB64)) };
}
}
return null;
}
// Pin the sequencer key once (here read from /api/log; better: keep the value you saw first).
const { sequencer } = await (await fetch("https://mlx.app/api/log")).json();
const note = await (await fetch("https://mlx.app/api/log/checkpoint")).text();
console.log(verifyCheckpoint(note, sequencer));{
origin: 'mlx.app/ledger/mlx-1',
size: 398,
root: 'cb97aebd459faf519f4d967d5b6a98fafff2df0f6a681f4bf89d34560b253d7e'
}The same check ships in the explorer, the wallet and MLX Node (parseCheckpoint in lib/chain/tlog.ts, ported to Swift). It needs @noble/curves and @noble/hashes (tested with 1.9 and 1.8).
Keep what you saw
A signature alone only shows the sequencer signed something. The protection comes from comparing checkpoints over time:
Save the checkpoint
Keep the size and root (or the whole note) of a checkpoint you verified.
Later, fetch the new one
Verify its signature the same way. Its size must be at least the size you kept; a smaller size, or the same size with another root, is proof of a rewrite: keep both notes.
Check that it extends the old one
Ask for a consistency proof from your size to the new size and verify it: Proofs. If it verifies, the new log contains the old one unchanged.
Keep the new one
Replace the saved checkpoint and repeat next time.
That is what explorer.mlx.app/integrity does with the checkpoint your browser saw last, and what every witness Mac does before it cosigns. A checkpoint is also carried by the anchors, so you can check one you never fetched yourself.