MLXDocs
Integrity

Checkpoints

Markdown

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

GEThttps://mlx.app/api/log/checkpoint

Returns 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.

bash
curl https://mlx.app/api/log/checkpoint
Response
mlx.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:

LineValue
1The origin, the log's name: mlx.app/ledger/<chain>.
2The log size: how many blocks it holds. Blocks are numbered from 0, so a size of 398 covers blocks 0 to 397.
3The root hash of the log at that size, base64 (32 bytes).
4Empty: the end of the note body.
5The 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

checkpoint.mjs
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));
Output (when the log held 398 blocks)
{
  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:

  1. Save the checkpoint

    Keep the size and root (or the whole note) of a checkpoint you verified.

  2. 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.

  3. 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.

  4. 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.