From c3572acc57ea5d4b8a05e01600399a9eff7c7d03 Mon Sep 17 00:00:00 2001 From: William Aaron Cheung Date: Thu, 11 Jun 2026 03:00:01 +0000 Subject: [PATCH 1/4] docs: document mini-block sequencer signatures and verification --- docs/dev/faq.md | 1 + docs/dev/read/realtime-api.md | 1 + docs/dev/read/rpc/eth_subscribe.md | 23 ++--- docs/mini-block.md | 135 ++++++++++++++++++++++++++++- 4 files changed, 148 insertions(+), 12 deletions(-) diff --git a/docs/dev/faq.md b/docs/dev/faq.md index 346749b..cbb4d49 100644 --- a/docs/dev/faq.md +++ b/docs/dev/faq.md @@ -131,6 +131,7 @@ Mini-blocks are intentionally compact and do not include the same metadata field Yes. Preconfirmation of mini-blocks by the sequencer has the same level of guarantees as that of EVM blocks. +Every mini-block header is [signed by the sequencer](../mini-block.md#sequencer-signatures), so the preconfirmation is cryptographically verifiable. ### Does the performance dashboard (uptime.megaeth.com) display the block height in mini-blocks or EVM blocks? diff --git a/docs/dev/read/realtime-api.md b/docs/dev/read/realtime-api.md index 0347636..a98c1fe 100644 --- a/docs/dev/read/realtime-api.md +++ b/docs/dev/read/realtime-api.md @@ -98,6 +98,7 @@ Use `latest` or `pending` as the block tag and you get mini-block-level freshnes Mini-blocks carry the same preconfirmation guarantee as EVM blocks. The sequencer treats them identically — results returned by the Realtime API are not "tentative" or "unconfirmed." +Each mini-block header is signed by the sequencer, making the commitment independently verifiable — see [Sequencer Signatures](../../mini-block.md#sequencer-signatures). ### Example: real-time balance query diff --git a/docs/dev/read/rpc/eth_subscribe.md b/docs/dev/read/rpc/eth_subscribe.md index f2ff618..3f19dbb 100644 --- a/docs/dev/read/rpc/eth_subscribe.md +++ b/docs/dev/read/rpc/eth_subscribe.md @@ -118,16 +118,19 @@ Streams mini-blocks as they are produced by the sequencer. **Notification schema:** -| Field | Type | Notes | -| ---------------------- | --------------- | -------------------------------------------------------- | -| `block_number` | `Quantity` | EVM block number that this mini-block belongs to | -| `block_timestamp` | `Quantity` | EVM block timestamp | -| `index` | `Quantity` | Index of this mini-block within the EVM block | -| `mini_block_number` | `Quantity` | Global mini-block height | -| `mini_block_timestamp` | `Quantity` | Creation timestamp (Unix microseconds) | -| `gas_used` | `Quantity` | Gas consumed in this mini-block | -| `transactions` | `Transaction[]` | Transactions (same schema as `eth_getTransactionByHash`) | -| `receipts` | `Receipt[]` | Receipts (same schema as `eth_getTransactionReceipt`) | +| Field | Type | Notes | +| ---------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `block_number` | `Quantity` | EVM block number that this mini-block belongs to | +| `block_timestamp` | `Quantity` | EVM block timestamp | +| `index` | `Quantity` | Index of this mini-block within the EVM block | +| `mini_block_number` | `Quantity` | Global mini-block height | +| `mini_block_timestamp` | `Quantity` | Creation timestamp (Unix microseconds) | +| `gas_used` | `Quantity` | Gas consumed in this mini-block | +| `transactions` | `Transaction[]` | Transactions (same schema as `eth_getTransactionByHash`) | +| `receipts` | `Receipt[]` | Receipts (same schema as `eth_getTransactionReceipt`) | +| `transaction_root` | `Hash` | Merkle (MPT) root of `transactions` | +| `receipt_root` | `Hash` | Merkle (MPT) root of `receipts` | +| `signature` | `Object` | Sequencer's ECDSA signature over the header hash, as `r`, `s`, and `yParity` fields. Absent for pre-Rex5 mini-blocks. See [Sequencer Signatures](../../../mini-block.md#sequencer-signatures) | ### `newHeads` diff --git a/docs/mini-block.md b/docs/mini-block.md index 2742366..034c777 100644 --- a/docs/mini-block.md +++ b/docs/mini-block.md @@ -58,18 +58,147 @@ Mini-blocks share the core properties you would expect from any block: - **Ordered.** Mini-blocks are totally ordered by height, starting from 0 at genesis. - **Complete.** Every transaction processed by the sequencer appears in exactly one mini-block. -- **Preconfirmed.** A transaction included in a mini-block carries the same preconfirmation guarantee as one included in an EVM block — the sequencer has committed to its ordering and result. +- **Preconfirmed.** A transaction included in a mini-block carries the same preconfirmation guarantee as one included in an EVM block — the sequencer has committed to its ordering and result, and [signs every mini-block header](#sequencer-signatures) to make that commitment verifiable. Where they differ from EVM blocks: | Property | EVM Block | Mini-Block | | ------------------------------ | ----------- | -------------------------------------------------------------------------------------------------------- | | Production interval | ~1 second | ~10 milliseconds | -| Header size | ~500+ bytes | Compact (no Merkle roots, no bloom filter) | +| Header size | ~500+ bytes | Compact (no state root, no bloom filter) | | Timestamp resolution | 1 second | Microsecond (via [High-Precision Timestamp](dev/execution/system-contracts.md#high-precision-timestamp)) | | Compatible with standard tools | Yes | Requires [Realtime API](dev/read/realtime-api.md) | | Contains state root | Yes | No | +## Sequencer Signatures + +The preconfirmation guarantee is not just a promise — it is cryptographically enforced. +The sequencer signs every mini-block header with its sequencer key, and the signature is delivered alongside the mini-block in the [Realtime API](dev/read/realtime-api.md) stream. +A signed mini-block is a binding commitment: if the sequencer ever sealed an EVM block that contradicts a mini-block it signed, anyone holding the signed header could prove the misbehavior. + +The signing key is registered on-chain in the [SequencerRegistry](https://docs.megaeth.com/spec/system-contracts/sequencer-registry) system contract at `0x6342000000000000000000000000000000000006`, introduced in Rex5. +The key can be rotated by scheduling a change in the registry; rotations take effect at an EVM block boundary, and the full change history remains queryable on-chain. + +The signature is a standard secp256k1 ECDSA signature over `keccak256(rlp(header))`, where the header is the following eight fields of the [`miniBlocks` subscription payload](dev/read/rpc/eth_subscribe.md#miniblocks), RLP-encoded in this order: + +| # | Payload field | Type | +| --- | ---------------------- | ------------ | +| 1 | `block_number` | integer | +| 2 | `block_timestamp` | integer | +| 3 | `index` | integer | +| 4 | `mini_block_number` | integer | +| 5 | `mini_block_timestamp` | integer | +| 6 | `gas_used` | integer | +| 7 | `transaction_root` | 32-byte hash | +| 8 | `receipt_root` | 32-byte hash | + +Integers are RLP-encoded in their minimal big-endian form. +The digest is signed directly — there is no [EIP-191](https://eips.ethereum.org/EIPS/eip-191) prefix — so the signature is verifiable on-chain with `ecrecover`. +The `signature` field of the payload carries the components as an object: `r`, `s`, and `yParity`. + +{% hint style="info" %} +Mini-blocks produced before Rex5 are unsigned — the `signature` field is absent from their payloads. +{% endhint %} + +### Verifying a Mini-Block Signature + +To verify a mini-block, rebuild the header hash, recover the signer from the signature, and compare it against the sequencer key registered on-chain. + +{% tabs %} +{% tab title="TypeScript" %} +The example below uses [viem](https://viem.sh) and takes a notification payload `mb` exactly as delivered by the [`miniBlocks` subscription](dev/read/rpc/eth_subscribe.md#miniblocks). + +```typescript +import { + createPublicClient, + http, + keccak256, + toRlp, + recoverAddress, + parseAbi, +} from "viem"; + +const client = createPublicClient({ + transport: http("https://mainnet.megaeth.com/rpc"), +}); + +// RLP integer form: minimal big-endian bytes; zero is the empty byte string +const int = (hex: `0x${string}`): `0x${string}` => { + let h = BigInt(hex).toString(16); + if (h === "0") return "0x"; + return `0x${h.length % 2 ? "0" + h : h}`; +}; + +// `mb` is a notification payload from the `miniBlocks` subscription +async function isSignedBySequencer(mb: any): Promise { + // 1. Rebuild the signed digest: keccak256(rlp(header)) + const hash = keccak256( + toRlp([ + int(mb.block_number), + int(mb.block_timestamp), + int(mb.index), + int(mb.mini_block_number), + int(mb.mini_block_timestamp), + int(mb.gas_used), + mb.transaction_root, + mb.receipt_root, + ]), + ); + // 2. Recover the signer from the raw digest (no EIP-191 prefix) + const signer = await recoverAddress({ hash, signature: mb.signature }); + // 3. Look up the sequencer key that was active for this block + const sequencer = await client.readContract({ + address: "0x6342000000000000000000000000000000000006", + abi: parseAbi(["function sequencerAt(uint256) view returns (address)"]), + functionName: "sequencerAt", + args: [BigInt(mb.block_number)], + }); + return signer.toLowerCase() === sequencer.toLowerCase(); +} +``` + +{% endtab %} +{% tab title="Solidity" %} +On-chain, recover the signer with `ecrecover` and compare it against the registry. +Computing `headerHash` requires RLP-encoding the eight header fields, which is typically done off-chain. + +```solidity +pragma solidity ^0.8.0; + +interface ISequencerRegistry { + function currentSequencer() external view returns (address); + function sequencerAt(uint256 blockNumber) external view returns (address); +} + +address constant SEQUENCER_REGISTRY = 0x6342000000000000000000000000000000000006; + +/// @param headerHash keccak256 of the RLP-encoded mini-block header +/// @param blockNumber the mini-block's `block_number` field +/// @param yParity the signature's `yParity` field (0 or 1) +function isSignedBySequencer( + bytes32 headerHash, + uint256 blockNumber, + uint8 yParity, + bytes32 r, + bytes32 s +) view returns (bool) { + address signer = ecrecover(headerHash, yParity + 27, r, s); + return signer != address(0) && + signer == ISequencerRegistry(SEQUENCER_REGISTRY).sequencerAt(blockNumber); +} +``` + +{% endtab %} +{% endtabs %} + +When verifying historical mini-blocks, use `sequencerAt(blockNumber)` rather than `currentSequencer()` — the sequencer key can rotate, and the registry resolves which key was active at any block. +`currentSequencer()` is sufficient when verifying live mini-blocks as they stream in. + +{% hint style="warning" %} +A valid signature proves the sequencer committed to the mini-block's contents — it does not by itself prove finality. +Full finality still depends on the containing EVM block being posted to and finalized on the L1. +{% endhint %} + ## Relationship to EVM Blocks Every mini-block belongs to exactly one EVM block — its transactions never span multiple EVM blocks. @@ -102,4 +231,6 @@ Each notification delivers the mini-block's transactions, receipts, and state ch - [Architecture](architecture.md) — how transactions flow through the MegaETH network - [Realtime API](dev/read/realtime-api.md) — subscribe to mini-blocks and get execution results with minimum latency +- [eth_subscribe](dev/read/rpc/eth_subscribe.md) — full reference of the `miniBlocks` subscription payload - [High-Precision Timestamp](dev/execution/system-contracts.md#high-precision-timestamp) — microsecond timestamps available within mini-blocks +- [SequencerRegistry (spec)](https://docs.megaeth.com/spec/system-contracts/sequencer-registry) — on-chain registry of the sequencer signing key From cf70ecbb317ca05e9b832ff414b579e9927ff660 Mon Sep 17 00:00:00 2001 From: William Aaron Cheung Date: Thu, 11 Jun 2026 03:04:59 +0000 Subject: [PATCH 2/4] docs: apply sentence-case headings and onchain terminology --- docs/mini-block.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/mini-block.md b/docs/mini-block.md index 034c777..6324e31 100644 --- a/docs/mini-block.md +++ b/docs/mini-block.md @@ -70,14 +70,14 @@ Where they differ from EVM blocks: | Compatible with standard tools | Yes | Requires [Realtime API](dev/read/realtime-api.md) | | Contains state root | Yes | No | -## Sequencer Signatures +## Sequencer signatures The preconfirmation guarantee is not just a promise — it is cryptographically enforced. The sequencer signs every mini-block header with its sequencer key, and the signature is delivered alongside the mini-block in the [Realtime API](dev/read/realtime-api.md) stream. A signed mini-block is a binding commitment: if the sequencer ever sealed an EVM block that contradicts a mini-block it signed, anyone holding the signed header could prove the misbehavior. -The signing key is registered on-chain in the [SequencerRegistry](https://docs.megaeth.com/spec/system-contracts/sequencer-registry) system contract at `0x6342000000000000000000000000000000000006`, introduced in Rex5. -The key can be rotated by scheduling a change in the registry; rotations take effect at an EVM block boundary, and the full change history remains queryable on-chain. +The signing key is registered onchain in the [SequencerRegistry](https://docs.megaeth.com/spec/system-contracts/sequencer-registry) system contract at `0x6342000000000000000000000000000000000006`, introduced in Rex5. +The key can be rotated by scheduling a change in the registry; rotations take effect at an EVM block boundary, and the full change history remains queryable onchain. The signature is a standard secp256k1 ECDSA signature over `keccak256(rlp(header))`, where the header is the following eight fields of the [`miniBlocks` subscription payload](dev/read/rpc/eth_subscribe.md#miniblocks), RLP-encoded in this order: @@ -93,16 +93,16 @@ The signature is a standard secp256k1 ECDSA signature over `keccak256(rlp(header | 8 | `receipt_root` | 32-byte hash | Integers are RLP-encoded in their minimal big-endian form. -The digest is signed directly — there is no [EIP-191](https://eips.ethereum.org/EIPS/eip-191) prefix — so the signature is verifiable on-chain with `ecrecover`. +The digest is signed directly — there is no [EIP-191](https://eips.ethereum.org/EIPS/eip-191) prefix — so the signature is verifiable onchain with `ecrecover`. The `signature` field of the payload carries the components as an object: `r`, `s`, and `yParity`. {% hint style="info" %} Mini-blocks produced before Rex5 are unsigned — the `signature` field is absent from their payloads. {% endhint %} -### Verifying a Mini-Block Signature +### Verifying a mini-block signature -To verify a mini-block, rebuild the header hash, recover the signer from the signature, and compare it against the sequencer key registered on-chain. +To verify a mini-block, rebuild the header hash, recover the signer from the signature, and compare it against the sequencer key registered onchain. {% tabs %} {% tab title="TypeScript" %} @@ -159,8 +159,8 @@ async function isSignedBySequencer(mb: any): Promise { {% endtab %} {% tab title="Solidity" %} -On-chain, recover the signer with `ecrecover` and compare it against the registry. -Computing `headerHash` requires RLP-encoding the eight header fields, which is typically done off-chain. +Onchain, recover the signer with `ecrecover` and compare it against the registry. +Computing `headerHash` requires RLP-encoding the eight header fields, which is typically done offchain. ```solidity pragma solidity ^0.8.0; @@ -233,4 +233,4 @@ Each notification delivers the mini-block's transactions, receipts, and state ch - [Realtime API](dev/read/realtime-api.md) — subscribe to mini-blocks and get execution results with minimum latency - [eth_subscribe](dev/read/rpc/eth_subscribe.md) — full reference of the `miniBlocks` subscription payload - [High-Precision Timestamp](dev/execution/system-contracts.md#high-precision-timestamp) — microsecond timestamps available within mini-blocks -- [SequencerRegistry (spec)](https://docs.megaeth.com/spec/system-contracts/sequencer-registry) — on-chain registry of the sequencer signing key +- [SequencerRegistry (spec)](https://docs.megaeth.com/spec/system-contracts/sequencer-registry) — onchain registry of the sequencer signing key From 19f551c199cadc5b4bd202dc2c2f97fc09d6e9c0 Mon Sep 17 00:00:00 2001 From: William Aaron Cheung Date: Thu, 11 Jun 2026 03:10:47 +0000 Subject: [PATCH 3/4] docs: sentence-case link text for sequencer signatures section --- docs/dev/read/realtime-api.md | 2 +- docs/dev/read/rpc/eth_subscribe.md | 2 +- docs/mini-block.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/read/realtime-api.md b/docs/dev/read/realtime-api.md index a98c1fe..f666c5d 100644 --- a/docs/dev/read/realtime-api.md +++ b/docs/dev/read/realtime-api.md @@ -98,7 +98,7 @@ Use `latest` or `pending` as the block tag and you get mini-block-level freshnes Mini-blocks carry the same preconfirmation guarantee as EVM blocks. The sequencer treats them identically — results returned by the Realtime API are not "tentative" or "unconfirmed." -Each mini-block header is signed by the sequencer, making the commitment independently verifiable — see [Sequencer Signatures](../../mini-block.md#sequencer-signatures). +Each mini-block header is signed by the sequencer, making the commitment independently verifiable — see [Sequencer signatures](../../mini-block.md#sequencer-signatures). ### Example: real-time balance query diff --git a/docs/dev/read/rpc/eth_subscribe.md b/docs/dev/read/rpc/eth_subscribe.md index 3f19dbb..3574602 100644 --- a/docs/dev/read/rpc/eth_subscribe.md +++ b/docs/dev/read/rpc/eth_subscribe.md @@ -130,7 +130,7 @@ Streams mini-blocks as they are produced by the sequencer. | `receipts` | `Receipt[]` | Receipts (same schema as `eth_getTransactionReceipt`) | | `transaction_root` | `Hash` | Merkle (MPT) root of `transactions` | | `receipt_root` | `Hash` | Merkle (MPT) root of `receipts` | -| `signature` | `Object` | Sequencer's ECDSA signature over the header hash, as `r`, `s`, and `yParity` fields. Absent for pre-Rex5 mini-blocks. See [Sequencer Signatures](../../../mini-block.md#sequencer-signatures) | +| `signature` | `Object` | Sequencer's ECDSA signature over the header hash, as `r`, `s`, and `yParity` fields. Absent for pre-Rex5 mini-blocks. See [Sequencer signatures](../../../mini-block.md#sequencer-signatures) | ### `newHeads` diff --git a/docs/mini-block.md b/docs/mini-block.md index 6324e31..9ce352b 100644 --- a/docs/mini-block.md +++ b/docs/mini-block.md @@ -93,7 +93,7 @@ The signature is a standard secp256k1 ECDSA signature over `keccak256(rlp(header | 8 | `receipt_root` | 32-byte hash | Integers are RLP-encoded in their minimal big-endian form. -The digest is signed directly — there is no [EIP-191](https://eips.ethereum.org/EIPS/eip-191) prefix — so the signature is verifiable onchain with `ecrecover`. +The signature is verifiable onchain with `ecrecover`. The `signature` field of the payload carries the components as an object: `r`, `s`, and `yParity`. {% hint style="info" %} From 900e6f6eb13afea83aa434245adaf8bbe842cc07 Mon Sep 17 00:00:00 2001 From: William Aaron Cheung Date: Thu, 11 Jun 2026 03:21:36 +0000 Subject: [PATCH 4/4] docs: keep only the TypeScript verification example --- docs/mini-block.md | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/docs/mini-block.md b/docs/mini-block.md index 9ce352b..1e02fb8 100644 --- a/docs/mini-block.md +++ b/docs/mini-block.md @@ -104,8 +104,6 @@ Mini-blocks produced before Rex5 are unsigned — the `signature` field is absen To verify a mini-block, rebuild the header hash, recover the signer from the signature, and compare it against the sequencer key registered onchain. -{% tabs %} -{% tab title="TypeScript" %} The example below uses [viem](https://viem.sh) and takes a notification payload `mb` exactly as delivered by the [`miniBlocks` subscription](dev/read/rpc/eth_subscribe.md#miniblocks). ```typescript @@ -157,40 +155,6 @@ async function isSignedBySequencer(mb: any): Promise { } ``` -{% endtab %} -{% tab title="Solidity" %} -Onchain, recover the signer with `ecrecover` and compare it against the registry. -Computing `headerHash` requires RLP-encoding the eight header fields, which is typically done offchain. - -```solidity -pragma solidity ^0.8.0; - -interface ISequencerRegistry { - function currentSequencer() external view returns (address); - function sequencerAt(uint256 blockNumber) external view returns (address); -} - -address constant SEQUENCER_REGISTRY = 0x6342000000000000000000000000000000000006; - -/// @param headerHash keccak256 of the RLP-encoded mini-block header -/// @param blockNumber the mini-block's `block_number` field -/// @param yParity the signature's `yParity` field (0 or 1) -function isSignedBySequencer( - bytes32 headerHash, - uint256 blockNumber, - uint8 yParity, - bytes32 r, - bytes32 s -) view returns (bool) { - address signer = ecrecover(headerHash, yParity + 27, r, s); - return signer != address(0) && - signer == ISequencerRegistry(SEQUENCER_REGISTRY).sequencerAt(blockNumber); -} -``` - -{% endtab %} -{% endtabs %} - When verifying historical mini-blocks, use `sequencerAt(blockNumber)` rather than `currentSequencer()` — the sequencer key can rotate, and the registry resolves which key was active at any block. `currentSequencer()` is sufficient when verifying live mini-blocks as they stream in.