MLXDocs
Integrity

State commitments

Markdown

Once an hour the sequencer commits to every account's balances in a Merkle sum tree whose root also carries the total USDC the network owes, and any wallet can check its own balance is counted in it.

What is committed

At most once an hour, and only after new blocks, the sequencer takes every account (system accounts and wallets), sorts them by name and builds a Merkle sum tree over them (commitState in lib/chain/integrity.ts). The tree has the same shape as the log; the difference is that every node also carries the USDC below it, so the root commits to the total.

Each leaf is one account:

FieldTypeNotes
nstringThe account name.
nonceintegerIts nonce.
usdcstringUSDC balance, base units.
usdtstringOlder USDT balance, base units.
mlxstringUnlocked MLX, base units.
lockedstringMLX still locked at the time of the commitment, base units.

What the network owes an account is usdc + usdt (both are dollars 1:1).

Hashing rules

lib/chain/tlog.ts, with every sum written as 16 bytes, big-endian:

SumHash
Leafusdc + usdtSHA-256(0x00 ‖ sum ‖ leaf JSON)
Inner nodeleft sum + right sumSHA-256(0x01 ‖ left hash ‖ right hash ‖ sum)

The leaf JSON is the leaf's fields with keys in alphabetical order and no whitespace, the numbers as strings except nonce:

Leaf JSON
{"locked":"0","mlx":"1499995840385794","n":"treasury.mlx","nonce":0,"usdc":"1677000000","usdt":"0"}

The root's sum is the commitment's usdc: the total USDC (including older USDT) every account on MLX Network holds.

The signed statement

The sequencer signs each commitment with its Ed25519 key, over this text (commitStatement), one item per line, each line ending in \n:

Statement for the commitment at height 397
mlx-state/v1
mlx-1
height 397
state e61dd19f5dffb8ba9b465106faa6a7a2928cfd2bbabd7ef7af13d39d39c5448d
usdc 11285277511
accounts 68
log 398 cb97aebd459faf519f4d967d5b6a98fafff2df0f6a681f4bf89d34560b253d7e

height is the block the balances are after, accounts the number of leaves, and log the log size and root at that block. That last line ties the balances to the history: a consistency proof from that size to any later checkpoint shows they belong to the same log.

Check one wallet

GEThttps://mlx.app/api/log/state/treasury.mlx

The account's leaf in a state commitment and the sibling path from the leaf to the root.

ParameterTypeNotes
namepathThe account, for example luca.mlx.
heightintegerOptional. The commitment made after this block (heights are listed in history of GET /api/log); default: the newest.
Response
{
  "commit": {
    "height": 397,
    "root": "e61dd19f5dffb8ba9b465106faa6a7a2928cfd2bbabd7ef7af13d39d39c5448d",
    "usdc": "11285277511",
    "accounts": 68,
    "size": 398,
    "logRoot": "cb97aebd459faf519f4d967d5b6a98fafff2df0f6a681f4bf89d34560b253d7e",
    "ts": 1793115783839,
    "sig": "78fc7df1a95d357b45aecb195a95c216600cd95f48391f9a604a05e1e89c516c0da09883bd70e5d787d97d11b3eceb11d7a5a9f94fea4b6f0cf39eb869d04704"
  },
  "leaf": { "n": "treasury.mlx", "nonce": 0, "usdc": "1677000000", "usdt": "0", "mlx": "1499995840385794", "locked": "0" },
  "index": 57,
  "count": 68,
  "path": [
    { "h": "6967c23af794d67ebc045bed87802e3d27c748b55023d396ef07601b235d6d0e", "s": "2000000" },
    { "h": "31b98b772c654e3358947d062d2f7a40745c04e12b56345df9eba4c279e448e0", "s": "0" },
    { "h": "ee827ef10ea9a0bd3d39a8b278d612b426b5b13ceb805f7ebde6e4ffa0cf0ce8", "s": "0" },
    { "h": "1cb09f7643ecdf3c129e22a924b7f255d90011ed30bd653264e646b6233d2458", "s": "116929" },
    { "h": "dd2fbd52953500bb53c8dc1927a00d6e5c4e65ca32c4339ec55933609a51cd47", "s": "50000000" },
    { "h": "b4907a105ef58c08c92a08707be44ae138a61c2d62923a9f3b66fd29f55d9dcf", "s": "9296000000" },
    { "h": "940fe1cee0286e8df785f55070738a31051196d8bc78a00977d24b340a0f3789", "s": "260160582" }
  ]
}
FieldTypeNotes
commitobjectThe commitment: height, root (hex), usdc (total, base units), accounts, size and logRoot (the log at that block), ts, sig (Ed25519 over the statement above).
leafobjectThe account's leaf.
indexintegerThe leaf's position (accounts sorted by name).
countintegerThe number of leaves.
patharrayThe siblings from the leaf up to the root, each with its hash h and its USDC sum s.

Other answers, all 200 with commit:

  • "absent": true: no account of that name was in the commitment.
  • "missing": true: the commitment exists, but the leaves behind it are no longer kept (they are kept for a few days), so no path can be given.

404 No state commitment yet when there is no commitment, or none at the height asked for.

Verify a balance in JavaScript

state.mjs
import { ed25519 } from "@noble/curves/ed25519";
import { sha256 } from "@noble/hashes/sha2";
import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from "@noble/hashes/utils";

// 16-byte big-endian sum
const u128 = (v) => {
  const out = new Uint8Array(16);
  for (let i = 15; i >= 0; i--) { out[i] = Number(v & 0xffn); v >>= 8n; }
  return out;
};
// The leaf's JSON, keys in this exact order
const leafJson = (l) =>
  `{"locked":${JSON.stringify(l.locked)},"mlx":${JSON.stringify(l.mlx)},"n":${JSON.stringify(l.n)},` +
  `"nonce":${l.nonce},"usdc":${JSON.stringify(l.usdc)},"usdt":${JSON.stringify(l.usdt)}}`;
const splitPoint = (n) => { let k = 1; while (k * 2 < n) k *= 2; return k; };

function verifyBalance({ commit, leaf, index, count, path }) {
  let sum = BigInt(leaf.usdc) + BigInt(leaf.usdt);
  let hash = sha256(concatBytes(Uint8Array.of(0x00), u128(sum), utf8ToBytes(leafJson(leaf))));
  // which side the sibling is on at each level, found by replaying the tree's splits from the top
  const right = [];
  for (let lo = 0, hi = count, m = index; hi - lo > 1; ) {
    const k = splitPoint(hi - lo);
    if (m < k) { right.push(true); hi = lo + k; } else { right.push(false); lo += k; m -= k; }
  }
  if (right.length !== path.length) return false;
  right.reverse(); // the path runs bottom up
  path.forEach((step, i) => {
    const s = BigInt(step.s);
    if (s < 0n) throw new Error("negative sum");
    sum += s;
    const [l, r] = right[i] ? [hash, hexToBytes(step.h)] : [hexToBytes(step.h), hash];
    hash = sha256(concatBytes(Uint8Array.of(0x01), l, r, u128(sum)));
  });
  return bytesToHex(hash) === commit.root && sum.toString() === commit.usdc;
}

const commitStatement = (c, chain) =>
  `mlx-state/v1\n${chain}\nheight ${c.height}\nstate ${c.root}\nusdc ${c.usdc}\naccounts ${c.accounts}\nlog ${c.size} ${c.logRoot}\n`;

const { chain, sequencer } = await (await fetch("https://mlx.app/api/log")).json();
const proof = await (await fetch("https://mlx.app/api/log/state/treasury.mlx")).json();
console.log("balance in the root:", verifyBalance(proof));
console.log("commitment signed:", ed25519.verify(hexToBytes(proof.commit.sig), utf8ToBytes(commitStatement(proof.commit, chain)), hexToBytes(sequencer)));
Output
balance in the root: true
commitment signed: true

Replace treasury.mlx with your wallet. Also check that leaf shows the balances you expect: the proof shows that these balances are in the total, not that they are right.

Because every inner node carries a sum and no sum can be negative, a balance can't be hidden or counted twice without changing the root or the total. The explorer's Integrity page runs the same balance check for any name you type, and the MLX Wallet runs it for the open wallet ("Verified · block N").

The total against the reserves

The commitment's usdc is everything the network owes in dollars. Compare it with the reserves on mlx.app/reserves. USDC enters the ledger only through system transactions anyone can read: deposits and seat payments carry their on-chain payment, and every issue names the reserve transfer behind it (system transactions).