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..f666c5d 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..3574602 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..1e02fb8 100644 --- a/docs/mini-block.md +++ b/docs/mini-block.md @@ -58,18 +58,111 @@ 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 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: + +| # | 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 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 + +To verify a mini-block, rebuild the header hash, recover the signer from the signature, and compare it against the sequencer key registered onchain. + +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(); +} +``` + +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 +195,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) — onchain registry of the sequencer signing key