# Blocks

The ledger is a chain of signed blocks, each naming the one before it by hash. List them newest first, filter them, read one with its transactions, and check its hash and signature yourself.

## GET /api/blocks {#list}

```endpoint try
GET /api/blocks?limit=2
```

Blocks, newest first, with the chain head. Without filters it is a fast page through the newest blocks; with any of `sort`, `min`, `period` or `cursor` it becomes a filtered scan.

```bash tab=curl
curl "https://mlx.app/api/blocks?limit=2"
```

```js tab=JavaScript
const { head, blocks } = await (await fetch("https://mlx.app/api/blocks?limit=2")).json();
for (const b of blocks) console.log(b.height, b.count, new Date(b.ts).toISOString());
```

```python tab=Python
import requests

d = requests.get("https://mlx.app/api/blocks", params={"limit": 2}).json()
for b in d["blocks"]:
    print(b["height"], b["count"], b["ts"])
```

### Newest first

| Parameter | Type | Notes |
| --- | --- | --- |
| `limit` | integer | How many blocks. The route has a default and a maximum, and clamps anything outside it. |
| `before` | integer | Start below this height, for the next page: pass the last `height` you got. |

```json title="Response"
{
  "head": {
    "height": 397,
    "hash": "ef5f250abbe3aa7f14767ea19ce5b1b67525fff8a8c039f0e72d5a7d33d04601",
    "ts": 1793113786120
  },
  "blocks": [
    {
      "height": 397,
      "prev": "3e8c4828eaf513d12d0096afc3db2b8a7833c6eb8e24d913df7dd6a5a66d0d63",
      "ts": 1793113786120,
      "txRoot": "26c5d2d2f219185f18af55dca8b38e793688d8ae61a86ab397c78b0769e9c31a",
      "count": 1,
      "producer": "92114fe09a90d1a3aced63f35463e4120969d1b323c41070a9ee3ca3cf6ae9a3",
      "hash": "ef5f250abbe3aa7f14767ea19ce5b1b67525fff8a8c039f0e72d5a7d33d04601",
      "txs": ["26c5d2d2f219185f18af55dca8b38e793688d8ae61a86ab397c78b0769e9c31a"],
      "sig": "a3e6fcda3caafa0083396f24bc5d198e1375ed7e62ba957673104ba7800f88ae6b0420273174f7b22fe82edd00dbc8190de9f0564b5608208e92cefd48d9f408"
    },
    {
      "height": 396,
      "prev": "afaccda78033a68bd9298a5d3d5c5040869f7e3cf288909f6c385887c47e84b0",
      "ts": 1793113778971,
      "txRoot": "13e04bcbfca47659cfd1d1fc07ee93125cc8d67ac5ca889b8578dc559f4d378e",
      "count": 1,
      "producer": "92114fe09a90d1a3aced63f35463e4120969d1b323c41070a9ee3ca3cf6ae9a3",
      "hash": "3e8c4828eaf513d12d0096afc3db2b8a7833c6eb8e24d913df7dd6a5a66d0d63",
      "txs": ["13e04bcbfca47659cfd1d1fc07ee93125cc8d67ac5ca889b8578dc559f4d378e"],
      "sig": "a5d4de677c3fecb889b8e0e9a33ea726105da17f7563c0595a1b9dd8bedf74dd12b1183db2b47958a22e91e55331a0ef2e1d3dec1c381fa7429060622703ab0e"
    }
  ]
}
```

Notice that block 397's `prev` is block 396's `hash`: that is the link.

### Filtered

| Parameter | Type | Notes |
| --- | --- | --- |
| `sort` | `new` or `old` | Newest first (default) or oldest first. |
| `min` | integer | Only blocks with at least this many transactions. |
| `period` | `1h`, `24h`, `7d`, `30d` or `all` | Only blocks from that window. |
| `cursor` | integer | The `next` value of the previous page. |
| `limit` | integer | Page size, clamped like the unfiltered list. |

A filtered call looks at a bounded number of heights and stops, so a page can hold fewer blocks than `limit` (even none) while `next` is still set. Keep calling with `cursor=<next>` until `next` is `null`.

```json title="Response (GET /api/blocks?sort=old&min=1&period=24h&limit=1)"
{
  "head": { "height": 399, "hash": "321578e08b05821326965df711b4d96cbcfafc0960704eac13e49d4167f1e8cd", "ts": 1793116923973 },
  "blocks": [
    {
      "height": 353,
      "prev": "80e6d40836464c3f4cba025ec73675d42732c918cb7a8f5b87709a3f00065219",
      "ts": 1793034123926,
      "txRoot": "bea17547c9147d16e96e7622f6fd086980318eacbf830020c87e456644c26aa0",
      "count": 1,
      "producer": "92114fe09a90d1a3aced63f35463e4120969d1b323c41070a9ee3ca3cf6ae9a3",
      "hash": "d784c9a78d8be81a83c78b8176154ac5435ef5b7baffe4c489463f60ccc04873",
      "txs": ["bea17547c9147d16e96e7622f6fd086980318eacbf830020c87e456644c26aa0"],
      "sig": "64b18347c567098599e1a9005b268d5dc2bfbc050540ffd230e082f89845d7792c0f6111395c90a473ea235995dfb076fe722d3653169156d22f754845f61b0e"
    }
  ],
  "next": 354,
  "scanned": 1
}
```

| Field | Type | Notes |
| --- | --- | --- |
| `next` | integer or null | The height to continue from; `null` when the scan reached the end. |
| `scanned` | integer | Heights looked at in this call. |

## GET /api/block/{height} {#block}

```endpoint try
GET /api/block/1
```

One block and every transaction in it, in order. A sealed block never changes, so the response is cached as `immutable`.

| Parameter | Type | Notes |
| --- | --- | --- |
| `height` | integer (path) | `0` is the genesis block. |

```json title="Response"
{
  "block": {
    "height": 1,
    "prev": "b5100a4ff5e14bd01a6240ea2809770985cbb944d83d4a940f87b75efcd1f770",
    "ts": 1792312855965,
    "txRoot": "a3f53b6b0d575b5540b9310a7fd247ad5c0260e09c61bd9c3faa4ea3f530f1a3",
    "count": 1,
    "producer": "92114fe09a90d1a3aced63f35463e4120969d1b323c41070a9ee3ca3cf6ae9a3",
    "hash": "73c50d2f0bfd7df198234734ab6e9d6287653b49a6a5e61543ee5a05e80c25d5",
    "txs": ["a3f53b6b0d575b5540b9310a7fd247ad5c0260e09c61bd9c3faa4ea3f530f1a3"],
    "sig": "30159ff85784b0ead874031d7c848524f2a51f4442d420d8936ba99380e47de799c82c520cac56fbabf6dc4259b04e80995a60002bd9bd44e8b0b0b4333ee80a"
  },
  "txs": [
    {
      "hash": "a3f53b6b0d575b5540b9310a7fd247ad5c0260e09c61bd9c3faa4ea3f530f1a3",
      "tx": {
        "chain": "mlx-1",
        "from": "luca.mlx",
        "nonce": 0,
        "ts": 1792312855563,
        "pk": "53fbe3a2e4ee9ee2ad907174562e8f2fc9b2bb3e1074f8ce274935439422c4ab",
        "body": { "type": "create_account" },
        "sig": "c461d94586ddd6c63c235984332a9508b5f04f1fd8c0f7f32d35377152c4d658a4951ce88ffce0275eb4bbf33bbbeedc9755234ec22cda1b06d85ff6be89fe06"
      },
      "status": "success",
      "receivedAt": 1792312855939,
      "effects": [{ "kind": "account", "to": "luca.mlx" }],
      "height": 1,
      "index": 0
    }
  ]
}
```

The transactions are in the record format described on [Transactions](/network/transactions#record), with Mac names hidden the same way.

## The block format {#format}

| Field | Type | Notes |
| --- | --- | --- |
| `height` | integer | Position in the chain, from `0`. |
| `prev` | string | The previous block's `hash` (64 zeros for the genesis block). |
| `ts` | integer | When the block was sealed, ms. Block times only increase. |
| `txRoot` | string | Merkle root of the transaction hashes, in order (below). |
| `count` | integer | Number of transactions. |
| `txs` | string[] | The transaction hashes, in the order they were applied. |
| `producer` | string | Public key that sealed the block: the sequencer's (`sequencer` on [`/api/stats`](/network/stats)). |
| `hash` | string | SHA-256 of the block header (below), hex. |
| `sig` | string | Ed25519 signature by `producer` over the 32 bytes of `hash`, hex. |

Blocks are sealed by the sequencer from its mempool, usually as a transaction is submitted (the submit call waits for it), and otherwise by the sequencer's scheduled run.

## Check a block yourself {#verify}

Everything needed is in two public responses: the block, and `chain` and `sequencer` from `/api/stats`.

:::steps
### Rebuild the header hash
Make the header object `{ chain, height, prev, ts, txRoot, count, producer }`, with `chain` from `/api/stats`. Write it as canonical JSON: keys sorted, no whitespace, standard JSON escaping. Its SHA-256, as lowercase hex, must equal `hash`.

### Rebuild the Merkle root
Start with the `txs` hashes in order. While more than one is left, pair them up: each pair becomes SHA-256 of the two hashes' **bytes** joined (32 + 32 bytes, not the hex text). An odd one out at the end of a level is paired with itself. The last hash left must equal `txRoot`. A block with no transactions would have SHA-256 of the empty string as its root.

### Check the signature
`producer` must be the `sequencer` key from `/api/stats`. Verify `sig` as an Ed25519 signature over the 32 bytes of `hash` (decoded from hex), with that key.

### Follow the link
Fetch the block at `height - 1`: its `hash` must equal this block's `prev`. Walking back to height `0` checks the whole chain.
:::

```js tab=JavaScript
// npm i @noble/curves @noble/hashes
import { ed25519 } from "@noble/curves/ed25519";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/hashes/utils";

const API = "https://mlx.app/api";
const canonical = (v) =>
  v === null || typeof v !== "object"
    ? JSON.stringify(v)
    : Array.isArray(v)
      ? "[" + v.map(canonical).join(",") + "]"
      : "{" + Object.keys(v).filter((k) => v[k] !== undefined).sort().map((k) => JSON.stringify(k) + ":" + canonical(v[k])).join(",") + "}";
const hex = (data) => bytesToHex(sha256(typeof data === "string" ? utf8ToBytes(data) : data));
const merkleRoot = (hashes) => {
  if (!hashes.length) return hex("");
  let level = hashes;
  while (level.length > 1) {
    const next = [];
    for (let i = 0; i < level.length; i += 2) next.push(hex(hexToBytes(level[i] + (level[i + 1] ?? level[i]))));
    level = next;
  }
  return level[0];
};

const { chain, sequencer } = await (await fetch(`${API}/stats`)).json();
const { block } = await (await fetch(`${API}/block/1`)).json();
const { height, prev, ts, txRoot, count, producer } = block;

console.log("hash", hex(canonical({ chain, height, prev, ts, txRoot, count, producer })) === block.hash);
console.log("root", merkleRoot(block.txs) === txRoot);
console.log("sig ", producer === sequencer && ed25519.verify(hexToBytes(block.sig), hexToBytes(block.hash), hexToBytes(sequencer)));
```

```python tab=Python
# pip install requests cryptography
import hashlib, json, requests
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

API = "https://mlx.app/api"
canonical = lambda v: json.dumps(v, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
sha = lambda b: hashlib.sha256(b).hexdigest()

stats = requests.get(f"{API}/stats").json()
block = requests.get(f"{API}/block/1").json()["block"]

header = {k: block[k] for k in ("height", "prev", "ts", "txRoot", "count", "producer")}
header["chain"] = stats["chain"]
assert sha(canonical(header).encode()) == block["hash"]

level = block["txs"] or [sha(b"")]
while len(level) > 1:
    level = [sha(bytes.fromhex(level[i] + (level[i + 1] if i + 1 < len(level) else level[i]))) for i in range(0, len(level), 2)]
assert level[0] == block["txRoot"]

assert block["producer"] == stats["sequencer"]
key = Ed25519PublicKey.from_public_bytes(bytes.fromhex(stats["sequencer"]))
key.verify(bytes.fromhex(block["sig"]), bytes.fromhex(block["hash"]))  # raises if it doesn't match
print("block", block["height"], "verified")
```

Each transaction in the block can be checked the same way: see [Keys and signing](/transactions/keys-and-signing). One exception: a Mac link (`bind_device`) is shown with the kind of Mac in place of its name, so its own hash can't be rebuilt from the public copy, though its hash still sits in `txs` and `txRoot`. For proof that a block is in the published log, and of an account's balances at a height, see [Integrity](/integrity/overview).

## Errors

| Status | When |
| --- | --- |
| 400 | `/api/block/{height}`: the height isn't a whole number (`"Block height must be a whole number"`). |
| 404 | `/api/block/{height}`: no block at that height yet (`"Block not found"`). |
| 500 | The ledger couldn't be read. |

`/api/blocks` never answers 400: an unknown `sort` or `period` falls back to the default, and an out-of-range `limit` is clamped.
