# BaseHub — full content > BaseHub — reference for agents on Base. Full Markdown corpus, concatenated. 205 pages. See /llms.txt for the index. Source: https://basehub.org Index: https://basehub.org/llms.txt --- # debug_traceBlockByHash Source: https://basehub.org/api-reference/debug/debug_traceblockbyhash/ Description: Replays every transaction in a block identified by hash and returns an EVM execution trace for each one. Replays all transactions in a block identified by its hash and returns an execution trace for each. Debug methods replay every transaction in the block and are computationally expensive. Availability varies by node provider. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `blockHash` | string | Yes | The 32-byte block hash. | | `traceOptions` | object | No | Optional trace configuration. Accepts the same fields as [`debug_traceTransaction`](/api-reference/debug/debug_traceTransaction/). | ## Returns `result` is an array of trace result objects, one per transaction in the block. | Field | Type | Description | | --- | --- | --- | | `txHash` | string | The transaction hash. | | `result` | object | The execution trace for this transaction. Same format as [`debug_traceTransaction`](/api-reference/debug/debug_traceTransaction/). | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "debug_traceBlockByHash", "params": [ "0x3a4e8c5d7f2b1a6e9d0c4f8b3e7a2d5c8f1b4e7a0d3c6f9b2e5a8d1c4f7b0e3", { "tracer": "callTracer" } ], "id": 1 } ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": [ { "txHash": "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238", "result": { "type": "CALL", "from": "0xd3cda913deb6f4967b2ef66ae97de114a83bcc01", "to": "0x4200000000000000000000000000000000000006", "value": "0x2c68af0bb14000", "gas": "0x5208", "gasUsed": "0x5208", "input": "0x", "output": "0x", "calls": [] } } ] } ``` --- # debug_traceBlockByNumber Source: https://basehub.org/api-reference/debug/debug_traceblockbynumber/ Description: Returns EVM execution traces for every transaction in a block identified by its block number or block tag. Returns the EVM execution traces for all transactions in a block identified by its number. Debug methods replay every transaction in the block and are computationally expensive. Availability varies by node provider. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `block` | string | Yes | A block number in hex, or one of `"latest"`, `"earliest"`, `"safe"`, `"finalized"`. | | `tracerConfig` | object | No | Optional tracer configuration. See [`debug_traceTransaction`](/api-reference/debug/debug_traceTransaction/) for available options. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | array | Array of trace objects, one per transaction in the block. | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "debug_traceBlockByNumber", "params": ["latest", {"tracer": "callTracer"}], "id": 1 } ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": [ { "result": { "type": "CALL", "gasUsed": "0xab3f" } } ] } ``` --- # debug_traceTransaction Source: https://basehub.org/api-reference/debug/debug_tracetransaction/ Description: Replays a transaction and returns its full EVM execution trace, with opcode-level gas, stack, memory, and storage detail. Replays a transaction and returns its complete EVM execution trace, including every opcode executed, gas consumed at each step, stack contents, and storage changes. Debug methods replay transactions and are computationally expensive. Availability and rate limits vary by node provider — avoid calling them in hot paths. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `transactionHash` | string | Yes | The 32-byte transaction hash to trace. | | `traceOptions` | object | No | Optional tracing configuration. | The `traceOptions` object accepts: | Field | Type | Description | | --- | --- | --- | | `tracer` | string | Built-in tracer name. `"callTracer"` returns a call tree. `"prestateTracer"` returns the pre-execution account state. Omit to use the default struct log tracer. | | `tracerConfig` | object | Options for the selected tracer. For `"callTracer"`, `\{ "onlyTopCall": true \}` skips internal calls. | | `disableStorage` | boolean | If `true`, omits storage capture from struct logs. Reduces response size. Defaults to `false`. | | `disableMemory` | boolean | If `true`, omits memory capture from struct logs. Reduces response size. Defaults to `false`. | | `disableStack` | boolean | If `true`, omits stack capture from struct logs. Defaults to `false`. | | `timeout` | string | Execution timeout as a Go duration string (for example, `"10s"`, `"30s"`). Defaults to `"5s"`. | ## Returns `result` is the execution trace. The shape depends on the `tracer` option. ### Default struct log trace | Field | Type | Description | | --- | --- | --- | | `gas` | number | Total gas provided for the transaction. | | `failed` | boolean | Whether the transaction failed (reverted). | | `returnValue` | string | Hex-encoded return value from the execution. | | `structLogs` | array | Array of struct log entries, one per EVM opcode executed. | Each entry in `structLogs` contains: | Field | Type | Description | | --- | --- | --- | | `pc` | number | Program counter position. | | `op` | string | EVM opcode name (for example, `"PUSH1"`, `"SLOAD"`). | | `gas` | number | Remaining gas at this step. | | `gasCost` | number | Gas cost of this opcode. | | `depth` | number | Call depth (1 = top-level call). | | `stack` | array | EVM stack values at this step. | | `memory` | array | EVM memory contents as 32-byte chunks. | | `storage` | object | Contract storage changes at this step (slot to value). | ### callTracer result | Field | Type | Description | | --- | --- | --- | | `type` | string | Call type: `"CALL"`, `"STATICCALL"`, `"DELEGATECALL"`, or `"CREATE"`. | | `from` | string | Sender address. | | `to` | string | Recipient address. | | `value` | string | ETH value sent with the call. | | `gas` | string | Gas provided for the call. | | `gasUsed` | string | Gas actually consumed. | | `input` | string | Call data sent. | | `output` | string | Return data from the call. | | `error` | string | Error message if the call reverted. Optional. | | `calls` | array | Array of nested call objects for internal calls. | ## Example ### Request (default struct log) ```json { "jsonrpc": "2.0", "method": "debug_traceTransaction", "params": [ "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238", {} ], "id": 1 } ``` ### Request (callTracer) ```json { "jsonrpc": "2.0", "method": "debug_traceTransaction", "params": [ "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238", { "tracer": "callTracer" } ], "id": 1 } ``` ### Response (default) ```json { "jsonrpc": "2.0", "id": 1, "result": { "gas": 21000, "failed": false, "returnValue": "", "structLogs": [ { "pc": 0, "op": "PUSH1", "gas": 21000, "gasCost": 3, "depth": 1, "stack": [], "memory": [], "storage": {} } ] } } ``` ### Response (callTracer) ```json { "jsonrpc": "2.0", "id": 1, "result": { "type": "CALL", "from": "0xd3cda913deb6f4967b2ef66ae97de114a83bcc01", "to": "0x4200000000000000000000000000000000000006", "value": "0x2c68af0bb14000", "gas": "0x5208", "gasUsed": "0x5208", "input": "0x", "output": "0x", "calls": [] } } ``` --- # eth_blockNumber Source: https://basehub.org/api-reference/eth/eth_blocknumber/ Description: Returns the latest block number on Base — the most recent block the node has imported, encoded as a hexadecimal string. Returns the number of the most recently mined block. ## Parameters None. ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The current block number as a hexadecimal string. | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 } ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": "0x158a0e9" } ``` --- # eth_call Source: https://basehub.org/api-reference/eth/eth_call/ Description: Executes a read-only message call against Base state without broadcasting a transaction; pair with pending on Flashblocks for pre-confirmed reads. import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; Runs a message call immediately against node state without broadcasting a transaction or consuming on-chain gas. Use it to read contract state or simulate calls. ## Parameters ### `transaction` (object, required) The transaction call object. | Field | Type | Description | | --- | --- | --- | | `from` | string | Address the call is sent from. Optional; defaults to the zero address. | | `to` | string | Address the call is directed to. Required. | | `gas` | string | Gas provided for the call as a hexadecimal integer. Defaults to a high limit if omitted. | | `gasPrice` | string | Gas price in wei as a hexadecimal integer. For legacy transactions. Optional. | | `maxFeePerGas` | string | EIP-1559 maximum total fee per gas. Optional. | | `maxPriorityFeePerGas` | string | EIP-1559 maximum priority fee per gas. Optional. | | `value` | string | Value transferred in wei as a hexadecimal integer. Optional. | | `data` | string | ABI-encoded call data: the 4-byte function selector followed by encoded arguments. Optional. | ### `block` (string, required) Block number in hex, or `"latest"`, `"pending"`, `"safe"`, `"finalized"`, `"earliest"`. Pass `"pending"` to call against pre-confirmed state. ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The return value of the call as a hex-encoded byte array. | ## Error codes | Code | Message | Description | | --- | --- | --- | | `-32000` | execution reverted | The call reverted. The `data` field in the error object contains the ABI-encoded revert reason when available. | ## Example ### Request ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_call", "params": [ { "to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "data": "0x70a082310000000000000000000000004200000000000000000000000000000000000006" }, "latest" ], "id": 1 }' ``` ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_call", "params": [ { "to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "data": "0x70a082310000000000000000000000004200000000000000000000000000000000000006" }, "pending" ], "id": 1 }' ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": "0x0000000000000000000000000000000000000000000000000000000005f5e100" } ``` --- # eth_chainId Source: https://basehub.org/api-reference/eth/eth_chainid/ Description: Returns the EIP-695 chain ID for the connected Base network — 0x2105 for Mainnet and 0x14a34 for Sepolia. Returns the chain ID of the current network, per [EIP-695](https://eips.ethereum.org/EIPS/eip-695). ## Parameters None. ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The chain ID as a hexadecimal string. `"0x2105"` (8453) for Base Mainnet and `"0x14a34"` (84532) for Base Sepolia. | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "eth_chainId", "params": [], "id": 1 } ``` ### Response Base Mainnet: ```json { "jsonrpc": "2.0", "id": 1, "result": "0x2105" } ``` Base Sepolia: ```json { "jsonrpc": "2.0", "id": 1, "result": "0x14a34" } ``` --- # eth_estimateGas Source: https://basehub.org/api-reference/eth/eth_estimategas/ Description: Estimates the gas required to execute a transaction on Base; use pending on Flashblocks to estimate against pre-confirmed state. import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; Returns an estimate of how much gas a transaction would require. The reported value may exceed what the transaction actually consumes when executed. ## Parameters ### `transaction` (object, required) The transaction object to estimate gas for. | Field | Type | Description | | --- | --- | --- | | `from` | string | Address the transaction is sent from. Optional. | | `to` | string | Address the transaction is sent to. Optional for contract deployments. | | `gas` | string | Gas limit. Optional; a high default is used if omitted. | | `gasPrice` | string | Gas price in wei for legacy transactions. Optional. | | `maxFeePerGas` | string | EIP-1559 maximum total fee per gas. Optional. | | `maxPriorityFeePerGas` | string | EIP-1559 maximum priority fee per gas. Optional. | | `value` | string | Value to transfer in wei. Optional. | | `data` | string | ABI-encoded call data. Optional. | ### `block` (string, optional) Block to estimate against. Defaults to `"latest"`. Pass `"pending"` to estimate against pre-confirmed state. ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The estimated gas amount as a hexadecimal integer. | ## Error codes | Code | Message | Description | | --- | --- | --- | | `-32000` | execution reverted | The transaction would revert. The error `data` field may contain a revert reason. | ## Example ### Request ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_estimateGas", "params": [{ "from": "0xd3CdA913deB6f4967b2Ef66ae97DE114a83bcc01", "to": "0x4200000000000000000000000000000000000006", "value": "0x2c68af0bb14000" }], "id": 1 }' ``` ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_estimateGas", "params": [{ "from": "0xd3CdA913deB6f4967b2Ef66ae97DE114a83bcc01", "to": "0x4200000000000000000000000000000000000006", "value": "0x2c68af0bb14000" }, "pending"], "id": 1 }' ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": "0x5208" } ``` --- # eth_feeHistory Source: https://basehub.org/api-reference/eth/eth_feehistory/ Description: Returns historical base fees, gas-used ratios, and priority-fee percentile rewards across a range of Base blocks for fee estimation. Returns historical gas data for a contiguous range of blocks, including per-block base fees and the distribution of priority fees. Useful for building EIP-1559 fee-estimation strategies. ## Parameters | Position | Name | Type | Description | | --- | --- | --- | --- | | 1 | `blockCount` | string \| number | Number of blocks to return. Decimal or hexadecimal integer. The maximum is typically 1024. Required. | | 2 | `newestBlock` | string | The highest block to include, as a block number in hex or a block tag (`"latest"`, `"pending"`, etc.). Required. | | 3 | `rewardPercentiles` | array | Array of percentile values (0–100) to sample from each block's priority fees. For example, `[25, 50, 75]` returns the 25th, 50th, and 75th percentile priority fees. Required. | ## Returns `result` (object): | Field | Type | Description | | --- | --- | --- | | `oldestBlock` | string | The oldest block number in the result set (hex). | | `baseFeePerGas` | array | Base fees per gas for each block, with one extra trailing entry for the next pending block, so the length is `blockCount + 1`. | | `gasUsedRatio` | array | Gas-used to gas-limit ratios for each block (0.0 to 1.0); length is `blockCount`. | | `baseFeePerBlobGas` | array | Base fees per blob gas for each block plus one extra entry for the next pending block (EIP-4844), giving a length of `blockCount + 1`. Currently always `"0x1"` on Base. | | `blobGasUsedRatio` | array | Blob-gas-used ratios for each block (0.0 to 1.0), used to derive the next blob base fee (EIP-4844); length is `blockCount`. | | `reward` | array | Two-dimensional array of priority-fee percentiles per block, matching the requested percentile values. | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "eth_feeHistory", "params": ["0xa", "latest", [25, 50, 75]], "id": 1 } ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": { "oldestBlock": "0x158a0e0", "baseFeePerGas": ["0xf5", "0xf7", "0xfa", "0xfc", "0xf8", "0xf5", "0xf2", "0xf0", "0xef", "0xf1", "0xf3"], "gasUsedRatio": [0.45, 0.62, 0.38, 0.71, 0.55, 0.49, 0.43, 0.37, 0.52, 0.64], "baseFeePerBlobGas": ["0x1", "0x1", "0x1", "0x1", "0x1", "0x1", "0x1", "0x1", "0x1", "0x1", "0x1"], "blobGasUsedRatio": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "reward": [ ["0x1", "0x1", "0x2"], ["0x1", "0x1", "0x1"] ] } } ``` --- # eth_gasPrice Source: https://basehub.org/api-reference/eth/eth_gasprice/ Description: Returns the current gas price on Base in wei as a hexadecimal string; for EIP-1559 fee estimation use eth_maxPriorityFeePerGas and eth_feeHistory. Returns the current gas price in wei. For EIP-1559 transactions, prefer [`eth_maxPriorityFeePerGas`](/api-reference/eth/eth_maxpriorityfeepergas/) and [`eth_feeHistory`](/api-reference/eth/eth_feehistory/) instead. ## Parameters None. ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The current gas price in wei as a hexadecimal string. | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "eth_gasPrice", "params": [], "id": 1 } ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": "0x5b8d80" } ``` --- # eth_getBalance Source: https://basehub.org/api-reference/eth/eth_getbalance/ Description: Returns the ETH balance of an account at a given block on Base; use pending on Flashblocks for ~200ms pre-confirmed balances. import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; Returns the ETH balance of an address at a given block. ## Parameters | Position | Name | Type | Description | | --- | --- | --- | --- | | 1 | `address` | string | The 20-byte address to query. Required. | | 2 | `block` | string | Block number in hex, or `"latest"`, `"pending"`, `"safe"`, `"finalized"`, `"earliest"`. Pass `"pending"` for the pre-confirmed balance. Required. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The balance in wei as a hexadecimal string. | ## Example ### Request ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getBalance", "params": ["0x742d35Cc6634C0532925a3b8D4C9dD0b4f3BaEa", "latest"], "id": 1 }' ``` ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getBalance", "params": ["0x742d35Cc6634C0532925a3b8D4C9dD0b4f3BaEa", "pending"], "id": 1 }' ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": "0x1a055690d9db80000" } ``` --- # eth_getBlockByHash Source: https://basehub.org/api-reference/eth/eth_getblockbyhash/ Description: Returns block information for a given 32-byte block hash on Base, with full transaction objects or just transaction hashes. Returns information about a block identified by its hash. ## Parameters | Position | Name | Type | Description | | --- | --- | --- | --- | | 1 | `blockHash` | string | The 32-byte block hash. Required. | | 2 | `fullTransactions` | boolean | When `true`, returns full transaction objects; when `false`, returns only transaction hashes. Required. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | object \| null | A block object, or `null` if no block was found. See [`eth_getBlockByNumber`](/api-reference/eth/eth_getblockbynumber/) for the full field list. | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "eth_getBlockByHash", "params": ["0x5c330e55a190f82ea486b61e5b12e27dfb4fb3cecfc5746886ef38ca1281bce8", false], "id": 1 } ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": { "number": "0x158a0e9", "hash": "0x5c330e55a190f82ea486b61e5b12e27dfb4fb3cecfc5746886ef38ca1281bce8", "parentHash": "0x9edc29b8b0a1e31d28616e40c16132ad0d58faa8bb952595b557526bdb9a960a", "timestamp": "0x67bf8332", "gasLimit": "0x3938700", "gasUsed": "0xab3f", "transactions": ["0x7ef8f8a0..."] } } ``` --- # eth_getBlockByNumber Source: https://basehub.org/api-reference/eth/eth_getblockbynumber/ Description: Returns block data by number on Base; the pending tag returns the live pre-confirmed block updated every ~200ms. import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; Returns information about a block identified by its number. ## Parameters | Position | Name | Type | Description | | --- | --- | --- | --- | | 1 | `block` | string | Block number in hex, or `"latest"`, `"pending"`, `"safe"`, `"finalized"`, `"earliest"`. Pass `"pending"` to get the in-progress block. Required. | | 2 | `fullTransactions` | boolean | When `true`, returns full transaction objects; when `false`, returns only transaction hashes. Required. | ## Returns `result` (object or `null` if no block was found): | Field | Type | Description | | --- | --- | --- | | `number` | string | Block number in hex. `null` when pending. | | `hash` | string | Block hash. `null` when pending. | | `parentHash` | string | Hash of the parent block. | | `nonce` | string | PoW nonce. Always `"0x0000000000000000"` on Base (PoS). | | `sha3Uncles` | string | Hash of the uncles list. Always empty on Base. | | `logsBloom` | string | Bloom filter for the block's logs. | | `transactionsRoot` | string | Root of the transaction trie. | | `stateRoot` | string | Root of the final state trie. | | `receiptsRoot` | string | Root of the receipts trie. | | `miner` | string | Address of the fee recipient (coinbase). | | `difficulty` | string | Always `"0x0"` on Base (PoS). | | `totalDifficulty` | string | Always `"0x0"` on Base (PoS). | | `mixHash` | string | Present on every block; repurposed for PoS consensus (bytes32 hex). | | `extraData` | string | Arbitrary data field set by the sequencer. | | `size` | string | Block size in bytes (hex). | | `gasLimit` | string | Maximum gas allowed in this block (hex). | | `gasUsed` | string | Total gas used in this block (hex). | | `timestamp` | string | Unix timestamp (hex). | | `transactions` | array | Array of transaction hashes or full transaction objects. | | `uncles` | array | Always `[]` on Base. | | `withdrawals` | array | Always `[]` on Base. | | `withdrawalsRoot` | string | Merkle root of the withdrawals list (EIP-4895, bytes32 hex). | | `baseFeePerGas` | string | EIP-1559 base fee per gas (hex). | | `blobGasUsed` | string | Total blob gas used (EIP-4844, hex). | | `excessBlobGas` | string | Excess blob gas for blob fee calculation (EIP-4844, hex). | | `parentBeaconBlockRoot` | string | Parent beacon block root (EIP-4788). | | `requestsHash` | string | Hash of requests (EIP-7685). | ## Flashblock-specific response fields When you query `"pending"`, the response is a live snapshot of the block being built. A few fields behave differently: | Field | Standard `latest` | Flashblocks `pending` | | --- | --- | --- | | `number` | Sealed block number | Current block number (being built) | | `hash` | Final block hash | Hash of the partial block at this Flashblock index | | `gasUsed` | Final gas used | Cumulative gas used up to this Flashblock | | `transactions` | All sealed transactions | Transactions pre-confirmed so far | | `blobGasUsed` | Final blob gas used | Propagated from cumulative Flashblock state | ## Example ### Request ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": ["latest", false], "id": 1 }' ``` ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": ["pending", false], "id": 1 }' ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": { "number": "0x158a0e9", "hash": "0x5c330e55a190f82ea486b61e5b12e27dfb4fb3cecfc5746886ef38ca1281bce8", "parentHash": "0x9edc29b8b0a1e31d28616e40c16132ad0d58faa8bb952595b557526bdb9a960a", "timestamp": "0x67bf8332", "gasLimit": "0x3938700", "gasUsed": "0xab3f", "baseFeePerGas": "0xfa", "transactions": ["0x7ef8f8a0..."], "withdrawals": [], "miner": "0x4200000000000000000000000000000000000011" } } ``` --- # eth_getBlockReceipts Source: https://basehub.org/api-reference/eth/eth_getblockreceipts/ Description: Returns every transaction receipt for a block on Base in one call; pair with pending on Flashblocks for pre-confirmed receipts. import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; Returns every transaction receipt for a given block in a single call. ## Parameters | Position | Name | Type | Description | | --- | --- | --- | --- | | 1 | `block` | string | Block number in hex, or `"latest"`, `"pending"`, `"safe"`, `"finalized"`, `"earliest"`. Required. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | array | Array of receipt objects, one per transaction in the block. See [`eth_getTransactionReceipt`](/api-reference/eth/eth_gettransactionreceipt/) for the receipt object shape. | ## Example ### Request ```bash curl https://mainnet.base.org \ -X POST -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_getBlockReceipts","params":["latest"],"id":1}' ``` ```bash curl https://mainnet.base.org \ -X POST -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_getBlockReceipts","params":["pending"],"id":1}' ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": [ { "transactionHash": "0xabc123...", "blockNumber": "0x158a0e9", "status": "0x1", "gasUsed": "0x5208", "type": "0x2" } ] } ``` --- # eth_getBlockTransactionCountByHash Source: https://basehub.org/api-reference/eth/eth_getblocktransactioncountbyhash/ Description: Returns the number of transactions included in the Base block matching the supplied 32-byte block hash. Returns the number of transactions in a block matching the given block hash. ## Parameters | Position | Name | Type | Description | | --- | --- | --- | --- | | 1 | `blockHash` | string | The 32-byte block hash. Required. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The number of transactions in the block as a hexadecimal integer. `null` if no block was found. | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "eth_getBlockTransactionCountByHash", "params": ["0x5c330e55a190f82ea486b61e5b12e27dfb4fb3cecfc5746886ef38ca1281bce8"], "id": 1 } ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": "0x1f" } ``` --- # eth_getBlockTransactionCountByNumber Source: https://basehub.org/api-reference/eth/eth_getblocktransactioncountbynumber/ Description: Returns the transaction count for a Base block by number; the pending tag counts pre-confirmed transactions in the live Flashblock. import { Aside } from '@astrojs/starlight/components'; Returns the number of transactions in a block matching the given block number. ## Parameters | Position | Name | Type | Description | | --- | --- | --- | --- | | 1 | `block` | string | Block number in hex, or `"latest"`, `"pending"`, `"safe"`, `"finalized"`, `"earliest"`. Required. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The number of transactions in the block as a hexadecimal integer. | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "eth_getBlockTransactionCountByNumber", "params": ["latest"], "id": 1 } ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": "0x1f" } ``` --- # eth_getCode Source: https://basehub.org/api-reference/eth/eth_getcode/ Description: Returns the deployed bytecode at an address on Base; use pending on Flashblocks to detect newly deployed contracts before block finalization. import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; Returns the compiled bytecode at a given address. Returns `"0x"` for externally owned accounts (EOAs). ## Parameters | Position | Name | Type | Description | | --- | --- | --- | --- | | 1 | `address` | string | The 20-byte address to query. Required. | | 2 | `block` | string | Block number in hex, or `"latest"`, `"pending"`, `"safe"`, `"finalized"`, `"earliest"`. Required. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The bytecode at the address as a hex string. `"0x"` if there is no code. | ## Example ### Request ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getCode", "params": ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "latest"], "id": 1 }' ``` ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getCode", "params": ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "pending"], "id": 1 }' ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": "0x608060405234801561001057600080fd5b50..." } ``` --- # eth_getLogs Source: https://basehub.org/api-reference/eth/eth_getlogs/ Description: Returns event logs on Base matching a filter object; pair with pending on Flashblocks to query logs from pre-confirmed transactions. import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; Returns an array of all logs matching a filter object. The primary tool for indexing on-chain events. ## Parameters ### `filter` (object, required) Filter options. At least one criterion should be provided. | Field | Type | Description | | --- | --- | --- | | `fromBlock` | string | Start of the block range. Block number in hex or a block tag. Pass `"pending"` to include pre-confirmed logs. Defaults to `"latest"`. | | `toBlock` | string | End of the block range. Block number in hex or a block tag. Defaults to `"latest"`. | | `address` | string \| array | A contract address or array of addresses to filter by. Optional. | | `topics` | array | Array of 32-byte topic filters. Each position can be `null` (match any), a single topic hex string, or an array of hex strings (match any in the array). Position 0 is typically the `keccak256` hash of the event signature. Optional. | | `blockHash` | string | Restricts logs to the block with this hash. When supplied, `fromBlock` and `toBlock` are ignored. Optional. | ## Returns `result` (array of log objects): | Field | Type | Description | | --- | --- | --- | | `address` | string | 20-byte address of the contract that emitted the log. | | `topics` | array | Array of 0–4 indexed 32-byte topics. Topic 0 is typically the event signature hash. | | `data` | string | ABI-encoded non-indexed event parameters. | | `blockNumber` | string | Block number in which this log was emitted (hex). | | `blockTimestamp` | string | Unix timestamp (hex) of the block that contains this log. An OP Stack extension to the standard Ethereum log schema. | | `transactionHash` | string | 32-byte hash of the transaction that emitted this log. | | `transactionIndex` | string | Index of the transaction in the block (hex). | | `blockHash` | string | 32-byte hash of the block. | | `logIndex` | string | The log's index position within the block (hex). | | `removed` | boolean | `true` if the log was removed due to a chain reorganization. | ## Example ### Request ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getLogs", "params": [{ "fromBlock": "0x12ced00", "toBlock": "0x12ced28", "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"] }], "id": 1 }' ``` ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getLogs", "params": [{ "fromBlock": "pending", "toBlock": "pending", "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"] }], "id": 1 }' ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": [ { "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000d3cda913deb6f4967b2ef66ae97de114a83bcc01", "0x0000000000000000000000004200000000000000000000000000000000000006" ], "data": "0x0000000000000000000000000000000000000000000000000000000005f5e100", "blockNumber": "0x12ced28", "blockTimestamp": "0x6a1092f7", "transactionHash": "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238", "transactionIndex": "0x0", "blockHash": "0x3a4e8c5d7f2b1a6e9d0c4f8b3e7a2d5c8f1b4e7a0d3c6f9b2e5a8d1c4f7b0e3", "logIndex": "0x0", "removed": false } ] } ``` --- # eth_getStorageAt Source: https://basehub.org/api-reference/eth/eth_getstorageat/ Description: Returns the 32-byte value stored at a contract storage slot on Base; use pending on Flashblocks for pre-confirmed storage reads. import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; Returns the value held at a storage position for a given contract address. ## Parameters | Position | Name | Type | Description | | --- | --- | --- | --- | | 1 | `address` | string | The 20-byte address that owns the storage. Required. | | 2 | `position` | string | The storage slot position as a hexadecimal integer. Required. | | 3 | `block` | string | Block number in hex, or `"latest"`, `"pending"`, `"safe"`, `"finalized"`, `"earliest"`. Required. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The value at the storage position as a 32-byte hex string. | ## Example ### Request ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getStorageAt", "params": ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "0x0", "latest"], "id": 1 }' ``` ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getStorageAt", "params": ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "0x0", "pending"], "id": 1 }' ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": "0x0000000000000000000000000000000000000000000000000000000000000001" } ``` --- # eth_getTransactionByBlockHashAndIndex Source: https://basehub.org/api-reference/eth/eth_gettransactionbyblockhashandindex/ Description: Returns transaction data on Base by combining a 32-byte block hash with the transaction's zero-based index inside that block. Returns information about a transaction identified by a block hash and the transaction's index position within that block. ## Parameters | Position | Name | Type | Description | | --- | --- | --- | --- | | 1 | `blockHash` | string | The 32-byte block hash. Required. | | 2 | `index` | string | The transaction index position as a hexadecimal integer. Required. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | object \| null | A transaction object, or `null` if not found. See [`eth_getTransactionByHash`](/api-reference/eth/eth_gettransactionbyhash/) for the full field list. | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "eth_getTransactionByBlockHashAndIndex", "params": ["0x5c330e55a190f82ea486b61e5b12e27dfb4fb3cecfc5746886ef38ca1281bce8", "0x0"], "id": 1 } ``` ### Response The example below is a type `0x7e` deposit transaction at index `0x0`, which carries the OP Stack deposit fields (`sourceHash`, `mint`, `depositReceiptVersion`). See [`eth_getTransactionByHash`](/api-reference/eth/eth_gettransactionbyhash/) for the full field list. ```json { "jsonrpc": "2.0", "id": 1, "result": { "blockHash": "0x491bca01d4bc076d60833dbd973fe031a74e7ae31866bf70d077619e09edb6ff", "blockNumber": "0x2c31b0b", "depositReceiptVersion": "0x1", "from": "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", "gas": "0xf4240", "gasPrice": "0x0", "hash": "0x03c8f106f18ad94190e763e21b584c5825b2f4c61f1274c0e8abe65b4476cd51", "input": "0x3db6be2b...", "mint": "0x0", "nonce": "0x2c31b0e", "r": "0x0", "s": "0x0", "sourceHash": "0xe40ffb1b9f98a24b21e90e3a3cfe49de1eed195618e943da4d029881d3b3e055", "to": "0x4200000000000000000000000000000000000015", "transactionIndex": "0x0", "type": "0x7e", "v": "0x0", "value": "0x0", "yParity": "0x0" } } ``` --- # eth_getTransactionByBlockNumberAndIndex Source: https://basehub.org/api-reference/eth/eth_gettransactionbyblocknumberandindex/ Description: Returns transaction data on Base by pairing a block number or tag with the transaction's zero-based index inside that block. Returns information about a transaction identified by a block number and the transaction's index position within that block. ## Parameters | Position | Name | Type | Description | | --- | --- | --- | --- | | 1 | `block` | string | Block number in hex, or `"latest"`, `"pending"`, `"safe"`, `"finalized"`, `"earliest"`. Required. | | 2 | `index` | string | The transaction index position as a hexadecimal integer. Required. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | object \| null | A transaction object, or `null` if not found. See [`eth_getTransactionByHash`](/api-reference/eth/eth_gettransactionbyhash/) for the full field list. | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "eth_getTransactionByBlockNumberAndIndex", "params": ["latest", "0x0"], "id": 1 } ``` ### Response The example below is a type `0x7e` deposit transaction at index `0x0`, which carries the OP Stack deposit fields (`sourceHash`, `mint`, `depositReceiptVersion`). See [`eth_getTransactionByHash`](/api-reference/eth/eth_gettransactionbyhash/) for the full field list. ```json { "jsonrpc": "2.0", "id": 1, "result": { "blockHash": "0x491bca01d4bc076d60833dbd973fe031a74e7ae31866bf70d077619e09edb6ff", "blockNumber": "0x2c31b0b", "depositReceiptVersion": "0x1", "from": "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", "gas": "0xf4240", "gasPrice": "0x0", "hash": "0x03c8f106f18ad94190e763e21b584c5825b2f4c61f1274c0e8abe65b4476cd51", "input": "0x3db6be2b...", "mint": "0x0", "nonce": "0x2c31b0e", "r": "0x0", "s": "0x0", "sourceHash": "0xe40ffb1b9f98a24b21e90e3a3cfe49de1eed195618e943da4d029881d3b3e055", "to": "0x4200000000000000000000000000000000000015", "transactionIndex": "0x0", "type": "0x7e", "v": "0x0", "value": "0x0", "yParity": "0x0" } } ``` --- # eth_getTransactionByHash Source: https://basehub.org/api-reference/eth/eth_gettransactionbyhash/ Description: Returns full transaction data for a 32-byte transaction hash on Base, or null when the node has not seen the transaction. Returns information about a transaction given its hash. Returns `null` for unknown transactions. ## Parameters | Position | Name | Type | Description | | --- | --- | --- | --- | | 1 | `transactionHash` | string | The 32-byte transaction hash. Required. | ## Returns `result` (object, or `null` if the transaction was not found): | Field | Type | Description | | --- | --- | --- | | `hash` | string | 32-byte transaction hash. | | `nonce` | string | Number of transactions sent by the sender prior to this one (hex). | | `blockHash` | string | 32-byte hash of the block containing this transaction. `null` if pending. | | `blockNumber` | string | Block number (hex). `null` if pending. | | `transactionIndex` | string | Index position in the block (hex). `null` if pending. | | `from` | string | 20-byte sender address. | | `to` | string | 20-byte recipient address. `null` for contract deployments. | | `value` | string | ETH value transferred in wei (hex). | | `gas` | string | Gas provided by the sender (hex). | | `gasPrice` | string | Gas price in wei. For EIP-1559 transactions, this is the effective gas price paid (hex). | | `maxFeePerGas` | string | EIP-1559 maximum total fee per gas (hex). Present for type `0x2` transactions only; absent on type `0x7e`. | | `maxPriorityFeePerGas` | string | EIP-1559 maximum priority fee per gas (hex). Present for type `0x2` transactions only; absent on type `0x7e`. | | `input` | string | ABI-encoded call data. `"0x"` for plain ETH transfers. | | `type` | string | Transaction type: `"0x0"` Legacy, `"0x1"` Access List, `"0x2"` EIP-1559, `"0x7e"` Deposit (L1→L2). | | `chainId` | string | Chain ID the transaction is valid for. `"0x2105"` for Base Mainnet, `"0x14a34"` for Base Sepolia. Not present on type `0x7e`. | | `accessList` | array | List of addresses and storage keys pre-declared by the transaction (EIP-2930). Present for type `0x1` and `0x2` transactions. | | `v` | string | ECDSA recovery ID (hex). | | `r` | string | 32-byte ECDSA signature component r (hex). Always `"0x0"` for type `0x7e`. | | `s` | string | 32-byte ECDSA signature component s (hex). Always `"0x0"` for type `0x7e`. | | `yParity` | string | Signature parity (hex). Always `"0x0"` for type `0x7e` deposits. | | `sourceHash` | string | Identifies the deposit source (32-byte hex). Present on type `0x7e` only. | | `mint` | string | ETH minted on L2 as part of the deposit (hex), usually `"0x0"`. Present on type `0x7e` only. | | `depositReceiptVersion` | string | Version of the deposit receipt format (hex). Present on type `0x7e` only. | Base carries the Ethereum-standard transaction types — `0x0` legacy, `0x1` EIP-2930, `0x2` EIP-1559 — plus the OP Stack deposit type `0x7e`. Deposits are injected by the sequencer at the top of each block: they omit the fee and signature fields a normal transaction carries (`maxFeePerGas`, `accessList`, `chainId`) and instead expose `sourceHash`, `mint`, `depositReceiptVersion`, and a zeroed `yParity`. ## Example ### Request ```json { "jsonrpc": "2.0", "method": "eth_getTransactionByHash", "params": ["0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238"], "id": 1 } ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": { "hash": "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238", "nonce": "0x1b", "blockHash": "0x3a4e8c5d7f2b1a6e9d0c4f8b3e7a2d5c8f1b4e7a0d3c6f9b2e5a8d1c4f7b0e3", "blockNumber": "0x12ced28", "transactionIndex": "0x0", "from": "0xd3CdA913deB6f4967b2Ef66ae97DE114a83bcc01", "to": "0x4200000000000000000000000000000000000006", "value": "0x2c68af0bb14000", "gas": "0x5208", "gasPrice": "0x3b9aca00", "maxFeePerGas": "0x77359400", "maxPriorityFeePerGas": "0x3b9aca00", "input": "0x", "type": "0x2", "chainId": "0x2105", "accessList": [], "v": "0x1", "r": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b", "s": "0x2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c" } } ``` When the node has not seen the transaction: ```json { "jsonrpc": "2.0", "id": 1, "result": null } ``` --- # eth_getTransactionCount Source: https://basehub.org/api-reference/eth/eth_gettransactioncount/ Description: Returns the transaction count (nonce) for an address. Use the pending tag to include pre-confirmed transactions. Returns the number of transactions sent from an address. The value is the account nonce — pass it as `nonce` when constructing the next transaction. Querying with `"pending"` against `https://mainnet.base.org` returns the nonce inclusive of every pre-confirmed transaction and refreshes every ~200ms. This eliminates nonce gaps when an agent submits transactions at high frequency. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | Yes | The 20-byte address to query. | | `block` | string | Yes | A block number in hex, or one of `"latest"`, `"pending"`, `"safe"`, `"finalized"`, `"earliest"`. Use `"pending"` to include all pre-confirmed transactions in the count. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The transaction count (nonce) as a hexadecimal string. | ## Example ### Request (standard, latest confirmed) ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getTransactionCount", "params": ["0x742d35Cc6634C0532925a3b8D4C9dD0b4f3BaEa", "latest"], "id": 1 }' ``` ### Request (Flashblocks pending nonce, ~200ms) ```bash curl https://mainnet.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_getTransactionCount", "params": ["0x742d35Cc6634C0532925a3b8D4C9dD0b4f3BaEa", "pending"], "id": 1 }' ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": "0x4d2" } ``` --- # eth_getTransactionReceipt Source: https://basehub.org/api-reference/eth/eth_gettransactionreceipt/ Description: Returns the receipt for a mined transaction by hash, or null when the transaction is still pending or has not been included in a block. Returns the receipt for a transaction identified by its hash. Returns `null` while the transaction is still pending or has not yet been mined. Receipts are only emitted once a transaction is included in a block. To track a transaction beforehand, call [`base_transactionStatus`](/api-reference/flashblocks/base_transactionStatus/) to confirm it has reached the mempool, or subscribe to [`newFlashblockTransactions`](/api-reference/flashblocks/newFlashblockTransactions/) to detect its pre-confirmation inside a Flashblock. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `transactionHash` | string | Yes | The 32-byte transaction hash. | ## Returns `result` is the receipt object, or `null` if the transaction has not been mined. | Field | Type | Description | | --- | --- | --- | | `transactionHash` | string | 32-byte transaction hash. | | `transactionIndex` | string | Position of the transaction in the block (hex). | | `blockHash` | string | 32-byte hash of the block containing this transaction. | | `blockNumber` | string | Block number (hex). | | `from` | string | 20-byte sender address. | | `to` | string | 20-byte recipient address. `null` for contract deployments. | | `cumulativeGasUsed` | string | Total gas used in the block up to and including this transaction (hex). | | `effectiveGasPrice` | string | Actual gas price paid per unit of gas for this transaction (hex). | | `gasUsed` | string | Gas used by this specific transaction (hex). | | `contractAddress` | string \| null | Address of the deployed contract, or `null` if the transaction was not a deployment. | | `logs` | array | Array of log objects emitted by this transaction. | | `logsBloom` | string | 256-byte bloom filter for the logs in this receipt. | | `type` | string | Transaction type: `"0x0"` Legacy, `"0x1"` Access List, `"0x2"` EIP-1559, `"0x7e"` Deposit (L1→L2). | | `status` | string | `"0x1"` for success, `"0x0"` for failure (revert). | | `blobGasUsed` | string | Blob gas consumed by this transaction (EIP-4844). `null` for non-blob transactions. | | `blobGasPrice` | string | Blob gas price at the time of this transaction (EIP-4844). `null` for non-blob transactions. | | `l1Fee` | string | Total L1 data fee paid for this transaction (hex). OP Stack L2 field. | | `l1GasUsed` | string | L1 gas attributed to this transaction's L1 data portion (hex). OP Stack L2 field. | | `l1GasPrice` | string | L1 gas price at the time of inclusion (hex). OP Stack L2 field. | | `l1BlobBaseFee` | string | L1 blob base fee at the time of inclusion (hex). OP Stack L2 field. | | `l1BlobBaseFeeScalar` | string | Scalar applied to the blob base fee when computing the L1 fee (hex). OP Stack L2 field. | | `l1BaseFeeScalar` | string | Scalar applied to the L1 base fee when computing the L1 fee (hex). OP Stack L2 field. | | `daFootprintGasScalar` | string | Base-specific data-availability footprint scalar (hex). | | `depositNonce` | string | Nonce of the deposit transaction (hex). Present on type `0x7e` transactions only. | | `depositReceiptVersion` | string | Deposit receipt format version (hex). Present on type `0x7e` transactions only. | ## Error codes | Code | Message | Description | | --- | --- | --- | | `-32000` | transaction indexing is in progress | The node is still indexing transactions. Retry once the node finishes syncing. | ## Pre-confirmed receipt data Earlier client versions surfaced abbreviated receipts inside the `metadata.receipts` map of the Flashblocks infrastructure stream. That field was removed in client version v0.8.0 — `metadata` now carries only `block_number`. To obtain receipt data before a block seals, poll this method with the `"pending"` tag, or subscribe to [`newFlashblockTransactions`](/api-reference/flashblocks/newFlashblockTransactions/) with `full: true` for a real-time stream of pre-confirmed transactions that includes their logs. ## Example ### Request ```json { "jsonrpc": "2.0", "method": "eth_getTransactionReceipt", "params": ["0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238"], "id": 1 } ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": { "transactionHash": "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238", "transactionIndex": "0x0", "blockHash": "0x3a4e8c5d7f2b1a6e9d0c4f8b3e7a2d5c8f1b4e7a0d3c6f9b2e5a8d1c4f7b0e3", "blockNumber": "0x12ced28", "from": "0xd3CdA913deB6f4967b2Ef66ae97DE114a83bcc01", "to": "0x4200000000000000000000000000000000000006", "cumulativeGasUsed": "0x5208", "effectiveGasPrice": "0x3b9aca00", "gasUsed": "0x5208", "contractAddress": null, "logs": [], "logsBloom": "0x00000000000000000000000000000000...", "status": "0x1", "type": "0x2" } } ``` ### Response (not found) ```json { "jsonrpc": "2.0", "id": 1, "result": null } ``` --- # eth_maxPriorityFeePerGas Source: https://basehub.org/api-reference/eth/eth_maxpriorityfeepergas/ Description: Returns the suggested EIP-1559 priority fee (tip) per gas to pay the sequencer on top of the base fee. Returns a suggested value for `maxPriorityFeePerGas` to use in an EIP-1559 transaction. The value is the tip paid to the sequencer on top of the base fee. ## Parameters None. ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The suggested priority fee per gas in wei as a hexadecimal string. | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "eth_maxPriorityFeePerGas", "params": [], "id": 1 } ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": "0xf4240" } ``` --- # eth_sendRawTransaction Source: https://basehub.org/api-reference/eth/eth_sendrawtransaction/ Description: Submits a pre-signed RLP-encoded transaction to the network. Submit to a Flashblocks preconf endpoint for ~200ms pre-confirmation. Submits a pre-signed, RLP-encoded transaction to the network and returns the resulting transaction hash. Submit to `https://mainnet.base.org` to have the transaction considered for the next Flashblock (~200ms). After submission, call [`base_transactionStatus`](/api-reference/flashblocks/base_transactionStatus/) to confirm receipt in the mempool, then subscribe to [`newFlashblockTransactions`](/api-reference/flashblocks/newFlashblockTransactions/) to detect pre-confirmation. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `data` | string | Yes | The signed transaction as an RLP-encoded hex string. Typically produced by a wallet library such as viem, ethers.js, or web3.js. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The 32-byte transaction hash, returned when the transaction is accepted into the mempool. | ## Error codes | Code | Message | Description | | --- | --- | --- | | `-32000` | nonce too low | The transaction nonce is below the current account nonce. | | `-32000` | insufficient funds for gas * price + value | The sender's balance cannot cover the gas cost plus the value transferred. | | `-32000` | already known | An identical transaction is already in the mempool. | | `-32000` | replacement transaction underpriced | A replacement transaction must increase the gas price by at least 10%. | ## Example ### Request (standard) ```bash curl https://mainnet.base.org \ -X POST -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["0x02f86b82210501843b9aca008477359400825208944200000000000000000000000000000000000006872c68af0bb1400080c001a0..."],"id":1}' ``` ### Request (Flashblocks preconf submission) ```bash curl https://mainnet.base.org \ -X POST -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["0x02f86b82210501843b9aca008477359400825208944200000000000000000000000000000000000006872c68af0bb1400080c001a0..."],"id":1}' ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238" } ``` --- # eth_subscribe Source: https://basehub.org/api-reference/eth/eth_subscribe/ Description: Opens a real-time WebSocket subscription for new blocks, logs, and pending transactions over a node-provider WebSocket endpoint. Opens a real-time event subscription over a WebSocket connection. Returns a subscription ID; events arrive as `eth_subscription` notifications without the client needing to poll. The public Base endpoints (`mainnet.base.org`, `sepolia.base.org`) are HTTP only — WebSocket connections are not served there. For `eth_subscribe` and the Flashblocks streams, connect through a node provider such as Alchemy or QuickNode that offers WebSocket support for Base. On a Flashblocks-aware endpoint, `newHeads` fires every ~200ms (once per Flashblock) rather than every ~2 seconds, and three additional subscription types become available: [`newFlashblockTransactions`](/api-reference/flashblocks/newFlashblockTransactions/), [`pendingLogs`](/api-reference/flashblocks/pendingLogs/), and [`newFlashblocks`](/api-reference/flashblocks/newFlashblocks/). ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `subscriptionType` | string | Yes | The event type to subscribe to. | | `filterOptions` | object | No | Optional filter object. Only applicable for the `"logs"` subscription type. | The `filterOptions` object accepts: | Field | Type | Description | | --- | --- | --- | | `address` | string \| array | A contract address or array of addresses to filter by. Optional. | | `topics` | array | Array of topic filters in the same format as `eth_getLogs`. Optional. | ## Subscription types | Type | Description | Notification payload | | --- | --- | --- | | `newHeads` | Fires for each new block appended to the chain | Full block header object — same shape as [`eth_getBlockByNumber`](/api-reference/eth/eth_getblockbynumber/) with `hydrated: false` | | `logs` | Fires for each new log matching the filter criteria | Log object | | `newPendingTransactions` | Fires for each new transaction hash added to the mempool | Transaction hash string | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | A hex-encoded subscription ID. Every event notification from this subscription includes the same ID under `params.subscription`. | Event notifications arrive as unsolicited JSON-RPC messages: ```json { "jsonrpc": "2.0", "method": "eth_subscription", "params": { "subscription": "0x1887ec8b9589ccad00000000000532da", "result": { ... } } } ``` ## Example ### Subscribe to newHeads ```json {"jsonrpc": "2.0", "method": "eth_subscribe", "params": ["newHeads"], "id": 1} ``` ### Subscribe to logs (with filter) ```json { "jsonrpc": "2.0", "id": 1, "method": "eth_subscribe", "params": [ "logs", { "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"] } ] } ``` ### Subscription ID response ```json {"jsonrpc": "2.0", "id": 1, "result": "0x1887ec8b9589ccad00000000000532da"} ``` ### newHeads event ```json { "jsonrpc": "2.0", "method": "eth_subscription", "params": { "subscription": "0x1887ec8b9589ccad00000000000532da", "result": { "number": "0x12ced29", "hash": "0x4b5e8c5d7f2b1a6e9d0c4f8b3e7a2d5c8f1b4e7a0d3c6f9b2e5a8d1c4f7b0e4", "parentHash": "0x3a4e8c5d7f2b1a6e9d0c4f8b3e7a2d5c8f1b4e7a0d3c6f9b2e5a8d1c4f7b0e3", "gasLimit": "0x1c9c380", "gasUsed": "0xa410", "timestamp": "0x6783a5d2", "baseFeePerGas": "0x3b9aca00" } } } ``` ### logs event ```json { "jsonrpc": "2.0", "method": "eth_subscription", "params": { "subscription": "0x2a7bc8d4e3f5a6b1c2d3e4f5a6b7c8d9", "result": { "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000d3cda913deb6f4967b2ef66ae97de114a83bcc01", "0x0000000000000000000000004200000000000000000000000000000000000006" ], "data": "0x0000000000000000000000000000000000000000000000000000000005f5e100", "blockNumber": "0x12ced29", "transactionHash": "0xc903239f...", "blockHash": "0x4b5e8c5d...", "logIndex": "0x0", "removed": false } } } ``` --- # eth_syncing Source: https://basehub.org/api-reference/eth/eth_syncing/ Description: Returns the sync status of the node. Returns false when the node is fully synced, otherwise an object describing sync progress. Returns the sync status of the node. Returns `false` when the node is fully synced. ## Parameters None. ## Returns `result` is `false` if the node is fully synced. Otherwise it is a sync status object with the following fields: | Field | Type | Description | | --- | --- | --- | | `startingBlock` | string | Block at which the sync started (hex). | | `currentBlock` | string | Current block being processed (hex). | | `highestBlock` | string | Estimated highest block (hex). | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "eth_syncing", "params": [], "id": 1 } ``` ### Response (synced) ```json { "jsonrpc": "2.0", "id": 1, "result": false } ``` --- # eth_unsubscribe Source: https://basehub.org/api-reference/eth/eth_unsubscribe/ Description: Cancels an active WebSocket subscription created with eth_subscribe and invalidates the subscription ID. Cancels a subscription created with [`eth_subscribe`](/api-reference/eth/eth_subscribe/). The subscription ID is no longer valid after this call. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `subscriptionId` | string | Yes | The subscription ID returned by `eth_subscribe`. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | boolean | `true` if the subscription was successfully cancelled, `false` if the subscription ID was not found. | ## Example ### Request ```json { "jsonrpc": "2.0", "id": 1, "method": "eth_unsubscribe", "params": ["0x1887ec8b9589ccad00000000000532da"] } ``` ### Response (success) ```json { "jsonrpc": "2.0", "id": 1, "result": true } ``` ### Response (not found) ```json { "jsonrpc": "2.0", "id": 1, "result": false } ``` --- # net_version Source: https://basehub.org/api-reference/eth/net_version/ Description: Returns the current network ID as a decimal string. Base Mainnet is 8453 and Base Sepolia is 84532. Returns the current network ID as a decimal string. ## Parameters None. ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The network ID as a decimal string. `"8453"` for Base Mainnet, `"84532"` for Base Sepolia. | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "net_version", "params": [], "id": 1 } ``` ### Response (Base Mainnet) ```json { "jsonrpc": "2.0", "id": 1, "result": "8453" } ``` --- # web3_clientVersion Source: https://basehub.org/api-reference/eth/web3_clientversion/ Description: Returns the version string identifying the underlying node client software running the JSON-RPC endpoint. import { Aside } from '@astrojs/starlight/components'; Returns the version string of the node client software. ## Parameters None. ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | The client version string, formatted as `/-//base/`. Treat it as opaque — the format shifts with node software updates, so avoid parsing it. | ## Example ### Request ```json { "jsonrpc": "2.0", "method": "web3_clientVersion", "params": [], "id": 1 } ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": "reth/v1.11.3-2ac58a2/x86_64-unknown-linux-gnu/base/v0.9.0" } ``` --- # base_transactionStatus Source: https://basehub.org/api-reference/flashblocks/base_transactionstatus/ Description: Reports whether a transaction hash is present in the node mempool, useful for confirming submission before pre-confirmation lands. Reports whether a specific transaction is present in the node mempool. Use it to confirm that a submitted transaction has been received before it appears in a Flashblock. A Base pre-confirmation method served on the standard public endpoints (`https://mainnet.base.org` / `https://sepolia.base.org`), which are all Flashblocks-enabled. Requires [base/base](https://github.com/base/base) minimum client version v0.3.0. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `transactionHash` | string | Yes | The 32-byte transaction hash to query. | ## Returns `result` is the transaction status object: | Field | Type | Description | | --- | --- | --- | | `status` | string | `"Known"` if the transaction is present in the mempool. `"Unknown"` if the node has not seen it. | ## Example ### Request ```bash curl https://mainnet.base.org \ -X POST -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"base_transactionStatus","params":["0xabc123..."],"id":1}' ``` ### Response (Known) ```json {"jsonrpc": "2.0", "id": 1, "result": {"status": "Known"}} ``` ### Response (Unknown) ```json {"jsonrpc": "2.0", "id": 1, "result": {"status": "Unknown"}} ``` --- # eth_simulateV1 Source: https://basehub.org/api-reference/flashblocks/eth_simulatev1/ Description: Simulates one or more transaction bundles against the current pre-confirmed Flashblock state, with state overrides and optional transfer tracing. Simulates one or more transaction bundles against the current pre-confirmed Flashblock state. Supports state overrides, multi-block simulation, and optional transfer tracing. A Base pre-confirmation method served on the standard public endpoints (`https://mainnet.base.org` / `https://sepolia.base.org`), which are all Flashblocks-enabled. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `simulationPayload` | object | Yes | The simulation configuration. | | `blockParameter` | string | Yes | Use `"pending"` to simulate against the current Flashblock state. | The `simulationPayload` object accepts: | Field | Type | Required | Description | | --- | --- | --- | --- | | `blockStateCalls` | array | Yes | Array of block state call objects. Each object represents one simulated block. | | `traceTransfers` | boolean | No | If `true`, ETH transfer events are included as logs in the result. Defaults to `false`. | | `validation` | boolean | No | If `true`, transaction validation (nonce, balance) is enforced. Defaults to `false`. | Each entry in `blockStateCalls` accepts: | Field | Type | Description | | --- | --- | --- | | `calls` | array | Array of transaction call objects to simulate within this block. | | `stateOverrides` | object | Per-address state overrides applied before simulation (for example, balance, nonce, code, storage). Optional. | | `blockOverrides` | object | Block-level overrides (for example, `number`, `timestamp`). Optional. | ## Returns `result` is an array of simulated block results, one per entry in `blockStateCalls`. Each result is a full block object that embeds a `calls` array alongside the standard block fields: | Field | Type | Description | | --- | --- | --- | | `number` | string | Simulated block number (hex). | | `hash` | string | Simulated block hash. | | `parentHash` | string | Parent block hash. | | `timestamp` | string | Block timestamp (hex). | | `gasLimit` | string | Gas limit (hex). | | `gasUsed` | string | Total gas used by the simulated calls (hex). | | `baseFeePerGas` | string | Base fee per gas (hex). | | `stateRoot` | string | Always `"0x000...000"`, because simulation never commits state to the trie. | | `calls` | array | Array of individual call results. | Each entry in `calls` has: | Field | Type | Description | | --- | --- | --- | | `status` | string | `"0x1"` for success, `"0x0"` for failure. | | `gasUsed` | string | Gas used as a hexadecimal integer. | | `returnData` | string | Hex-encoded return data. | | `logs` | array | Logs emitted (including ETH transfer logs if `traceTransfers` is `true`). | | `error` | string | Revert reason if the call failed. Optional. | ## Example ### Request ```bash curl https://sepolia.base.org \ -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_simulateV1", "params": [ { "blockStateCalls": [ { "calls": [{"to": "0x...", "data": "0x..."}], "stateOverrides": {} } ], "traceTransfers": true, "validation": true }, "pending" ], "id": 1 }' ``` ### Response ```json { "jsonrpc": "2.0", "id": 1, "result": [ { "baseFeePerGas": "0x0", "blobGasUsed": "0x39d0", "calls": [ { "gasUsed": "0x5208", "logs": [], "returnData": "0x", "status": "0x1" } ], "difficulty": "0x0", "excessBlobGas": "0x0", "extraData": "0x01000000640000000500000000004c4b40", "gasLimit": "0x17d78400", "gasUsed": "0x5208", "hash": "0x2f2f692821995e39653f63164b2d5d0e0bba66c86c2a199fd3009c0b9906c7b0", "logsBloom": "0x000...000", "miner": "0x4200000000000000000000000000000000000011", "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "nonce": "0x0000000000000000", "number": "0x2c31c49", "parentBeaconBlockRoot": "0x64e625f8bc74f78539f962aa09d522c63576ff6ad57170c668882d99e669ef52", "parentHash": "0x9653660afa4fca3976a21d42ebf849c337e9840993f050fee3affc673a573bf8", "receiptsRoot": "0xf78dfb743fbd92ade140711c8bbc542b5e307f0ab7984eff35d751969fe57efa", "requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", "size": "0x2a6", "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", "timestamp": "0x6a10957f", "transactions": [ "0xa401668a06b038c488c1abc013676dfe63fc645d182ece34d8b3f40f45689279" ], "transactionsRoot": "0x0b1328c457d7a8108ea9f2559142890491b680fdb691720b3d0c857c3d11002c", "uncles": [], "withdrawals": [], "withdrawalsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000" } ] } ``` --- # newFlashblocks Source: https://basehub.org/api-reference/flashblocks/newflashblocks/ Description: Subscribe to the full Flashblock payload stream and receive each pre-confirmed block delta as the sequencer builds it, every ~200ms. Subscribe via `eth_subscribe` to receive full block state updates as each Flashblock is built. Each message contains the accumulated pre-confirmed state for the block in progress. This subscription needs a WebSocket-enabled endpoint. The public Base endpoints (`mainnet.base.org`, `sepolia.base.org`) are HTTP only, so connect through a node provider such as Alchemy or QuickNode that offers WebSocket support for Base. Requires [base/base](https://github.com/base/base) minimum client version v0.3.1. Each subscription emits one Flashblock object per WebSocket message, with events arriving roughly every 200ms. If your handler does heavy work per event, throttle or debounce it to avoid blocking. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `subscriptionType` | string | Yes | Must be `"newFlashblocks"`. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | Hex-encoded subscription ID. Each event notification delivers a Flashblock object — not a standard block object. The payload contains `payload_id`, `index`, `diff`, and (on index 0) `base`. See the [infrastructure stream schema](/api-reference/flashblocks/overview/#flashblock-object) for the full structure. | ## Example ### Subscribe ```json {"jsonrpc": "2.0", "id": 1, "method": "eth_subscribe", "params": ["newFlashblocks"]} ``` ### Subscription ID response ```json {"jsonrpc": "2.0", "id": 1, "result": "0x3b8cd9e5f4a7b2c1d0e3f4a5b6c7d8e9"} ``` ### JavaScript ```javascript import WebSocket from 'ws'; // Use a WebSocket-enabled node provider endpoint const ws = new WebSocket('wss://your-provider-endpoint'); ws.on('open', () => { ws.send(JSON.stringify({ jsonrpc: '2.0', method: 'eth_subscribe', params: ['newFlashblocks'], id: 1 })); }); ws.on('message', (data) => { const msg = JSON.parse(data.toString()); if (msg.method === 'eth_subscription') { // Fires every ~200ms with the latest Flashblock state console.log('Flashblock update:', msg.params.result); } }); ``` --- # newFlashblockTransactions Source: https://basehub.org/api-reference/flashblocks/newflashblocktransactions/ Description: Subscribe to receive each transaction the sequencer pre-confirms into a Flashblock, optionally with full transaction and log data. Subscribe via `eth_subscribe` to receive each transaction as it is pre-confirmed into a Flashblock. Pass `true` as the second parameter to receive full transaction and log data. This subscription needs a WebSocket-enabled endpoint. The public Base endpoints (`mainnet.base.org`, `sepolia.base.org`) are HTTP only, so connect through a node provider such as Alchemy or QuickNode that offers WebSocket support for Base. Requires [base/base](https://github.com/base/base) minimum client version v0.8.0. When `full` is `true`, each notification carries the transaction object with its receipt fields (including logs) embedded directly rather than in a nested receipt. Each subscription emits one item per WebSocket message, with events arriving roughly every 200ms. If your handler does heavy work per event, throttle or debounce it to avoid blocking. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `subscriptionType` | string | Yes | Must be `"newFlashblockTransactions"`. | | `full` | boolean | No | If `true`, each notification includes the full transaction object and associated logs. Defaults to `false` (minimal data only). | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | Hex-encoded subscription ID. | ## Example ### Subscribe ```json {"jsonrpc": "2.0", "id": 1, "method": "eth_subscribe", "params": ["newFlashblockTransactions"]} ``` ### Subscribe (full data) ```json {"jsonrpc": "2.0", "id": 1, "method": "eth_subscribe", "params": ["newFlashblockTransactions", true]} ``` ### Subscription ID response ```json {"jsonrpc": "2.0", "id": 1, "result": "0x1887ec8b9589ccad00000000000532da"} ``` ## Notifications Every update arrives as a standard `eth_subscription` message. What lands in `params.result` depends on the `full` flag. With **`full: false`**, `result` is just the transaction hash: ```json { "jsonrpc": "2.0", "method": "eth_subscription", "params": { "subscription": "0x1887ec8b9589ccad00000000000532da", "result": "0xe26de91f9037e903eefe70b28f613019253da603e67e0dbfe2f656dce5444311" } } ``` With **`full: true`**, `result` is a transaction object that folds its receipt fields in directly (no nested receipt object). The schema below was verified live against `base/v0.9.0`: | Field | Type | Description | | --- | --- | --- | | `type` | string | Transaction type: `0x0` Legacy, `0x2` EIP-1559, `0x7e` Deposit. | | `chainId` | string | Chain ID (hex). | | `nonce` | string | Sender nonce (hex). | | `gas` | string | Gas limit (hex). | | `maxFeePerGas` | string | EIP-1559 max fee per gas (hex). | | `maxPriorityFeePerGas` | string | EIP-1559 max priority fee per gas (hex). | | `to` | string | Recipient address. | | `value` | string | ETH value transferred (hex). | | `accessList` | array | EIP-2930 access list. | | `input` | string | Transaction input data (hex). | | `r` | string | Signature `r` component. | | `s` | string | Signature `s` component. | | `yParity` | string | Signature parity (hex). | | `v` | string | Signature `v` value (hex). | | `hash` | string | Transaction hash. | | `blockHash` | null | Always `null` — the transaction is pre-confirmed, not yet in a finalized block. | | `blockNumber` | string | Block number (hex) of the in-progress Flashblock. | | `transactionIndex` | string | Index within the block (hex). | | `from` | string | Sender address. | | `gasPrice` | string | Effective gas price (hex). | | `gasUsed` | string | Gas used by this transaction (hex). Changed from an integer to a hex string in v0.8.0. | | `status` | string | `0x1` for success, `0x0` for failure. | | `cumulativeGasUsed` | string | Total gas used in the block up to and including this transaction (hex). | | `contractAddress` | string \| null | Address of the created contract, or `null`. | | `logsBloom` | string | Bloom filter of logs (hex). | | `logs` | array | Array of log objects emitted by this transaction. | **Heads up:** `gasUsed` is delivered as a hex string (for example `"0x26132"`), not a number. This changed in v0.8.0, so update any parser that still expects an integer. ```json { "jsonrpc": "2.0", "method": "eth_subscription", "params": { "subscription": "0x1887ec8b9589ccad00000000000532da", "result": { "type": "0x2", "chainId": "0x2105", "nonce": "0x34ed", "gas": "0x7a1200", "maxFeePerGas": "0x257ab3c", "maxPriorityFeePerGas": "0x419c7c", "to": "0x6211a3742cf9d3b6677ecc7fd9dd102ab101d8e2", "value": "0x0", "accessList": [], "input": "0x...", "r": "0xa7cd30d21c30d4d60d27073c8bbc3ef5778527cf98eae0433e9d1f18c929dd5d", "s": "0x08c75921e6bb75e19112300f80998f88a2b0f1adc52df2c3597b171d8c8de68d", "yParity": "0x1", "v": "0x1", "hash": "0x6a010a5ce041ff0ee5a926db65d1ef512836cae822d5f2d58b63981bfa40aa7f", "blockHash": null, "blockNumber": "0x2c679a1", "transactionIndex": "0x83", "from": "0x2ad149d3d3099532d7c25c47cce37db6c4677b3a", "gasPrice": "0x8de7bc", "gasUsed": "0x26132", "status": "0x1", "cumulativeGasUsed": "0x16cb406", "contractAddress": null, "logsBloom": "0x00...00", "logs": [] } } } ``` --- # Flashblocks API overview Source: https://basehub.org/api-reference/flashblocks/overview/ Description: Flashblocks-specific RPC methods, WebSocket subscriptions, and the infrastructure stream schema for Base pre-confirmations. Every public Base endpoint (`mainnet.base.org` / `sepolia.base.org`) is Flashblocks-enabled, serving the full set of standard Ethereum JSON-RPC methods plus a set of pre-confirmation-specific additions. Together they let you read state, simulate transactions, and stream events against sequencer-ordered data up to ~1.8 seconds before a block seals. Every [standard Ethereum JSON-RPC method](/api-reference/rpc-overview/) accepts the `"pending"` block tag to resolve against pre-confirmed state instead of the transaction pool. See the [RPC overview](/api-reference/rpc-overview/) for endpoint URLs. ## HTTP methods | Method | Description | | --- | --- | | [`eth_simulateV1`](/api-reference/flashblocks/eth_simulateV1/) | Simulate transaction bundles against pre-confirmed state. | | [`base_transactionStatus`](/api-reference/flashblocks/base_transactionStatus/) | Check whether a transaction has been received by the node mempool. | ## WebSocket subscriptions Over a WebSocket connection, `eth_subscribe` with `newHeads` emits a new event approximately every 200ms per Flashblock instead of every 2 seconds, and three additional subscription types become available. Note that the public Base endpoints are HTTP only — WebSocket subscriptions require a WebSocket-enabled node provider such as Alchemy or QuickNode. | Subscription | Description | | --- | --- | | [`newFlashblockTransactions`](/api-reference/flashblocks/newFlashblockTransactions/) | Stream individual transactions as they are pre-confirmed (~200ms each). | | [`pendingLogs`](/api-reference/flashblocks/pendingLogs/) | Stream filtered event logs from pre-confirmed transactions. | | [`newFlashblocks`](/api-reference/flashblocks/newFlashblocks/) | Stream full Flashblock payload objects from the sequencer. | ## Infrastructure stream The raw Flashblocks infrastructure stream is the upstream WebSocket feed consumed by Flashblocks-aware RPC nodes. It emits a new message approximately every 200ms as the sequencer pre-confirms transactions. Applications should not connect directly to the infrastructure stream — these endpoints are reserved for node operators. App developers should use the [WebSocket subscription methods](#websocket-subscriptions) above through a Flashblocks-aware RPC provider. | Network | Raw stream URL | | --- | --- | | Mainnet | `wss://mainnet.flashblocks.base.org/ws` | | Sepolia | `wss://sepolia.flashblocks.base.org/ws` | ### Flashblock object The root structure of each infrastructure stream message. | Field | Type | Description | | --- | --- | --- | | `payload_id` | string | Unique identifier for the block being built. Remains consistent across all Flashblocks within a single full block. | | `index` | number | Flashblock index within the current block. Starts at 0 (system transactions only). User transactions begin at index 1. Typically reaches 9–10 per block, but may exceed 10 during sequencer timing drift. | | `base` | Base Object | Block header properties. **Only present when `index` is `0`.** See [Base object](#base-object). | | `diff` | Diff Object | Incremental block state changes for this Flashblock. Present in every message. See [Diff object](#diff-object). | | `metadata` | Metadata Object | Supplemental data. **Unstable — fields may change without notice.** See [Metadata object](#metadata-object). | ### Base object Contains full block header properties. Only present in the `index: 0` message (the first Flashblock of each full block). | Field | Type | Description | | --- | --- | --- | | `parent_hash` | string | Hash of the parent block. | | `fee_recipient` | string | Address receiving transaction fees (coinbase). | | `block_number` | string | Block number in hex. | | `gas_limit` | string | Maximum gas allowed in this block (hex). | | `timestamp` | string | Unix timestamp of block creation (hex). | | `base_fee_per_gas` | string | EIP-1559 base fee per gas (hex). | | `prev_randao` | string | Previous RANDAO value used for on-chain randomness. | | `extra_data` | string | Arbitrary data field set by the sequencer. | | `parent_beacon_block_root` | string | Root of the parent beacon block (EIP-4788). | ### Diff object Contains the incremental block state changes for this specific Flashblock. Present in every message. | Field | Type | Description | | --- | --- | --- | | `state_root` | string | Merkle root of the state trie after applying this Flashblock's transactions. | | `block_hash` | string | Hash of the partial block at this Flashblock index. Changes with each Flashblock as more transactions are pre-confirmed. | | `gas_used` | string | Cumulative gas used up to and including this Flashblock (hex). | | `blob_gas_used` | string | Cumulative blob gas used (EIP-4844, hex). | | `transactions` | string[] | Array of RLP-encoded transactions included in this Flashblock. | | `withdrawals` | array | Validator withdrawals (always empty on Base L2). | | `receipts_root` | string | Merkle root of transaction receipts. | | `logs_bloom` | string | Bloom filter for logs in this Flashblock. | | `withdrawals_root` | string | Merkle root of withdrawals. | ### Metadata object The `metadata` object is not stable. Fields may be added, modified, or removed without prior notice. Do not build production dependencies on it — use the [`diff`](#diff-object) object or query finalized block data via standard RPC instead. As of client version v0.8.0, `new_account_balances` and `receipts` are no longer carried in `metadata`; `block_number` is the only field that remains. | Field | Type | Description | | --- | --- | --- | | `block_number` | number | Block number as a decimal integer. | ### Receipt object `metadata.receipts` was dropped in v0.8.0; this schema is retained only as a reference for older node versions. On v0.8.0 and later, poll [`eth_getTransactionReceipt`](/api-reference/eth/eth_gettransactionreceipt/) for receipt data, or subscribe to [`newFlashblockTransactions`](/api-reference/flashblocks/newFlashblockTransactions/) with `full: true` for a live stream of pre-confirmed transaction data including logs. Check the `type` field to determine the transaction type. | Field | Type | Description | | --- | --- | --- | | `type` | string | Transaction type: `0x0` Legacy, `0x1` Access List, `0x2` EIP-1559, `0x7e` Deposit (L1→L2). | | `status` | string | Transaction status: `0x1` for success, `0x0` for failure. | | `cumulativeGasUsed` | string | Total gas used in the block up to and including this transaction (hex). | | `logs` | Log[] | Array of event logs emitted by the transaction. See [Log object](#log-object). | | `logsBloom` | string | Bloom filter for the logs in this receipt. | | `transactionIndex` | string | Index of the transaction within the block (hex). | ### Log object | Field | Type | Description | | --- | --- | --- | | `address` | string | Contract address that emitted the event. | | `topics` | string[] | Array of indexed event parameters. Topic 0 is typically the event signature hash. | | `data` | string | ABI-encoded non-indexed event parameters. | | `blockHash` | string | Hash of the block containing this log. | | `blockNumber` | string | Block number in hex. | | `blockTimestamp` | string | Unix timestamp of the block as a hex string. OP Stack extension to the standard Ethereum log schema. | | `transactionHash` | string | Hash of the transaction that emitted this log. | | `transactionIndex` | string | Index of the transaction in the block (hex). | | `logIndex` | string | Log's index position within the block (hex). | | `removed` | boolean | `true` if the log was removed due to a chain reorg. | ### Complete examples Index 0 — includes the `base` object (block header): ```json { "payload_id": "0x03997352d799c31a", "index": 0, "base": { "parent_hash": "0x9edc29b8b0a1e31d28616e40c16132ad0d58faa8bb952595b557526bdb9a960a", "fee_recipient": "0x4200000000000000000000000000000000000011", "block_number": "0x158a0e9", "gas_limit": "0x3938700", "timestamp": "0x67bf8332", "base_fee_per_gas": "0xfa", "parent_beacon_block_root": "0x15b9e7c8ac4cbe92dafc849ed30a23e91624bbe5cbe199c0ccea3f7de7fc6d49" }, "diff": { "state_root": "0x208fd63edc0681161105f27d03daf9f8c726d8c94e584a3c0696c98291c24333", "block_hash": "0x5c330e55a190f82ea486b61e5b12e27dfb4fb3cecfc5746886ef38ca1281bce8", "gas_used": "0xab3f", "transactions": ["0x7ef8f8a0b4afc0b7ce10e150801bbaf08ac33fecb0f38311793abccb022120d321c6d276..."], "withdrawals": [] }, "metadata": { "block_number": 22585577 } } ``` Index 1–N (diff only) — no `base` object: ```json { "payload_id": "0x03997352d799c31a", "index": 4, "diff": { "state_root": "0x7a8f45038665072f382730e689f4a1561835c9987fca8942fa95872fb9367eaa", "block_hash": "0x9b32f7a14cbd1efc8c2c5cad5eb718ec9e0c5da92c3ba7080f8d4c49d660c332", "gas_used": "0x1234f", "transactions": ["0x02f90133...", "0x02f90196..."], "withdrawals": [] }, "metadata": { "block_number": 22585577 } } ``` --- # pendingLogs Source: https://basehub.org/api-reference/flashblocks/pendinglogs/ Description: Subscribe to logs from pre-confirmed transactions matching an optional address and topics filter, with sub-block latency over a WebSocket connection. Subscribe via `eth_subscribe` to receive logs from pre-confirmed transactions matching an optional filter. Useful for monitoring contract events with sub-block latency. This subscription needs a WebSocket-enabled endpoint. The public Base endpoints (`mainnet.base.org`, `sepolia.base.org`) are HTTP only, so connect through a node provider such as Alchemy or QuickNode that offers WebSocket support for Base. Requires [base/base](https://github.com/base/base) minimum client version v0.3.1. Each subscription emits one item per WebSocket message, with events arriving roughly every 200ms. If your handler does heavy work per event, throttle or debounce it to avoid blocking. ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `subscriptionType` | string | Yes | Must be `"pendingLogs"`. | | `filterOptions` | object | No | Optional log filter. | The `filterOptions` object accepts: | Field | Type | Description | | --- | --- | --- | | `address` | string \| array | A single contract address or array of addresses to filter by. | | `topics` | array | Array of topic filters in the same format as `eth_getLogs`. | ## Returns | Field | Type | Description | | --- | --- | --- | | `result` | string | Hex-encoded subscription ID. | ## Example ### Subscribe (with filter) ```json { "jsonrpc": "2.0", "id": 1, "method": "eth_subscribe", "params": [ "pendingLogs", { "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"] } ] } ``` ### Subscription ID response ```json {"jsonrpc": "2.0", "id": 1, "result": "0x2a7bc8d4e3f5a6b1c2d3e4f5a6b7c8d9"} ``` --- # Rollup node RPC Source: https://basehub.org/api-reference/rollup-node-rpc/ Description: Reference for the Base rollup node's optimism_outputAtBlock method, which returns the L2 output root and sync metadata for a given block, along with the supporting BlockID, L1BlockRef, L2BlockRef, and SyncStatus structures. Beyond the execution-layer JSON-RPC namespaces, the Base rollup node exposes its own RPC surface. The method documented here, `optimism_outputAtBlock`, is the one most integrators reach for: it hands back the 32-byte [L2 output root](/specifications/proofs-overview/) for a chosen block, which is the value bridges and proof systems anchor against. ## `optimism_outputAtBlock` Given an L2 block number, this method returns that block's output root together with the metadata needed to reason about it. ### Parameters | Name | Type | Description | | :--- | :--- | :--- | | `blockNumber` | `QUANTITY`, 64 bits | L2 integer block number | ### Returns | Name | Type | Description | | :--- | :--- | :--- | | `version` | `DATA`, 32 Bytes | Output root version number, beginning with `0` | | `outputRoot` | `DATA`, 32 Bytes | The output root | | `blockRef` | `Object` | Instance of [`L2BlockRef`](#l2blockref) | | `withdrawalStorageRoot` | `DATA`, 32 Bytes | Storage root of the `L2ToL1MessagePasser` contract | | `stateRoot` | `DATA`, 32 Bytes | The state root | | `syncStatus` | `Object` | Instance of [`SyncStatus`](#syncstatus) | The input and output types follow the [engine API specification][engine-structures]. ## Structures The object types referenced by the rollup node API are listed below. They extend the structures defined in the [engine API specs][engine-structures]. ### BlockID - `hash`: `DATA`, 32 Bytes - `number`: `QUANTITY`, 64 Bits ### L1BlockRef - `hash`: `DATA`, 32 Bytes - `number`: `QUANTITY`, 64 Bits - `parentHash`: `DATA`, 32 Bytes - `timestamp`: `QUANTITY`, 64 Bits ### L2BlockRef - `hash`: `DATA`, 32 Bytes - `number`: `QUANTITY`, 64 Bits - `parentHash`: `DATA`, 32 Bytes - `timestamp`: `QUANTITY`, 64 Bits - `l1origin`: `BlockID` - `sequenceNumber`: `QUANTITY`, 64 Bits - distance to first block of epoch ### SyncStatus A point-in-time snapshot of the rollup driver's view of both chains. - `current_l1`: `Object` - instance of [`L1BlockRef`](#l1blockref). - `current_l1_finalized`: `Object` - instance of [`L1BlockRef`](#l1blockref). - `head_l1`: `Object` - instance of [`L1BlockRef`](#l1blockref). - `safe_l1`: `Object` - instance of [`L1BlockRef`](#l1blockref). - `finalized_l1`: `Object` - instance of [`L1BlockRef`](#l1blockref). - `unsafe_l2`: `Object` - instance of [`L2BlockRef`](#l2blockref). - `safe_l2`: `Object` - instance of [`L2BlockRef`](#l2blockref). - `finalized_l2`: `Object` - instance of [`L2BlockRef`](#l2blockref). - `pending_safe_l2`: `Object` - instance of [`L2BlockRef`](#l2blockref). - `queued_unsafe_l2`: `Object` - instance of [`L2BlockRef`](#l2blockref). [engine-structures]: https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#structures --- # RPC overview Source: https://basehub.org/api-reference/rpc-overview/ Description: Full reference for the JSON-RPC and Flashblocks methods exposed by Base nodes across the eth, net, web3, debug, txpool, and base namespaces. import { Tabs, TabItem, Aside } from '@astrojs/starlight/components'; Base exposes a single, fully EVM-compatible JSON-RPC API. There is now one public endpoint per network rather than separate standard and pre-confirmation URLs — every endpoint is Flashblocks-enabled, so the `pending` block tag resolves against pre-confirmed state with no change of host. ## Networks | Network | Chain ID | Type | Archive | | :--- | :--- | :--- | :--- | | Mainnet | `8453` | Production | Yes | | Sepolia | `84532` | Testnet | Yes | ## Endpoints | | HTTP | | :--- | :--- | | RPC | `https://mainnet.base.org` | | | HTTP | | :--- | :--- | | RPC | `https://sepolia.base.org` | ## Flashblocks Every public Base endpoint is Flashblocks-enabled, so all standard `eth_` methods behave identically and the `pending` block tag resolves against the **pre-confirmed block currently being built**, refreshed every ~200ms with new batches of sequencer-ordered transactions. Methods such as `eth_getBalance`, `eth_getStorageAt`, and `eth_call` execute against real sequencer state up to 1.8 seconds before a block seals, with sub-second latency. ## Namespaces Base nodes implement the standard Ethereum namespaces plus a small set of Base-specific methods. - **`eth`** — core Ethereum protocol methods for accounts, blocks, transactions, gas, and logs. The `pending` tag resolves against pre-confirmed state. - **`net`** — network identification (`net_version`). - **`web3`** — client identification (`web3_clientVersion`). - **`debug`** — replay and tracing utilities for deep transaction inspection. Computationally expensive; availability depends on the node provider. - **`txpool`** — inspection of the local mempool. - **`base`** — Base-specific helpers, including pre-confirmation status checks (`base_transactionStatus`). - **Flashblocks** — pre-confirmation methods (`eth_simulateV1`) and WebSocket streams (`newFlashblockTransactions`, `pendingLogs`, `newFlashblocks`). The streaming subscriptions need a WebSocket-enabled node provider. - **`optimism`** — rollup-node methods served by the consensus client rather than the execution layer, including `optimism_outputAtBlock` for retrieving an L2 output root. ## Method index ### Ethereum JSON-RPC API Methods marked with a check mark support the `"pending"` block tag, which resolves against the pre-confirmed Flashblock in progress. | Method | Description | Flashblocks `pending` | | :--- | :--- | :--- | | [eth_blockNumber](/api-reference/eth/eth_blocknumber/) | Current block number | — | | [eth_getBalance](/api-reference/eth/eth_getbalance/) | Account ETH balance | yes | | [eth_getTransactionCount](/api-reference/eth/eth_gettransactioncount/) | Account nonce / tx count | yes | | [eth_getCode](/api-reference/eth/eth_getcode/) | Deployed contract bytecode | yes | | [eth_getStorageAt](/api-reference/eth/eth_getstorageat/) | Contract storage slot value | yes | | [eth_call](/api-reference/eth/eth_call/) | Execute read-only call | yes | | [eth_getBlockByNumber](/api-reference/eth/eth_getblockbynumber/) | Block data by number | yes | | [eth_getBlockByHash](/api-reference/eth/eth_getblockbyhash/) | Block data by hash | — | | [eth_getBlockReceipts](/api-reference/eth/eth_getblockreceipts/) | All receipts for a block | yes | | [eth_getBlockTransactionCountByNumber](/api-reference/eth/eth_getblocktransactioncountbynumber/) | Tx count by block number | yes | | [eth_getBlockTransactionCountByHash](/api-reference/eth/eth_getblocktransactioncountbyhash/) | Tx count by block hash | — | | [eth_getTransactionByHash](/api-reference/eth/eth_gettransactionbyhash/) | Transaction data by hash | — | | [eth_getTransactionByBlockHashAndIndex](/api-reference/eth/eth_gettransactionbyblockhashandindex/) | Tx by block hash and index | — | | [eth_getTransactionByBlockNumberAndIndex](/api-reference/eth/eth_gettransactionbyblocknumberandindex/) | Tx by block number and index | — | | [eth_getTransactionReceipt](/api-reference/eth/eth_gettransactionreceipt/) | Receipt for a mined tx | — | | [eth_sendRawTransaction](/api-reference/eth/eth_sendrawtransaction/) | Submit signed transaction | — | | [eth_gasPrice](/api-reference/eth/eth_gasprice/) | Current gas price | — | | [eth_maxPriorityFeePerGas](/api-reference/eth/eth_maxpriorityfeepergas/) | Max priority fee estimate | — | | [eth_feeHistory](/api-reference/eth/eth_feehistory/) | Historical base fee and rewards | — | | [eth_estimateGas](/api-reference/eth/eth_estimategas/) | Estimate gas for a tx | yes | | [eth_getLogs](/api-reference/eth/eth_getlogs/) | Query event logs by filter | yes | | [eth_chainId](/api-reference/eth/eth_chainid/) | Network chain ID | — | | [eth_syncing](/api-reference/eth/eth_syncing/) | Node sync status | — | | [net_version](/api-reference/eth/net_version/) | Network version ID | — | | [web3_clientVersion](/api-reference/eth/web3_clientversion/) | Client version string | — | | [eth_subscribe](/api-reference/eth/eth_subscribe/) | Subscribe to events (WSS) | yes | | [eth_unsubscribe](/api-reference/eth/eth_unsubscribe/) | Cancel a subscription (WSS) | — | ### Flashblocks API Pre-confirmation methods that deliver sub-second transaction signals on Base. They let you read state, simulate bundles, and stream events up to ~1.8 seconds ahead of block sealing. The streaming methods require a WebSocket-enabled node provider. | Method | Description | | --- | --- | | [eth_simulateV1](/api-reference/flashblocks/eth_simulatev1/) | Simulate transaction bundles against pre-confirmed state | | [base_transactionStatus](/api-reference/flashblocks/base_transactionstatus/) | Check whether a transaction has been received by the mempool | | [newFlashblockTransactions](/api-reference/flashblocks/newflashblocktransactions/) | Subscribe to individual pre-confirmed transactions | | [pendingLogs](/api-reference/flashblocks/pendinglogs/) | Subscribe to filtered logs from pre-confirmed transactions | | [newFlashblocks](/api-reference/flashblocks/newflashblocks/) | Subscribe to the full Flashblock payload stream | ### Debug API Development and debugging utilities for transaction replay and block inspection. Debug methods replay execution and are computationally expensive — availability and rate limits vary by node provider. | Method | Description | | --- | --- | | [debug_traceTransaction](/api-reference/debug/debug_tracetransaction/) | Full EVM execution trace for a transaction | | [debug_traceBlockByHash](/api-reference/debug/debug_traceblockbyhash/) | EVM traces for every transaction in a block by hash | | [debug_traceBlockByNumber](/api-reference/debug/debug_traceblockbynumber/) | EVM traces for every transaction in a block by number | ### Rollup node RPC Methods exposed by the rollup (consensus) node rather than the execution client. | Method | Description | | --- | --- | | [optimism_outputAtBlock](/api-reference/rollup-node-rpc/) | Output root and sync metadata for an L2 block | ## Request and response format All requests are HTTP `POST` with `Content-Type: application/json`. | Field | Type | Description | | :--- | :--- | :--- | | `jsonrpc` | string | Always `"2.0"` | | `method` | string | The RPC method name | | `params` | array | Method parameters in order | | `id` | number \| string | Identifier echoed back in the response | **Request:** ```json { "jsonrpc": "2.0", "method": "eth_getBalance", "params": ["0x742d35Cc6634C0532925a3b8D4C9dD0b4f3BaEa", "pending"], "id": 1 } ``` **Success response:** ```json { "jsonrpc": "2.0", "id": 1, "result": "0x1a055690d9db80000" } ``` **Error response:** ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "Invalid params" } } ``` ## Error codes | Code | Name | Description | | --- | --- | --- | | `-32700` | Parse error | Invalid JSON | | `-32600` | Invalid request | Not a valid JSON-RPC 2.0 object | | `-32601` | Method not found | Method does not exist or is unavailable | | `-32602` | Invalid params | Invalid method parameters | | `-32603` | Internal error | Internal JSON-RPC error | | `-32000` | Server error | Node-specific error (see message) | ## Block parameters | Value | Resolves to | | --- | --- | | `"latest"` | Most recently sealed block | | `"pending"` | **Current Flashblock in progress (~200ms resolution)** | | `"safe"` | Latest safe block | | `"finalized"` | Latest finalized block | | `"earliest"` | Genesis block | | `"0x"` | Specific block by number | --- # Crate Dependency Graph Source: https://basehub.org/architecture/crate-graph/ Description: Visual map of workspace dependencies across the 8 crate groups in base/base. The `base/base` workspace organizes 8 top-level crate groups into a directed acyclic dependency graph. This page traces how the groups connect, which crates are foundational, and where the integration boundaries sit. ## High-level dependency flow The dependency graph flows downward: higher-level groups depend on lower-level ones, but not the reverse. ``` ┌─────────────────────────────────────────────────────┐ │ infra │ │ based, basectl, ingress-rpc, websocket-proxy, ... │ └───────────┬──────────┬──────────┬───────────────────┘ │ │ │ v v v ┌──────────────┐ ┌──────────┐ ┌──────────┐ │ builder │ │ client │ │consensus │ │ core, pub, │ │ node,cli,│ │ protocol,│ │ metering │ │ engine, │ │ rpc,kona │ │ │ │ flash │ │ │ └──────┬───────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ ┌────┘ │ │ v │ │ ┌──────────┐ │ └───►│execution │◄──────────┘ └────┬─────┘ │ v ┌──────────┐ ┌──────────┐ │ shared │◄────│ proof │ └────┬─────┘ └──────────┘ │ v ┌──────────┐ │ alloy │ └──────────┘ ``` **Key observations:** - **`alloy`** and **`shared`** sit at the bottom of the graph. They have no internal workspace dependencies and are consumed by everything above them. - **`execution`** depends on `shared` and `alloy` for types, and is consumed by both `client` and `builder`. - **`client`** is the main integration point -- it pulls in execution, consensus, and shared. - **`infra`** binaries sit at the top, assembling components from multiple groups. - **`proof`** is relatively isolated. It depends on `shared` for types but has its own Kona-derived execution and derivation logic (designed to run inside a fault proof VM). ## Detailed crate-level dependencies ### Alloy crates ``` alloy/ └── (external: alloy-rs ecosystem) No internal workspace dependencies. Consumed by: shared, client, consensus, builder, execution ``` The alloy crates define Ethereum and Optimism wire types using the [Alloy](https://github.com/alloy-rs/alloy) framework. They are leaf dependencies with no intra-workspace imports. ### Shared crates ``` shared/ ├── base-primitives ← foundational types (blocks, txs, receipts) ├── base-bundles ← depends on: base-primitives ├── base-jwt ← standalone (JWT auth tokens) ├── base-cli-utils ← standalone (CLI helpers) ├── base-engine-ext ← depends on: base-primitives ├── base-access-lists ← depends on: base-primitives ├── base-reth-rpc-types ← depends on: base-primitives └── base-txpool-rpc ← depends on: base-primitives, base-reth-rpc-types ``` [`base-primitives`](https://github.com/base/base/tree/main/crates/shared/primitives) is the single most depended-upon crate. It defines `BaseTxEnvelope`, `BaseBlock`, chain IDs, and fee calculation types. Almost every other workspace crate imports it directly or transitively. ### Execution crates ``` execution/ └── (execution crates) depends on: base-primitives, alloy types consumed by: client, builder ``` The execution group provides Base's EVM configuration. It depends on `base-primitives` for block and transaction types and on alloy types for encoding. The `client` and `builder` groups both consume execution crates to run EVM state transitions. ### Consensus crates ``` consensus/ ├── base-protocol ← depends on: base-primitives ├── base-consensus-rpc ← depends on: base-protocol, base-primitives │ └── kona-* (vendored) ├── kona-node-service ← top-level; depends on most kona-* below ├── kona-engine ← depends on: kona-derive, kona-providers-alloy ├── kona-derive ← depends on: kona-sources, kona-genesis, kona-hardforks ├── kona-gossip ← depends on: kona-disc, kona-peers ├── kona-sources ← depends on: kona-providers-alloy ├── kona-providers-alloy ← depends on: kona-genesis ├── kona-genesis ← depends on: kona-registry ├── kona-registry ← depends on: kona-hardforks ├── kona-hardforks ← leaf ├── kona-disc ← leaf (peer discovery) ├── kona-peers ← leaf (peer management) ├── kona-cli ← leaf (CLI utils) └── kona-macros ← leaf (proc macros) ``` The Kona crates form their own internal dependency tree. `kona-node-service` is the top-level entry point, and `kona-derive` is the core derivation pipeline. The Base-native consensus crates (`base-protocol`, `base-consensus-rpc`) integrate with the Kona stack and provide Base-specific protocol constants. ### Builder crates ``` builder/ ├── base-builder-core ← depends on: base-primitives, base-bundles, │ execution crates, base-engine-ext ├── base-builder-publish ← depends on: base-builder-core, base-primitives └── base-builder-metering ← depends on: base-builder-core ``` The builder's dependency chain: ``` base-builder-metering ──► base-builder-core ──► base-primitives │ │ base-builder-publish ───────────┘ v alloy types │ v execution crates ``` `base-builder-core` contains the block building loop and depends on execution crates for EVM access and on `base-primitives` for block/transaction types. `base-builder-publish` handles the output side -- streaming flashblocks and submitting sealed blocks. ### Client crates ``` client/ ├── base-client-node ← depends on: base-client-cli, base-client-engine, │ base-flashblocks, base-metering, base-txpool-tracing, │ execution crates, consensus crates, shared crates ├── base-client-cli ← depends on: base-cli-utils ├── base-client-engine ← depends on: base-primitives, base-engine-ext ├── base-flashblocks ← depends on: base-primitives ├── base-metering ← depends on: base-primitives, base-reth-rpc-types └── base-txpool-tracing ← depends on: base-primitives ``` `base-client-node` is the integration hub. It is where Reth's `NodeBuilder` is configured with all Base-specific components. Its dependency list is the longest in the workspace because it wires together execution, consensus, flashblocks, metering, and the transaction pool. ### Proof crates ``` proof/ ├── kona-proof ← depends on: kona-driver, kona-mpt ├── kona-driver ← depends on: kona-executor, kona-preimage ├── kona-executor ← depends on: kona-mpt, kona-preimage ├── kona-mpt ← leaf (Merkle Patricia Trie) ├── kona-preimage ← leaf (preimage oracle interface) ├── kona-std-fpvm ← leaf (FPVM runtime) └── kona-std-fpvm-proc ← leaf (proc macros for FPVM) ``` The proof group is intentionally isolated from the rest of the workspace. It must be compilable to FPVM targets (MIPS/RISC-V), so it avoids pulling in networking, RPC, or OS-specific dependencies. It re-implements derivation and execution logic using `kona-preimage` to read data from the host environment. ### Infra crates ``` infra/ ├── based ← depends on: client, builder, consensus, shared ├── basectl-cli ← depends on: shared (base-cli-utils, base-primitives) ├── ingress-rpc-lib ← depends on: shared (base-jwt, base-primitives) ├── websocket-proxy ← depends on: shared (base-primitives) ├── mempool-rebroadcaster ← depends on: shared (base-primitives, base-txpool-rpc) ├── audit-archiver-lib ← depends on: shared (base-primitives) └── system-tests ← depends on: client, shared, infra binaries ``` Infra crates are consumers. They import from the lower layers but are not imported by any other workspace crate (except `system-tests`, which depends on infra binaries for integration testing). ## Dependency rules The workspace follows these conventions: 1. **No circular dependencies.** The crate graph is a DAG. 2. **Shared and alloy are leaf layers.** They never depend on higher-level groups. 3. **Proof is sandboxed.** Proof crates do not depend on client, builder, or infra crates, keeping them FPVM-compilable. 4. **External Reth dependencies flow through execution and client.** Other groups do not directly import `reth-*` crates; they go through the abstractions in execution or shared. 5. **Kona crates are vendored, not patched.** The consensus and proof groups contain full copies of Kona crates (not Git patches), allowing Base-specific modifications while preserving the ability to pull upstream changes. ## Workspace Cargo.toml structure All inter-crate dependencies are declared as workspace dependencies in the root [`Cargo.toml`](https://github.com/base/base/blob/main/Cargo.toml). Individual crate `Cargo.toml` files reference these with `workspace = true`: ```toml # Root Cargo.toml (excerpt) [workspace.dependencies] base-primitives = { path = "crates/shared/primitives" } base-builder-core = { path = "crates/builder/core" } base-client-node = { path = "crates/shared/node" } # ... etc. ``` This centralized dependency management ensures version consistency and makes it straightforward to audit the full dependency graph. --- # Engineering Blog Index Source: https://basehub.org/architecture/engineering-blog/ Description: Running paraphrased index of architecture-relevant posts from the Base engineering blog. A running index of paraphrased entries from the [Base engineering blog](https://blog.base.dev/) that touch on `base/base` architecture — proof systems, builder internals, decentralization milestones, and protocol upgrades. Entries are paraphrased summaries; follow the linked post for full detail. ## 2026 ### Postmortem: June 25th Block Production Outage — 27 June 2026 Base published a detailed postmortem for the two mainnet block production halts on June 25 and 26, 2026 (116 minutes and 20 minutes). Both traced to the same sequencer bug: a failed transaction left the EVM journal state dirty, so a following valid transaction was charged the wrong gas and produced a block no other node would accept, stopping the chain. The patch ([PR #3806](https://github.com/base/base/pull/3806)) restored correct journal handling, and a separate engine-reset race in the sequencers ([PR #3805](https://github.com/base/base/pull/3805/changes)) explains the second-day recurrence. See the local [June 2026 block production outage](/architecture/june-2026-block-production-outage/) deep-dive for the full timeline, or read the original at [blog.base.dev/postmortem-june-25th-block-production-outage](https://blog.base.dev/postmortem-june-25th-block-production-outage). ### Introducing Base Beryl — 18 June 2026 Beryl, Base's second network upgrade, is live on testnet and reaches mainnet on June 25, 2026. It turns Base into a first-class issuance platform through the B20 native token standard, tightens capital efficiency by shortening the withdrawal delay, and raises throughput with Reth V2. The post frames Beryl as the next step after Azul, which laid down the Base Stack and the multiproof system. See the local [Beryl hardfork overview](/specifications/beryl-overview/) for the paraphrased spec, or read the original at [blog.base.dev/introducing-base-beryl](https://blog.base.dev/introducing-base-beryl). ### Multiproofs on Base — 21 May 2026 Base has shipped its multiproof system on mainnet, pairing TEE and ZK provers so the chain can detect and respond to bugs onchain — the technical bar required to reach Stage 2 decentralization in Vitalik Buterin's framework. The new system also speeds up withdrawals by removing the long optimistic challenge window and lowers the bond capital honest defenders previously needed to keep posted. Read the full post at [blog.base.dev/multiproofs-on-base](https://blog.base.dev/multiproofs-on-base). --- # Execution Pipeline Source: https://basehub.org/architecture/execution-pipeline/ Description: Transaction flow from RPC receipt through the transaction pool, payload building, EVM execution, state trie update, and block production. A transaction enters the Base node through the RPC layer, lands in the pool, gets selected by the builder, executes against the EVM, and lands in a sealed block committed to the state trie. Each stage below names the crate that owns it. ## Pipeline overview ``` User/Wallet │ ▼ ┌──────────────────┐ │ 1. RPC Ingress │ ingress-rpc-lib, base-reth-rpc-types └────────┬─────────┘ ▼ ┌──────────────────┐ │ 2. Transaction │ base-txpool-rpc, base-txpool-tracing, │ Pool │ base-access-lists, base-bundles └────────┬─────────┘ ▼ ┌──────────────────┐ │ 3. Payload │ base-builder-core, base-engine-ext │ Building │ └────────┬─────────┘ ▼ ┌──────────────────┐ │ 4. EVM │ execution crates (EvmConfig), │ Execution │ base-primitives └────────┬─────────┘ ▼ ┌──────────────────┐ │ 5. State Trie │ Reth's state database, │ Update │ kona-mpt (for proofs) └────────┬─────────┘ ▼ ┌──────────────────┐ │ 6. Block │ base-builder-publish, │ Sealing │ base-client-engine └──────────────────┘ ``` ## Stage 1: RPC ingress Transactions enter the system through JSON-RPC endpoints. The Base node exposes the standard Ethereum JSON-RPC API (`eth_sendRawTransaction`) plus Base-specific extensions. **Relevant crates:** - [`ingress-rpc-lib`](https://github.com/base/base/tree/main/crates/infra/ingress-rpc) -- in production deployments, an ingress proxy sits in front of the node. It handles rate limiting, authentication (via `base-jwt`), and request routing. The proxy forwards validated requests to the node's internal RPC server. - [`base-reth-rpc-types`](https://github.com/base/base/tree/main/crates/shared/rpc-types) -- defines the request and response types for Base-specific RPC methods. When the node receives `eth_sendRawTransaction`, it: 1. Decodes the RLP-encoded transaction bytes into a `BaseTxEnvelope` (defined in `base-primitives`). 2. Validates the signature, chain ID, and basic transaction fields. 3. Submits the transaction to the transaction pool. For bundle submissions (used by MEV searchers), the `base-bundles` crate defines the `BaseBundle` type, which groups multiple transactions with ordering constraints and tip payments. ## Stage 2: Transaction pool The transaction pool holds pending transactions that are valid but not yet included in a block. Base uses Reth's built-in transaction pool with custom extensions. **Relevant crates:** - [`base-txpool-rpc`](https://github.com/base/base/tree/main/crates/shared/txpool-rpc) -- RPC methods for querying pool state (`txpool_content`, `txpool_status`, and Base-specific methods). - [`base-txpool-tracing`](https://github.com/base/base/tree/main/crates/client/txpool-tracing) -- emits tracing events for transaction lifecycle (received, promoted, dropped, included). Useful for debugging and monitoring. - [`base-access-lists`](https://github.com/base/base/tree/main/crates/shared/access-lists) -- access list generation. When a transaction declares storage slots it will touch, the pool can pre-validate these and the execution engine can optimize state loading. - [`base-bundles`](https://github.com/base/base/tree/main/crates/shared/bundles) -- bundle handling logic. Bundles are stored separately from individual transactions and are evaluated atomically during payload building. The pool maintains several sub-pools: - **Pending** -- transactions ready for immediate inclusion (correct nonce, sufficient balance). - **Queued** -- transactions with future nonces, waiting for gaps to fill. - **Basefee** -- transactions whose `maxFeePerGas` is below the current base fee. They become pending when the base fee drops. Transactions are sorted by **effective tip** (the priority fee the builder would receive). The builder pulls from the pending pool in tip-descending order. ## Stage 3: Payload building Payload building is the process of selecting transactions from the pool and assembling them into a candidate block. In Base, this happens in the builder, which operates on a configurable interval. **Relevant crates:** - [`base-builder-core`](https://github.com/base/base/tree/main/crates/builder/core) -- the main building loop. It: 1. Receives a `PayloadAttributes` message from the consensus layer (via `engine_forkchoiceUpdated`). This message includes the parent block hash, timestamp, and any **deposit transactions** from L1. 2. Pulls pending transactions from the pool, sorted by effective tip. 3. Applies each transaction against the current state to check validity and compute gas used. 4. Respects the block gas limit and any per-transaction gas limits. 5. Optionally includes bundles, evaluating them atomically (all-or-nothing). - [`base-engine-ext`](https://github.com/base/base/tree/main/crates/shared/engine-ext) -- Engine API extensions that carry additional payload attributes specific to Base (e.g., Flashblocks configuration, builder identity). ### Deposit transactions Every L2 block begins with **deposit transactions** -- L1-to-L2 messages derived from L1 by the consensus layer. These are mandatory and always placed at the top of the block, before any user transactions. The builder does not select them from the pool; they are injected by the engine. The ordering within a block is: ``` ┌──────────────────────────────────────┐ │ Deposit transactions (from L1) │ ← mandatory, first ├──────────────────────────────────────┤ │ User transactions (from pool) │ ← sorted by effective tip ├──────────────────────────────────────┤ │ Bundle transactions (if any) │ ← evaluated atomically └──────────────────────────────────────┘ ``` ### Flashblocks sub-intervals In Flashblocks mode, the builder does not wait for the full 2-second block time. Instead, it produces partial payloads (**flashblocks**) every 200 milliseconds. Each flashblock contains whatever transactions have been processed since the last interval. See [Flashblocks Pipeline](/architecture/flashblocks-pipeline/) for details. ## Stage 4: EVM execution Each transaction is executed against the Ethereum Virtual Machine. Base uses Reth's EVM with an Optimism-specific configuration. **Relevant crates:** - Execution crates in [`crates/execution/`](https://github.com/base/base/tree/main/crates/execution) -- provide the `EvmConfig` implementation that configures: - **L1 data fee** -- Optimism charges an additional fee based on the transaction's L1 data cost (the cost of posting the transaction data to L1). This is calculated using the L1 base fee and blob base fee, which are read from the L1 attributes deposit transaction at the start of each block. - **Custom precompiles** -- Base may add precompiles beyond the standard Ethereum set. - **System transactions** -- the L1 attributes deposit transaction updates on-chain variables (`l1BaseFee`, `l1BlobBaseFee`, `sequenceNumber`, etc.) by calling the `L1Block` system contract. - [`base-primitives`](https://github.com/base/base/tree/main/crates/shared/primitives) -- defines the transaction envelope types that the EVM processes, including Optimism deposit transactions (`TxDeposit`). The EVM execution loop for each transaction: 1. **Load sender state** -- retrieve the sender's nonce and balance from the state trie. 2. **Validate** -- check nonce matches, balance covers `gasLimit * gasPrice + value`, and the transaction is well-formed. 3. **Deduct upfront cost** -- subtract the maximum possible gas cost from the sender's balance. 4. **Execute** -- run the EVM bytecode. For contract creations, deploy the code. For calls, execute the target contract. 5. **Apply state changes** -- write modified storage slots, created/destroyed accounts, and balance transfers to the state. 6. **Calculate gas refund** -- refund unused gas to the sender (up to the refund cap). 7. **Pay fees** -- transfer the priority fee to the builder's fee recipient. The base fee is burned. The L1 data fee is sent to the L1 fee vault. 8. **Emit receipt** -- produce a transaction receipt with status, gas used, and logs. ## Stage 5: State trie update After all transactions in the payload are executed, the resulting state changes are committed to the state trie. Base uses Reth's state storage, which maintains: - **Account trie** -- maps addresses to account state (nonce, balance, storage root, code hash). - **Storage tries** -- per-account Merkle Patricia Tries mapping storage slots to values. - **State root** -- the root hash of the account trie, included in the block header. The state root is a cryptographic commitment to the entire world state after the block's execution. It is used by: - **Consensus** -- the consensus layer verifies that the state root in a proposed block matches the result of re-executing all transactions. - **Fault proofs** -- the proof system (`kona-mpt`) uses Merkle Patricia Trie proofs to verify individual state transitions without re-executing the entire block. For fault proof purposes, [`kona-mpt`](https://github.com/base/base/tree/main/crates/proof/mpt) provides a standalone MPT implementation that can verify inclusion/exclusion proofs against a known state root inside the FPVM. ## Stage 6: Block sealing Once all transactions are executed and the state root is computed, the block is sealed. **Relevant crates:** - [`base-builder-publish`](https://github.com/base/base/tree/main/crates/builder/publish) -- the builder packages the sealed payload (transactions, receipts, state root, block header) and publishes it. - [`base-client-engine`](https://github.com/base/base/tree/main/crates/client/engine) -- the node's engine handler receives the sealed payload via `engine_newPayload` and: 1. Validates the payload (re-executes transactions and checks the state root). 2. Stores the block in the database. 3. Updates the chain tip via `engine_forkchoiceUpdated`. The sealed block header contains: | Field | Source | |---|---| | `parentHash` | Previous block's hash | | `stateRoot` | Computed from stage 5 | | `transactionsRoot` | Merkle root of the transaction list | | `receiptsRoot` | Merkle root of the receipt list | | `logsBloom` | Bloom filter over all logs | | `gasUsed` | Total gas consumed by all transactions | | `timestamp` | From the `PayloadAttributes` | | `baseFeePerGas` | Calculated from parent block's gas usage via EIP-1559 | | `extraData` | Builder-specific metadata | ## End-to-end latency In normal operation (non-Flashblocks mode), the full pipeline from transaction receipt to block sealing takes up to 2 seconds (the L2 block time). In Flashblocks mode, the first confirmation comes within 200 milliseconds because the builder streams partial results as they are produced. | Mode | First confirmation | Final block | |---|---|---| | Standard | ~2 seconds | 2 seconds | | Flashblocks | ~200 milliseconds | 2 seconds | The Flashblocks pipeline does not change the final block structure -- it only provides earlier visibility into which transactions will be included. The sealed block at the end of the 2-second interval is identical regardless of whether Flashblocks streaming was active. --- # Flashblocks Pipeline Source: https://basehub.org/architecture/flashblocks-pipeline/ Description: How the builder produces flashblocks at 200ms sub-block intervals, streams them over WebSockets, and reconciles state at block boundaries. Flashblocks are partial block updates the builder emits every 200 milliseconds within a 2-second L2 block. They deliver sub-second inclusion confirmation without altering the sealed block or the consensus protocol. The sections below cover how the builder produces flashblocks, how they stream to consumers, and how state reconciles when the block seals. ## Motivation Standard L2 blocks on Base are produced every 2 seconds. For latency-sensitive applications (trading, gaming, real-time UIs), 2 seconds is too slow for a responsive experience. Flashblocks reduce the effective confirmation time to approximately 200 milliseconds by streaming the builder's work-in-progress. A flashblock is not a new consensus primitive. It is an **application-layer optimization**: the builder shares its intermediate results before the block is finalized. Consumers who trust the sequencer can act on flashblocks immediately; consumers who require consensus finality wait for the sealed block as usual. ## Architecture The Flashblocks pipeline involves three crate groups: ``` ┌──────────────────────────────────────────────────────────────┐ │ Builder │ │ │ │ ┌───────────────────┐ ┌───────────────────┐ │ │ │ base-builder-core │───►│base-builder-publish│──► Engine API │ │ │ │ │ │ │ │ │ tx selection, │ │ flashblock framing│ │ │ │ EVM execution, │ │ WebSocket publish │ │ │ │ 200ms timer │ │ sealed block emit │ │ │ └───────────────────┘ └────────┬──────────┘ │ │ │ │ │ ┌───────────────────┐ │ │ │ │base-builder-meter.│ │ │ │ │ │ │ │ │ │ build time, tx │ │ │ │ │ count, revenue │ │ │ │ └───────────────────┘ │ │ └────────────────────────────────────┼──────────────────────────┘ │ ▼ ┌───────────────────┐ │ websocket-proxy │ (infra) │ │ │ fan-out to │ │ subscribers │ └────────┬──────────┘ │ ┌──────────────┼──────────────┐ ▼ ▼ ▼ Client A Client B Client C ``` ``` ┌──────────────────────────────────────────────────────────────┐ │ Client Node │ │ │ │ ┌───────────────────┐ ┌───────────────────┐ │ │ │ base-flashblocks │◄───│ base-client-engine│ │ │ │ │ │ │ │ │ │ flashblock state │ │ engine API handler│ │ │ │ tracking, sub │ │ payload validation│ │ │ │ management │ │ │ │ │ └───────────────────┘ └───────────────────┘ │ └──────────────────────────────────────────────────────────────┘ ``` ## The build loop The core building loop in [`base-builder-core`](https://github.com/base/base/tree/main/crates/builder/core) operates on a timer-driven cycle within each 2-second block interval. ### Step 1: Receive payload attributes The consensus layer sends `engine_forkchoiceUpdated` with `PayloadAttributes` to begin a new block. The attributes include: - **Parent block hash** -- the block to build on top of. - **Timestamp** -- the L2 timestamp for the new block. - **Deposit transactions** -- mandatory L1-to-L2 messages that must appear at the start of the block. - **Fee recipient** -- the address that receives priority fees. - **Flashblocks configuration** -- whether flashblocks are enabled and the sub-interval duration (default: 200ms). ### Step 2: Initialize the build context The builder sets up: - A mutable state overlay on top of the parent block's state. - A running transaction list (starts with the deposit transactions). - A cumulative gas counter. - A 200ms interval timer. - A flashblock sequence counter (starting at 0). ### Step 3: Execute transactions in sub-intervals The builder enters a loop that repeats every 200 milliseconds until the block time expires: ``` for each 200ms interval: 1. Pull pending transactions from the pool (sorted by effective tip) 2. Execute each transaction against the state overlay: - If the transaction succeeds: append to the running tx list, update cumulative gas, record the receipt - If the transaction fails (reverts, out-of-gas, nonce mismatch): skip it and try the next 3. Compute the intermediate state root 4. Emit a flashblock containing: - The transactions processed in this interval - The receipts for those transactions - The cumulative gas used so far - The intermediate state root - The flashblock index (0, 1, 2, ...) - The block number and parent hash 5. Increment the flashblock sequence counter ``` A typical 2-second block produces up to **10 flashblocks** (2000ms / 200ms). In practice, the number may be fewer if the builder finishes processing all pending transactions before the block time expires. ### Step 4: Seal the block When the block timer expires, the builder: 1. Stops accepting new transactions. 2. Computes the final state root. 3. Assembles the complete block header (with all transactions from all flashblocks). 4. Sends the sealed payload to the node via `engine_newPayload`. The sealed block is identical to what would have been produced without Flashblocks. The only difference is that consumers received incremental updates during construction. ## Publishing: `base-builder-publish` The [`base-builder-publish`](https://github.com/base/base/tree/main/crates/builder/publish) crate handles the output side of the pipeline. It is responsible for two output channels: ### Engine API output The sealed block payload is sent to the node's Engine API (`engine_newPayload`), followed by a `engine_forkchoiceUpdated` call to update the canonical chain tip. This is the standard Reth/OP Stack block import path. ### WebSocket streaming output Flashblocks are streamed in real time over a WebSocket connection. Each flashblock is serialized as a JSON object and pushed to connected subscribers. The publish crate: 1. Maintains a set of WebSocket connections (via the `websocket-proxy` infra binary, which handles fan-out). 2. Serializes each flashblock into the `flashblocks_v0` JSON-RPC notification format. 3. Sends the notification to the proxy, which distributes it to all subscribers. 4. At the end of the block, sends a final `flashblocks_v0` notification with `sealed: true`, signaling that the block is complete. ### Flashblock message format Each flashblock notification contains: ```json { "jsonrpc": "2.0", "method": "flashblocks_v0", "params": { "index": 3, "blockNumber": "0x1a2b3c", "parentHash": "0xabc...def", "timestamp": "0x67890", "transactions": ["0x...rlp1", "0x...rlp2"], "receipts": [{ ... }, { ... }], "stateRoot": "0x...", "gasUsed": "0x...", "sealed": false } } ``` When `sealed` is `true`, the message represents the final state of the block. Consumers can treat a `sealed: true` flashblock as equivalent to a full block notification. ## WebSocket proxy: fan-out and backpressure The [`websocket-proxy`](https://github.com/base/base/tree/main/crates/infra/websocket-proxy) binary sits between the builder and external consumers. It exists because the builder should focus on building blocks, not managing thousands of WebSocket connections. The proxy: - Accepts incoming WebSocket connections from clients. - Subscribes to the builder's flashblock stream (a single connection). - Fans out each flashblock notification to all connected clients. - Applies backpressure: if a client cannot keep up, the proxy drops messages for that client rather than blocking the builder. - Handles connection lifecycle (ping/pong, reconnection, graceful shutdown). This separation ensures that a slow or misbehaving consumer cannot affect block building performance. ## Client-side: `base-flashblocks` On the node side, the [`base-flashblocks`](https://github.com/base/base/tree/main/crates/client/flashblocks) crate manages flashblock state within the running node. Its responsibilities: - **Tracking in-progress flashblocks** -- maintains a buffer of flashblocks for the current block interval, updating as new ones arrive from the builder. - **Subscription management** -- the node exposes its own `flashblocks_subscribe` RPC method for local consumers. The `base-flashblocks` crate manages these subscriptions and forwards flashblock notifications. - **State reconciliation** -- when the sealed block arrives via `engine_newPayload`, the crate reconciles the flashblock state with the final block. Any speculative state derived from flashblocks is replaced by the canonical block state. ## State reconciliation Flashblocks introduce a concept of **speculative state**: consumers act on transactions that are included in a flashblock but not yet in a sealed block. In the normal case, the sealed block contains exactly the union of all flashblock transactions, so speculative state matches final state perfectly. However, there are edge cases: ### Reorgs within a block interval If the builder receives a new `forkchoiceUpdated` that changes the parent block during construction (e.g., due to an L1 reorg affecting deposit transactions), the builder: 1. Discards the in-progress block. 2. Sends a `flashblocks_v0` notification with `"reorg": true` to signal that previous flashblocks for this block number are invalid. 3. Starts building a new block from the updated parent. Consumers must handle reorg notifications by discarding any speculative state derived from the invalidated flashblocks. ### Builder restarts If the builder process restarts mid-block, flashblock numbering resets. The `websocket-proxy` detects the upstream disconnection and sends a disconnect notification to subscribers. When the builder reconnects, it starts a fresh block interval. ## Metering: `base-builder-metering` The [`base-builder-metering`](https://github.com/base/base/tree/main/crates/builder/metering) crate collects performance metrics from the build loop: | Metric | Description | |---|---| | `builder_flashblock_build_time_ms` | Time to produce each flashblock | | `builder_flashblock_tx_count` | Number of transactions per flashblock | | `builder_block_total_gas` | Total gas used in the sealed block | | `builder_block_tx_count` | Total transactions in the sealed block | | `builder_block_revenue_wei` | Total priority fees earned by the builder | | `builder_block_build_time_ms` | Wall-clock time for the full 2-second build cycle | | `builder_flashblock_count` | Number of flashblocks produced per block | These metrics are exported via Prometheus and are critical for monitoring builder performance in production. A drop in `flashblock_tx_count` or a spike in `flashblock_build_time_ms` can indicate EVM execution bottlenecks or transaction pool issues. ## Timeline of a single block The following timeline illustrates a typical 2-second block with Flashblocks: ``` Time (ms) Event ───────── ───── 0 forkchoiceUpdated received; builder starts block N 0-10 Deposit transactions executed 10-200 User transactions executed from pool 200 Flashblock 0 emitted (deposits + first batch of user txs) 200-400 More user transactions executed 400 Flashblock 1 emitted 400-600 More user transactions executed 600 Flashblock 2 emitted ... (continues every 200ms) 1800-2000 Final batch of user transactions executed 2000 Flashblock 9 emitted (final interval) 2000 Block sealed; engine_newPayload sent 2000+ forkchoiceUpdated confirms block N as canonical ``` In this example, a user whose transaction was included in Flashblock 0 received confirmation at t=200ms -- a 10x improvement over the 2-second block time. ## Integration with the execution pipeline Flashblocks do not change the execution pipeline described in [Execution Pipeline](/architecture/execution-pipeline/). The EVM execution, state trie updates, and block sealing steps are identical. Flashblocks simply add **observation points** at 200ms intervals, allowing external consumers to see progress before the block is complete. The key invariant is: **the set of transactions in the sealed block is exactly the union of all flashblock transaction sets for that block**. There are no transactions that appear in a flashblock but not in the sealed block (barring the reorg case described above), and no transactions that appear in the sealed block but not in any flashblock. --- # June 2026 Block Production Outage Source: https://basehub.org/architecture/june-2026-block-production-outage/ Description: Postmortem of the two June 2026 Base mainnet block production halts caused by stale EVM journal state in the sequencer, plus the mitigation and follow-up work. import { Aside } from '@astrojs/starlight/components'; Base mainnet halted block production twice in late June 2026. The first halt began at 11:47am EDT on June 25 and ran for 116 minutes; the second began at 11:28am EDT on June 26 and lasted 20 minutes. Both share a single root cause, so this page treats them together. Chain integrity was preserved and user funds were never at risk — sequencing resumed safely once the underlying bug was patched. ## Root cause The defect lived in the sequencer's block-building path: after a transaction failed validation, the EVM journal — the record of accounts and storage slots touched during execution — was not reset. When an invalid transaction arrived at the builder, it failed during execution as designed, but the dirty journal state survived instead of being rolled back. The next valid transaction then executed against that leftover state before the journal was cleared. Because the journal no longer reflected reality, the gas charged to the valid transaction was wrong, and the block it landed in carried an invalid state transition. No other node would accept a block produced from that corrupted state, so the chain stopped advancing. ## Impact For the duration of each outage, two effects were visible across the network: - **Block production stopped.** No new L2 blocks were sealed. Both sequencer and validator nodes were stuck behind the invalid block and could not move forward until sequencing was restored. - **Transactions could not be included.** Pending transactions piled up in the mempool, which eventually exceeded its storage capacity and began rejecting new submissions. Calls to `eth_sendRawTransaction` returned errors throughout the incident. ## Mitigation The fix patched the sequencers so the journal state is correctly updated during execution. It shipped as [base/base PR #3806](https://github.com/base/base/pull/3806). Recovery took longer than the patch alone would suggest, because of an unrelated infrastructure condition. The sequencers in the active/passive cluster sync at the consensus layer rather than via execution sync like validator nodes do, and the engine reset path carried a race condition that stopped the sequencers from catching back up to the chain tip after a restart. That second bug — visible in [base/base PR #3805](https://github.com/base/base/pull/3805/changes) — affected only the sequencers and is why the chain halted again the following day. ## Incident timeline ### Phase 1 — the chain halt | Time | Event | What happened | |------|-------|---------------| | 11:47:13 AM ET (15:47:13 UTC) | Journal left dirty | An invalid transaction fails while block 47806543 is being built, but the error path never clears the in-memory journal, so it stays dirty. | | 11:47:13 AM ET (15:47:13 UTC) | Execution on bad state | In that same ~2-second window, the next valid transaction runs against the dirty journal and computes a wrong receipts root. The bad block is then sealed and broadcast by the sequencer. | | 11:47:13 AM ET (15:47:13 UTC) | Validators reject, chain halts | Validators re-execute the block. Because they skip the excluded invalid transaction, they derive a different receipts root from a clean journal, reject the block, and the chain stops at block 47806542. | ### Phase 2 — response and triage | Time | Milestone | Action | |------|-----------|--------| | 11:49:00 AM ET (15:49:00 UTC) | T + 2 minutes | Monitors detect the production stall and fire an automated alert. | | 11:52:00 AM ET (15:52:00 UTC) | T + 5 minutes | An engineering war room opens; within three minutes of the alert the bad block hash is identified. | | 11:53:00 AM ET (15:53:00 UTC) | T + 6 minutes | The public status page is updated. | ### Phase 3 — mitigation and recovery | Time | Status | Steps | |------|--------|-------| | 12:24 PM ET (16:24 UTC) | Investigating | Teams work multiple parallel tracks to isolate the cause. | | 12:55 PM ET (16:55 UTC) | Root cause found | The cause is confirmed and the invalid block pinpointed; fix implementation and testing begin. | | 1:20 PM ET (17:20 UTC) | Internal nodes recover | The EVM journal patch reaches internal sequencers and core nodes, which achieve preliminary recovery. | | 1:51 PM ET (17:51 UTC) | Blocks resume | New blocks start sealing again; downstream ecosystem operators are told to restart stalled nodes. Total outage ~116 minutes. | | 1:58 PM ET (17:58 UTC) | Network stable | Ecosystem-wide syncing is verified and the network holds stable under active monitoring. | The June 26 halt shared the same root cause, started at 11:28am EDT, and was resolved in roughly 20 minutes. ## Follow-up work Base outlined several reliability investments coming out of the incident: - Stronger protocol fuzz testing and load-testing harnesses, aimed at surfacing adversarial transaction patterns that can trigger bugs like the journal-state defect, alongside broader operational and monitoring improvements. - Graceful recovery support in `base-consensus`, since this incident required validator nodes to be restarted before they could resume syncing. The goal is faster, lower-effort recovery across the ecosystem in similar situations. Node operators and integrators can subscribe to updates at [status.base.org](https://status.base.org) to stay informed about future events. --- # Multiproofs on Base Source: https://basehub.org/architecture/multiproofs/ Description: How Base's multiproof system combines TEE and ZK provers to enable fast finality, satisfy Stage 2 decentralization requirements, and strengthen the proof system's security posture. The multiproof system on Base mainnet pairs a TEE prover with a ZK prover to validate state transitions, shortening withdrawal finality to at most one day when both proofs agree, and giving the chain the onchain bug-detection capability required for Stage 2 decentralization. ZK proofs can override TEE proofs, and contradictions across provers raise a soundness alert that disables the affected prover automatically. ## Why move beyond optimistic proofs Base reached Stage 1 in October 2024 with permissionless fault proofs and again in April 2025 with the Security Council that approves upgrades. The fault dispute protocol behind those milestones works, but is interactive (a dispute can span dozens of transactions), demands a smart contract that emulates a virtual machine, and requires defenders to lock up hundreds of ETH in bonds in the worst case. Those costs make optimistic systems an awkward foundation for the next stage of decentralization. Validity systems flip the model: every block in a proposal is proved correct upfront and verified onchain in a single transaction, eliminating the multi-round dispute game and the bond requirements. Two flavors lead the field: - **ZK provers** use cryptography to attest that the chain advanced correctly. Real-time proving is expensive today, and the offchain machinery is hard to implement soundly, but the trust model is the cleanest available. - **TEE provers** lean on hardware attestation. They are fast and cheap to verify, but introduce trust in the hardware vendor (Intel, AWS) and remain susceptible to side-channel attacks on physical TEEs. ## The multiproof design Following Vitalik's L2 finalization roadmap, Base finalizes a proposal as soon as either a TEE or ZK proof is present. When both are present and agree, the proposal finalizes quickly — within a day at most. A ZK proof can overrule a TEE proof, and any contradiction between proofs (for example, two ZK proofs disagreeing on the same block) triggers a soundness alert that pauses the implicated prover. That layout buys three things at once: - **Faster withdrawals** when both provers are active, improving liquidity efficiency across bridges. - **Permissionless decentralization**, because ZK proposing remains open and the permissionless path always overrides the permissioned one. - **Stage 2 readiness**, since prover bugs become detectable onchain rather than relying on offchain coordination. Base treats TEE proofs as the near-term workhorse while confidence in the ZK stack matures. The current ZK prover is SP1, chosen for performance, audit coverage, and ongoing formal verification work. ## Defense in depth: intermediary roots and soundness alerts To keep defender requirements low, the proof system embeds **intermediary roots** — subclaims published every few blocks within a proposal. Contradicting a single subclaim is enough to invalidate the whole proposal and surface a soundness alert. A defender no longer needs to disprove an entire proposal to flag a faulty prover; one disagreeing block is sufficient. Soundness alerts are the system's circuit breaker. When two proofs disagree on the same block, the implicated prover is disabled automatically until reviewed, preserving the assurance that any finalized proposal was vouched for by at least one honest prover. ## Where this goes next The endpoint is full ZK proving with near-instant withdrawals. Reaching that state depends on two parallel tracks: - **Improving ZK security and efficiency**, including chain-side changes that make real-time proving tractable and ongoing evaluation of competing ZKVMs. - **Shortening finality**, from the current one-day window down toward a few minutes as confidence in ZK provers grows. The multiproof launch on mainnet is the architectural step that satisfies Stage 2's technical requirement for onchain bug detection while leaving room for the chain to keep tightening finality as the underlying proving technology matures. --- # Architecture Overview Source: https://basehub.org/architecture/overview/ Description: How the 8 crate groups in base/base fit together — from Reth integration to Flashblocks production. `base/base` is a Rust workspace that implements the Base L2 node on top of [Reth](https://github.com/paradigmxyz/reth) v1.11.0. It layers Optimism rollup semantics, a Flashblocks-aware block builder, and a [Kona](https://github.com/op-rs/kona)-derived fault proof pipeline onto the Reth execution client. The workspace is organized into **8 top-level crate groups**, each in its own subdirectory under `crates/`. These groups map to the major subsystems of an L2 node: receiving transactions, building blocks, executing the EVM, reaching consensus with L1, generating fault proofs, and operating the infrastructure that ties it all together. ## System diagram At the highest level, the system looks like this: ``` L1 (Ethereum) | ┌───────────────┼───────────────┐ v v v ┌─────────┐ ┌───────────┐ ┌───────────┐ │consensus│ │ proof │ │ builder │ │ (kona) │ │ (kona) │ │(flashblocks│ └────┬────┘ └───────────┘ │ + core) │ │ └─────┬─────┘ v v ┌─────────┐ ┌───────────┐ │ client │◄─────────────────►│ execution │ │(node + │ │ (EVM + │ │ engine) │ │ payload) │ └────┬────┘ └───────────┘ │ v ┌─────────┐ ┌───────────┐ ┌───────────┐ │ shared │ │ alloy │ │ infra │ │(prims, │ │ (types + │ │(binaries +│ │ utils) │ │ codecs) │ │ tooling) │ └─────────┘ └───────────┘ └───────────┘ ``` ## Crate groups ### 1. Client — the node core **Path:** [`crates/client/`](https://github.com/base/base/tree/main/crates/client) The client group contains the node implementation itself. It is the entry point for the `base-reth-node` binary and assembles all the other subsystems into a running process. | Crate | Purpose | |---|---| | `base-client-node` | Node configuration, component wiring, Reth `NodeBuilder` integration | | `base-client-cli` | CLI argument parsing and subcommand definitions | | `base-client-engine` | Engine API handler — receives payloads from the consensus layer and drives execution | | `base-flashblocks` | Flashblocks state management, WebSocket streaming, sub-block interval logic | | `base-metering` | Gas metering extensions and RPC metering endpoints | | `base-txpool-tracing` | Transaction pool event tracing and diagnostics | The client group depends on nearly every other group. It pulls primitives from **shared**, types from **alloy**, EVM logic from **execution**, and consensus derivation from **consensus**. ### 2. Execution — EVM and payload construction **Path:** [`crates/execution/`](https://github.com/base/base/tree/main/crates/execution) This group wraps Reth's EVM execution with Base-specific customizations. It is responsible for: - Configuring the `EvmConfig` with Optimism-specific precompiles and fee logic - Building execution payloads (the block body that the engine seals) - Applying L1-to-L2 deposit transactions that arrive via the derivation pipeline Reth's modular `EvmConfig` trait allows Base to inject its own gas pricing rules (L1 data fee), custom precompiles, and Optimism system transaction handling without forking the core EVM loop. ### 3. Consensus — L1 derivation **Path:** [`crates/consensus/`](https://github.com/base/base/tree/main/crates/consensus) Consensus is split into two sub-groups: **Base-native crates:** | Crate | Purpose | |---|---| | `base-protocol` | Protocol constants, chain config, fork schedule | | `base-consensus-rpc` | RPC interface for consensus queries | **Kona-derived crates** (forked/vendored from the [Kona](https://github.com/op-rs/kona) project): | Crate | Purpose | |---|---| | `kona-node-service` | Top-level consensus node service | | `kona-engine` | Engine driver — sends `engine_forkchoiceUpdated` / `engine_newPayload` calls | | `kona-derive` | L1 derivation pipeline — reads batches from L1, produces L2 payload attributes | | `kona-gossip` | P2P gossip protocol for unsafe block propagation | | `kona-sources` | Data source adapters (L1 RPC, blobs, DA) | | `kona-providers-alloy` | Alloy-based provider implementations for L1/L2 data | | `kona-genesis` | Genesis state and rollup config loading | | `kona-registry` | Chain registry (rollup configs for known chains) | | `kona-hardforks` | Hardfork activation logic | | `kona-disc` | Peer discovery | | `kona-peers` | Peer management and scoring | | `kona-cli` | CLI utilities for consensus binaries | | `kona-macros` | Procedural macros used across kona crates | The derivation pipeline reads L1 blocks, extracts batch data (from calldata or EIP-4844 blobs), and derives the sequence of L2 blocks that the execution layer must process. This is the mechanism by which Base inherits Ethereum's security. ### 4. Builder — block construction and Flashblocks **Path:** [`crates/builder/`](https://github.com/base/base/tree/main/crates/builder) The builder group implements Base's custom block builder, which goes beyond standard Reth payload building by producing **Flashblocks** -- partial block updates streamed every 200 milliseconds. | Crate | Purpose | |---|---| | `base-builder-core` | Core block building loop, transaction ordering, Flashblock interval timer | | `base-builder-publish` | Publishing built blocks/flashblocks to consumers (WebSocket, engine) | | `base-builder-metering` | Builder-specific metrics (build times, transaction counts, revenue) | The builder runs as a separate binary (`builder`) and communicates with the node via the Engine API. See [Flashblocks Pipeline](/architecture/flashblocks-pipeline/) for a detailed walkthrough. ### 5. Proof — fault proof generation **Path:** [`crates/proof/`](https://github.com/base/base/tree/main/crates/proof) The proof group contains the machinery for generating fault proofs that can be verified on L1. These crates are also derived from Kona and target execution inside a fault proof virtual machine (FPVM). | Crate | Purpose | |---|---| | `kona-proof` | Top-level proof orchestration | | `kona-driver` | Drives the derivation + execution pipeline inside the FPVM | | `kona-executor` | EVM executor adapted for the proof environment | | `kona-mpt` | Merkle Patricia Trie operations for state proof verification | | `kona-preimage` | Preimage oracle client — requests data from the host during proof execution | | `kona-std-fpvm` | Standard FPVM runtime (syscall interface, memory management) | | `kona-std-fpvm-proc` | Procedural macros for FPVM entry points | In a fault proof, the entire derivation-then-execution pipeline runs deterministically inside a sandboxed VM. The `kona-driver` crate replays the same logic as the consensus layer's derivation pipeline, but reads data through a preimage oracle rather than from the network. ### 6. Shared — common types and utilities **Path:** [`crates/shared/`](https://github.com/base/base/tree/main/crates/shared) Shared crates provide types, utilities, and abstractions used across multiple other groups. | Crate | Purpose | |---|---| | `base-primitives` | Core types: `BaseTxEnvelope`, `BaseBlock`, chain IDs, fee types | | `base-bundles` | Bundle types for MEV/searcher bundle submission | | `base-jwt` | JWT authentication for Engine API and inter-service communication | | `base-cli-utils` | Common CLI argument types and parsing helpers | | `base-engine-ext` | Engine API extensions (additional methods beyond the standard spec) | | `base-access-lists` | Access list generation and optimization | | `base-reth-rpc-types` | Custom RPC type definitions | | `base-txpool-rpc` | Transaction pool RPC method types | `base-primitives` is the most widely depended-upon crate in the workspace. It defines the canonical transaction, block, and receipt types that every other group imports. ### 7. Alloy — type system and codecs **Path:** [`crates/alloy/`](https://github.com/base/base/tree/main/crates/alloy) The alloy group provides Ethereum/Optimism type definitions and encoding/decoding logic built on the [Alloy](https://github.com/alloy-rs/alloy) library ecosystem. These crates define the wire format for transactions, receipts, and other protocol objects, ensuring compatibility with the broader Ethereum tooling ecosystem. ### 8. Infra — binaries and operational tooling **Path:** [`crates/infra/`](https://github.com/base/base/tree/main/crates/infra) The infra group contains the supporting binaries and tooling that run alongside the core node. | Crate/Binary | Purpose | |---|---| | `based` | Sequencer orchestrator daemon | | `basectl-cli` | Operator CLI tool for node management and diagnostics | | `ingress-rpc-lib` | RPC ingress proxy — routes and load-balances incoming RPC requests | | `websocket-proxy` | WebSocket proxy for Flashblocks streaming | | `mempool-rebroadcaster` | Rebroadcasts mempool transactions to peers | | `audit-archiver-lib` | Archives audit logs for compliance and debugging | | `system-tests` | End-to-end integration test harness | ## Reth integration Base does not fork Reth. Instead, it uses Reth as a library dependency and extends it through Reth's plugin architecture: 1. **`NodeBuilder`** — the client group uses Reth's `NodeBuilder` to assemble a node with custom components (EVM config, transaction pool, payload builder). 2. **`EvmConfig`** — the execution group provides an Optimism-aware EVM configuration that plugs into Reth's execution pipeline. 3. **`PayloadBuilder`** — the builder group implements a custom payload builder that produces Flashblocks. 4. **`EngineApi`** — the client engine crate extends the standard Engine API with additional methods for Flashblocks and metering. This library-based integration means Base automatically picks up upstream Reth improvements (perf optimizations, new EVM opcodes, database improvements) by updating the Reth dependency version. ## Optimism component usage Base is an OP Stack chain, and the codebase relies on several Optimism-specific components: - **`reth-optimism-*` crates** — Reth ships first-party Optimism support. Base uses these for deposit transaction handling, L1 fee calculation, and the Optimism-specific Engine API extensions (`op-engine-api`). - **Kona** — the consensus and proof groups are built on Kona, the Rust implementation of the OP Stack's derivation pipeline and fault proof program. Base vendors these crates to apply Base-specific patches (e.g., Flashblocks-aware derivation). - **Alloy Optimism types** — the alloy group extends Alloy's Optimism type definitions with Base-specific transaction types and receipt fields. ## Build toolchain The workspace uses: - **Rust edition 2024** with a minimum supported Rust version of **1.88** - **MIT license** for all crates - **Just** as the task runner (see `justfile` at the repo root) - **Docker** for containerized builds and deployments --- # Protocol Overview Source: https://basehub.org/architecture/protocol-overview/ Description: A tour of the Base rollup protocol — the network participants, the consensus, execution, bridging, batcher, and proof components, and the deposit, transaction, and withdrawal flows that connect L1 and L2. Base is an Ethereum rollup: L2 transaction data is published to Ethereum so anyone can reconstruct the chain, and proofs let any participant challenge an invalid state transition. This page walks through the protocol-level pieces and the user flows that tie L1 and L2 together. For the codebase-level view of how these subsystems map to Rust crates, see [Architecture Overview](/architecture/overview/). ## Network Participants Three kinds of actors interact with Base: users, sequencers, and validators. ```mermaid graph TD EthereumL1(Ethereum L1) subgraph "L2 Participants" Users(Users) Sequencers(Sequencers) Validators(Validators) end Validators -.->|fetch transaction batches| EthereumL1 Validators -.->|fetch deposit data| EthereumL1 Validators -->|submit/validate/challenge output proposals| EthereumL1 Validators -.->|fetch realtime P2P updates| Sequencers Users -->|submit deposits/withdrawals| EthereumL1 Users -->|submit transactions| Sequencers Users -->|query data| Validators Sequencers -->|submit transaction batches| EthereumL1 Sequencers -.->|fetch deposit data| EthereumL1 classDef l1Contracts stroke:#bbf,stroke-width:2px; classDef l2Components stroke:#333,stroke-width:2px; classDef systemUser stroke:#f9a,stroke-width:2px; class EthereumL1 l1Contracts; class Users,Sequencers,Validators l2Components; ``` ### Users Users are the broad set of participants who send transactions — either through the sequencer or by calling contracts directly on Ethereum — and who read transaction data back from the interfaces that validators expose. ### Sequencers The sequencer is Base's block producer. The network runs a single active sequencer today, and it: - Accepts transactions directly from Users. - Observes "deposit" transactions generated on Ethereum. - Consolidates both transaction streams into ordered L2 blocks. - Submits information to L1 that is sufficient to fully reproduce those L2 blocks. - Provides real-time access to pending L2 blocks that have not yet been confirmed on L1. - Produces Flashblocks every 200ms, committing to the ordering of transactions within the block as it is being built. Although it is central to how the chain runs, the sequencer is not a trusted party. Its job is to improve the experience — ordering transactions far faster and more cheaply than would be possible if every user submitted directly to L1 — not to be relied upon for correctness. ### Validators Validators run the L2 state transition function for themselves, independently of the sequencer. By doing so they keep the network honest and serve chain data back to users. In general they: - Sync rollup data from L1 and the Sequencer. - Use rollup data to execute the L2 state transition function. - Serve rollup data and computed L2 state information to Users. A validator can additionally take on the Proposer and/or Challenger role, which means it will: - Submit assertions about the state of the L2 to a smart contract on L1. - Validate assertions made by other participants. - Dispute invalid assertions made by other participants. ## High-Level System Diagram The diagram below traces how the main components talk to each other across L1 and L2. ```mermaid graph LR subgraph "Ethereum L1" OptimismPortal(OptimismPortal) BatchInbox(Batch Inbox Address) DisputeGameFactory(DisputeGameFactory) end subgraph "L2 Node" RollupNode(Consensus) ExecutionEngine(Execution Engine) end Batcher(Batcher) Proposers(Proposers) Challengers(Challengers) Users(Users) Users -->|deposits / withdrawals| OptimismPortal Users -->|transactions| ExecutionEngine Batcher -->|post transaction batches| BatchInbox Batcher -.->|fetch batch data| RollupNode RollupNode -.->|fetch batches| BatchInbox RollupNode -.->|fetch deposit events| OptimismPortal RollupNode -->|Engine API| ExecutionEngine Proposers -->|submit output proposals| DisputeGameFactory Proposers -.->|fetch outputs| RollupNode Challengers -->|verify / challenge games| DisputeGameFactory OptimismPortal -.->|query state proposals| DisputeGameFactory classDef l1Contracts stroke:#bbf,stroke-width:2px; classDef l2Components stroke:#333,stroke-width:2px; classDef systemUser stroke:#f9a,stroke-width:2px; class OptimismPortal,BatchInbox,DisputeGameFactory l1Contracts; class RollupNode,ExecutionEngine l2Components; class Batcher,Proposers,Challengers,Users systemUser; ``` ## Protocol Components ### Consensus The consensus layer derives the canonical L2 chain from L1 data. It pulls transaction batches out of the Batch Inbox and deposit events out of `OptimismPortal`, assembles payload attributes from them, and drives the execution engine through the Engine API. Newly built but still-unconfirmed blocks are gossiped to peers over a dedicated P2P network, so validators see them with low latency well before the corresponding batches reach L1. ```mermaid graph LR L1(Ethereum L1) subgraph "Rollup Node" BatchDecoding(Batch Decoding) Derivation(Derivation Pipeline) end EngineAPI(Engine API) EE(Execution Engine) L2(L2 Blocks) L1 -->|batches + deposit events| BatchDecoding BatchDecoding --> Derivation Derivation -->|payload attributes| EngineAPI EngineAPI --> EE EE --> L2 classDef l1 stroke:#bbf,stroke-width:2px; classDef l2 stroke:#333,stroke-width:2px; class L1 l1; class EE,L2 l2; ``` ### Execution Execution runs on a Reth-based engine that speaks the standard Ethereum JSON-RPC API and processes the blocks consensus hands it. Predeploys (system contracts living at fixed L2 addresses), precompiles, and preinstalls extend the EVM with the rollup-specific behavior it needs — fee distribution, L1 block attribute injection, and cross-domain messaging among them. ### Bridging Deposits move from the `OptimismPortal` contract on L1 into L2 as special deposit transactions placed at the top of each L2 block. Withdrawals run the other way: a user starts a withdrawal transaction on L2, a proposer files an output root with `DisputeGameFactory`, and once the challenge period elapses the user proves and finalizes the withdrawal back on L1 through `OptimismPortal`. ```mermaid graph LR subgraph "Deposit Path" User1(User) OP1(OptimismPortal) DepTx(Deposit Transaction on L2) end subgraph "Withdrawal Path" User2(User) WdTx(Withdrawal Tx on L2) DGF(DisputeGameFactory) OP2(OptimismPortal) end User1 -->|depositTransaction| OP1 OP1 -->|TransactionDeposited event| DepTx User2 -->|initiates withdrawal| WdTx WdTx -->|output root proposed| DGF User2 -->|prove + finalize| OP2 OP2 -.->|verify game| DGF classDef l1 stroke:#bbf,stroke-width:2px; classDef systemUser stroke:#f9a,stroke-width:2px; class OP1,OP2,DGF l1; class User1,User2 systemUser; ``` ### Batcher The batcher is a sequencer-operated service that compresses L2 transaction data into channel frames and posts them — as calldata or blobs — to the Batch Inbox Address on L1. This is the data availability layer: with it, any validator can rebuild the full L2 chain from L1 alone. ```mermaid graph LR Sequencer(Sequencer) Batcher(Batcher) BatchInbox(Batch Inbox Address) RollupNode(Rollup Node) Sequencer -->|L2 blocks| Batcher Batcher -->|compressed channel frames| BatchInbox BatchInbox -.->|fetch batches| RollupNode classDef l1 stroke:#bbf,stroke-width:2px; classDef l2 stroke:#333,stroke-width:2px; classDef systemUser stroke:#f9a,stroke-width:2px; class BatchInbox l1; class RollupNode l2; class Batcher,Sequencer systemUser; ``` ### Proofs Output proposals and the proofs behind them are what make the L2 state verifiable. A proposer opens a checkpoint game through `DisputeGameFactory`, the onchain verifier contracts check the proof material, and challengers can dispute any claim they believe is wrong. A withdrawal can only be finalized through `OptimismPortal` after the game backing it resolves in the proposer's favor. The current proof topology is the Azul multiproof design — see [Azul Proof System](/specifications/azul-proofs/) for the dispute-game and finality details. ```mermaid graph LR Proposer(Proposer) DGF(DisputeGameFactory) Game(AggregateVerifier game) Challengers(Challengers) OP(OptimismPortal) Proposer -->|submit checkpoint proof| DGF DGF -->|create game| Game Challengers -->|challenge invalid claims| Game Game -->|resolved result| OP classDef l1 stroke:#bbf,stroke-width:2px; classDef systemUser stroke:#f9a,stroke-width:2px; class DGF,Game,OP l1; class Proposer,Challengers systemUser; ``` ## Core User Flows ### Depositing ETH to Base Most users arrive on L2 by bridging ETH over from L1; once they hold ETH for fees, they begin transacting on Base. The flow below shows that first hop and the components it touches. ```mermaid graph TD subgraph "Ethereum L1" OptimismPortal(OptimismPortal) BatchInbox(Batch Inbox Address) end Sequencer(Sequencer) Users(Users) %% Interactions Users -->|1. submit deposit| OptimismPortal Sequencer -.->|2. fetch deposit events| OptimismPortal Sequencer -->|3. generate deposit block| Sequencer Users -->|4. send transactions| Sequencer Sequencer -->|5. submit transaction batches| BatchInbox classDef l1Contracts stroke:#bbf,stroke-width:2px; classDef l2Components stroke:#333,stroke-width:2px; classDef systemUser stroke:#f9a,stroke-width:2px; class OptimismPortal,BatchInbox l1Contracts; class Sequencer l2Components; class Users systemUser; ``` ### Sending Transactions on Base Transacting on Base is identical to transacting on Ethereum: a user signs a transaction and submits it via `eth_sendRawTransaction` to any node's JSON-RPC endpoint. The sequencer takes it from the mempool, slots it into an L2 block, and eventually posts the containing batch to L1. ### Withdrawing from Base Moving ETH or ERC-20 tokens from Base back to Ethereum starts as an ordinary L2 transaction but finishes with transactions on L1. A withdrawal has to reference a valid proof game that asserts the L2 state at a particular point, and only completes once that game has finalized. ```mermaid graph LR subgraph "Ethereum L1" BatchInbox(Batch Inbox Address) DisputeGameFactory(DisputeGameFactory) ProofGame(AggregateVerifier game) OptimismPortal(OptimismPortal) ExternalContracts(External Contracts) end Sequencer(Sequencer) Proposers(Proposers) Users(Users) %% Interactions Users -->|1. send withdrawal initialization txn| Sequencer Sequencer -->|2. submit transaction batch| BatchInbox Proposers -->|3. submit output proposal| DisputeGameFactory DisputeGameFactory -->|4. generate game| ProofGame Users -->|5. submit withdrawal proof| OptimismPortal Users -->|6. wait for finalization| ProofGame Users -->|7. submit withdrawal finalization| OptimismPortal OptimismPortal -->|8. check game validity| ProofGame OptimismPortal -->|9. execute withdrawal transaction| ExternalContracts %% Styling classDef l1Contracts stroke:#bbf,stroke-width:2px; classDef l2Components stroke:#333,stroke-width:2px; classDef systemUser stroke:#f9a,stroke-width:2px; class BatchInbox,DisputeGameFactory,ProofGame,OptimismPortal l1Contracts; class Sequencer l2Components; class Users,Proposers systemUser; ``` --- # audit-archiver Source: https://basehub.org/binaries/audit-archiver/ Description: Kafka-to-S3 audit log archival service with configurable worker pools. import { LinkCard } from '@astrojs/starlight/components'; `audit-archiver` consumes audit log messages from the Kafka topics that `ingress-rpc` writes to and archives them to S3-compatible storage for long-term retention, debugging, and compliance. ## Package - **Crate name:** `audit-archiver` - **Source:** [`bin/audit-archiver`](https://github.com/base/base/tree/main/bin/audit-archiver) - **Library:** [`crates/infra/audit`](https://github.com/base/base/tree/main/crates/infra/audit) (`audit-archiver-lib`) ## Architecture The archiver runs a configurable pool of workers that consume messages from Kafka topics and write them in batches to S3: ``` Kafka topics → audit-archiver (worker pool) → S3 bucket ``` The service supports a `noop-archive` mode for testing where messages are consumed and logged but not written to S3. ## Features | Feature | Description | |---------|-------------| | **Worker pool** | Configurable pool of up to 80 concurrent workers | | **S3 archival** | Writes audit logs to S3-compatible object storage | | **Noop mode** | `noop-archive` mode for testing without S3 writes | | **Metrics** | Exposes Prometheus metrics on port 9002 | | **Batch processing** | Groups messages into batches for efficient writes | ## Ports | Port | Protocol | Purpose | |------|----------|---------| | 9002 | HTTP | Prometheus metrics | ## Key Dependencies - `audit-archiver-lib` — Core archival logic - `rdkafka` — Kafka consumer - `aws-sdk-s3` — S3 client - `tokio` — Async runtime ## Build ```bash cargo build --bin audit-archiver --release ``` ## Usage ```bash ./target/release/audit-archiver [OPTIONS] ``` ## Source --- # base-reth-node Source: https://basehub.org/binaries/base-reth-node/ Description: The main Base Reth Node binary — a Reth-based Ethereum execution client for the Base L2 network. import { LinkCard } from '@astrojs/starlight/components'; `base-reth-node` runs the Base L2 execution client. It extends the standard Reth OP node with five extensions covering Flashblocks streaming, transaction pool RPC, resource metering, and proofs history retention. ## Package - **Crate name:** `base-reth-node` - **Source:** [`bin/node`](https://github.com/base/base/tree/main/bin/node) - **Dockerfile:** `etc/docker/Dockerfile.client` ## Extensions The node binary installs five extensions on top of the standard Reth OP node: | Extension | Crate | Purpose | |-----------|-------|---------| | `TxPoolRpcExtension` | `base-txpool-rpc` | Exposes custom transaction pool RPC methods | | `TxPoolExtension` | `base-txpool-tracing` | Adds transaction pool tracing and diagnostics | | `MeteringExtension` | `base-metering` | Resource metering RPC for gas accounting | | `FlashblocksExtension` | `base-flashblocks` | Flashblocks streaming provider over WebSocket | | `ProofsHistoryExtension` | `base-proofs-history` | Retains trie proofs for the fault proof window | These extensions are registered via Reth's `NodeComponents` system. Each one hooks into the node lifecycle to provide additional RPC methods or background services. ## Key Dependencies - `reth-optimism-node` — Base Reth node builder - `reth-optimism-cli` — CLI framework with jemalloc and OTel - `base-flashblocks` — Flashblocks RPC provider - `base-metering` — Resource metering RPC - `base-txpool-rpc` — Transaction pool RPC extensions - `base-txpool-tracing` — Transaction pool tracing - `base-proofs-history` — Proofs history extension - `base-cli-utils` — CLI utilities (logging, metrics) ## Build ```bash # Build just this binary just build-node # Or using cargo directly cargo build --bin base-reth-node # Release build cargo build --release --bin base-reth-node # Max performance build (PGO + LTO) just build-maxperf ``` ## Usage ```bash ./target/release/base-reth-node node \ --chain base \ --datadir /data/base \ --http \ --http.api eth,net,web3,debug,txpool \ --ws \ --ws.api eth,net,web3 ``` View all available options: ```bash ./target/release/base-reth-node --help ``` ## Docker ```bash # Build the image docker build -t base-reth-node -f etc/docker/Dockerfile.client . # Run docker run -it --rm \ -v /data/base:/data \ -p 8545:8545 \ -p 8546:8546 \ base-reth-node node --chain base --datadir /data ``` ## Source --- # basectl Source: https://basehub.org/binaries/basectl/ Description: TUI management utility for configuring and inspecting Base nodes across different network targets. import { LinkCard } from '@astrojs/starlight/components'; `basectl` is the operator TUI for configuring and inspecting Base nodes across multiple network targets. The interface is built with [`ratatui`](https://github.com/ratatui/ratatui). ## Package - **Crate name:** `basectl` - **Source:** [`bin/basectl`](https://github.com/base/base/tree/main/bin/basectl) - **Library:** [`crates/infra/basectl`](https://github.com/base/base/tree/main/crates/infra/basectl) (`basectl-cli`) ## Architecture `basectl` is split into two crates: - **`basectl`** (binary) — The CLI entry point that parses arguments and launches the TUI - **`basectl-cli`** (library) — The core logic for configuration management, subcommands, and the TUI renderer The TUI is built with [`ratatui`](https://github.com/ratatui/ratatui), providing an interactive terminal interface for node management. ## Subcommands | Subcommand | Purpose | |------------|---------| | `config` | Generate or inspect node configuration for a target network | | `flashblocks` | Inspect and manage flashblocks streaming state | | `da` | Data availability layer inspection and diagnostics | | `command-center` | Interactive TUI dashboard for monitoring and managing the node | ## Configuration Targets | Config | Network | |--------|---------| | `mainnet` | Base Mainnet | | `sepolia` | Base Sepolia Testnet | | `devnet` | Local Development Network | | `` | Custom configuration file path | ## Key Dependencies - `basectl-cli` — Configuration logic library - `ratatui` — TUI framework - `clap` — CLI argument parsing - `anyhow` — Error handling - `tokio` — Async runtime ## Usage ```bash # Via cargo cargo run -p basectl --release -- -c # Via Just (preferred) just basectl mainnet just basectl sepolia just basectl devnet ``` ## Source --- # based Source: https://basehub.org/binaries/based/ Description: Block-building sidecar service with health checks and StatsD metrics. import { LinkCard } from '@astrojs/starlight/components'; `based` runs as a sidecar to the block builder, exposing HTTP health check endpoints and forwarding metrics over StatsD to Datadog or compatible backends. ## Package - **Crate name:** `based-bin` - **Binary name:** `based` - **Source:** [`bin/based`](https://github.com/base/base/tree/main/bin/based) - **Library:** [`crates/infra/based`](https://github.com/base/base/tree/main/crates/infra/based) (`based`) ## Architecture `based` runs as a sidecar alongside the block builder. It: 1. Exposes HTTP health check endpoints for load balancer integration 2. Reports block-building metrics via StatsD to Datadog or compatible backends 3. Reads configuration from environment variables for deployment flexibility ## Key Dependencies - `based` — Core daemon library - `clap` — CLI argument parsing - `tracing` / `tracing-subscriber` — Structured logging - `tokio` — Async runtime - `cadence` — StatsD metrics client (for Datadog integration) ## Environment Variables | Variable | Purpose | |----------|---------| | `STATSD_HOST` | StatsD server hostname | | `STATSD_PORT` | StatsD server port | | `STATSD_PREFIX` | Metric name prefix | | `CODEFLOW_*` | Codeflow environment integration variables | ## Build ```bash cargo build --bin based --release ``` ## Usage ```bash ./target/release/based [OPTIONS] ``` ## Source --- # builder Source: https://basehub.org/binaries/builder/ Description: The Base block builder binary for flashblock production and block building. import { LinkCard } from '@astrojs/starlight/components'; `base-builder` produces flashblocks every 200 milliseconds and assembles the sealed canonical blocks for Base. It orchestrates the build pipeline through `FlashblocksServiceBuilder`. ## Package - **Crate name:** `base-builder-bin` - **Binary name:** `base-builder` - **Source:** [`bin/builder`](https://github.com/base/base/tree/main/bin/builder) - **Dockerfile:** `etc/docker/Dockerfile.builder` ## Architecture The builder binary bootstraps using `FlashblocksServiceBuilder`, which wires together: 1. **Block building** — Assembles transactions from the pool into execution payloads 2. **Flashblock production** — Produces 200ms sub-block preconfirmations streamed to subscribers 3. **Metering** — Tracks gas usage and resource consumption via `MeteringStoreExtension` 4. **Transaction pool RPC** — Exposes custom pool methods via `TxPoolRpcExtension` The builder uses jemalloc as its global allocator for improved memory allocation performance. ## Key Extensions | Extension | Purpose | |-----------|---------| | `FlashblocksServiceBuilder` | Orchestrates the full flashblocks pipeline | | `MeteringStoreExtension` | Resource metering data store | | `TxPoolRpcExtension` | Transaction pool RPC methods | ## Key Dependencies - `base-cli-utils` — CLI utilities (logging, metrics) - `base-builder-core` — Core block building logic - `base-builder-metering` — Resource metering - `base-builder-publish` — Block publishing and streaming - `reth-optimism-node` — Reth node builder - `jemalloc` — Memory allocator ## Build ```bash # Build the builder binary cargo build --bin base-builder --release ``` ## Docker ```bash # Build the image docker build -t base-builder -f etc/docker/Dockerfile.builder . # Run docker run -it --rm base-builder [OPTIONS] ``` ## Usage ```bash ./target/release/base-builder [OPTIONS] ``` ## Source --- # consensus Source: https://basehub.org/binaries/consensus/ Description: The Base consensus node binary implementing the OP Stack consensus protocol via Kona. import { LinkCard } from '@astrojs/starlight/components'; `base-consensus` runs the Base consensus layer, implementing the OP Stack consensus protocol on top of the [Kona](https://github.com/op-rs/kona) derivation framework. ## Package - **Crate name:** `base-consensus` - **Source:** [`bin/consensus`](https://github.com/base/base/tree/main/bin/consensus) ## Architecture The consensus binary: 1. **L1 derivation** — Connects to L1 Ethereum to read rollup data and derives L2 blocks from L1 batches using the derivation pipeline 2. **P2P networking** — Manages peer-to-peer networking for block gossip and state sync 3. **Engine API** — Communicates with the execution layer (base-reth-node) via the Engine API to deliver derived payloads 4. **Kona integration** — Uses `kona-node-service` for the core consensus loop and `kona-genesis` for chain configuration ## Key Dependencies - `base-cli-utils` — CLI utilities (logging, metrics) - `base-client-cli` — Client CLI extensions - `kona-genesis` — Genesis configuration - `kona-cli` — Consensus CLI framework - `kona-node-service` — Node service orchestration ## Build ```bash cargo build --bin base-consensus --release ``` ## Usage ```bash ./target/release/base-consensus [OPTIONS] ``` The consensus node must be configured with: - An L1 Ethereum RPC endpoint for reading rollup data - An Engine API endpoint for communicating with the execution layer - A P2P configuration for block gossip ## Source --- # ingress-rpc Source: https://basehub.org/binaries/ingress-rpc/ Description: JSON-RPC ingress proxy with Kafka audit trail for transaction and bundle submission. import { LinkCard } from '@astrojs/starlight/components'; `ingress-rpc` fronts the node, accepting JSON-RPC requests and routing each one to the right backend. Every request is mirrored to Kafka for audit and replay. ## Package - **Crate name:** `ingress-rpc` - **Source:** [`bin/ingress-rpc`](https://github.com/base/base/tree/main/bin/ingress-rpc) - **Library:** [`crates/infra/ingress-rpc`](https://github.com/base/base/tree/main/crates/infra/ingress-rpc) (`ingress-rpc-lib`) ## Architecture The ingress RPC proxy handles three categories of traffic: 1. **Transaction submission** — `eth_sendRawTransaction` calls forwarded to the transaction pool 2. **Bundle submission** — `eth_sendBundle` and related methods forwarded to the builder 3. **Mempool/simulation/raw TX forwarding** — Read and simulation requests forwarded to the appropriate backend All requests are logged to Kafka for audit and replay purposes. The proxy uses `jsonrpsee` for its JSON-RPC server implementation. ## Request Flow ``` Client → ingress-rpc → Kafka (audit log) → Backend (node/builder/mempool) → Client (response) ``` ## Key Dependencies - `ingress-rpc-lib` — Core proxy logic - `clap` — CLI argument parsing - `tokio` — Async runtime - `rdkafka` — Kafka client for audit logging - `jsonrpsee` — JSON-RPC server - `dotenvy` — Environment variable loading ## Build ```bash cargo build --bin ingress-rpc --release ``` ## Usage ```bash ./target/release/ingress-rpc [OPTIONS] ``` ## Source --- # mempool-rebroadcaster Source: https://basehub.org/binaries/mempool-rebroadcaster/ Description: Bidirectional Geth-Reth mempool transaction rebroadcasting service. import { LinkCard } from '@astrojs/starlight/components'; `mempool-rebroadcaster` bridges the Geth and Reth transaction pools, forwarding pending transactions in both directions so the two clients stay in sync. ## Package - **Crate name:** `mempool-rebroadcaster-bin` - **Binary name:** `mempool-rebroadcaster` - **Source:** [`bin/mempool-rebroadcaster`](https://github.com/base/base/tree/main/bin/mempool-rebroadcaster) - **Library:** [`crates/infra/mempool-rebroadcaster`](https://github.com/base/base/tree/main/crates/infra/mempool-rebroadcaster) ## Architecture The rebroadcaster connects to both a Geth node and a Reth node, monitoring each transaction pool and forwarding new transactions to the other: ``` Geth txpool ←→ mempool-rebroadcaster ←→ Reth txpool ``` This ensures that during the migration from Geth to Reth, or in hybrid deployments where both clients are running, transactions submitted to either node are visible to both. The service periodically polls both pools and rebroadcasts any transactions that are missing from the other. ## Key Dependencies - `mempool-rebroadcaster` — Core rebroadcasting logic - `clap` — CLI argument parsing - `dotenvy` — Environment variable loading - `tracing` / `tracing-subscriber` — Structured logging - `tokio` — Async runtime ## Build ```bash cargo build --bin mempool-rebroadcaster --release ``` ## Usage ```bash ./target/release/mempool-rebroadcaster [OPTIONS] ``` ## Source --- # websocket-proxy Source: https://basehub.org/binaries/websocket-proxy/ Description: Fan-out WebSocket proxy with Brotli compression, API key authentication, and rate limiting. import { LinkCard } from '@astrojs/starlight/components'; `websocket-proxy` fans out flashblock streams from one or more upstream builders to connected clients. It handles Brotli compression, API key authentication, and per-IP rate limiting. ## Package - **Crate name:** `websocket-proxy-bin` - **Binary name:** `websocket-proxy` - **Source:** [`bin/websocket-proxy`](https://github.com/base/base/tree/main/bin/websocket-proxy) - **Library:** [`crates/infra/websocket-proxy`](https://github.com/base/base/tree/main/crates/infra/websocket-proxy) - **Dockerfile:** `etc/docker/Dockerfile.websocket-proxy` ## Architecture The proxy operates as a fan-out service: ``` Upstream 1 ─┐ Upstream 2 ─┤→ websocket-proxy ─→ Client A Upstream N ─┘ ─→ Client B ─→ Client N ``` It connects to one or more upstream WebSocket URIs (e.g., builder nodes producing flashblocks) and fans out the messages to all connected downstream clients. This enables horizontal scaling of flashblock delivery. ## Features | Feature | Description | |---------|-------------| | **Fan-out** | Multiplexes messages from upstream sources to all connected clients | | **Brotli compression** | Compresses WebSocket frames for bandwidth-efficient delivery | | **API key auth** | Authenticates clients via API keys before allowing connections | | **Per-IP rate limiting** | Prevents abuse by limiting connection rates per source IP | | **Metrics** | Exposes Prometheus metrics on port 9000 | | **Multiple upstreams** | Connects to multiple upstream URIs for redundancy | ## Ports | Port | Protocol | Purpose | |------|----------|---------| | 8545 | WebSocket | Client-facing flashblock stream | | 9000 | HTTP | Prometheus metrics | ## Key Dependencies - `websocket-proxy` — Core proxy logic - `brotli` — Brotli compression - `futures` — Async stream utilities - `hostname` — Hostname resolution - `tokio` — Async runtime ## Docker ```bash # Build the image docker build -t websocket-proxy -f etc/docker/Dockerfile.websocket-proxy . # Run docker run -it --rm \ -p 8545:8545 \ -p 9000:9000 \ websocket-proxy [OPTIONS] ``` ## Build ```bash cargo build --bin websocket-proxy --release ``` ## Usage ```bash ./target/release/websocket-proxy [OPTIONS] ``` ## Source --- # Contributing Source: https://basehub.org/contributing/ Description: Issues and pull requests are off. Reach out on X. Issues and pull requests on this repository are intentionally turned off — contributions aren't accepted through GitHub. If you've spotted a bug, want to suggest content, or have feedback, tag [@wbnns on X](https://x.com/wbnns). ## Running it locally ```bash npm install npm run dev # http://localhost:4321 with HMR npm run build # production build into dist/ npx astro check # type check ``` Node 20+ required. ## License The site and the [`@wbnns/base-mcp`](https://www.npmjs.com/package/@wbnns/base-mcp) package are MIT-licensed (see [`LICENSE`](https://github.com/wbnns/basehub/blob/main/LICENSE)) — fork and adapt freely. --- # alloy/consensus Source: https://basehub.org/crates/alloy/consensus/ Description: Base alloy consensus types — transactions, receipts with Base-specific fields. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-alloy-consensus` crate defines Base consensus types, including custom transaction and receipt types. It extends the standard Alloy consensus types for the OP Stack L2 environment. ## Key Dependencies - `alloy-consensus` — Upstream consensus types - `op-alloy-consensus` — OP Stack consensus types ## Source --- # alloy/evm Source: https://basehub.org/crates/alloy/evm/ Description: EVM implementation with Base hardfork awareness. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-alloy-evm` crate implements the EVM with Base hardfork awareness. It builds on `op-revm` to customize execution for the Base L2 chain. ## Key Dependencies - `op-revm` — OP Stack REVM extensions - `revm` — Rust EVM implementation - `base-alloy-hardforks` — Base hardfork definitions ## Source --- # alloy/hardforks Source: https://basehub.org/crates/alloy/hardforks/ Description: Named bindings for Base-specific hardforks. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-alloy-hardforks` crate defines named bindings for Base hardforks. Other crates use these bindings to gate hardfork-aware behavior. ## Source --- # alloy/network Source: https://basehub.org/crates/alloy/network/ Description: Base blockchain RPC behavior abstraction at the network layer. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-alloy-network` crate defines how the Base chain implements Alloy's network traits. It abstracts RPC behavior at the network layer. ## Source --- # alloy Source: https://basehub.org/crates/alloy/overview/ Description: Alloy type extensions for Base — consensus types, EVM customizations, RPC types, and network definitions. The `alloy` crate group extends [Alloy](https://github.com/alloy-rs/alloy), the standard Rust toolkit for Ethereum types and RPC, with Base-specific consensus types, RPC types, EVM behavior, and hardfork definitions. ## Sub-crates | Crate | Package Name | Purpose | |-------|-------------|---------| | [consensus](/crates/alloy/consensus/) | `base-alloy-consensus` | Base-specific consensus types | | [evm](/crates/alloy/evm/) | `base-alloy-evm` | EVM customizations for Base | | [hardforks](/crates/alloy/hardforks/) | `base-alloy-hardforks` | Base hardfork definitions | | [network](/crates/alloy/network/) | `base-alloy-network` | Network type definitions | | [provider](/crates/alloy/provider/) | `base-alloy-provider` | Alloy provider extensions | | [rpc-jsonrpsee](/crates/alloy/rpc-jsonrpsee/) | `base-alloy-rpc-jsonrpsee` | JSON-RPC server traits (jsonrpsee) | | [rpc-types-engine](/crates/alloy/rpc-types-engine/) | `base-alloy-rpc-types-engine` | Engine API RPC types | | [rpc-types](/crates/alloy/rpc-types/) | `base-alloy-rpc-types` | General RPC type extensions | ## Role in the Architecture The alloy crates sit at the foundation of the dependency graph. They define the types that flow through every other layer — from how blocks and transactions are represented in consensus, to the RPC types returned to users, to the EVM execution context. These crates extend the upstream Alloy types with Base-specific fields and behaviors needed for the OP Stack L2 environment. ## Source [`crates/alloy/`](https://github.com/base/base/tree/main/crates/alloy) --- # alloy/provider Source: https://basehub.org/crates/alloy/provider/ Description: Interface with a Base blockchain via the Engine API provider. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-alloy-provider` crate exposes an Alloy provider for the Base chain, including Engine API extensions used by the consensus layer. ## Source --- # alloy/rpc-jsonrpsee Source: https://basehub.org/crates/alloy/rpc-jsonrpsee/ Description: Base RPC client using the jsonrpsee framework. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-alloy-rpc-jsonrpsee` crate implements the Base RPC client on top of the [jsonrpsee](https://github.com/paritytech/jsonrpsee) framework. ## Source --- # alloy/rpc-types Source: https://basehub.org/crates/alloy/rpc-types/ Description: Base RPC types for blocks and transactions. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-alloy-rpc-types` crate defines Base-specific RPC types for blocks and transactions. It extends the standard Ethereum JSON-RPC types. ## Source --- # alloy/rpc-types-engine Source: https://basehub.org/crates/alloy/rpc-types-engine/ Description: Base RPC types for the engine namespace — SSZ, snappy, and sha2 hashing. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-alloy-rpc-types-engine` crate defines RPC types for the `engine` namespace, with SSZ serialization, snappy compression, and sha2 hashing support. ## Source --- # builder/core Source: https://basehub.org/crates/builder/core/ Description: Core block builder for the OP Stack — the main builder logic for sequencing and flashblock production. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-builder-core` crate drives block building for the OP Stack. It handles sequencing, flashblock production at ~200ms intervals, and canonical block assembly. This is the largest crate in the builder group, with dependencies on reth, alloy, revm, and the shared primitives. It integrates transaction bundling, gas metering, caching (via moka), and secp256k1 signing. ## Key Dependencies - `base-bundles` — Transaction bundle types - `base-client-node` — Node harness - `base-primitives` — Core types - `base-access-lists` — FAL implementation - `reth-*` / `reth-optimism-*` — Reth execution crates - `alloy-*` / `op-alloy-*` — Alloy types - `revm` / `op-revm` — EVM execution ## Source --- # builder/metering Source: https://basehub.org/crates/builder/metering/ Description: Resource metering backend for the OP Stack block builder. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-builder-metering` crate is the resource metering backend for the block builder. It tracks gas usage and resource consumption during block construction. ## Source --- # builder Source: https://basehub.org/crates/builder/overview/ Description: Block building, flashblock production, metering, and block publishing for Base. The `builder` crate group produces blocks and flashblocks. It assembles flashblocks at 200ms intervals and publishes them to streaming clients. ## Sub-crates | Crate | Package Name | Purpose | |-------|-------------|---------| | [core](/crates/builder/core/) | `base-builder-core` | Core block building logic and flashblock production | | [metering](/crates/builder/metering/) | `base-builder-metering` | Resource metering for block building | | [publish](/crates/builder/publish/) | `base-builder-publish` | Block/flashblock publishing and distribution | ## Role in the Architecture The builder sits between the execution layer and the consensus layer. It: 1. Receives transactions from the transaction pool 2. Builds execution payloads using the EVM 3. Produces flashblocks at ~200ms intervals during the 2-second block time 4. Publishes flashblocks via WebSocket for real-time streaming 5. Finalizes the canonical block at the end of each slot The `base-builder-core` crate contains the main building logic, `base-builder-metering` handles gas and resource accounting, and `base-builder-publish` handles distribution of produced blocks. ## Source [`crates/builder/`](https://github.com/base/base/tree/main/crates/builder) --- # builder/publish Source: https://basehub.org/crates/builder/publish/ Description: WebSocket broadcast publisher for flashblock distribution. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-builder-publish` crate is the WebSocket broadcast publisher for the OP Stack block builder. It streams produced flashblocks and canonical blocks to connected clients. ## Source --- # client/cli Source: https://basehub.org/crates/client/cli/ Description: CLI argument types for Base node consensus clients. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-client-cli` crate defines the CLI argument types for Base node consensus clients. It integrates Kona and Alloy for chain configuration and argument parsing. ## Source --- # client/engine Source: https://basehub.org/crates/client/engine/ Description: Engine validator for the Base Node — handles Engine API, trie computation, and flashblocks. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-client-engine` crate implements the engine validator for the Base Node. It handles Engine API communication, trie root computation, and flashblocks integration between the execution and consensus layers. ## Source --- # client/flashblocks Source: https://basehub.org/crates/client/flashblocks/ Description: Flashblocks state management — WebSocket streaming, pending state, EVM re-execution. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-flashblocks` crate manages the core flashblocks state. It handles: - WebSocket streaming of flashblock updates from the builder - Pending state cache for preconfirmed transactions - EVM re-execution for state queries against the pending block - The `pending` tag support for standard Ethereum JSON-RPC methods This is the crate that powers the [Flashblocks RPC](/specifications/flashblocks/). ## Source --- # client/flashblocks-node Source: https://basehub.org/crates/client/flashblocks-node/ Description: Flashblocks node extension — integrates flashblocks into the node lifecycle. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-flashblocks-node` crate wires flashblocks into the Reth node lifecycle. It registers the flashblocks RPC, WebSocket streaming, and pending state management. ## Source --- # client/metering Source: https://basehub.org/crates/client/metering/ Description: Metering RPC for the Base node — resource and gas metering per block. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-metering` crate exposes metering RPC endpoints for the Base node. It tracks gas usage and resource consumption per block. ## Source --- # client Source: https://basehub.org/crates/client/overview/ Description: Node client — CLI, engine, flashblocks integration, metering, proofs, and transaction pool tracing. The `client` crate group assembles the full Base Reth Node. It supplies the CLI, Engine API integration, flashblocks support, metering RPC, proof extensions, and transaction pool tracing. ## Sub-crates | Crate | Package Name | Purpose | |-------|-------------|---------| | [cli](/crates/client/cli/) | `base-client-cli` | CLI argument parsing and node configuration | | [engine](/crates/client/engine/) | `base-client-engine` | Engine API client for consensus-execution communication | | [flashblocks-node](/crates/client/flashblocks-node/) | `base-flashblocks-node` | Flashblocks-enabled node assembly | | [flashblocks](/crates/client/flashblocks/) | `base-flashblocks` | Flashblocks RPC provider and pending state cache | | [metering](/crates/client/metering/) | `base-metering` | Metering RPC endpoints for resource tracking | | [proofs](/crates/client/proofs/) | `base-proofs-extension` | Proof generation extensions | | [txpool-tracing](/crates/client/txpool-tracing/) | `base-txpool-tracing` | Transaction pool tracing and diagnostics | ## Role in the Architecture The client crate group is the primary interface for node operators. It: 1. Parses CLI arguments and configures the node (`cli`) 2. Manages the Engine API connection between execution and consensus (`engine`) 3. Assembles the flashblocks-enabled node with all components (`flashblocks-node`) 4. Provides the Flashblocks RPC for querying preconfirmed state (`flashblocks`) 5. Exposes metering endpoints for gas and resource tracking (`metering`) 6. Supports proof generation for the fault proof system (`proofs`) 7. Adds tracing to the transaction pool for debugging (`txpool-tracing`) The `base-reth-node` binary (in `bin/node`) depends primarily on this crate group. ## Source [`crates/client/`](https://github.com/base/base/tree/main/crates/client) --- # client/proofs Source: https://basehub.org/crates/client/proofs/ Description: Proofs extension for the Base node — fault proof data via Execution Extensions. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-proofs-extension` crate is the proofs extension for the Base node. It uses Reth's Execution Extensions (ExEx) framework to generate and serve fault proof data. ## Source --- # client/txpool-tracing Source: https://basehub.org/crates/client/txpool-tracing/ Description: Transaction tracing extensions — LRU caching, metrics, and streaming. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-txpool-tracing` crate adds transaction tracing extensions to the Base node. It supplies LRU caching, metrics collection, and streaming support for transaction pool diagnostics. ## Source --- # consensus/cli Source: https://basehub.org/crates/consensus/cli/ Description: Shared CLI utilities for Kona crates — tracing, metrics, and clap argument parsing. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `kona-cli` crate provides shared CLI utilities for Kona consensus crates, including tracing, metrics, and clap argument parsing. ## Source --- # consensus/derive Source: https://basehub.org/crates/consensus/derive/ Description: A `no_std` derivation pipeline implementation for the OP Stack. Derives L2 blocks from L1 data. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `kona-derive` crate implements the OP Stack derivation pipeline. It derives L2 blocks from L1 data and runs in `no_std` environments. ## Source --- # consensus/disc Source: https://basehub.org/crates/consensus/disc/ Description: Discovery service for the OP Stack using discv5 and libp2p. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `kona-disc` crate runs the peer discovery service for the OP Stack consensus layer. It uses discv5 and libp2p to find peers on the network. ## Source --- # consensus/engine Source: https://basehub.org/crates/consensus/engine/ Description: OP Stack engine client implementation — makes Engine API calls to the execution layer. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `kona-engine` crate implements the OP Stack engine client. It makes Engine API calls from the consensus layer to the execution layer. ## Source --- # consensus/genesis Source: https://basehub.org/crates/consensus/genesis/ Description: Optimism genesis types including rollup config, system config, and hardfork definitions. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `kona-genesis` crate defines Optimism genesis types, including the rollup config, system config, and hardfork definitions. ## Source --- # consensus/gossip Source: https://basehub.org/crates/consensus/gossip/ Description: Gossip protocol implementation for the OP Stack using libp2p gossipsub. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `kona-gossip` crate implements the OP Stack gossip protocol on top of libp2p gossipsub. ## Source --- # consensus/hardforks Source: https://basehub.org/crates/consensus/hardforks/ Description: Consensus hardfork type definitions for the OP Stack. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `kona-hardforks` crate defines the consensus hardfork types for the OP Stack. ## Source --- # consensus/macros Source: https://basehub.org/crates/consensus/macros/ Description: Utility procedural macros for kona crates. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `kona-macros` crate supplies utility procedural macros used across the Kona consensus crates. ## Source --- # consensus Source: https://basehub.org/crates/consensus/overview/ Description: OP Stack consensus implementation — derivation pipeline, gossip networking, P2P discovery, and sequencing. The `consensus` crate group implements the OP Stack consensus protocol for Base. The crates derive from the [Kona](https://github.com/op-rs/kona) project and handle L1 derivation, P2P networking, gossip, and block sequencing. ## Sub-crates | Crate | Package Name | Purpose | |-------|-------------|---------| | [cli](/crates/consensus/cli/) | `kona-cli` | Consensus node CLI | | [derive](/crates/consensus/derive/) | `kona-derive` | L1 derivation pipeline | | [disc](/crates/consensus/disc/) | `kona-disc` | Peer discovery | | [engine](/crates/consensus/engine/) | `kona-engine` | Consensus engine driver | | [genesis](/crates/consensus/genesis/) | `kona-genesis` | Genesis state and chain configuration | | [gossip](/crates/consensus/gossip/) | `kona-gossip` | Gossip protocol implementation | | [hardforks](/crates/consensus/hardforks/) | `kona-hardforks` | OP Stack hardfork definitions | | [macros](/crates/consensus/macros/) | `kona-macros` | Procedural macros for consensus code | | [peers](/crates/consensus/peers/) | `kona-peers` | Peer management | | [protocol](/crates/consensus/protocol/) | `base-protocol` | Base-specific protocol extensions | | [providers-alloy](/crates/consensus/providers-alloy/) | `kona-providers-alloy` | Alloy-based data providers for L1/L2 | | [registry](/crates/consensus/registry/) | `kona-registry` | Chain and rollup configuration registry | | [rpc](/crates/consensus/rpc/) | `base-consensus-rpc` | Consensus RPC endpoints | | [service](/crates/consensus/service/) | `kona-node-service` | Consensus node service orchestration | | [sources](/crates/consensus/sources/) | `kona-sources` | Data source abstractions for derivation | ## Role in the Architecture The consensus layer runs independently from the execution layer, communicating via the Engine API. It: 1. Connects to L1 Ethereum to read rollup data (`sources`, `providers-alloy`) 2. Derives L2 blocks from L1 data using the derivation pipeline (`derive`) 3. Manages P2P networking for block gossip (`gossip`, `disc`, `peers`) 4. Drives the consensus engine that feeds payloads to the execution layer (`engine`) 5. Handles chain configuration and hardfork activation (`genesis`, `hardforks`, `registry`) The `base-consensus` binary (in `bin/consensus`) runs this layer. ## Source [`crates/consensus/`](https://github.com/base/base/tree/main/crates/consensus) --- # consensus/peers Source: https://basehub.org/crates/consensus/peers/ Description: Network peers library for the OP Stack — peer management, discv5, and libp2p integration. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `kona-peers` crate manages network peers for the OP Stack consensus layer. It integrates discv5 and libp2p for peer tracking and connection lifecycle. ## Source --- # consensus/protocol Source: https://basehub.org/crates/consensus/protocol/ Description: Base-specific protocol types — batches, channels, frames, and compression (brotli, zlib). import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-protocol` crate defines Base-specific protocol types: batches, channels, frames, and compression via brotli and zlib. ## Source --- # consensus/providers-alloy Source: https://basehub.org/crates/consensus/providers-alloy/ Description: Alloy-backed data providers for L1/L2 data including beacon, execution, and blob fetching with c-kzg. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `kona-providers-alloy` crate supplies Alloy-backed data providers for L1 and L2 data, covering beacon, execution, and blob fetching with c-kzg. ## Source --- # consensus/registry Source: https://basehub.org/crates/consensus/registry/ Description: Registry of superchain configurations — chain configs loaded from TOML. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `kona-registry` crate holds the registry of superchain configurations, loaded from TOML. ## Source --- # consensus/rpc Source: https://basehub.org/crates/consensus/rpc/ Description: RPC types and client for Base consensus using jsonrpsee. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `base-consensus-rpc` crate defines the RPC types and client for the Base consensus layer, built on jsonrpsee. ## Source --- # consensus/service Source: https://basehub.org/crates/consensus/service/ Description: OP Stack consensus node service — orchestrates derivation, gossip, discovery, and engine communication. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `kona-node-service` crate orchestrates the OP Stack consensus node. It coordinates derivation, gossip, discovery, and engine communication. ## Source --- # consensus/sources Source: https://basehub.org/crates/consensus/sources/ Description: Data source types and utilities for the consensus node including remote signer and transport abstractions. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `kona-sources` crate defines data source types and utilities for the consensus node, including remote signer and transport abstractions. ## Source --- # execution/chainspec Source: https://basehub.org/crates/execution/chainspec/ Description: Defines OP chain parameters, genesis state, and hardfork activation for the Base execution layer. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/chainspec` sub-crate (`reth-optimism-chainspec`) is part of the [execution crate group](/crates/execution/overview/). It defines the OP chain spec, including genesis state, gas parameters, and hardfork activation rules consumed by the EVM and consensus layers. ## Source --- # execution/cli Source: https://basehub.org/crates/execution/cli/ Description: Command-line entry point for the OP execution node with jemalloc, OpenTelemetry, and asm-keccak. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/cli` sub-crate (`reth-optimism-cli`) is part of the [execution crate group](/crates/execution/overview/). It exposes the command-line interface for the OP execution node and wires in jemalloc, OpenTelemetry tracing, and the asm-keccak backend. ## Source --- # execution/consensus Source: https://basehub.org/crates/execution/consensus/ Description: Validates OP blocks and verifies trie roots inside the execution layer. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/consensus` sub-crate (`reth-optimism-consensus`) is part of the [execution crate group](/crates/execution/overview/). It validates OP block headers and bodies and verifies state, transaction, and receipt trie roots before blocks enter the chain. ## Source --- # execution/evm Source: https://basehub.org/crates/execution/evm/ Description: OP EVM block executor and state transition logic for the Base execution environment. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/evm` sub-crate (`reth-optimism-evm`) is part of the [execution crate group](/crates/execution/overview/). It runs the OP-flavored EVM, executing blocks and applying state transitions for the Base execution environment. ## Source --- # execution/exex Source: https://basehub.org/crates/execution/exex/ Description: Execution Extensions for OP-Reth, including the proofs ExEx that emits trie updates. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/exex` sub-crate (`reth-optimism-exex`) is part of the [execution crate group](/crates/execution/overview/). It hosts Execution Extensions for OP-Reth, including the proofs ExEx that streams trie updates alongside committed blocks. ## Source --- # execution/hardforks Source: https://basehub.org/crates/execution/hardforks/ Description: Defines Optimism hardforks and activation rules used throughout op-reth. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/hardforks` sub-crate (`reth-optimism-forks`) is part of the [execution crate group](/crates/execution/overview/). It enumerates Optimism hardforks and their activation rules, supplying the fork schedule consumed across op-reth. ## Source --- # execution/node Source: https://basehub.org/crates/execution/node/ Description: Assembles the OP execution node from EVM, RPC, payload, pool, and storage components. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/node` sub-crate (`reth-optimism-node`) is part of the [execution crate group](/crates/execution/overview/). It assembles the OP execution node, wiring the EVM, RPC, payload builder, transaction pool, and storage into a runnable binary. ## Source --- # execution Source: https://basehub.org/crates/execution/overview/ Description: Execution layer crates covering the EVM, chain spec, payload building, storage, transaction pool, and state. The `execution` crate group runs the Base node's execution layer. It spans EVM execution, state storage, transaction pool, and payload construction. ## Sub-crates | Crate | Package Name | Purpose | |-------|-------------|---------| | [chainspec](/crates/execution/chainspec/) | `reth-optimism-chainspec` | EVM chain spec for Optimism | | [cli](/crates/execution/cli/) | `reth-optimism-cli` | Execution layer CLI (jemalloc, OTel) | | [consensus](/crates/execution/consensus/) | `reth-optimism-consensus` | Block validation and trie roots | | [evm](/crates/execution/evm/) | `reth-optimism-evm` | Block executor and state transitions | | [exex](/crates/execution/exex/) | `reth-optimism-exex` | Execution Extensions — proofs with trie updates | | [hardforks](/crates/execution/hardforks/) | `reth-optimism-forks` | Optimism hardfork definitions | | [node](/crates/execution/node/) | `reth-optimism-node` | Node builder — assembles all components | | [payload](/crates/execution/payload/) | `reth-optimism-payload-builder` | Payload construction from pending transactions | | [primitives](/crates/execution/primitives/) | `reth-optimism-primitives` | Custom transaction and receipt types | | [reth](/crates/execution/reth/) | `reth-op` | Meta-crate re-exporting all OP reth components | | [rpc](/crates/execution/rpc/) | `reth-optimism-rpc` | eth\_, debug\_, op\_ RPC namespaces | | [storage](/crates/execution/storage/) | `reth-optimism-storage` | Database codec implementations | | [trie](/crates/execution/trie/) | `reth-optimism-trie` | Trie storage for fault proof window | | [txpool](/crates/execution/txpool/) | `reth-optimism-txpool` | Transaction pool with L1 cost calculation | ## Role in the Architecture The execution crate group is the heart of the node. It: 1. Defines the Base chain specification including gas limits, hardforks, and genesis (`chainspec`, `hardforks`) 2. Configures and runs the EVM for transaction execution (`evm`) 3. Builds execution payloads from pending transactions (`payload`) 4. Manages the transaction pool for pending transactions (`txpool`) 5. Handles state storage and the Merkle Patricia Trie (`storage`, `trie`) 6. Provides RPC endpoints for querying execution state (`rpc`) 7. Integrates with Reth's execution pipeline (`reth`, `node`) 8. Supports Execution Extensions for custom indexing (`exex`) Built on top of [Reth](https://github.com/paradigmxyz/reth) v1.11.0, these crates customize Reth's execution engine for the Base L2 environment. ## Source [`crates/execution/`](https://github.com/base/base/tree/main/crates/execution) --- # execution/payload Source: https://basehub.org/crates/execution/payload/ Description: Builds OP execution payloads from pending transactions for the engine API. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/payload` sub-crate (`reth-optimism-payload-builder`) is part of the [execution crate group](/crates/execution/overview/). It builds OP execution payloads from the pending transaction set, returning blocks ready for the engine API to commit. ## Source --- # execution/primitives Source: https://basehub.org/crates/execution/primitives/ Description: OP-specific transaction, receipt, and block primitive types shared across the execution layer. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/primitives` sub-crate (`reth-optimism-primitives`) is part of the [execution crate group](/crates/execution/overview/). It defines the OP transaction, receipt, and block primitive types reused across the EVM, RPC, payload, and storage crates. ## Source --- # execution/reth Source: https://basehub.org/crates/execution/reth/ Description: Meta-crate that re-exports all OP reth components behind feature gates. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/reth` sub-crate (`reth-op`) is part of the [execution crate group](/crates/execution/overview/). It re-exports the full OP reth surface behind `full`, `node`, `evm`, and `rpc` feature gates so downstream crates pull in only what they need. ## Source --- # execution/rpc Source: https://basehub.org/crates/execution/rpc/ Description: Implements the eth_, debug_, and op_ JSON-RPC namespaces for the OP execution layer. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/rpc` sub-crate (`reth-optimism-rpc`) is part of the [execution crate group](/crates/execution/overview/). It implements the `eth_`, `debug_`, and `op_` JSON-RPC namespaces that clients use to query state, trace execution, and read OP-specific data. ## Source --- # execution/storage Source: https://basehub.org/crates/execution/storage/ Description: Database codecs for OP storage types used by the execution layer. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/storage` sub-crate (`reth-optimism-storage`) is part of the [execution crate group](/crates/execution/overview/). It implements database codecs for OP-specific storage types so the execution layer can persist and read them through Reth's database stack. ## Source --- # execution/trie Source: https://basehub.org/crates/execution/trie/ Description: Stores trie nodes so the node can serve proofs across the fault proof window. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/trie` sub-crate (`reth-optimism-trie`) is part of the [execution crate group](/crates/execution/overview/). It persists Merkle Patricia Trie nodes so the node can serve state and storage proofs throughout the fault proof window. ## Source --- # execution/txpool Source: https://basehub.org/crates/execution/txpool/ Description: OP-Reth transaction pool with gas estimation and L1 data cost accounting. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `execution/txpool` sub-crate (`reth-optimism-txpool`) is part of the [execution crate group](/crates/execution/overview/). It manages the OP-Reth transaction pool, applying gas estimation and L1 data fee accounting to pending transactions before they enter blocks. ## Source --- # infra/audit Source: https://basehub.org/crates/infra/audit/ Description: Consumes transaction events from Kafka and archives them to S3 for audit retention. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `infra/audit` sub-crate (`audit-archiver-lib`) is part of the [infra crate group](/crates/infra/overview/). It consumes transaction events from Kafka and archives them to S3, producing the audit trail used for operational review. ## Source --- # infra/basectl Source: https://basehub.org/crates/infra/basectl/ Description: Ratatui-based terminal UI for interacting with Base, with WebSocket, clipboard, and contract support. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `infra/basectl` sub-crate (`basectl-cli`) is part of the [infra crate group](/crates/infra/overview/). It ships a ratatui terminal UI for interacting with Base nodes, including WebSocket subscriptions, clipboard helpers, and contract calls. ## Source --- # infra/based Source: https://basehub.org/crates/infra/based/ Description: Shared library for block header fetching and StatsD metrics emission via cadence. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `infra/based` sub-crate (`based`) is part of the [infra crate group](/crates/infra/overview/). It provides shared building blocks for fetching block headers and emitting StatsD metrics through cadence across the infra binaries. ## Source --- # infra/ingress-rpc Source: https://basehub.org/crates/infra/ingress-rpc/ Description: Ingress JSON-RPC server that forwards transactions to the builder and writes a Kafka audit trail. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `infra/ingress-rpc` sub-crate (`ingress-rpc-lib`) is part of the [infra crate group](/crates/infra/overview/). It runs the ingress JSON-RPC server, forwarding incoming transactions to the builder and writing each one to Kafka for the audit trail. ## Source --- # infra/mempool-rebroadcaster Source: https://basehub.org/crates/infra/mempool-rebroadcaster/ Description: Rebroadcasts mempool transactions to multiple upstream providers to ensure propagation. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `infra/mempool-rebroadcaster` sub-crate (`mempool-rebroadcaster`) is part of the [infra crate group](/crates/infra/overview/). It rebroadcasts pending mempool transactions to multiple upstream providers so they propagate even when individual peers drop them. ## Source --- # infra Source: https://basehub.org/crates/infra/overview/ Description: Operational tooling for Base — basectl, ingress RPC, WebSocket proxy, mempool rebroadcaster, and tests. The `infra` crate group runs the operational services around the Base node. It powers the auxiliary binaries that sit beside the main client. ## Sub-crates | Crate | Package Name | Purpose | |-------|-------------|---------| | [audit](/crates/infra/audit/) | `audit-archiver-lib` | Audit log archival | | [basectl](/crates/infra/basectl/) | `basectl-cli` | Node management utility | | [based](/crates/infra/based/) | `based` | Base daemon service | | [ingress-rpc](/crates/infra/ingress-rpc/) | `ingress-rpc-lib` | RPC ingress proxy (Kafka-based) | | [mempool-rebroadcaster](/crates/infra/mempool-rebroadcaster/) | `mempool-rebroadcaster` | Transaction rebroadcasting service | | [system-tests](/crates/infra/system-tests/) | `system-tests` | End-to-end system test framework | | [websocket-proxy](/crates/infra/websocket-proxy/) | `websocket-proxy` | WebSocket proxy for flashblock streaming | ## Role in the Architecture The infra crates provide the operational tooling around the core node: - **basectl** — A management utility for configuring and inspecting Base nodes. Supports mainnet, sepolia, and devnet configurations. - **based** — The Base daemon that orchestrates node services. - **ingress-rpc** — An RPC proxy that routes incoming JSON-RPC requests, using Kafka for message queuing. - **websocket-proxy** — Proxies WebSocket connections for flashblock streaming with Brotli compression. - **mempool-rebroadcaster** — Rebroadcasts transactions from the mempool to ensure propagation. - **audit** — Archives audit logs for operational monitoring. - **system-tests** — Integration and end-to-end tests using Docker Compose. ## Source [`crates/infra/`](https://github.com/base/base/tree/main/crates/infra) --- # infra/system-tests Source: https://basehub.org/crates/infra/system-tests/ Description: End-to-end test harness exercising the Base stack against Kafka, S3, and multiple providers. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `infra/system-tests` sub-crate (`system-tests`) is part of the [infra crate group](/crates/infra/overview/). It runs end-to-end tests against the full Base stack, standing up Kafka, S3, and multiple provider configurations to validate behavior. ## Source --- # infra/websocket-proxy Source: https://basehub.org/crates/infra/websocket-proxy/ Description: WebSocket proxy built on axum with Brotli compression and exponential backoff reconnects. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `infra/websocket-proxy` sub-crate (`websocket-proxy`) is part of the [infra crate group](/crates/infra/overview/). It runs an axum-based WebSocket proxy that fans out flashblock streams with Brotli compression and exponential backoff on reconnects. ## Source --- # Crate Reference Source: https://basehub.org/crates/overview/ Description: The 8 top-level crate groups that make up the base/base Cargo workspace. The `base/base` Cargo workspace splits into 8 top-level crate groups. Each group owns a domain of the Base Reth Node and contains its own set of sub-crates. ## Workspace Structure | Crate Group | Sub-crates | Purpose | |-------------|-----------|---------| | [alloy](/crates/alloy/overview/) | 8 | Alloy type extensions for Base (consensus types, RPC types, EVM, hardforks) | | [builder](/crates/builder/overview/) | 3 | Block building, flashblock production, and metering | | [client](/crates/client/overview/) | 7 | Node client — CLI, engine, flashblocks integration, metering, RPC | | [consensus](/crates/consensus/overview/) | 15 | OP Stack consensus — derivation, gossip, P2P, sequencing (Kona-based) | | [execution](/crates/execution/overview/) | 14 | Execution layer — EVM, chainspec, payload building, storage, txpool | | [infra](/crates/infra/overview/) | 7 | Infrastructure tooling — basectl, ingress RPC, WebSocket proxy | | [proof](/crates/proof/overview/) | 7 | Fault proof system — FPVM, preimage oracle, proof driver (Kona-based) | | [shared](/crates/shared/overview/) | 9 | Shared utilities — primitives, access lists, JWT, bundles, CLI utils | ## Naming Conventions Crate names in `Cargo.toml` follow these patterns: - **`base-*`** — Core Base crates (e.g., `base-primitives`, `base-flashblocks`, `base-builder-core`) - **`kona-*`** — Consensus and proof crates derived from the Kona project (e.g., `kona-derive`, `kona-proof`) - **Infrastructure** — Named after their binary or function (e.g., `based`, `basectl-cli`, `ingress-rpc-lib`) ## Dependency Flow At a high level, dependencies flow in this direction: ``` shared, alloy (foundation types) ↓ execution (EVM, state, txpool) ↓ client (node assembly, CLI, RPC) ↓ builder (block building, flashblocks) consensus (independent OP Stack consensus) proof (independent fault proof system) infra (infrastructure binaries, depends on various crates) ``` The `shared` and `alloy` crates provide foundational types used throughout the workspace. The `execution` crates build on these to implement the EVM and state management. The `client` crates assemble the full node, and the `builder` crates handle block production. The `consensus` and `proof` crate groups are relatively independent — they implement the OP Stack consensus protocol and fault proof system respectively, with their own internal dependency chains. ## Source Code All crates live under [`crates/`](https://github.com/base/base/tree/main/crates) in the repository. --- # proof/driver Source: https://basehub.org/crates/proof/driver/ Description: no_std driver that sequences derivation and execution steps inside the fault proof program. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `proof/driver` sub-crate (`kona-driver`) is part of the [proof crate group](/crates/proof/overview/). It drives the `no_std` derivation pipeline, sequencing derive and execute steps so the fault proof program can advance the L2 chain step by step. ## Source --- # proof/executor Source: https://basehub.org/crates/proof/executor/ Description: no_std stateless OP Stack block builder that runs the EVM inside the FPVM. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `proof/executor` sub-crate (`kona-executor`) is part of the [proof crate group](/crates/proof/overview/). It builds OP Stack blocks statelessly inside the FPVM, executing the EVM against witness data supplied through the preimage oracle. ## Source --- # proof/mpt Source: https://basehub.org/crates/proof/mpt/ Description: Merkle Patricia Trie utilities for traversal, lookup, and proof verification inside the FPVM. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `proof/mpt` sub-crate (`kona-mpt`) is part of the [proof crate group](/crates/proof/overview/). It implements Merkle Patricia Trie traversal, lookup, and proof verification used by the fault proof program to read state. ## Source --- # proof Source: https://basehub.org/crates/proof/overview/ Description: Fault proof crates covering the FPVM runtime, preimage oracle, driver, executor, and MPT verification. The `proof` crate group implements Base's fault proof stack. The crates derive from [Kona](https://github.com/op-rs/kona) and cover proof generation, verification, and the Fault Proof Virtual Machine (FPVM) interface. ## Sub-crates | Crate | Package Name | Purpose | |-------|-------------|---------| | [driver](/crates/proof/driver/) | `kona-driver` | Proof generation driver | | [executor](/crates/proof/executor/) | `kona-executor` | Stateless block executor for proofs | | [mpt](/crates/proof/mpt/) | `kona-mpt` | Merkle Patricia Trie for proofs | | [preimage](/crates/proof/preimage/) | `kona-preimage` | Preimage oracle interface | | [proof](/crates/proof/proof/) | `kona-proof` | Core proof logic | | [std-fpvm](/crates/proof/std-fpvm/) | `kona-std-fpvm` | Standard FPVM runtime | | [std-fpvm-proc](/crates/proof/std-fpvm-proc/) | `kona-std-fpvm-proc` | Procedural macros for FPVM | ## Role in the Architecture The proof system enables trustless verification of L2 state transitions on L1. It: 1. Re-executes L2 blocks in a deterministic environment (`executor`) 2. Uses the Merkle Patricia Trie for state proof verification (`mpt`) 3. Interfaces with the preimage oracle for reading L1/L2 data (`preimage`) 4. Runs inside the Fault Proof Virtual Machine (`std-fpvm`) 5. Coordinates the proof generation process (`driver`, `proof`) These crates compile to targets that run inside the FPVM (e.g., MIPS or RISC-V) for on-chain dispute resolution. ## Source [`crates/proof/`](https://github.com/base/base/tree/main/crates/proof) --- # proof/preimage Source: https://basehub.org/crates/proof/preimage/ Description: Rust bindings and types for the PreimageOracle ABI used by fault proof programs. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `proof/preimage` sub-crate (`kona-preimage`) is part of the [proof crate group](/crates/proof/overview/). It provides Rust bindings and typed wrappers over the PreimageOracle ABI, the channel through which fault proof programs read host data. ## Source --- # proof/proof Source: https://basehub.org/crates/proof/proof/ Description: OP Stack Proof SDK with KZG via ark-bls12-381, preimage oracle integration, and the full proof pipeline. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `proof/proof` sub-crate (`kona-proof`) is part of the [proof crate group](/crates/proof/overview/). It is the OP Stack Proof SDK, wiring KZG verification via `ark-bls12-381`, the preimage oracle, and the end-to-end proof pipeline. ## Source --- # proof/std-fpvm Source: https://basehub.org/crates/proof/std-fpvm/ Description: Platform APIs and a buddy allocator for programs targeting Fault Proof VM kernels. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `proof/std-fpvm` sub-crate (`kona-std-fpvm`) is part of the [proof crate group](/crates/proof/overview/). It exposes platform-specific APIs for Fault Proof VM kernels and ships a buddy allocator that proof programs use as their global allocator. ## Source --- # proof/std-fpvm-proc Source: https://basehub.org/crates/proof/std-fpvm-proc/ Description: Procedural macros that emit the entry point for kona-std-fpvm proof programs. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `proof/std-fpvm-proc` sub-crate (`kona-std-fpvm-proc`) is part of the [proof crate group](/crates/proof/overview/). It supplies procedural macros that generate the entry point and runtime glue for programs targeting `kona-std-fpvm`. ## Source --- # shared/access-lists Source: https://basehub.org/crates/shared/access-lists/ Description: Implements Flashblock-level Access Lists (FAL), the EIP-7928 adaptation used by Base. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `shared/access-lists` sub-crate (`base-access-lists`) is part of the [shared crate group](/crates/shared/overview/). It implements Flashblock-level Access Lists (FAL), Base's adaptation of EIP-7928 for the OP Stack. ## Source --- # shared/bundles Source: https://basehub.org/crates/shared/bundles/ Description: Defines transaction bundle types with UUID IDs, signing, and gas estimation for ordering. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `shared/bundles` sub-crate (`base-bundles`) is part of the [shared crate group](/crates/shared/overview/). It defines transaction bundle types, including UUID identification, signing, and gas estimation that other crates use to order bundles. ## Source --- # shared/cli-utils Source: https://basehub.org/crates/shared/cli-utils/ Description: Shared CLI helpers for tracing, Prometheus export, clap parsing, and Kona registry lookups. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `shared/cli-utils` sub-crate (`base-cli-utils`) is part of the [shared crate group](/crates/shared/overview/). It provides shared CLI helpers across the workspace: tracing setup, Prometheus metric export, clap argument helpers, and Kona registry chain lookup. ## Source --- # shared/engine-ext Source: https://basehub.org/crates/shared/engine-ext/ Description: In-process engine client that talks directly to Reth's execution layer without going over RPC. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `shared/engine-ext` sub-crate (`base-engine-ext`) is part of the [shared crate group](/crates/shared/overview/). It exposes an in-process engine client so consensus components can drive Reth's execution layer directly without round-tripping through JSON-RPC. ## Source --- # shared/jwt Source: https://basehub.org/crates/shared/jwt/ Description: Loads and validates JWT secrets that authenticate Engine API traffic between node components. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `shared/jwt` sub-crate (`base-jwt`) is part of the [shared crate group](/crates/shared/overview/). It loads and validates the JWT secrets that authenticate Engine API traffic between the consensus and execution components. ## Source --- # shared/node Source: https://basehub.org/crates/shared/node/ Description: Core node harness with the types and traits used to build Base node runner extensions. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `shared/node` sub-crate (`base-client-node`) is part of the [shared crate group](/crates/shared/overview/). It defines the core node harness, including the primitive types and traits that Base node runner extensions implement. ## Source --- # shared Source: https://basehub.org/crates/shared/overview/ Description: Foundational crates for primitives, access lists, JWT auth, bundles, CLI utilities, and RPC types. The `shared` crate group holds the foundational types and utilities used across the workspace. It defines primitives, access lists, JWT auth, and RPC extensions consumed by every other crate group. ## Sub-crates | Crate | Package Name | Purpose | |-------|-------------|---------| | [access-lists](/crates/shared/access-lists/) | `base-access-lists` | Flashblock-level Access Lists (FAL) implementation | | [bundles](/crates/shared/bundles/) | `base-bundles` | Transaction bundle types | | [cli-utils](/crates/shared/cli-utils/) | `base-cli-utils` | CLI utilities (logging, metrics, clap helpers) | | [engine-ext](/crates/shared/engine-ext/) | `base-engine-ext` | Engine API extensions | | [jwt](/crates/shared/jwt/) | `base-jwt` | JWT authentication for Engine API | | [node](/crates/shared/node/) | `base-client-node` | Shared node configuration | | [primitives](/crates/shared/primitives/) | `base-primitives` | Core primitive types (includes Solidity contracts for tests) | | [reth-rpc-types](/crates/shared/reth-rpc-types/) | `base-reth-rpc-types` | Reth-compatible RPC type definitions | | [txpool-rpc](/crates/shared/txpool-rpc/) | `base-txpool-rpc` | Transaction pool RPC extensions | ## Role in the Architecture The shared crates form the foundation layer that all other crate groups depend on: - **primitives** — Defines the core types (transactions, blocks, receipts) used everywhere. Includes Solidity contracts compiled with Foundry for testing. - **access-lists** — Implements the [FAL specification](/specifications/access-lists/) for flashblock-level access lists, adapting EIP-7928 for the OP Stack. - **cli-utils** — Provides standardized CLI setup including clap argument parsing, tracing/logging configuration, and Prometheus metrics exporting. - **jwt** — Handles JWT-based authentication for the Engine API connection between execution and consensus layers. - **engine-ext** — Extends the Engine API with Base-specific payload attributes. - **txpool-rpc** — Adds custom RPC methods for interacting with the transaction pool. - **bundles** — Defines transaction bundle types for MEV and priority ordering. - **reth-rpc-types** — Bridges Base types to Reth's RPC type system. ## Source [`crates/shared/`](https://github.com/base/base/tree/main/crates/shared) --- # shared/primitives Source: https://basehub.org/crates/shared/primitives/ Description: Shared primitive types, Brotli helpers, bundle types, and Solidity bindings used across Base. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `shared/primitives` sub-crate (`base-primitives`) is part of the [shared crate group](/crates/shared/overview/). It defines the workspace-wide primitive types and ships Brotli helpers, bundle types, and Solidity contract bindings used by tests and runtime code. ## Source --- # shared/reth-rpc-types Source: https://basehub.org/crates/shared/reth-rpc-types/ Description: Re-exports Reth RPC types so external consumers of the Base RPC interface get a stable surface. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `shared/reth-rpc-types` sub-crate (`base-reth-rpc-types`) is part of the [shared crate group](/crates/shared/overview/). It re-exports the Reth RPC types so external consumers of the Base RPC interface depend on one stable surface. ## Source --- # shared/txpool-rpc Source: https://basehub.org/crates/shared/txpool-rpc/ Description: Adds Base-specific RPC methods for inspecting and interacting with the transaction pool. import { LinkCard } from '@astrojs/starlight/components'; ## Overview The `shared/txpool-rpc` sub-crate (`base-txpool-rpc`) is part of the [shared crate group](/crates/shared/overview/). It extends the JSON-RPC surface with Base-specific methods for inspecting and interacting with the transaction pool. ## Source --- # App Integration Source: https://basehub.org/flashblocks/app-integration/ Description: Integrate Flashblocks for 200ms confirmations using the pending RPC tag with Wagmi, Viem, Ethers, and Flashblocks-aware node providers. import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; Choose the integration that matches your app's needs and connect through a Flashblocks-aware RPC endpoint. | Use case | Recommended approach | Documentation | |----------|----------------------|---------------| | **Apps needing instant UX** | Flashblocks-aware RPC with the `pending` tag. | [API Reference](/api-reference/flashblocks/flashblocks-api-overview/) | | **Infrastructure providers** | Host Flashblocks-aware RPC nodes. | [Enable Flashblocks](/node-operations/run-a-base-node/#enable-flashblocks) | | **Standard apps** | Continue using regular RPCs. | [JSON-RPC API Reference](/api-reference/rpc-overview/) | ## RPC endpoints For HTTP and WebSocket endpoint URLs, see the [Flashblocks API reference](/api-reference/flashblocks/flashblocks-api-overview/). Public endpoints are rate-limited; for production, route through a Flashblocks-enabled provider such as Alchemy, QuickNode, or dRPC. ## Performance characteristics | Metric | Value | |--------|-------| | Flashblock build time (P50) | ~10ms | | Preconfirmation latency | ~200ms | | Full block time | 2 seconds | | Flashblocks per block | 10 | | Reorg rate | < 0.1% | ## Gas and transaction sizing The gas budget is **cumulative**, not per-Flashblock. Each Flashblock unlocks an additional 1/10 of the total block gas: | Flashblock | Cumulative gas available | |------------|--------------------------| | 1 | 1/10 of block limit (~40M gas) | | 2 | 2/10 of block limit (~80M gas) | | 3 | 3/10 of block limit (~120M gas) | | ... | ... | | 10 | Full block limit (~400M gas) | **Implications for large transactions:** - In practice, gas size is rarely the limiting factor. Base's **per-transaction gas maximum** of 16,777,216 gas (~16.7M) is already below Flashblock 1's ~40M budget, so any valid transaction qualifies for the first Flashblock on size alone. - Inclusion timing is governed primarily by priority fee relative to others competing in the same 200ms window, not by transaction size. ## Reliability and fallback Base targets a **< 0.1% Flashblock reorg rate** — the share of preconfirmations that were streamed but missed inclusion in the final block. This is rare, but apps should plan for it. If Flashblocks become unavailable, the sequencer keeps running and confirmation falls back to standard 2-second blocks. Apps should handle both modes. ## Library examples A Flashblocks-aware RPC endpoint is required to use Flashblocks with these libraries. ### [Wagmi](https://wagmi.sh) To use Flashblocks with Wagmi, set the `basePreconf` chain in the Wagmi config (see `config.ts`). ```tsx import { useSendTransaction, useWaitForTransactionReceipt } from "wagmi"; function SendTransaction() { const { data: hash, sendTransaction } = useSendTransaction(); const { data: receipt } = useWaitForTransactionReceipt({ hash }); return (
{hash &&
Hash: {hash}
} {receipt &&
Included on block number: {receipt.blockNumber}
}
) } ```
```tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { WagmiProvider, useAccount } from 'wagmi' import { config } from './config' import { Example } from './Example' const queryClient = new QueryClient() function App() { return ( ) } ``` ```tsx import { createConfig, http } from "wagmi"; import { baseSepoliaPreconf } from "wagmi/chains"; import { baseAccount } from "wagmi/connectors"; export const config = createConfig({ chains: [baseSepoliaPreconf], connectors: [baseAccount()], transports: { [baseSepoliaPreconf.id]: http(), }, }); ```
### [Viem](https://viem.sh) ```ts import { createWalletClient, http, parseEther, publicActions } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { baseSepoliaPreconf } from "viem/chains"; // Create client with the Flashblocks-aware chain. const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`); const client = createWalletClient({ account, chain: baseSepoliaPreconf, transport: http(), }) .extend(publicActions); const submissionTime = new Date(); console.log(`Submitting transaction at: ${submissionTime.toISOString()}`); // Send transaction. const hash = await client.sendTransaction({ to: "0x...", value: parseEther('0.0001'), }); console.log(`Transaction hash: ${hash}`); // Wait for transaction to be included. const receipt = await client.waitForTransactionReceipt({ hash }); const confirmTime = new Date(); console.log(`Transaction included at: ${confirmTime.toISOString()}`); console.log(`Time difference: ${confirmTime - submissionTime}ms`); ``` ### [Ethers](https://github.com/ethers-io/ethers.js) ```jsx const providerA = new ethers.JsonRpcProvider( "https://sepolia.base.org" ); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, providerA); try { // Create a simple transaction (sending 0.001 ETH to a random address) const tx = { to: "", value: ethers.parseEther("0.0000001"), }; // Submit transaction const submissionTime = new Date(); const transaction = await wallet.sendTransaction(tx); console.log(`Submitting transaction at: ${submissionTime.toISOString()}`); console.log(`Transaction hash: ${transaction.hash}`); await transaction.wait(0); // Make sure to set the confirmation count to 0 console.log("Transaction confirmed"); const confirmationTime = new Date(); console.log(`Transaction confirmed at: ${confirmationTime.toISOString()}`); console.log(`Time difference: ${confirmationTime - submissionTime}ms`); } ``` Confirmation times against this endpoint should be substantially lower than the standard RPC. ## Support For feedback, support, or questions about Flashblocks, reach the team in the `#developer-chat` channel on the [Base Discord](https://base.org/discord). --- # Flashblocks Architecture Source: https://basehub.org/flashblocks/architecture/ Description: Sequencer topology and infrastructure components — rollup-boost, base-builder, websocket-proxy, and base — that power Flashblocks on Base. The Flashblocks pipeline layers four new components — rollup-boost, base-builder, websocket-proxy, and base — onto Base's existing high-availability sequencer. ## Before Flashblocks Base operates a high-availability sequencer with five sequencer instances: | Component | Role | |-----------|------| | **base-consensus** | Consensus layer (CL) — took over from op-node after Azul. | | **base-reth-node** | Execution layer (EL) — took over from op-geth after Azul. | | **op-conductor** | High availability controller using Raft consensus for leader election. | One sequencer instance is the **leader** and produces blocks, propagating them over P2P. The other four follow and sync the chain. Leadership transfers when the current leader stops producing blocks. ## With Flashblocks Flashblocks add several new infrastructure components: | Component | Purpose | What it unlocks | |-----------|---------|-----------------| | **rollup-boost** | CL↔EL Engine API proxy. | Streams Flashblocks to the EL without modifying the CL. Provides a stable seam for future block-building changes such as multi-builder. | | **base-builder** | Out-of-protocol builder running at 200ms cadence. | Produces sub-second Flashblocks decoupled from the EL and enables pluggable builder mechanisms. | | **websocket-proxy** | Fan-out for the Flashblocks stream. | Lets many consumers read the stream without overwhelming the builder. | | **[base](https://github.com/base/base)** | RPC surface that exposes preconfirmations. | Turns streamed Flashblocks into familiar RPC methods so apps and wallets can consume preconfirmed state. | Flashbots builds and maintains [rollup-boost](https://github.com/flashbots/rollup-boost); Base maintains [base-builder](https://github.com/base/base/tree/main/crates/builder), the websocket-proxy, and the [base](https://github.com/base/base) component. --- ## Further reading - [Flashblocks Overview](/flashblocks/overview/) — key concepts and transaction lifecycle. - [Enable Flashblocks](/node-operations/run-a-base-node/#enable-flashblocks) — run your own Flashblocks-aware RPC node. - [API Reference](/api-reference/flashblocks/flashblocks-api-overview/) — RPC methods, WebSocket subscriptions, and infrastructure stream schema. - [Flashblocks deep dive](https://blog.base.dev/flashblocks-deep-dive) — engineering blog post with implementation details. --- # Flashblocks FAQ Source: https://basehub.org/flashblocks/faq/ Description: Answers to common questions about Flashblocks block building, WebSocket data, RPC behavior, and node setup on Base. import { Aside } from '@astrojs/starlight/components'; Common questions about Flashblocks block building, WebSocket access, RPC behavior, and node setup. ## Block building ### Are Flashblocks optional? Every block on Base is built by the Flashblocks builder, so **Flashblocks are always live**. Apps that prefer not to consume preconfirmations can keep using standard RPCs without any Flashblocks integration. ### Is transaction inclusion different for Flashblocks vs. 2-second blocks? Not in any meaningful way — both order transactions by fee. The difference is timing: Flashblocks arrive every 200ms instead of every 2 seconds. See [Block Building](/network/block-building/#flashblocks) for details. ### Can the sequencer stop publishing Flashblocks? The sequencer will not stop publishing Flashblocks unless an extreme situation makes them unsafe to operate. If that happens, preconfirmations are disabled network-wide and confirmation falls back to standard 2-second blocks. The sequencer continues operating normally. ### Why is my transaction having trouble getting included? Inclusion timing is driven mainly by priority fee, not transaction size. The builder allocates gas cumulatively — Flashblock `j` may use up to `j/10` of the total block gas limit — so in principle a very large transaction has a harder time landing in the first Flashblock. In practice this rarely bites: Base's [per-transaction gas maximum](/network/block-building/#per-transaction-gas-maximum) of 16,777,216 gas (~16.7M) is already below Flashblock 1's ~40M budget, so any valid transaction fits in the first Flashblock on size alone. When a transaction is slow to include, the usual cause is a low priority fee relative to others competing in the same 200ms window. See [Block Building](/network/block-building/) for the gas budget breakdown and throughput limits. ### How do I make sure my transaction lands in the first Flashblock? There is no way to guarantee inclusion in a specific Flashblock, just as there is no way to guarantee a specific block. Since the per-transaction gas maximum (~16.7M) sits below Flashblock 1's ~40M capacity, gas size is not the limiting factor — every valid transaction is eligible for the first Flashblock. To improve the odds of fast inclusion, set a higher priority fee. ### Why do transactions sometimes appear out of fee order? The Flashblock builder uses a **dynamic mempool** that keeps accepting new transactions while it builds. The design favors **low inclusion latency** over strict fee ordering. **What that means in practice:** - Transactions are ordered by fee *at the moment they are selected* for inclusion. - A high-fee transaction that arrives after a lower-fee transaction has already been committed to the current Flashblock will appear after it (or in the next Flashblock). - This is expected — the builder does not "reorder" already-committed transactions. **Why this trade-off?** A "snapshot" mempool that froze the txpool at the start of each block would guarantee strict fee ordering but at the cost of higher inclusion latency. The dynamic approach gets transactions into blocks faster, occasionally at the expense of breaking strict priority gas auction (PGA) ordering. **For traders and bots:** if strict fee-based ordering is essential to your strategy, arrival timing matters as much as fee amount within any 200ms Flashblock window. ### How frequently do Flashblock reorgs happen? Base targets a Flashblock reorg rate of < 0.1%. Reorgs are rare, but applications handling critical operations should still implement fallback logic. Live metrics are at [base.org/stats](https://base.org/stats). ### What does it mean when a Flashblock is reorged? A reorg means a Flashblock was streamed as a preconfirmation but did not make it into the final block. Architectural improvements in rollup-boost prevent tail-Flashblock reorgs, so this is uncommon — but apps should still handle the case. --- ## WebSocket ### Can I connect directly to the Flashblocks WebSocket stream? **No.** The raw Flashblocks WebSocket (`wss://mainnet.flashblocks.base.org/ws`) is reserved for infrastructure-to-node data syncing. Applications should not connect to it directly. Instead, query your RPC node or node provider (QuickNode, Alchemy, Infura, dRPC, etc.) for Flashblocks data via: - **RPC API** — standard JSON-RPC methods with the `pending` tag. - **WebSocket subscriptions** — `eth_subscribe` through your node provider's WebSocket endpoint. See [App Integration](/flashblocks/app-integration/) for implementation details. ### Why are there 11 Flashblock indices (0-10)? Index 0 carries only system transactions and uses no gas budget. Indexes 1-10 are the actual Flashblocks that pull pending transactions from the txpool. ### Why are there sometimes fewer than 10 Flashblocks? Expected behavior. When the previous block takes longer to build, the system compensates by allocating less time to the next block, which leaves room for fewer Flashblocks. ### Can the Flashblock index exceed 10? Is that a bug? **Not a bug.** Indices of 10, 11, or higher show up in normal operation. The standard math — 2-second block time divided by 200ms per Flashblock — yields exactly 10 Flashblocks (indices 0-9). In practice, the transition from one full L2 block to the next is not always perfectly aligned with the 200ms timer. Two situations create extra indices: 1. **Sequencer delay.** If sealing the full block takes slightly longer than 2000ms, the Flashblock stream keeps emitting incremental updates for the current block to keep the stream live. 2. **Timing drift.** If the internal 200ms clock drifts or starts early relative to the L2 block's canonical start time, an extra update can fit inside the 2-second window. **What this means for your implementation:** - Do not hardcode `9` or `10` as the final index — the last Flashblock for a given block is not predictable by index alone. - Track `payloadId` instead. The most reliable signal that a block has finished is when `payloadId` changes, or when the full block confirms via standard RPC. All Flashblocks sharing the same `payloadId` belong to the same block, no matter how high the index goes. - Once the sequencer advances to the next block, `payloadId` resets and `index` returns to `0`. ### What encoding format is the transaction data in? Transaction data in the [`diff.transactions`](/api-reference/flashblocks/flashblocks-api-overview/#diff-object) array is Recursive Length Prefix (RLP) encoded. ### Why am I getting rate limited on the WebSocket? The public WebSocket caps the number of connections. For production use: 1. Run your own [Flashblocks-aware RPC node](/node-operations/run-a-base-node/#enable-flashblocks). 2. Use a third-party node provider with Flashblocks support. --- ## RPC ### Why am I getting rate limited using `mainnet.base.org`? The public endpoint applies explicit rate limits. For production traffic: - Use a third-party node provider with Flashblocks support (Alchemy, Infura, QuickNode, dRPC). - Run your own [Flashblocks-aware RPC node](/node-operations/run-a-base-node/#enable-flashblocks). ### Why does `eth_call "pending"` report a block number several blocks behind tip? This is expected. Flashblocks-aware nodes retain up to 5 historical blocks of Flashblocks state to avoid race conditions. When `eth_call "pending"` runs, it executes on top of that historical base, so the block number visible in the call context (such as `block.number`) may show as N-5. When `eth_call "pending"` executes, the entire block context — `block.number`, `block.timestamp`, `block.basefee`, and the rest — corresponds to that historical base block (potentially N-5), not the current chain tip. **The call result is correct** in that all received Flashblocks state has been applied on top, but contracts that depend on block context properties should be aware that those values may be several blocks behind. For nodes in regions where P2P latency is comparable to WebSocket stream latency, lowering `MAX_PENDING_BLOCKS_DEPTH` reduces this gap. The setting controls how many historical blocks of Flashblocks state the node retains; a lower value keeps the block context closer to tip at the cost of less tolerance for P2P latency spikes. ### What RPC methods support Flashblocks? The following methods are Flashblocks-enabled: | Method | Usage | |--------|-------| | `eth_getBlockByNumber` | Use the `pending` tag. | | `eth_getBalance` | Use the `pending` tag. | | `eth_getTransactionReceipt` | Returns preconfirmed receipts. | | `eth_getTransactionByHash` | Use the `pending` tag. | | `eth_getTransactionCount` | Use the `pending` tag. | | `eth_call` | Use the `pending` tag. | | `eth_simulateV1` | Use the `pending` tag. | | `eth_estimateGas` | Use the `pending` tag. | | `eth_getLogs` | Use `pending` for `toBlock`. | | `eth_subscribe` | Stream Flashblock data in real time. | | `base_transactionStatus` | Check if a transaction is in the mempool (Beta). | See the [Flashblocks API reference](/api-reference/flashblocks/flashblocks-api-overview/) for full method details and examples. --- ## Node setup ### How do I set up a Flashblocks-aware RPC node? Use the Reth binary from the [Base node repository](https://github.com/base/node/tree/main/reth). Full instructions are in [Enable Flashblocks](/node-operations/run-a-base-node/#enable-flashblocks). --- # Flashblocks Overview Source: https://basehub.org/flashblocks/overview/ Description: Flashblocks deliver 200ms preconfirmed sub-blocks on Base, streaming state updates 10x faster than the standard 2-second block cadence. import { Aside } from '@astrojs/starlight/components'; Flashblocks are 200ms preconfirmed sub-blocks streamed by the Base sequencer, giving applications fee-ordered state updates ten times per standard 2-second block. ## What are Flashblocks Flashblocks add **200ms** incremental state updates to Base, cutting perceived latency for end users. The mechanism, built with [Flashbots](https://www.flashbots.net/), streams sub-blocks inside the regular 2-second block interval. Apps see preconfirmations from the sequencer well before the full block is gossiped to the network. ### Key concepts | Term | Definition | |------|------------| | **Flashblock** | A 200ms sub-block carrying part of the full block's transactions. | | **Preconfirmation** | An ultra-fast signal that a transaction will be included, before the full block is sealed. | | **Full Block** | A sequence of 10 Flashblocks combined into the complete 2-second block. | Each block contains **10 Flashblocks** (indexes 1-10), arriving every 200ms. Index 0 exists but only carries system transactions. See [Gas and transaction sizing](/flashblocks/app-integration/#gas--transaction-sizing) for how the gas budget evolves across the window. ## Architecture For details on the sequencer system and the Flashblocks infrastructure components (rollup-boost, base-builder, websocket-proxy, and base), see the [Architecture](/flashblocks/architecture/) page. ## Transaction lifecycle When a transaction is sent to Base, this is the path it takes: ### 1. Submission ``` User → DNS (mainnet.base.org) → Load Balancer → Proxyd → Mempool ``` The transaction lands in the private mempool and is added to the txpool as pending. ### 2. Distribution The mempool keeps P2P connections open with execution layers (base-reth-node, base-builder), keeping all pending transactions in sync for block building. ### 3. Block building In each 200ms loop, base-builder selects transactions using: 1. **Transaction fee** — transactions are ordered fee-first (highest first). 2. **Gas limit and remaining capacity** — Flashblock `j` may use up to `j/10` of the total block gas limit. ### 4. Block building algorithm For each 2-second block, the builder runs: ``` FOR each flashblock j FROM 0 TO 10: 1. Wait until next 200ms window 2. Calculate available gas: (j / 10) × total_block_gas_limit 3. Sort pending transactions by fee (descending) 4. Select top transactions that fit within available gas 5. Execute transactions and update state 6. Stream flashblock to websocket-proxy ``` ### 5. Preconfirmation delivery Once a transaction is included in a Flashblock, the builder streams it to the websocket-proxy. RPC nodes subscribe to that stream, so a Flashblocks-aware RPC method (such as `eth_getTransactionReceipt`) can return preconfirmed data straight from the node's cache. ## Further reading - [FAQ](/flashblocks/faq/) — common questions about Flashblocks. - [Block Building](/network/block-building/) — detailed block ordering configuration. - [Flashblocks deep dive](https://blog.base.dev/flashblocks-deep-dive) — engineering blog post with implementation details. --- # Building from Source Source: https://basehub.org/getting-started/building/ Description: Clone the base/base repository and compile binaries with Just recipes or Cargo. Building `base/base` from source means cloning the repository and compiling one or more workspace targets with Just or Cargo. Install all [prerequisites](/getting-started/prerequisites/) before continuing. ## Clone the Repository ```bash git clone https://github.com/base/base.git cd base ``` ## Standard Release Build The most common build command produces a fully optimized release binary for every binary target in the workspace: ```bash just build ``` Under the hood this runs `cargo build --release`. The compiled binaries are placed in `target/release/`. ## Maximum Performance Build For production deployments where you want every bit of throughput, use the `maxperf` profile. This enables all CPU-specific optimizations and links against jemalloc: ```bash just build-maxperf ``` This build takes longer to compile but produces a binary tuned for the machine it was compiled on. Do not distribute `maxperf` binaries to machines with different CPU micro-architectures. ## Building Individual Targets If you only need the node binary and want a faster compile: ```bash just build-node ``` Or build it directly with Cargo: ```bash cargo build --release --bin base-reth-node ``` The resulting binary is at `target/release/base-reth-node`. ## Building Solidity Test Contracts Several crates in the workspace depend on compiled Solidity contracts for their test suites. Build them with: ```bash just build-contracts ``` This changes into `crates/shared/primitives/contracts`, installs Soldeer dependencies, and runs `forge build`. You must have [Foundry](/getting-started/prerequisites/#foundry) installed for this to work. ## Debug Build (All Targets) During development you may want an unoptimized build with debug symbols for every target in the workspace: ```bash just build-all-targets ``` This compiles in debug mode, which is significantly faster to compile but produces slower binaries. Use this when you need to iterate quickly and do not care about runtime performance. ## Build Artifacts | Command | Profile | Output | |---|---|---| | `just build` | release | `target/release/*` | | `just build-maxperf` | maxperf (release + LTO + jemalloc) | `target/release/*` | | `just build-node` | release | `target/release/base-reth-node` | | `just build-all-targets` | debug | `target/debug/*` | | `just build-contracts` | N/A (Foundry) | Solidity artifacts in `crates/shared/primitives/contracts/out/` | ## Troubleshooting **Linker errors mentioning `clang` or `llvm`** -- Install `libclang-dev` (Debian/Ubuntu) or make sure Xcode Command Line Tools are installed (macOS). **Out of memory during linking** -- Release and maxperf builds with LTO can use a lot of RAM. Close other memory-heavy applications or add swap space. You can also reduce parallelism: ```bash CARGO_BUILD_JOBS=2 just build ``` **Stale Solidity artifacts** -- If contract tests fail after pulling new changes, re-run `just build-contracts` to recompile. Once the build completes, continue to [Running the Node](/getting-started/running/) or [Docker Builds](/getting-started/docker/). --- # Development Workflow Source: https://basehub.org/getting-started/development/ Description: Run the check, test, fix, and CI Just recipes used day-to-day on base/base. The `base/base` repository drives day-to-day development through [Just](https://github.com/casey/just) recipes for checking, testing, formatting, and CI parity. The recipes below cover the commands you will run most often. ## Quick Reference | Command | What it does | |---|---| | `just check` | Runs the full check suite (format, udeps, clippy, test, deny) | | `just test` | Runs the test suite via cargo-nextest | | `just fix` | Auto-fixes formatting, clippy, and zepter issues | | `just ci` | Full CI pipeline (fix, check, lychee, zepter, no-std check) | ## Running Tests The project uses [cargo-nextest](https://nexte.st/) as its test runner. Install it if you have not already: ```bash cargo install cargo-nextest ``` Run the full test suite: ```bash just test ``` This executes `cargo nextest` with all features enabled. The devnet tests are excluded by default since they require a running Docker Compose stack. To run tests for a specific crate: ```bash cargo nextest run -p base-execution-evm ``` To run a single test by name: ```bash cargo nextest run -p base-execution-evm -- test_name ``` ### Building Test Contracts Some tests depend on compiled Solidity contracts. If you see test failures related to missing contract artifacts, build them first: ```bash just build-contracts ``` See [Building from Source](/getting-started/building/#building-solidity-test-contracts) for details. ## Checking Code Quality The `check` recipe runs the full quality gate in sequence: ```bash just check ``` This is equivalent to running each of these in order: 1. **`check-format`** -- Verifies that all Rust source files match `rustfmt` style. 2. **`check-udeps`** -- Detects unused dependencies in `Cargo.toml` files. 3. **`check-clippy`** -- Runs Clippy with the project's configured lints. 4. **`test`** -- Runs the full test suite. 5. **`check-deny`** -- Checks dependencies for known vulnerabilities and license issues via `cargo-deny`. If any step fails, the pipeline stops. Fix the issue and re-run. ## Auto-Fixing Issues Most formatting and lint issues can be fixed automatically: ```bash just fix ``` This runs three fixers in sequence: 1. **`format-fix`** -- Applies `rustfmt` to all source files. 2. **`clippy-fix`** -- Applies Clippy's suggested fixes. 3. **`zepter-fix`** -- Fixes feature propagation issues detected by [zepter](https://github.com/nicknisi/zepter). Run `just fix` before committing to avoid CI failures. ## Full CI Pipeline To run the same checks that CI runs on pull requests: ```bash just ci ``` This encompasses: 1. `fix` -- auto-fix everything fixable 2. `check` -- the full quality gate 3. `lychee` -- checks for broken links in documentation 4. `zepter` -- validates Cargo feature propagation 5. `check-no-std` -- ensures `no_std` compatibility for crates that require it Running `just ci` locally before pushing is the best way to catch issues early. ## Formatting Check formatting without modifying files: ```bash just check-format ``` Auto-format all files: ```bash just format-fix ``` The project uses the standard `rustfmt` configuration. If your editor supports format-on-save with `rustfmt`, enable it for a smoother workflow. ## Clippy Run Clippy to catch common mistakes and non-idiomatic patterns: ```bash just check-clippy ``` Apply Clippy's suggestions automatically: ```bash just clippy-fix ``` ## Recommended Editor Setup For the best development experience, configure your editor with: - **rust-analyzer** -- Provides inline type hints, go-to-definition, and real-time diagnostics. - **Format on save** -- Set your editor to run `rustfmt` whenever you save a `.rs` file. - **Clippy as the check command** -- Configure rust-analyzer to use `clippy` instead of `cargo check` for richer diagnostics: ```json { "rust-analyzer.check.command": "clippy" } ``` ## Next Steps - [Local Devnet](/getting-started/devnet/) -- run a full local network for integration testing. - [Contributing](/contributing/) -- guidelines for submitting pull requests. --- # Local Devnet Source: https://basehub.org/getting-started/devnet/ Description: Start a Docker Compose devnet for integration testing, debugging, and flashblocks experiments. The Docker Compose devnet bundled with `base/base` spins up a complete local Base network for integration testing, debugging, and flashblocks experiments without touching a public testnet. Its configuration lives in `etc/docker/docker-compose.yml`. ## Prerequisites Before starting the devnet, make sure you have: - [Docker and Docker Compose](/getting-started/prerequisites/#docker-optional) installed and running. - The repository cloned and all [prerequisites](/getting-started/prerequisites/) in place. ### Pre-Pulling Images The devnet uses several Docker images. To avoid long waits on the first start, pull them ahead of time: ```bash just system-tests-pull-images ``` ## Starting the Devnet The `devnet` recipe handles the full lifecycle: it stops any existing devnet, builds fresh images, and starts the stack: ```bash just devnet ``` This is the recommended way to start or restart the devnet. It ensures you are running the latest code and that there is no stale state from a previous run. ## Stopping the Devnet To stop all containers and clean up resources: ```bash just devnet-down ``` This removes the containers and associated volumes, giving you a clean slate for the next start. ## Monitoring ### Block Status Check the current block numbers and sync status of the devnet nodes: ```bash just devnet-status ``` This prints block heights and indicates whether nodes are in sync. ### Funded Test Accounts View the pre-funded accounts available for testing: ```bash just devnet-accounts ``` These accounts come with a balance of test ETH and are ready to use for sending transactions. ### Container Logs Stream logs from one or more containers: ```bash # All containers just devnet-logs # Specific containers just devnet-logs sequencer proposer ``` Logs are streamed in real time. Press Ctrl+C to stop. ## Testing on the Devnet ### Smoke Tests Run a basic set of test transactions to verify the devnet is functioning correctly: ```bash just devnet-smoke ``` This sends a few transactions and verifies they are included in blocks. ### Combined Status and Smoke Run both status checks and smoke tests together: ```bash just devnet-checks ``` This is a convenient single command that first prints the devnet status and then runs the smoke tests. ### Load Testing Start the Contender load generator to stress-test the devnet: ```bash just devnet-load ``` This generates a sustained stream of transactions, which is useful for benchmarking block production, measuring throughput, and testing behavior under pressure. ## Flashblocks Streaming The devnet supports flashblocks -- sub-block-time transaction confirmations streamed over WebSocket. To connect and watch flashblocks: ```bash just devnet-flashblocks ``` This opens a WebSocket connection to the devnet and prints flashblock messages as they arrive (typically every 200ms). It is a quick way to verify that the flashblocks pipeline is working end-to-end. ## Typical Workflow A common development loop with the devnet looks like this: 1. Make code changes. 2. Start a fresh devnet: ```bash just devnet ``` 3. Verify it is running: ```bash just devnet-status ``` 4. Run smoke tests: ```bash just devnet-smoke ``` 5. Stream flashblocks to test your changes: ```bash just devnet-flashblocks ``` 6. Check logs for issues: ```bash just devnet-logs ``` 7. When finished, tear it down: ```bash just devnet-down ``` ## Troubleshooting **Containers fail to start** -- Make sure Docker is running and your user is in the `docker` group. Run `docker compose version` to verify the Compose plugin is installed. **Port conflicts** -- If another service is using the ports the devnet needs, stop that service first or edit `etc/docker/docker-compose.yml` to remap ports. **Stale state** -- If the devnet behaves unexpectedly after code changes, run `just devnet-down` followed by `just devnet` to start from a clean state. **Slow first start** -- The first run pulls several Docker images and may build containers from scratch. Run `just system-tests-pull-images` ahead of time to pre-pull base images. --- # Docker Builds Source: https://basehub.org/getting-started/docker/ Description: Build and run base/base from the Dockerfiles under etc/docker without installing Rust on the host. Docker builds compile `base/base` reproducibly without a host Rust toolchain. The repository ships several Dockerfiles under `etc/docker/` for the client, builder, devnet, and websocket proxy. ## Available Dockerfiles | File | Purpose | |---|---| | `etc/docker/Dockerfile.client` | Production client image containing `base-reth-node` | | `etc/docker/Dockerfile.builder` | Builder image used as a base for multi-stage builds | | `etc/docker/Dockerfile.devnet` | Devnet-specific image for local development networks | | `etc/docker/Dockerfile.websocket-proxy` | Standalone websocket proxy image | ## Building the Client Image From the repository root, build the production client image: ```bash docker build -t base-reth-node -f etc/docker/Dockerfile.client . ``` This performs a multi-stage build: it compiles the Rust binary inside a builder container and copies the resulting binary into a minimal runtime image. The build context is the repository root, so all workspace crates are available to the compiler. Depending on your hardware, the first build may take 15-30 minutes. Subsequent builds benefit from Docker layer caching. ## Running the Container Start the node with default settings: ```bash docker run -it --rm base-reth-node ``` Pass CLI options after the image name, exactly as you would with the bare binary: ```bash docker run -it --rm base-reth-node --help ``` ### Persisting Data By default, a container's filesystem is ephemeral. To persist chain data across restarts, mount a host directory to the node's data directory: ```bash docker run -it --rm \ -v /path/to/data:/data \ base-reth-node \ --datadir /data ``` ### Exposing Ports The node exposes several network ports. Map them to the host as needed: ```bash docker run -it --rm \ -v /path/to/data:/data \ -p 8545:8545 \ -p 8546:8546 \ -p 30303:30303 \ -p 30303:30303/udp \ base-reth-node \ --datadir /data \ --http \ --http.addr 0.0.0.0 \ --ws \ --ws.addr 0.0.0.0 ``` | Port | Protocol | Service | |---|---|---| | 8545 | TCP | HTTP JSON-RPC | | 8546 | TCP | WebSocket JSON-RPC | | 30303 | TCP + UDP | P2P discovery and communication | ### Running in the Background Use `-d` instead of `-it` to run detached: ```bash docker run -d --name base-node \ -v /path/to/data:/data \ -p 8545:8545 \ base-reth-node \ --datadir /data \ --http --http.addr 0.0.0.0 # View logs docker logs -f base-node # Stop docker stop base-node ``` ## Building Other Images Build the websocket proxy image: ```bash docker build -t base-websocket-proxy -f etc/docker/Dockerfile.websocket-proxy . ``` Build the devnet image (typically used by the docker-compose devnet stack rather than run directly): ```bash docker build -t base-devnet -f etc/docker/Dockerfile.devnet . ``` ## Tips - **Layer caching**: The Dockerfiles are structured so that dependency fetching is cached separately from source compilation. If you only change Rust source files, the dependency layers are reused. - **BuildKit**: Enable BuildKit for faster builds: `DOCKER_BUILDKIT=1 docker build ...` - **Cross-compilation**: If you need an `amd64` image on an ARM host (or vice versa), use `docker buildx build --platform linux/amd64 ...`. For running a full local devnet with Docker Compose, see [Local Devnet](/getting-started/devnet/). For production Docker deployments, see [Node Operations: Docker](/node-operations/docker/). --- # Prerequisites Source: https://basehub.org/getting-started/prerequisites/ Description: Install Rust, Just, Foundry, build essentials, and optionally Docker before working on base/base. Building or developing on `base/base` requires Rust, Just, Foundry, a C/C++ toolchain, and optionally Docker. Instructions below target Linux (Debian/Ubuntu); macOS users can substitute `brew install` where applicable. ## Rust The project requires **Rust 1.88 or later**. The recommended way to install and manage Rust toolchains is through [rustup](https://rustup.rs/): ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` Follow the on-screen prompts and then reload your shell: ```bash source "$HOME/.cargo/env" ``` Verify the installed version: ```bash rustc --version # rustc 1.88.0 (or later) ``` If you already have Rust installed, make sure it is up to date: ```bash rustup update ``` ## Just [Just](https://github.com/casey/just) is a command runner used throughout the project for build, test, and devnet workflows. Install it with one of: ```bash # Using cargo (works everywhere Rust is installed) cargo install just # Debian/Ubuntu sudo apt install just # macOS (Homebrew) brew install just ``` Confirm the installation: ```bash just --version ``` ## Foundry [Foundry](https://book.getfoundry.sh/) is required for compiling the Solidity test contracts used by several crates. Install via the official installer: ```bash curl -L https://foundry.paradigm.xyz | bash ``` Then run: ```bash foundryup ``` This gives you `forge`, `cast`, `anvil`, and `chisel`. Verify with: ```bash forge --version ``` ## Build Essentials A standard C/C++ toolchain and a few system libraries are needed to compile native dependencies: ```bash # Debian / Ubuntu sudo apt update sudo apt install -y git libclang-dev pkg-config curl build-essential ``` ```bash # macOS (Xcode Command Line Tools provide clang and related headers) xcode-select --install brew install pkg-config ``` ## Docker (Optional) Docker is only required if you plan to build container images or run the local devnet. Install Docker Engine by following the [official instructions](https://docs.docker.com/engine/install/) for your platform, or on Ubuntu: ```bash sudo apt install -y docker.io sudo systemctl enable --now docker sudo usermod -aG docker "$USER" ``` Log out and back in for the group change to take effect, then verify: ```bash docker --version docker compose version ``` The devnet uses Docker Compose (the `docker compose` plugin, not the legacy `docker-compose` binary). ## Quick Check Once everything is installed, you can run a quick sanity check from the repository root: ```bash rustc --version && cargo --version && just --version && forge --version && git --version ``` All five commands should succeed and print version information. With these prerequisites in place, proceed to [Building from Source](/getting-started/building/). --- # Running the Node Source: https://basehub.org/getting-started/running/ Description: Start the base-reth-node binary, configure RPC and metrics, and shut down cleanly. The `base-reth-node` binary starts the Base L2 execution client once you have a [source build](/getting-started/building/) or [Docker image](/getting-started/docker/). For production guidance, see [Node Operations](/node-operations/deployment/). ## Starting the Node Run the binary directly from the build output directory: ```bash ./target/release/base-reth-node ``` Or, if you used `just build-node`: ```bash ./target/release/base-reth-node ``` The node will begin syncing from the network. On the first run it creates a data directory (by default `~/.local/share/reth/` on Linux, `~/Library/Application Support/reth/` on macOS) and starts downloading blocks. ## CLI Help View all available options and subcommands: ```bash ./target/release/base-reth-node --help ``` This prints the full help text including every flag, option, and subcommand. It is the definitive reference for the CLI and is always in sync with the version you built. ## Common Options ### Data Directory Override the default data directory: ```bash ./target/release/base-reth-node --datadir /mnt/ssd/base-node ``` ### HTTP JSON-RPC Enable the HTTP JSON-RPC server: ```bash ./target/release/base-reth-node \ --http \ --http.addr 127.0.0.1 \ --http.port 8545 ``` By default the server only listens on localhost. Set `--http.addr 0.0.0.0` to accept connections from other machines (make sure your firewall is configured appropriately). ### WebSocket JSON-RPC Enable the WebSocket server alongside (or instead of) HTTP: ```bash ./target/release/base-reth-node \ --ws \ --ws.addr 127.0.0.1 \ --ws.port 8546 ``` ### Logging Control log verbosity with the `-v` flag (can be repeated) or the `RUST_LOG` environment variable: ```bash # Increase verbosity ./target/release/base-reth-node -vvv # Fine-grained control via RUST_LOG RUST_LOG=info,reth=debug ./target/release/base-reth-node ``` ### Metrics Enable the Prometheus metrics endpoint: ```bash ./target/release/base-reth-node \ --metrics 127.0.0.1:9001 ``` Point your Prometheus instance at `http://:9001/metrics` to scrape node metrics. ## Example: Full Node with RPC A typical invocation that enables both HTTP and WebSocket RPC, metrics, and stores data on a dedicated volume: ```bash ./target/release/base-reth-node \ --datadir /data/base-node \ --http \ --http.addr 0.0.0.0 \ --http.port 8545 \ --ws \ --ws.addr 0.0.0.0 \ --ws.port 8546 \ --metrics 0.0.0.0:9001 \ -vv ``` ## Enabling Flashblocks A synced node can serve ~200ms pre-confirmations to your applications once it is subscribed to the Flashblocks stream. Point the node at the Flashblocks WebSocket feed with `RETH_FB_WEBSOCKET_URL` when you launch it: ```bash RETH_FB_WEBSOCKET_URL="wss://mainnet.flashblocks.base.org/ws" ./target/release/base-reth-node ``` | Network | URL | |---------|-----| | Mainnet | `wss://mainnet.flashblocks.base.org/ws` | | Sepolia | `wss://sepolia.flashblocks.base.org/ws` | These WebSocket URLs are part of the node infrastructure and are not meant for direct use by applications — your own RPC endpoint is what apps should query for Flashblocks data. The binary tails this stream, caches the pre-confirmation data, and replies from that cache when a Flashblocks-aware RPC method is called. The full message schema is documented in the [Flashblocks API Overview](/api-reference/flashblocks/overview/). To confirm it is working, request a `pending` block: ```bash curl -X POST \ --data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["pending", false],"id":1}' \ http://localhost:8545 ``` A working setup returns data from the most recent Flashblock; if the stream is briefly unavailable the node falls back to the latest finalized block. ## Historical Proofs RPCs Serving `eth_getProof`, `debug_executionWitness`, and `debug_executePayload` at scale requires the historical proofs execution extension (ExEx), which maintains a dedicated database of the state those methods need. That database can grow by hundreds of gigabytes and wants a host with strong I/O, so enable it only if you actually need these endpoints — most operators do not. Turn the ExEx on with a single environment variable: ```bash RETH_HISTORICAL_PROOFS=true ``` On its first start the node backfills existing state into a new proofs database under `/proofs`. The backfill is lengthy — roughly 24-48 hours on mainnet. You can skip it entirely by [restoring a proofs snapshot](/node-operations/snapshots/#proofs-snapshots). The earliest block these RPCs can answer for is whichever block the ExEx first ran from. When the ExEx is active the `--rpc.eth-proof-window` flag has no effect. By default the ExEx retains 28 days of blocks; override that window with `RETH_PROOFS_HISTORY_WINDOW=`. The ExEx is fastest when it stays within 1024 blocks of the chain tip, so performance suffers while syncing up to head, and on an initial Base Mainnet sync it can fall behind far enough that it never catches up unaided. To prevent that, run `base-consensus` in follow mode so it tracks within 512 blocks of the proofs ExEx: ```bash BASE_NODE_SOURCE_L2_RPC= BASE_NODE_PROOFS=true ``` When the ExEx reports state-root and execution durations of 0, it is keeping up correctly — it is not re-executing blocks, only persisting data from already-executed ones. ## Stopping the Node Send `SIGINT` (Ctrl+C) or `SIGTERM` to gracefully shut down the node. It will flush pending state to disk before exiting. Avoid sending `SIGKILL` unless absolutely necessary, as it can leave the database in an inconsistent state requiring a repair on next startup. ## Next Steps - [Development Workflow](/getting-started/development/) -- learn the day-to-day check, test, and fix commands. - [Local Devnet](/getting-started/devnet/) -- spin up a full local network for integration testing. - [Configuration](/node-operations/configuration/) -- detailed configuration reference for production deployments. --- # Connecting to Base Source: https://basehub.org/integration-guides/connecting/ Description: Connect to Base mainnet or Sepolia using public endpoints, your own node, or a third-party provider. import CodeColumns from '../../../components/CodeColumns.astro'; Connect to Base mainnet or the Sepolia testnet using public RPC endpoints, your own node, or a third-party provider. ## Network parameters ### Base Mainnet | Parameter | Value | |-----------|-------| | Network name | Base | | Chain ID | `8453` (`0x2105`) | | Currency | ETH | | Block time | 2 seconds | | Flashblock interval | 200ms | | L1 settlement | Ethereum Mainnet | | Explorer | [basescan.org](https://basescan.org) | ### Base Sepolia (testnet) | Parameter | Value | |-----------|-------| | Network name | Base Sepolia | | Chain ID | `84532` (`0x14a34`) | | Currency | ETH (testnet) | | Block time | 2 seconds | | L1 settlement | Ethereum Sepolia | | Explorer | [sepolia.basescan.org](https://sepolia.basescan.org) | ## Public RPC endpoints Base provides public RPC endpoints for general use: | Network | HTTP Endpoint | WebSocket Endpoint | |---------|---------------|-------------------| | Mainnet | `https://mainnet.base.org` | `wss://mainnet.base.org` | | Sepolia | `https://sepolia.base.org` | `wss://sepolia.base.org` | :::note Public endpoints are rate-limited and intended for development and light usage. For production, run your own node or use a third-party provider. ::: ## Connecting with common tools ### curl
A raw JSON-RPC `POST` request returns the latest block number as a hex string. This is the lowest-common-denominator way to talk to Base — useful for sanity-checking that an endpoint is alive.
```bash curl -X POST https://mainnet.base.org \ -H "Content-Type: application/json" \ -d '{ "method": "eth_blockNumber", "params": [], "id": 1, "jsonrpc": "2.0" }' ```
### cast (Foundry)
Foundry's `cast` wraps every standard JSON-RPC method behind a clean CLI. Point it at any Base endpoint with `--rpc-url`.
```bash cast block-number --rpc-url https://mainnet.base.org cast balance 0xYourAddress --rpc-url https://mainnet.base.org cast chain-id --rpc-url https://mainnet.base.org ```
### ethers.js
`JsonRpcProvider` is the standard ethers entry point. It works against any Base endpoint — public, your own node, or a provider.
```javascript import { JsonRpcProvider } from "ethers"; const provider = new JsonRpcProvider("https://mainnet.base.org"); const blockNumber = await provider.getBlockNumber(); console.log("Current block:", blockNumber); const balance = await provider.getBalance("0xYourAddress"); console.log("Balance:", balance.toString(), "wei"); ```
### viem
viem ships chain definitions for both Base networks. `createPublicClient` selects the right RPC defaults automatically.
```typescript import { createPublicClient, http } from "viem"; import { base, baseSepolia } from "viem/chains"; const client = createPublicClient({ chain: base, transport: http(), }); const blockNumber = await client.getBlockNumber(); console.log("Current block:", blockNumber); const testnetClient = createPublicClient({ chain: baseSepolia, transport: http(), }); ```
### MetaMask / wallet configuration To add Base to a wallet manually: | Field | Mainnet | Sepolia | |-------|---------|---------| | Network Name | Base | Base Sepolia | | RPC URL | `https://mainnet.base.org` | `https://sepolia.base.org` | | Chain ID | `8453` | `84532` | | Currency Symbol | ETH | ETH | | Block Explorer URL | `https://basescan.org` | `https://sepolia.basescan.org` | ## Running your own node For production, running your own Base node gives full control over rate limits, latency, and data availability. ### Quick start The fastest path is the pre-built Docker image: ```bash docker pull ghcr.io/base/base-reth-node:latest docker run -d \ --name base-node \ -p 8545:8545 \ -p 8546:8546 \ -p 30303:30303 \ -v base-data:/data \ ghcr.io/base/base-reth-node:latest \ --http \ --http.addr 0.0.0.0 \ --http.port 8545 \ --http.api eth,net,web3,txpool \ --ws \ --ws.addr 0.0.0.0 \ --ws.port 8546 ``` For full instructions: - [Building from source](/getting-started/building/) — compile the node binary. - [Running the node](/getting-started/running/) — configuration and startup. - [Docker](/getting-started/docker/) — containerized deployment. - [Node deployment](/node-operations/deployment/) — production deployment guide. ### Node sync modes | Mode | Description | Disk usage | Sync time | |------|-------------|------------|-----------| | Full | Executes all transactions from genesis. | High | Days | | Snap | Downloads state snapshots, then follows chain tip. | Moderate | Hours | ## JSON-RPC interface Base nodes expose the standard Ethereum JSON-RPC interface. The following namespaces are available: ### Standard namespaces | Namespace | Description | |-----------|-------------| | `eth` | Core Ethereum methods — blocks, transactions, state queries, gas estimation. | | `net` | Network information — peer count, version, listening status. | | `web3` | Utility methods — client version, SHA3. | | `txpool` | Transaction pool inspection (see [Transaction Pool](/integration-guides/txpool/)). | | `debug` | Debug methods — trace transactions, get raw block data (enable with `--http.api debug`). | ### Base-specific namespaces | Namespace | Description | |-----------|-------------| | `base` | Base-specific methods including metering (see [Metering RPC](/integration-guides/metering-rpc/)). | ### Commonly used methods | Method | Description | |--------|-------------| | `eth_blockNumber` | Returns the latest block number. | | `eth_getBlockByNumber` | Returns block data by number or tag. | | `eth_getBlockByHash` | Returns block data by hash. | | `eth_getTransactionByHash` | Returns a transaction by hash. | | `eth_getTransactionReceipt` | Returns a transaction receipt. | | `eth_getBalance` | Returns account balance. | | `eth_getTransactionCount` | Returns account nonce. | | `eth_call` | Executes a call without creating a transaction. | | `eth_estimateGas` | Estimates gas for a transaction. | | `eth_sendRawTransaction` | Submits a signed transaction. | | `eth_getCode` | Returns contract bytecode. | | `eth_getStorageAt` | Returns storage value at a given position. | | `eth_getLogs` | Returns logs matching a filter. | | `eth_chainId` | Returns the chain ID. | ### Flashblocks support The `"pending"` block tag is supported across applicable methods when flashblocks are enabled. See [Flashblocks RPC](/integration-guides/flashblocks-rpc/) for details on querying preconfirmed state. ## Troubleshooting ### Common issues | Issue | Cause | Solution | |-------|-------|----------| | `429 Too Many Requests` | Rate limit exceeded on public endpoint. | Reduce request frequency or run your own node. | | Connection refused on port 8545 | Node HTTP RPC not enabled. | Start the node with `--http` and `--http.addr 0.0.0.0`. | | Empty response for `eth_getLogs` | Block range too large. | Reduce the `fromBlock`/`toBlock` range (max ~2000 blocks per query). | | Wrong chain ID | Connected to wrong network. | Verify the RPC URL matches the intended network. | | `nonce too low` | Transaction nonce already used. | Use `"pending"` tag with `eth_getTransactionCount` for the latest nonce. | ## Further reading - [Flashblocks RPC](/integration-guides/flashblocks-rpc/) — query preconfirmed state with the `"pending"` tag. - [Transaction Pool](/integration-guides/txpool/) — inspect and manage the txpool. - [Metering RPC](/integration-guides/metering-rpc/) — resource metering API. - [Node configuration](/node-operations/configuration/) — detailed node configuration options. - [Monitoring](/node-operations/monitoring/) — observability and metrics. --- # Flashblocks RPC Source: https://basehub.org/integration-guides/flashblocks-rpc/ Description: Read preconfirmed Base state and stream sub-block updates over JSON-RPC and WebSocket. import CodeColumns from '../../../components/CodeColumns.astro'; Flashblocks are sub-block preconfirmations produced every 200ms on Base. Read preconfirmed balances, nonces, receipts, and block data through standard JSON-RPC, or stream every flashblock as it lands. ## Overview A single Base block (produced every 2 seconds) is divided into ten **flashblocks** streamed at 200ms intervals. Each flashblock is a delta containing new transactions and the resulting state changes. The Flashblocks RPC exposes this preconfirmed state through the standard Ethereum JSON-RPC interface by supporting the `"pending"` block tag. ``` Block N (2s) ├── Flashblock 0 (0ms) ├── Flashblock 1 (200ms) ├── Flashblock 2 (400ms) ├── Flashblock 3 (600ms) ├── Flashblock 4 (800ms) ├── Flashblock 5 (1000ms) ├── Flashblock 6 (1200ms) ├── Flashblock 7 (1400ms) ├── Flashblock 8 (1600ms) └── Flashblock 9 (1800ms) Block N+1 (2s) └── ... ``` ## Querying preconfirmed state Pass `"pending"` to any supported JSON-RPC method to read the latest preconfirmed state. The same call against `"latest"` returns the last sealed block. ### Check an account balance
`eth_getBalance` with the `"pending"` tag returns the balance reflecting every transaction included in the most recent flashblock — usually within 200ms of the user's submission. The hex `result` is the balance in wei.
```bash curl -X POST https://your-base-rpc-endpoint \ -H "Content-Type: application/json" \ -d '{ "method": "eth_getBalance", "params": ["0xYourAddress", "pending"], "id": 1, "jsonrpc": "2.0" }' # { # "id": 1, # "jsonrpc": "2.0", # "result": "0x1bc16d674ec80000" # } ```
### Get a transaction receipt
After submitting a transaction, poll `eth_getTransactionReceipt` immediately — no need to wait for the next sealed block. A preconfirmed receipt is identifiable by its zero `blockHash`. Once the parent block seals, the same receipt comes back with a real block hash.
```bash curl -X POST https://your-base-rpc-endpoint \ -H "Content-Type: application/json" \ -d '{ "method": "eth_getTransactionReceipt", "params": ["0xYourTxHash"], "id": 1, "jsonrpc": "2.0" }' # { # "result": { # "transactionHash": "0x...", # "blockHash": "0x000...000", # "blockNumber": "0x123", # "status": "0x1", # "gasUsed": "0x5208", # "logs": [] # } # } ```
### Get the pending block
`eth_getBlockByNumber` with `"pending"` returns the in-progress block including every transaction landed in flashblocks so far. Pass `true` as the second parameter for full transaction objects, `false` for hashes only.
```bash curl -X POST https://your-base-rpc-endpoint \ -H "Content-Type: application/json" \ -d '{ "method": "eth_getBlockByNumber", "params": ["pending", true], "id": 1, "jsonrpc": "2.0" }' ```
### Get the pending nonce
`eth_getTransactionCount` against `"pending"` returns a nonce that already accounts for transactions sitting in flashblocks. This avoids nonce collisions when an account submits many transactions in rapid succession.
```bash curl -X POST https://your-base-rpc-endpoint \ -H "Content-Type: application/json" \ -d '{ "method": "eth_getTransactionCount", "params": ["0xYourAddress", "pending"], "id": 1, "jsonrpc": "2.0" }' # { # "result": "0x5" # } ```
## Using ethers.js or viem ### ethers.js
Pass `"pending"` as the block tag to any read method to get preconfirmed values. For receipts, poll `getTransactionReceipt` on a 200ms interval — that matches the flashblock cadence.
```javascript import { JsonRpcProvider } from "ethers"; const provider = new JsonRpcProvider("https://your-base-rpc-endpoint"); const balance = await provider.getBalance("0xYourAddress", "pending"); const nonce = await provider.getTransactionCount("0xYourAddress", "pending"); async function waitForFlashblockReceipt(txHash, intervalMs = 200) { while (true) { const receipt = await provider.getTransactionReceipt(txHash); if (receipt) return receipt; await new Promise((r) => setTimeout(r, intervalMs)); } } ```
### viem
viem accepts `blockTag: "pending"` on every read method. Receipts return as soon as the transaction is included in a flashblock.
```typescript import { createPublicClient, http } from "viem"; import { base } from "viem/chains"; const client = createPublicClient({ chain: base, transport: http("https://your-base-rpc-endpoint"), }); const balance = await client.getBalance({ address: "0xYourAddress", blockTag: "pending", }); const nonce = await client.getTransactionCount({ address: "0xYourAddress", blockTag: "pending", }); const receipt = await client.getTransactionReceipt({ hash: "0xYourTxHash", }); ```
## WebSocket streaming For real-time updates, connect to the WebSocket endpoint and subscribe to `flashblocks_v1`. Each flashblock arrives as it is produced — five times per second. ### Subscribing
Open a WebSocket, send an `eth_subscribe` request with the `flashblocks_v1` topic, and handle each `eth_subscription` message as it arrives. `flashblock.diff` carries the new transactions and state changes since the previous flashblock; `flashblock.index` runs from 0 to 9 within each parent block.
```javascript import WebSocket from "ws"; const ws = new WebSocket("wss://your-base-ws-endpoint"); ws.on("open", () => { ws.send( JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_subscribe", params: ["flashblocks_v1"], }) ); }); ws.on("message", (data) => { const msg = JSON.parse(data); if (msg.method === "eth_subscription") { const flashblock = msg.params.result; console.log("Index:", flashblock.index); console.log("Txs:", flashblock.diff.transactions?.length ?? 0); } }); ```
### Flashblock payload structure Each streamed flashblock contains: | Field | Description | |-------|-------------| | `payload_id` | Identifier for the parent payload (block being built). | | `index` | Flashblock index within the current block (0, 1, 2, ...). | | `base` | Present only on index 0. Contains the base execution payload. | | `diff` | Delta of transactions, receipts, and state changes since the previous flashblock. | | `metadata` | Additional metadata, including the flashblock access list if enabled. | ## Error handling and fallback ### Automatic fallback When the `"pending"` tag is used but no preconfirmed data is available — for example, during sequencer downtime or between blocks — the RPC falls back to the `"latest"` confirmed state. That means: - Applications do not need special error handling for missing flashblock state. - Results are always consistent: either preconfirmed or latest confirmed. - The caller cannot distinguish a "pending equals latest" response from a fallback, so applications should not assume the data is fresher than the latest block. ### Error responses Standard JSON-RPC error codes apply: | Scenario | Behavior | |----------|----------| | Invalid method parameters | Standard JSON-RPC error response with error code and message. | | Transaction not found | `null` result (not an error). | | Block not found | `null` result (not an error). | | Flashblocks disabled | Falls back to standard RPC behavior. No error is returned. | ### Detecting preconfirmed vs. finalized
A preconfirmed receipt can be identified by its `blockHash`: - **Preconfirmed** — `blockHash` is the zero hash. - **Finalized** — `blockHash` is a real block hash.
```javascript function isPreconfirmed(receipt) { return ( receipt.blockHash === "0x0000000000000000000000000000000000000000000000000000000000000000" ); } ```
## Further reading - [Flashblocks specification](/specifications/flashblocks/) — full RPC method reference with parameter and return type details. - [Access lists specification](/specifications/access-lists/) — the FAL spec that accompanies flashblocks. - [Flashblocks pipeline](/architecture/flashblocks-pipeline/) — how flashblocks are produced and streamed internally. --- # Metering RPC Source: https://basehub.org/integration-guides/metering-rpc/ Description: Simulate transactions to return gas estimates, resource breakdowns, and builder metering metadata. The Metering API simulates a transaction against current state and returns gas estimates, per-resource consumption, and the metering metadata the builder uses to price and prioritize inclusion. ## What Metering Provides Traditional gas estimation (`eth_estimateGas`) returns a single number. The Metering API goes further by simulating transactions against the current state and reporting: - **Gas consumption** -- total gas used, broken down by execution phase. - **Resource usage** -- compute, storage reads, storage writes, and memory allocation. - **Metering metadata** -- information used by the block builder to price and prioritize transactions. This is particularly useful for: - Block builders that need to meter transactions for optimal block packing. - Applications that want to display detailed cost breakdowns to users. - MEV searchers estimating transaction costs against live state. ## Relevant Crates The metering system is implemented across two crates in the `base/base` workspace: | Crate | Description | |-------|-------------| | `base-metering` | Core metering logic -- simulates transactions and computes resource consumption metrics. | | `base-builder-metering` | Builder-specific metering integration -- connects metering to the block building pipeline. | These crates live under [`crates/builder/`](https://github.com/base/base/tree/main/crates/builder) in the repository. ## RPC Endpoints ### `base_estimateMeteredGas` Simulates a transaction and returns a detailed metering report. #### Parameters | Name | Type | Description | |------|------|-------------| | `transaction` | `Object` | Transaction call object (`from`, `to`, `data`, `value`, `gas`, etc.). | | `blockNumber` | `String` | Block tag or number. Use `"latest"` or `"pending"` for current state. | #### Returns `Object` -- Metering result containing gas estimates and resource breakdown. #### Example ```bash curl -X POST https://your-base-rpc-endpoint \ -H "Content-Type: application/json" \ -d '{ "method": "base_estimateMeteredGas", "params": [ { "from": "0xSenderAddress", "to": "0xContractAddress", "data": "0xCalldata" }, "latest" ], "id": 1, "jsonrpc": "2.0" }' ``` ```json { "id": 1, "jsonrpc": "2.0", "result": { "gasUsed": "0x1a4c0", "gasLimit": "0x1e848", "computeUnits": "0x3200", "storageReads": "0x5", "storageWrites": "0x2", "success": true } } ``` ### `base_getMeteringConfig` Returns the current metering configuration parameters used by the builder. #### Parameters None. #### Returns `Object` -- Metering configuration including resource weights and limits. #### Example ```bash curl -X POST https://your-base-rpc-endpoint \ -H "Content-Type: application/json" \ -d '{ "method": "base_getMeteringConfig", "params": [], "id": 1, "jsonrpc": "2.0" }' ``` ```json { "id": 1, "jsonrpc": "2.0", "result": { "computeWeight": 1, "storageReadWeight": 2, "storageWriteWeight": 5, "maxComputeUnitsPerBlock": "0x989680", "maxStorageReadsPerBlock": "0x493e0", "maxStorageWritesPerBlock": "0x186a0" } } ``` ## Usage with ethers.js ```javascript import { JsonRpcProvider } from "ethers"; const provider = new JsonRpcProvider("https://your-base-rpc-endpoint"); // Estimate metered gas for a transaction async function estimateMeteredGas(tx) { const result = await provider.send("base_estimateMeteredGas", [ { from: tx.from, to: tx.to, data: tx.data, value: tx.value ?? "0x0", }, "latest", ]); console.log("Gas used:", parseInt(result.gasUsed, 16)); console.log("Compute units:", parseInt(result.computeUnits, 16)); console.log("Storage reads:", parseInt(result.storageReads, 16)); console.log("Storage writes:", parseInt(result.storageWrites, 16)); console.log("Success:", result.success); return result; } ``` ## Usage with viem ```typescript import { createPublicClient, http } from "viem"; import { base } from "viem/chains"; const client = createPublicClient({ chain: base, transport: http("https://your-base-rpc-endpoint"), }); // Estimate metered gas const result = await client.request({ method: "base_estimateMeteredGas" as any, params: [ { from: "0xSenderAddress", to: "0xContractAddress", data: "0xCalldata", }, "latest", ], }); ``` ## How Metering Works The metering pipeline operates as follows: 1. **Transaction submission** -- a transaction (or call object) is submitted to the metering RPC. 2. **State simulation** -- the transaction is executed against the current (or specified) state using the EVM, with metering instrumentation enabled. 3. **Resource accounting** -- during simulation, the metering engine tracks compute cycles, storage operations, and memory usage. 4. **Result assembly** -- the raw resource counts are combined with the current metering configuration (weights) to produce the final metering report. 5. **Builder integration** -- when used by the block builder, metering results feed into transaction ordering and block packing decisions. ``` Transaction │ ▼ ┌──────────────────┐ │ EVM Simulation │ │ (base-metering) │ └────────┬─────────┘ │ ▼ ┌──────────────────────────┐ │ Resource Accounting │ │ compute, storage, memory │ └────────┬─────────────────┘ │ ▼ ┌──────────────────────────────┐ │ Metering Report │ │ gas, compute units, storage │ └──────────────────────────────┘ ``` ## Error Handling | Scenario | Behavior | |----------|----------| | Transaction reverts during simulation | `success: false` in the result; gas and resource fields still populated. | | Invalid transaction parameters | Standard JSON-RPC error response. | | State unavailable for requested block | JSON-RPC error with appropriate code. | ## Further Reading - [Crate Reference](/crates/overview/) -- overview of all crate groups, including the builder crates. - [Execution Pipeline](/architecture/execution-pipeline/) -- how the EVM and state management work. --- # Transaction Pool Source: https://basehub.org/integration-guides/txpool/ Description: Inspect and monitor the Base txpool through RPC extensions and tracing instrumentation. The Base transaction pool holds pending transactions until the builder includes them in a block. The `base/base` repository ships RPC extensions and tracing instrumentation for inspecting pool contents and monitoring transaction lifecycle events. ## Relevant Crates | Crate | Description | |-------|-------------| | `base-txpool-rpc` | Txpool RPC namespace extensions -- provides JSON-RPC methods for querying pool contents. | | `base-txpool-tracing` | Tracing instrumentation for txpool operations -- emits structured logs and metrics for transaction lifecycle events. | These crates live under [`crates/execution/`](https://github.com/base/base/tree/main/crates/execution) in the repository. ## How Transactions Flow Through the Pool ``` User / Application │ ▼ ┌─────────────────┐ │ JSON-RPC │ eth_sendRawTransaction │ (ingress-rpc) │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Validation │ Nonce check, balance check, gas price check │ │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Transaction │ Pending pool (ready to include) │ Pool │ Queued pool (future nonce gap) └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Block Builder │ Selects transactions for next block/flashblock │ │ └─────────────────┘ ``` 1. **Ingress** -- transactions arrive via `eth_sendRawTransaction` through the ingress RPC. 2. **Validation** -- each transaction is validated: signature verification, nonce ordering, sufficient balance for gas and value, gas price meets the minimum. 3. **Pool insertion** -- valid transactions enter the pool. Transactions with the next expected nonce go into the **pending** sub-pool (ready for inclusion). Transactions with a future nonce go into the **queued** sub-pool (waiting for the gap to be filled). 4. **Promotion** -- when a pending transaction is included in a block or flashblock, queued transactions from the same sender may be promoted to pending if the nonce gap is closed. 5. **Selection** -- the block builder pulls transactions from the pending pool, ordered by effective gas price, for inclusion in the next flashblock delta. ## RPC Methods The `base-txpool-rpc` crate extends the standard txpool namespace with the following methods. ### `txpool_status` Returns the number of transactions in each sub-pool. ```bash curl -X POST https://your-base-rpc-endpoint \ -H "Content-Type: application/json" \ -d '{ "method": "txpool_status", "params": [], "id": 1, "jsonrpc": "2.0" }' ``` ```json { "id": 1, "jsonrpc": "2.0", "result": { "pending": "0x12", "queued": "0x3" } } ``` ### `txpool_content` Returns all transactions in the pool, grouped by sender address and nonce. ```bash curl -X POST https://your-base-rpc-endpoint \ -H "Content-Type: application/json" \ -d '{ "method": "txpool_content", "params": [], "id": 1, "jsonrpc": "2.0" }' ``` ```json { "id": 1, "jsonrpc": "2.0", "result": { "pending": { "0xSenderAddress": { "0": { "hash": "0x...", "gasPrice": "0x...", "nonce": "0x0", "..." : "..." }, "1": { "hash": "0x...", "gasPrice": "0x...", "nonce": "0x1", "..." : "..." } } }, "queued": { "0xAnotherSender": { "5": { "hash": "0x...", "gasPrice": "0x...", "nonce": "0x5", "..." : "..." } } } } } ``` ### `txpool_inspect` Returns a text summary of all transactions in the pool (lighter-weight than `txpool_content`). ```bash curl -X POST https://your-base-rpc-endpoint \ -H "Content-Type: application/json" \ -d '{ "method": "txpool_inspect", "params": [], "id": 1, "jsonrpc": "2.0" }' ``` ```json { "id": 1, "jsonrpc": "2.0", "result": { "pending": { "0xSenderAddress": { "0": "0xRecipient: 0 wei + 21000 gas x 1000000000 wei" } }, "queued": {} } } ``` ### `txpool_contentFrom` Returns transactions from a specific sender address. ```bash curl -X POST https://your-base-rpc-endpoint \ -H "Content-Type: application/json" \ -d '{ "method": "txpool_contentFrom", "params": ["0xSenderAddress"], "id": 1, "jsonrpc": "2.0" }' ``` ```json { "id": 1, "jsonrpc": "2.0", "result": { "pending": { "0": { "hash": "0x...", "nonce": "0x0", "..." : "..." } }, "queued": {} } } ``` ## Tracing and Observability The `base-txpool-tracing` crate instruments the transaction pool with structured tracing events. When enabled, it emits events for: | Event | Description | |-------|-------------| | `txpool.insert` | A transaction was inserted into the pool. Includes sender, nonce, hash, gas price. | | `txpool.promote` | A queued transaction was promoted to pending (nonce gap filled). | | `txpool.discard` | A transaction was removed from the pool (replaced, expired, or invalidated). | | `txpool.replace` | A transaction was replaced by a new one with the same nonce and higher gas price. | | `txpool.evict` | A transaction was evicted due to pool size limits. | These events can be consumed by any `tracing` subscriber -- written to stdout, sent to a logging backend, or exported as OpenTelemetry spans. ### Enabling Tracing Tracing is configured via the node's logging configuration. For example, to enable debug-level txpool tracing: ```bash RUST_LOG=base_txpool_tracing=debug base-reth-node \ --http \ --http.api eth,txpool ``` ## Practical Tips - **Nonce management** -- use `eth_getTransactionCount` with the `"pending"` tag (see [Flashblocks RPC](/integration-guides/flashblocks-rpc/)) to get the latest nonce including flashblock-preconfirmed transactions. This avoids nonce collisions when submitting transactions rapidly. - **Gas price monitoring** -- query `txpool_status` periodically to gauge pool congestion. A high pending count relative to block throughput suggests rising base fees. - **Transaction replacement** -- to speed up or cancel a pending transaction, submit a new transaction with the same nonce and a higher gas price (typically at least 10% higher to be accepted by the pool). - **Pool limits** -- the pool has configurable size limits. When full, the lowest-priced transactions are evicted first. Monitor `txpool.evict` tracing events to detect when your transactions might be at risk. ## Further Reading - [Crate Reference](/crates/overview/) -- overview of all crate groups, including the execution crates. - [Flashblocks RPC](/integration-guides/flashblocks-rpc/) -- using the `"pending"` tag for preconfirmed nonces and balances. - [Connecting to Base](/integration-guides/connecting/) -- network endpoints and chain parameters. --- # Base-Solana Bridge Source: https://basehub.org/introduction/base-solana-bridge/ Description: Bridge tokens and arbitrary messages between Base and Solana with the official Base-Solana bridge contracts and programs. import { Aside, Card, CardGrid } from '@astrojs/starlight/components'; The Base-Solana bridge moves tokens and arbitrary messages in both directions between Base and Solana, with optional contract-call execution at the destination. The bridge supports: - **Token transfers** between Base and Solana - **Arbitrary cross-chain messages** - **Combined flows** that pair a transfer with a destination contract call - **Wrapped token deployments** on either chain This page covers the bridge architecture, production contract addresses, and reference implementation patterns. ## How it works ### On Base The Base bridge contract locks or burns outgoing tokens and mints or unlocks incoming tokens. It also assembles outgoing messages into Merkle trees. Validators sign the Merkle root every ~300 finalized blocks and relay it to Solana, where users prove their message exists in the tree to complete the transfer. **Key smart contracts:** - [**Bridge Contract**](https://github.com/base/bridge/blob/main/base/src/Bridge.sol) — handles outgoing transfers - [**CrossChainERC20**](https://github.com/base/bridge/blob/main/base/src/CrossChainERC20.sol) — mintable/burnable tokens for cross-chain transfers - [**BridgeValidator**](https://github.com/base/bridge/blob/main/base/src/BridgeValidator.sol) — verifies messages with oracle signatures - [**Twin Contract**](https://github.com/base/bridge/blob/main/base/src/Twin.sol) — your personal smart contract on Base for executing calls forwarded from Solana ### On Solana The Solana bridge program manages token transfers by locking or burning the asset and emitting events. For messages, validators forward those events to Base, where execution runs through your Twin contract. **Key programs (Solana Mainnet-Beta):** - [**Bridge Program**](https://github.com/base/bridge/blob/main/solana/programs/bridge) — handles outgoing transfers and message commitments - [**Base Relayer Program**](https://github.com/base/bridge/blob/main/solana/programs/base_relayer) — optional relayer that can prepay gas on Base The full repository lives at [github.com/base/bridge](https://github.com/base/bridge). ## Bridging flows Push-based flow with an optional relayer for instant execution on Base. See [Solana to Base](#solana-to-base). Proof-based burn-and-unlock flow with full custody. See [Base to Solana](#base-to-solana). Production terminal UI for bridging plus contract calls. See [Terminally Onchain example](#terminally-onchain-example). ## Solana to Base **Flow:** Lock SOL/SPL → (optional) pay for relay → validators approve → mint and execute on Base. The Solana to Base direction uses a pull-based model with three steps: 1. **Initiate the bridge on Solana.** Lock your SOL or native SPL token in a Solana vault. 2. **Wait for validator pre-approval.** Validators verify and approve the bridge message. 3. **Execute on Base.** The approved message runs on Base to mint SOL and execute any attached calls. Reference scripts for auto-relay, token wrapping, and CLI utilities live in the `scripts/` directory of the official repository: [Solana to Base CLI scripts](https://github.com/base/bridge/tree/main/scripts/src/commands/sol/bridge). ### Auto-relay example This sample bridges SOL with auto-relay enabled. ```typescript solToBaseWithAutoRelay/index.ts expandable // Configure const TO = "0x8c1a617bdb47342f9c17ac8750e0b070c372c721"; // Base address const AMOUNT = 0.001; // SOL amount // Bridge SOL with auto-relay const ixs = [ getBridgeSolInstruction({ payer, from: payer, solVault: solVaultAddress, bridge: bridgeAccountAddress, outgoingMessage, to: toBytes(TO), remoteToken: toBytes("0xC5b9112382f3c87AFE8e1A28fa52452aF81085AD"), // SOL on Base amount: BigInt(AMOUNT * 10**9), }), await buildPayForRelayIx(RELAYER_PROGRAM_ID, outgoingMessage, payer) ]; await buildAndSendTransaction(SOLANA_RPC_URL, ixs, payer); ``` Full handler: [Solana to Base relay script](https://github.com/base/bridge/blob/main/scripts/src/commands/sol/bridge/solana-to-base/bridge-sol.handler.ts). ### Wrap custom SPL tokens The example above bridges native SOL. To bridge custom SPL tokens, deploy wrapped ERC20 representations on Base via `CrossChainERC20Factory`. Reference: [token wrapping example](https://github.com/base/bridge/blob/main/scripts/src/commands/sol/bridge/solana-to-base/wrap-token.handler.ts). ```typescript wrapSolTokenOnBase/index.ts expandable // Deploy wrapped token on Base const mintBytes32 = getBase58Codec().encode(SOLANA_SPL_MINT_ADDRESS).toHex(); await client.writeContract({ address: "0x58207331CBF8Af87BB6453b610E6579D9878e4EA", // Factory abi: TokenFactory, functionName: "deploy", args: [`0x${mintBytes32}`, "Token Name", "SYMBOL", 9], }); ``` ## Base to Solana **Flow:** Burn ERC20 SOL on Base → wait for finalization → generate Merkle proof → execute on Solana. The return path burns the wrapped token on Base, waits for the message to become provable, then submits the proof on Solana to release the native asset. This route preserves full custody and requires a prover. Reference: [Base to Solana example](https://github.com/base/bridge/blob/main/scripts/src/internal/sol/base.ts). ```typescript bridgeSolFromBaseToSolana/index.ts expandable // Step 1: Burn SOL on Base const transfer = { localToken: "0xC5b9112382f3c87AFE8e1A28fa52452aF81085AD", // SOL (on Base) remoteToken: pubkeyToBytes32(SOL_ADDRESS), to: pubkeyToBytes32(solanaAddress), remoteAmount: BigInt(AMOUNT * 10**9), }; const txHash = await client.writeContract({ address: "0xB2068ECCDb908902C76E3f965c1712a9cF64171E", // Bridge abi: Bridge, functionName: "bridgeToken", args: [transfer, []], }); // Step 2: Wait for finalization const isProvable = await isBridgeMessageProvable(txHash); // Step 3: Generate proof const { event, rawProof } = await generateProof(txHash, baseBlockNumber); // Step 4: Execute on Solana const proveIx = getProveMessageInstruction({ nonce: event.message.nonce, sender: toBytes(event.message.sender), data: toBytes(event.message.data), proof: rawProof.map(e => toBytes(e)), messageHash: toBytes(event.messageHash), }); const relayIx = getRelayMessageInstruction({ message: messagePda }); await buildAndSendTransaction(SOLANA_RPC_URL, [proveIx, relayIx], payer); ``` ## Utilities The repository ships utilities for converting between Solana and Base address formats, loading the Solana CLI keypair for signing, and building/sending Solana transactions: [scripts/src/commands](https://github.com/base/bridge/tree/main/scripts/src/commands). ### Address conversion Convert a Solana pubkey to bytes32 for Base contracts: ```typescript example.ts // Convert Solana pubkey to bytes32 for Base contracts import { pubkeyToBytes32 } from "./utils/pubkeyToBytes32"; const bytes32Address = pubkeyToBytes32(solanaAddress); ``` ### Keypair management Load the Solana CLI keypair for signing: ```typescript example.ts import { getSolanaCliConfigKeypairSigner } from "./utils/keypair"; const payer = await getSolanaCliConfigKeypairSigner(); ``` ### Transaction building Build and submit a Solana transaction: ```typescript example.ts import { buildAndSendTransaction } from "./utils/buildAndSendTransaction"; const signature = await buildAndSendTransaction(SOLANA_RPC_URL, ixs, payer); ``` ## Terminally Onchain example Repository: [github.com/base/sol2base](https://github.com/base/sol2base). [Terminally Onchain](https://terminallyonchain.com/) is a production Next.js app that exposes the bridge through a command terminal UI. Users connect a Solana wallet and run commands that bridge value and call a contract on Base: ```bash bridge 0.0001 sol 0xYourTwin --call-contract 0x311935Cd80B76769bF2ecC9D8Ab7635b2139cf82 \ --call-selector "transfer(address,uint256)" \ --call-args 0x0000000000000000000000000000000000000000 100000000000000 ``` The workflow: 1. **Parse command.** The terminal parser resolves the asset, destination, and optional Base call (selector, args, value). 2. **Stage bridge.** `queueBridge` validates SPL overrides, ABI-encodes the Base call via `encodeFunctionData`, and stages relay overrides. 3. **Execute.** `solanaBridge.bridge()` resolves the destination (ENS or Basename), confirms balances, and calls `realBridgeImplementation` to sign and submit the Solana transaction. 4. **Relay and call.** When relay gas is prepaid, the Base Relayer immediately runs the attached call from the user's Twin contract once ERC20 SOL is minted. Key implementation references: - `src/lib/bridge.ts` — asset resolution (including mint addresses), environment-aware RPC connections, and call attachment - `src/lib/realBridgeImplementation.ts` — builds Solana transactions with `PayForRelay` plus `bridge_sol`/`bridge_spl` instructions, using per-environment PDAs and gas-fee receivers - `src/components/MainContent.tsx` — terminal UI with command staging, log viewer, and ABI encoding for arbitrary Base calls - `src/components/WalletConnection.tsx` — fetches the deterministic Twin address on Base Mainnet/Sepolia for the connected Solana wallet ### Run the terminal locally ```bash Terminal git clone https://github.com/base/sol2base.git cd sol2base npm install --legacy-peer-deps # Configure env (RPC URLs, relayer addresses, CDP API keys, etc.) cp env.template .env.local npm run dev # defaults to http://localhost:3000 ``` ## Contract addresses ### Base Mainnet ```json { "Bridge": "0x3eff766C76a1be2Ce1aCF2B69c78bCae257D5188", "BridgeValidator": "0xAF24c1c24Ff3BF1e6D882518120fC25442d6794B", "CrossChainERC20Factory": "0xDD56781d0509650f8C2981231B6C917f2d5d7dF2", "SOL": "0x311935Cd80B76769bF2ecC9D8Ab7635b2139cf82" } ``` ### Solana Mainnet ```json { "BridgeProgram": "HNCne2FkVaNghhjKXapxJzPaBvAKDG1Ge3gqhZyfVWLM", "BaseRelayerProgram": "g1et5VenhfJHJwsdJsDbxWZuotD5H4iELNG61kS4fb9" } ``` ### Base Sepolia ```json { "Bridge": "0x01824a90d32A69022DdAEcC6C5C14Ed08dB4EB9B", "BridgeValidator": "0xa80C07DF38fB1A5b3E6a4f4FAAB71E7a056a4EC7", "CrossChainERC20Factory": "0x488EB7F7cb2568e31595D48cb26F63963Cc7565D", "SOL": "0xCace0c896714DaF7098FFD8CC54aFCFe0338b4BC", "FLYWHEEL_ADDRESS": "0x00000F14AD09382841DB481403D1775ADeE1179F", "BRIDGE_CAMPAIGN_ADDRESS": "0xE2AD1C34382410C30d826B019A0B3700F5c4e6c9" } ``` ### Solana Devnet ```json { "BridgeProgram": "7c6mteAcTXaQ1MFBCrnuzoZVTTAEfZwa6wgy4bqX3KXC", "BaseRelayerProgram": "56MBBEYAtQAdjT4e1NzHD8XaoyRSTvfgbSVVcEcHj51H", "GasFeeReceiver": "AFs1LCbodhvwpgX3u3URLsud6R1XMSaMiQ5LtXw4GKYT" } ``` ## Resources Source code, contracts, programs, and scripts: [github.com/base/bridge](https://github.com/base/bridge). Inspect Solana mainnet-beta transactions at [explorer.solana.com](https://explorer.solana.com/). Inspect Base Mainnet transactions at [basescan.org](https://basescan.org/). Get help from the Base community at [base.org/discord](https://base.org/discord). --- # Connecting to Base Source: https://basehub.org/introduction/connecting-to-base/ Description: Network parameters, RPC endpoints, and wallet setup for connecting to Base Mainnet and Base Sepolia testnet. import { Aside } from '@astrojs/starlight/components'; Connect to Base Mainnet or the Sepolia testnet using the public RPC endpoints, then configure any EVM-compatible wallet to use the network. ## Base Mainnet | Name | Value | | :--- | :--- | | Network Name | Base Mainnet | | Description | The public mainnet for Base. | | RPC Endpoint | [https://mainnet.base.org](https://mainnet.base.org) — Flashblocks-enabled; rate limited, not for production. | | Chain ID | 8453 | | Currency Symbol | ETH | | Block Explorer | [https://base.blockscout.com/](https://base.blockscout.com/) | ## Base Testnet (Sepolia) | Name | Value | | :--- | :--- | | Network Name | Base Sepolia | | Description | The public testnet for Base. | | RPC Endpoint | [https://sepolia.base.org](https://sepolia.base.org) — Flashblocks-enabled; rate limited, not for production. | | Chain ID | 84532 | | Currency Symbol | ETH | | Block Explorer | [https://sepolia-explorer.base.org](https://sepolia-explorer.base.org) | ## Base Vibenet (preview) Vibenet is Base's experimental preview network. Chain-level features land here first, ahead of any rollout to Sepolia or Mainnet, so it is the place to try upcoming capabilities before they stabilize. At the moment it carries [B20 tokens](/specifications/b20/) — an ERC-20 superset that adds roles, supply caps, pausing, and policy gating, with `permit` provided as a native precompile. Treat Vibenet as a sandbox for cutting-edge work only; it is not meant for production deployments or anything user-facing. See [vibes.base.org](https://vibes.base.org) for more. | Name | Value | | :--- | :--- | | Network Name | Base Vibenet | | Description | Base's experimental preview network. | | RPC Endpoint | [https://rpc.vibes.base.org](https://rpc.vibes.base.org) | | Chain ID | 84538453 | | Currency Symbol | ETH | | Faucet | [https://faucet.vibes.base.org](https://faucet.vibes.base.org) | | Block Explorer | [https://explorer.vibes.base.org](https://explorer.vibes.base.org) | ## Using Base with your wallet ### Coinbase Wallet The [Coinbase Wallet](https://chrome.google.com/webstore/detail/coinbase-wallet-extension/hnfanknocfeofbddgcijnmhnfnkdnaad?hl=en) browser extension supports Base out of the box. To switch to Base in Coinbase Wallet: 1. Open the Coinbase Wallet extension and sign in. 2. Connect to an app from within the wallet. 3. Click the network icon in the top right to open the network menu. 4. Choose **Base**. The active network now points at Base. ### Other wallets Base works as a custom network in any EVM-compatible wallet, including [MetaMask](https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn). #### MetaMask To add Base as a custom network in MetaMask: 1. Open the MetaMask extension. 2. Click the network dropdown at the top of the extension. 3. Click **Add network**. 4. Click **Add a network manually**. 5. Enter the Base mainnet values: | Name | Value | | :--- | :--- | | Network Name | Base Mainnet | | Description | The public mainnet for Base. | | RPC Endpoint | [https://mainnet.base.org](https://mainnet.base.org) | | Chain ID | 8453 | | Currency Symbol | ETH | | Block Explorer | [https://base.blockscout.com/](https://base.blockscout.com/) | 6. Click **Save**. Base now appears in the network dropdown. ## Testnet #### Coinbase Wallet supports Base Sepolia by default To use Base Sepolia in Coinbase Wallet: 1. Open the Coinbase Wallet extension and sign in. 2. Connect to an app. 3. Click the network icon at the top right. 4. Click **More networks**. 5. Open the **Testnets** tab. 6. Choose **Base Sepolia**. The active network is now the Base testnet. #### Other wallets Base Sepolia can also be added as a custom network in any EVM-compatible wallet such as [MetaMask](https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn). #### MetaMask To add Base Sepolia in MetaMask: 1. Open the MetaMask extension. 2. Click the network dropdown. 3. Click **Add network**. 4. Click **Add a network manually**. 5. Enter the Base Sepolia values: | Name | Value | | :--- | :--- | | Network Name | Base Sepolia | | RPC Endpoint | [https://sepolia.base.org](https://sepolia.base.org) | | Chain ID | 84532 | | Currency Symbol | ETH | | Block Explorer | [https://sepolia-explorer.base.org](https://sepolia-explorer.base.org) | 6. Click **Save**. Base Sepolia is now selectable from the network dropdown. --- # Deploy on Base Source: https://basehub.org/introduction/deploy-on-base/ Description: Set up Foundry, configure RPC and key management, and deploy a smart contract to Base Sepolia in a single workflow. import { Aside } from '@astrojs/starlight/components'; Set up a Foundry project, point it at Base, and deploy a contract to Base Sepolia in a single end-to-end workflow. ## What you build By the end of this guide you will have: - A Foundry-based development environment configured for Base - A smart contract deployed to Base Sepolia - A connection pattern your frontend can reuse against your contracts ## Set up your development environment 1. Create a project directory. ```bash mkdir my-base-project && cd my-base-project ``` 2. Install Foundry, the framework used throughout this guide. ```bash curl -L https://foundry.paradigm.xyz | bash foundryup ``` The installer pulls Foundry and updates it to the latest release. 3. Initialize a Solidity project. ```bash forge init ``` The Foundry project is ready. The starter contract sits at `src/Counter.sol` — this guide uses that contract as the deployment target. ## Configure Foundry for Base Deploying to Base requires two pieces: 1. An RPC connection to the network 2. A funded private key for signing the deployment transaction ### 1. Set up your RPC connection 1. Create a `.env` file at the project root. 2. Add the Base RPC URLs. ```bash BASE_RPC_URL="https://mainnet.base.org" BASE_SEPOLIA_RPC_URL="https://sepolia.base.org" ``` 3. Load the variables into your shell. ```bash source .env ``` ### 2. Secure your private key 1. Import your private key into Foundry's encrypted keystore. ```bash cast wallet import deployer --interactive ``` 2. Paste the key when prompted and set a password. The encrypted key is stored at `~/.foundry/keystores`, which is excluded from git. ## Deploy your contract With the environment configured, deploy to Base Sepolia. 1. (Optional) Run a dry run first to confirm the deployment is well-formed. ```bash forge create ./src/Counter.sol:Counter --rpc-url $BASE_SEPOLIA_RPC_URL --account deployer ``` This simulates the deploy without broadcasting. You will see the transaction details and ABI; nothing is written onchain. 2. Add `--broadcast` to actually deploy. ```bash forge create ./src/Counter.sol:Counter --rpc-url $BASE_SEPOLIA_RPC_URL --account deployer --broadcast ``` The contract argument follows the format `:`. 3. A successful deploy prints output similar to: ``` Deployer: 0x... Deployed to: 0x... <-- YOUR CONTRACT ADDRESS Transaction hash: 0x... ``` 4. Save the deployed address into `.env`. ```bash COUNTER_CONTRACT_ADDRESS="0x..." ``` Replace `0x...` with the address from the output. 5. Reload the variables. ```bash source .env ``` ### Verify your deployment To confirm the contract is live: 1. Look up the transaction hash on [Sepolia Basescan](https://sepolia.basescan.org/). 2. Read the contract directly with `cast`: ```bash cast call $COUNTER_CONTRACT_ADDRESS "number()(uint256)" --rpc-url $BASE_SEPOLIA_RPC_URL ``` The call returns the initial value of the Counter's `number` storage slot, which is `0`. The contract is now live on Base Sepolia. ## Next steps - Wire a frontend to your contracts using [wagmi](https://wagmi.sh) or [viem](https://viem.sh). - Dive deeper into Foundry workflows in the [Foundry deployment tutorial](https://docs.base.org/learn/foundry/deploy-with-foundry). --- # Why Base Source: https://basehub.org/introduction/why-base/ Description: Base delivers sub-cent, sub-second transactions on a global, permissionless platform with developer tooling, grants, and built-in distribution. Base is an Ethereum L2 designed for sub-second, sub-cent transactions on a permissionless platform with first-class developer tooling and built-in distribution. ## At a glance - **Open and inexpensive.** Base is a globally accessible network offering 1-second confirmations and transactions that cost less than a cent. - **First-class tooling.** Builders ship onchain experiences across AI, social, media, and entertainment with a mature developer stack. - **Funding for builders.** Base has issued grants to over 1,000 builders, with more on the way. - **Distribution channels.** Apps reach users through official Base distribution surfaces. ## A platform for new applications Base is a global onchain economy of people, builders, and businesses building the next iteration of the open internet. The chain is engineered so developers can: - Concentrate on real user problems - Ship apps that scale globally by default - Onboard new users in a single click - Take payments from anyone, anywhere It is fast, inexpensive, and permissionless — anyone can build on or use applications running on Base. The network has emerged as a hub for new use cases across media, entertainment, social, and AI. [Clanker](https://www.clanker.world), an autonomous AI agent operating on Base, generated more than $13 million in revenue in its first three months. Base is structured so developers retain more of the upside their products create. Conventional app distribution often forces developers to give up 30% of revenue to marketplaces. Those marketplaces can also drive practices that work against users — selling personal data, demanding identity disclosures, or denying access at will. A user in Nairobi, for example, may be locked out of many U.S.-based fintech services entirely. Onchain finance changes that math. Apps such as [Moonwell](https://moonwell.fi) make financial services available directly from a phone, anywhere in the world. A user in Nairobi gets the same access as a user in the United States, opening up opportunities that did not previously exist. ## Developer tooling Developers pick Base for: - An always-on, internet-speed payment rail with global reach - Per-transaction costs that are typically below a cent - A deep developer toolchain Apps on Base ship in hours instead of days or weeks, thanks to a broad set of tools — many of them open source. Two builders working from a garage can again credibly create a major business. Tools that make this practical: - **Base Account.** Onboard users quickly and securely without seed phrases. - **Coinbase Developer Platform.** Specialized services for onchain builders, including free node software, sponsored transactions, and tooling to refine your app. - **Basenames.** More than human-readable addresses — Basenames are the front door to a builder's onchain profile. - **Verifications.** Power tailored experiences for users with verified credentials. --- # Contract Addresses Source: https://basehub.org/network/base-contracts/ Description: Canonical L1 and L2 contract and admin addresses for Base Mainnet and Base Sepolia, with explorer links and notes on unused OP Stack contracts. The tables below list the canonical contract and admin addresses for Base Mainnet and Base Sepolia, with links to the corresponding block explorer. ## L2 contract addresses ### Base Mainnet | Name | Address | | :---------------------------- | :-------------------------------------------------------------------------------------------------------------------- | | WETH9 | [0x4200000000000000000000000000000000000006](https://basescan.org/address/0x4200000000000000000000000000000000000006) | | L2CrossDomainMessenger | [0x4200000000000000000000000000000000000007](https://basescan.org/address/0x4200000000000000000000000000000000000007) | | L2StandardBridge | [0x4200000000000000000000000000000000000010](https://basescan.org/address/0x4200000000000000000000000000000000000010) | | SequencerFeeVault | [0x4200000000000000000000000000000000000011](https://basescan.org/address/0x4200000000000000000000000000000000000011) | | OptimismMintableERC20Factory | [0xF10122D428B4bc8A9d050D06a2037259b4c4B83B](https://basescan.org/address/0xF10122D428B4bc8A9d050D06a2037259b4c4B83B) | | GasPriceOracle | [0x420000000000000000000000000000000000000F](https://basescan.org/address/0x420000000000000000000000000000000000000F) | | L1Block | [0x4200000000000000000000000000000000000015](https://basescan.org/address/0x4200000000000000000000000000000000000015) | | L2ToL1MessagePasser | [0x4200000000000000000000000000000000000016](https://basescan.org/address/0x4200000000000000000000000000000000000016) | | L2ERC721Bridge | [0x4200000000000000000000000000000000000014](https://basescan.org/address/0x4200000000000000000000000000000000000014) | | OptimismMintableERC721Factory | [0x4200000000000000000000000000000000000017](https://basescan.org/address/0x4200000000000000000000000000000000000017) | | ProxyAdmin | [0x4200000000000000000000000000000000000018](https://basescan.org/address/0x4200000000000000000000000000000000000018) | | BaseFeeVault | [0x4200000000000000000000000000000000000019](https://basescan.org/address/0x4200000000000000000000000000000000000019) | | L1FeeVault | [0x420000000000000000000000000000000000001a](https://basescan.org/address/0x420000000000000000000000000000000000001a) | | EAS | [0x4200000000000000000000000000000000000021](https://basescan.org/address/0x4200000000000000000000000000000000000021) | | EASSchemaRegistry | [0x4200000000000000000000000000000000000020](https://basescan.org/address/0x4200000000000000000000000000000000000020) | | LegacyERC20ETH | [0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000](https://basescan.org/address/0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000) | ### Base Sepolia | Name | Address | | :---------------------------- | :---------------------------------------------------------------------------------------------------------------------------- | | WETH9 | [0x4200000000000000000000000000000000000006](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000006) | | L2CrossDomainMessenger | [0x4200000000000000000000000000000000000007](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000007) | | L2StandardBridge | [0x4200000000000000000000000000000000000010](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000010) | | SequencerFeeVault | [0x4200000000000000000000000000000000000011](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000011) | | OptimismMintableERC20Factory | [0x4200000000000000000000000000000000000012](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000012) | | GasPriceOracle | [0x420000000000000000000000000000000000000F](https://sepolia.basescan.org/address/0x420000000000000000000000000000000000000F) | | L1Block | [0x4200000000000000000000000000000000000015](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000015) | | L2ToL1MessagePasser | [0x4200000000000000000000000000000000000016](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000016) | | L2ERC721Bridge | [0x4200000000000000000000000000000000000014](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000014) | | OptimismMintableERC721Factory | [0x4200000000000000000000000000000000000017](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000017) | | ProxyAdmin | [0x4200000000000000000000000000000000000018](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000018) | | BaseFeeVault | [0x4200000000000000000000000000000000000019](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000019) | | L1FeeVault | [0x420000000000000000000000000000000000001a](https://sepolia.basescan.org/address/0x420000000000000000000000000000000000001a) | | EAS | [0x4200000000000000000000000000000000000021](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000021) | | EASSchemaRegistry | [0x4200000000000000000000000000000000000020](https://sepolia.basescan.org/address/0x4200000000000000000000000000000000000020) | | LegacyERC20ETH | [0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000](https://sepolia.basescan.org/address/0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000) | Most L2 predeploys share the same address on Base Mainnet and Base Sepolia. The exceptions are network-specific contracts such as `OptimismMintableERC20Factory`, whose per-network address appears in the relevant table above. ## L1 contract addresses ### Ethereum Mainnet | Name | Address | | :--------------------------- | :-------------------------------------------------------------------------------------------------------------------- | | AddressManager | [0x8EfB6B5c4767B09Dc9AA6Af4eAA89F749522BaE2](https://etherscan.io/address/0x8EfB6B5c4767B09Dc9AA6Af4eAA89F749522BaE2) | | AggregateVerifier | [0xeEcb8A5944B217585817E802702b1262a049D259](https://etherscan.io/address/0xeEcb8A5944B217585817E802702b1262a049D259) | | AnchorStateRegistryProxy | [0x909f6cf47ed12f010A796527f562bFc26C7F4E72](https://etherscan.io/address/0x909f6cf47ed12f010A796527f562bFc26C7F4E72) | | DelayedWETHProxy | [0xd0D07924AdD740a87e41Ca8A0d4CBBf6b074EF71](https://etherscan.io/address/0xd0D07924AdD740a87e41Ca8A0d4CBBf6b074EF71) | | DisputeGameFactoryProxy | [0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e](https://etherscan.io/address/0x43edB88C4B80fDD2AdFF2412A7BebF9dF42cB40e) | | L1CrossDomainMessenger | [0x866E82a600A1414e583f7F13623F1aC5d58b0Afa](https://etherscan.io/address/0x866E82a600A1414e583f7F13623F1aC5d58b0Afa) | | L1ERC721Bridge | [0x608d94945A64503E642E6370Ec598e519a2C1E53](https://etherscan.io/address/0x608d94945A64503E642E6370Ec598e519a2C1E53) | | L1StandardBridge | [0x3154Cf16ccdb4C6d922629664174b904d80F2C35](https://etherscan.io/address/0x3154Cf16ccdb4C6d922629664174b904d80F2C35) | | NitroEnclaveVerifier | [0x7F3a16E1fe6Fda64c5AC4296E13ECB9F7B44F6fb](https://etherscan.io/address/0x7F3a16E1fe6Fda64c5AC4296E13ECB9F7B44F6fb) | | OptimismMintableERC20Factory | [0x05cc379EBD9B30BbA19C6fA282AB29218EC61D84](https://etherscan.io/address/0x05cc379EBD9B30BbA19C6fA282AB29218EC61D84) | | OptimismPortal | [0x49048044D57e1C92A77f79988d21Fa8fAF74E97e](https://etherscan.io/address/0x49048044D57e1C92A77f79988d21Fa8fAF74E97e) | | ProxyAdmin | [0x0475cBCAebd9CE8AfA5025828d5b98DFb67E059E](https://etherscan.io/address/0x0475cBCAebd9CE8AfA5025828d5b98DFb67E059E) | | RiscZeroSetVerifier | [0x5005aBa3DFf7C940fcc1e48DccCAD611a80eEB85](https://etherscan.io/address/0x5005aBa3DFf7C940fcc1e48DccCAD611a80eEB85) | | RiscZeroVerifierRouter | [0x8EaB2D97Dfce405A1692a21b3ff3A172d593D319](https://etherscan.io/address/0x8EaB2D97Dfce405A1692a21b3ff3A172d593D319) | | SystemConfig | [0x73a79Fab69143498Ed3712e519A88a918e1f4072](https://etherscan.io/address/0x73a79Fab69143498Ed3712e519A88a918e1f4072) | | SystemDictator | [0x1fE3fdd1F0193Dd657C0a9AAC37314D6B479E557](https://etherscan.io/address/0x1fE3fdd1F0193Dd657C0a9AAC37314D6B479E557) | | TEEProverRegistryProxy | [0x1af2A7E537DE2eE795DE5B8BfbB1Ad0DD513A5aA](https://etherscan.io/address/0x1af2A7E537DE2eE795DE5B8BfbB1Ad0DD513A5aA) | | TEEVerifier | [0x1FbA0C57b07Af804A9717e51dec9CC27FBC12228](https://etherscan.io/address/0x1FbA0C57b07Af804A9717e51dec9CC27FBC12228) | | ZkVerifier | [0xB88D95bDf6972508942d184866890c1834219B75](https://etherscan.io/address/0xB88D95bDf6972508942d184866890c1834219B75) | #### Unused OP Stack addresses The [OP Stack SDK](https://stack.optimism.io/docs/build/sdk/#unneeded-contract-addresses) requires the following addresses to be set even though Base does not use them. Pass the zero address for each: - `StateCommitmentChain` - `CanonicalTransactionChain` - `BondManager` ### Ethereum Sepolia | Name | Address | | :----------------------------- | :---------------------------------------------------------------------------------------------------------------------------- | | AddressManager | [0x709c2B8ef4A9feFc629A8a2C1AF424Dc5BD6ad1B](https://sepolia.etherscan.io/address/0x709c2B8ef4A9feFc629A8a2C1AF424Dc5BD6ad1B) | | AggregateVerifier (Multiproof) | [0xF3f0fA3124b7b0feB048A00404Fe4D5D49E60796](https://sepolia.etherscan.io/address/0xF3f0fA3124b7b0feB048A00404Fe4D5D49E60796) | | AnchorStateRegistryProxy | [0x2fF5cC82dBf333Ea30D8ee462178ab1707315355](https://sepolia.etherscan.io/address/0x2fF5cC82dBf333Ea30D8ee462178ab1707315355) | | DelayedWETHProxy (FDG) | [0xd3683e4947A7769603Ab6418eC02f000CE3cF30b](https://sepolia.etherscan.io/address/0xd3683e4947A7769603Ab6418eC02f000CE3cF30b) | | DelayedWETHProxy (Multiproof) | [0xD6e2d9D4f1f8865AC983eE848983fb1979429914](https://sepolia.etherscan.io/address/0xD6e2d9D4f1f8865AC983eE848983fb1979429914) | | DelayedWETHProxy (PDG) | [0x32cE910d9C6c8F78dc6779c1499aB05F281A054e](https://sepolia.etherscan.io/address/0x32cE910d9C6c8F78dc6779c1499aB05F281A054e) | | DisputeGameFactoryProxy | [0xd6E6dBf4F7EA0ac412fD8b65ED297e64BB7a06E1](https://sepolia.etherscan.io/address/0xd6E6dBf4F7EA0ac412fD8b65ED297e64BB7a06E1) | | FaultDisputeGame | [0x6dDBa09bc4cCB0D6Ca9Fc5350580f74165707499](https://sepolia.etherscan.io/address/0x6dDBa09bc4cCB0D6Ca9Fc5350580f74165707499) | | FaultDisputeGame (Kona) | [0x6dDBa09bc4cCB0D6Ca9Fc5350580f74165707499](https://sepolia.etherscan.io/address/0x6dDBa09bc4cCB0D6Ca9Fc5350580f74165707499) | | L1CrossDomainMessenger | [0xC34855F4De64F1840e5686e64278da901e261f20](https://sepolia.etherscan.io/address/0xC34855F4De64F1840e5686e64278da901e261f20) | | L1ERC721Bridge | [0x21eFD066e581FA55Ef105170Cc04d74386a09190](https://sepolia.etherscan.io/address/0x21eFD066e581FA55Ef105170Cc04d74386a09190) | | L1StandardBridge | [0xfd0Bf71F60660E2f608ed56e1659C450eB113120](https://sepolia.etherscan.io/address/0xfd0Bf71F60660E2f608ed56e1659C450eB113120) | | MIPS | [0x6463dEE3828677F6270d83d45408044fc5eDB908](https://sepolia.etherscan.io/address/0x6463dEE3828677F6270d83d45408044fc5eDB908) | | NitroEnclaveVerifier | [0x7D8EA07DB94128DBEe66bAfa3eBAa9668B413d72](https://sepolia.etherscan.io/address/0x7D8EA07DB94128DBEe66bAfa3eBAa9668B413d72) | | OptimismMintableERC20Factory | [0xb1efB9650aD6d0CC1ed3Ac4a0B7f1D5732696D37](https://sepolia.etherscan.io/address/0xb1efB9650aD6d0CC1ed3Ac4a0B7f1D5732696D37) | | OptimismPortal | [0x49f53e41452C74589E85cA1677426Ba426459e85](https://sepolia.etherscan.io/address/0x49f53e41452C74589E85cA1677426Ba426459e85) | | PermissionedDisputeGame | [0x58bf355C5d4EdFc723eF89d99582ECCfd143266A](https://sepolia.etherscan.io/address/0x58bf355C5d4EdFc723eF89d99582ECCfd143266A) | | PreimageOracle | [0x1fb8cdFc6831fc866Ed9C51aF8817Da5c287aDD3](https://sepolia.etherscan.io/address/0x1fb8cdFc6831fc866Ed9C51aF8817Da5c287aDD3) | | ProxyAdmin | [0x0389E59Aa0a41E4A413Ae70f0008e76CAA34b1F3](https://sepolia.etherscan.io/address/0x0389E59Aa0a41E4A413Ae70f0008e76CAA34b1F3) | | RiscZeroSetVerifier | [0xcb9D14347b1e816831ECeE46EC199144F360B55c](https://sepolia.etherscan.io/address/0xcb9D14347b1e816831ECeE46EC199144F360B55c) | | RiscZeroVerifierRouter | [0x925d8331ddc0a1F0d96E68CF073DFE1d92b69187](https://sepolia.etherscan.io/address/0x925d8331ddc0a1F0d96E68CF073DFE1d92b69187) | | SystemConfig | [0xf272670eb55e895584501d564AfEB048bEd26194](https://sepolia.etherscan.io/address/0xf272670eb55e895584501d564AfEB048bEd26194) | | TEEProverRegistryProxy | [0xf0d7E15673fBA052e83d7f2b26BB6071E86b972e](https://sepolia.etherscan.io/address/0xf0d7E15673fBA052e83d7f2b26BB6071E86b972e) | | TEEVerifier | [0x92F6dD3501E51B8b20C77b959becaaebeB210e17](https://sepolia.etherscan.io/address/0x92F6dD3501E51B8b20C77b959becaaebeB210e17) | | ZkVerifier | [0xF9780104117C0FaD3A9b1386FbF40a9F5857988A](https://sepolia.etherscan.io/address/0xF9780104117C0FaD3A9b1386FbF40a9F5857988A) | ## Base admin addresses ### Base Mainnet | Admin Role | Address | Type of Key | | :--------------------- | :-------------------------------------------------------------------------------------------------------------------- | :----------------------------------- | | Base Security Council | [0x20AcF55A3DCfe07fC4cecaCFa1628F788EC8A4Dd](https://etherscan.io/address/0x20AcF55A3DCfe07fC4cecaCFa1628F788EC8A4Dd) | Gnosis Safe | | Batch Sender | [0x5050f69a9786f081509234f1a7f4684b5e5b76c9](https://etherscan.io/address/0x5050f69a9786f081509234f1a7f4684b5e5b76c9) | EOA managed by Coinbase Technologies | | Batch Inbox | [0xff00000000000000000000000000000000008453](https://etherscan.io/address/0xff00000000000000000000000000000000008453) | EOA (with no known private key) | | CB Multisig | [0x9855054731540A48b28990B63DcF4f33d8AE46A1](https://etherscan.io/address/0x9855054731540A48b28990B63DcF4f33d8AE46A1) | Gnosis Safe | | Output Proposer | [0xc1366Fabe614d42D367A1ecE61821238A1d31cF5](https://etherscan.io/address/0xc1366Fabe614d42D367A1ecE61821238A1d31cF5) | EOA managed by Coinbase Technologies | | Proxy Admin Owner (L1) | [0x7bB41C3008B3f03FE483B28b8DB90e19Cf07595c](https://etherscan.io/address/0x7bB41C3008B3f03FE483B28b8DB90e19Cf07595c) | Gnosis Safe | | Challenger | [0x819501cdA743a606A93dbEF254FE0D263Ce7d102](https://etherscan.io/address/0x819501cdA743a606A93dbEF254FE0D263Ce7d102) | EOA managed by Coinbase Technologies | | SystemConfig owner | [0x14536667Cd30e52C0b458BaACcB9faDA7046E056](https://etherscan.io/address/0x14536667Cd30e52C0b458BaACcB9faDA7046E056) | Gnosis Safe | | Guardian | [0x7bB41C3008B3f03FE483B28b8DB90e19Cf07595c](https://etherscan.io/address/0x7bB41C3008B3f03FE483B28b8DB90e19Cf07595c) | Gnosis Safe | | Incident Multisig | [0x14536667Cd30e52C0b458BaACcB9faDA7046E056](https://etherscan.io/address/0x14536667Cd30e52C0b458BaACcB9faDA7046E056) | Gnosis Safe | | Registrar | [0xd87488Dbb5b6F47cc6c15Dd95Bb60c83D3031b04](https://etherscan.io/address/0xd87488Dbb5b6F47cc6c15Dd95Bb60c83D3031b04) | EOA managed by Coinbase Technologies | ### Base Sepolia | Admin Role | Address | Type of Key | | :--------------------- | :---------------------------------------------------------------------------------------------------------------------------- | :----------------------------------- | | Base Security Council | [0x6AF0674791925f767060Dd52f7fB20984E8639d8](https://sepolia.etherscan.io/address/0x6AF0674791925f767060Dd52f7fB20984E8639d8) | Gnosis Safe | | Batch Sender | [0x6CDEbe940BC0F26850285cacA097C11c33103E47](https://sepolia.etherscan.io/address/0x6CDEbe940BC0F26850285cacA097C11c33103E47) | EOA managed by Coinbase Technologies | | Batch Inbox | [0xff00000000000000000000000000000000084532](https://sepolia.etherscan.io/address/0xff00000000000000000000000000000000084532) | EOA (with no known private key) | | CB Multisig | [0x646132A1667ca7aD00d36616AFBA1A28116C770A](https://sepolia.etherscan.io/address/0x646132A1667ca7aD00d36616AFBA1A28116C770A) | Gnosis Safe | | Output Proposer | [0xdb84125f2f4229c81c579f41bc129c71b174eb58](https://sepolia.etherscan.io/address/0xdb84125f2f4229c81c579f41bc129c71b174eb58) | EOA managed by Coinbase Technologies | | Proxy Admin Owner (L1) | [0x0fe884546476dDd290eC46318785046ef68a0BA9](https://sepolia.etherscan.io/address/0x0fe884546476dDd290eC46318785046ef68a0BA9) | Gnosis Safe | | Challenger | [0xadc09b63a3ac57a2ce86d946617a18df9db029a1](https://sepolia.etherscan.io/address/0xadc09b63a3ac57a2ce86d946617a18df9db029a1) | EOA managed by Coinbase Technologies | | SystemConfig owner | [0x646132A1667ca7aD00d36616AFBA1A28116C770A](https://sepolia.etherscan.io/address/0x646132A1667ca7aD00d36616AFBA1A28116C770A) | Gnosis Safe | | Guardian | [0x0fe884546476dDd290eC46318785046ef68a0BA9](https://sepolia.etherscan.io/address/0x0fe884546476dDd290eC46318785046ef68a0BA9) | Gnosis Safe | | Incident Multisig | [0x646132A1667ca7aD00d36616AFBA1A28116C770A](https://sepolia.etherscan.io/address/0x646132A1667ca7aD00d36616AFBA1A28116C770A) | Gnosis Safe | --- # Block Building Source: https://basehub.org/network/block-building/ Description: How Base orders transactions inside blocks, including Flashblocks 200ms preconfirmations and the per-transaction gas cap enforced by the mempool. import { Aside } from '@astrojs/starlight/components'; Base block ordering is independent of the user-facing experience: the sequencer can build sub-block updates without exposing them, and the underlying ordering rules continue to apply. ## Current configuration | Network | Active configuration | Upcoming | |--------------|---------------------------------------------------------------------------------------|----------| | Base Mainnet | [Flashblocks](#flashblocks) and [per-transaction gas maximum](#per-transaction-gas-maximum) | | | Base Sepolia | [Flashblocks](#flashblocks) and [per-transaction gas maximum](#per-transaction-gas-maximum) | | The [configuration changelog](/network/configuration-changelog/) records every change to block building parameters and other network-level settings. ## Flashblocks Blocks are produced by [base-builder](https://github.com/base/base/tree/main/crates/builder), with a fresh priority fee auction running every **200ms**. The auction shortens effective block times from 2 seconds to 200 milliseconds via preconfirmations. Three properties distinguish Flashblocks from vanilla ordering: 1. **Timing** — The builder emits one Flashblock every 200ms, sealing a slice of the eventual block. Once a Flashblock is broadcast, its ordering is final; later transactions with higher priority fees cannot displace transactions already committed to an earlier Flashblock. 2. **Gas allocation** — Each Flashblock has a progressively larger gas budget. Flashblock 1 may consume up to one tenth of the block gas limit, Flashblock 2 up to two tenths, and so on through Flashblock 10, which may use the full limit. | Flashblock | Available gas | |------------|---------------| | 1 | ~40M gas (1/10) | | 2 | ~80M gas (2/10) | | 3 | ~120M gas (3/10) | | ... | ... | | 10 | ~400M gas (full) | 3. **Dynamic mempool** — The builder keeps accepting new transactions while a Flashblock is being assembled. This minimizes inclusion latency, but transactions are ordered by fee *at the moment of selection*, not against every transaction that arrived during the 200ms window. A high-fee transaction that arrives late may appear after a lower-fee transaction that was already committed. ## Per-transaction gas maximum Since the [Azul hardfork](/specifications/azul-overview/), Base applies a protocol-level ceiling of **16,777,216 gas (2^24)** to any single transaction, as specified by [EIP-7825](https://eips.ethereum.org/EIPS/eip-7825). A transaction that declares a gas limit above this figure is **rejected during block validation**, and `eth_sendTransaction` or `eth_sendRawTransaction` returns a JSON-RPC error — for instance, `exceeds maximum per-transaction gas limit`. Operators running bundlers for smart contract wallets need to size each bundle so that it stays within this limit. ## Vanilla ordering Under the legacy configuration, [op-geth](https://github.com/ethereum-optimism/op-geth) builds a block every 2 seconds and orders transactions inside the block by priority fee. See the [reference implementation](https://github.com/ethereum-optimism/op-geth/blob/optimism/miner/worker.go#L627). --- # Bridges Source: https://basehub.org/network/bridges/ Description: Third-party bridges for moving assets between Base and Ethereum, Solana, or Bitcoin, plus contract addresses for the Base-Solana bridge. import { Aside, Card, CardGrid } from '@astrojs/starlight/components'; Base supports asset transfers from Ethereum, Solana, and Bitcoin through a network of third-party bridge providers. The Base-operated bridge at bridge.base.org is no longer available; the providers below cover the same asset flows. For frequently asked questions, jump to the [FAQ](#faq). ## Ethereum and Base ### Superbridge Superbridge moves ETH and other supported assets between Ethereum mainnet (L1) and Base. Supported networks: - [Base Mainnet](https://superbridge.app/base) - [Base Sepolia (Testnet)](https://superbridge.app/base-sepolia) ### Brid.gg Brid.gg also bridges ETH and supported assets between Ethereum mainnet (L1) and Base. Supported networks: - [Base Mainnet](https://brid.gg/base) - [Base Sepolia (Testnet)](https://testnet.brid.gg/base-sepolia) ### Programmatic bridging from Ethereum The [sample code repository](https://github.com/base-org/guides/tree/main/bridge/native) demonstrates how to bridge ETH and ERC-20s from Ethereum to Base. ### Token issuers If you have an ERC-20 deployed on Ethereum and want to make it bridgeable to Base, follow the [bridge-an-L1-token guide](https://docs.base.org/base-chain/quickstart/bridge-token). It walks through deploying via the standard bridge contracts and adding your token to the Superchain token list. ## Solana and Base The Base-Solana bridge supports two-way token transfers and arbitrary message passing between Base and Solana. Capabilities include: - Transferring SOL and SPL tokens to and from Base - Sending arbitrary cross-chain messages - Deploying wrapped tokens on either side - Optional auto-relay for instant settlement [Complete bridge guide with code samples and contract addresses](/introduction/base-solana-bridge/) [Production terminal UI for bridging and contract calls](https://terminallyonchain.com/) ### Contract addresses | Network | Contract | Address | |---------|----------|---------| | Base Mainnet | Bridge | `0x3eff766C76a1be2Ce1aCF2B69c78bCae257D5188` | | Base Mainnet | SOL Token | `0x311935Cd80B76769bF2ecC9D8Ab7635b2139cf82` | | Solana Mainnet | Bridge Program | `HNCne2FkVaNghhjKXapxJzPaBvAKDG1Ge3gqhZyfVWLM` | ## Bitcoin to Base ### Garden Garden is a fast, non-custodial bridge that moves BTC and supported assets from Ethereum, Solana, and other chains directly to Base. Supported networks: - [Base Mainnet](https://app.garden.finance/?output-chain=base&output-asset=cbBTC) - [Base Sepolia (Testnet)](https://testnet.garden.finance/?output-chain=base_sepolia&output-asset=USDT) ## Disclaimer Coinbase Technologies, Inc. links to these independent providers for convenience and accepts no responsibility for their operations. Any interaction with a listed provider is solely between you and that provider. ## FAQ **Can I still use the bridge on bridge.base.org?** No. The bridge at bridge.base.org has been deprecated. **I used bridge.base.org previously — how do I find my deposit or withdrawal?** Look up your transaction on one of the Superchain Bridges listed above. **Why was the bridge on bridge.base.org deprecated?** Base is committed to decentralization and the Superchain. Handing bridging off to community-operated providers strengthens censorship resistance and reduces single points of control. **Who runs the Superchain Bridges (Garden.finance, Superbridge.app, Brid.gg)?** Superchain Bridges are operated by independent third parties, not by Coinbase Technologies, Inc. ("Coinbase"). Coinbase does not control or operate them and assumes no responsibility for their performance. Each provider may require you to accept its terms of service and privacy policy — review them before using a bridge. Listing a bridge here is not an endorsement. **How does the Base-Solana bridge work?** A validator set verifies cross-chain messages. Bridging from Solana to Base locks the source tokens on Solana and mints wrapped tokens on Base. Bridging from Base to Solana burns the wrapped tokens on Base and releases the originals on Solana. The [full bridge documentation](/introduction/base-solana-bridge/) covers the implementation. **Where can I get help if I have a question or problem?** The [Base Discord](https://base.org/discord) is staffed around the clock. Open a ticket in the `#general-support` channel for assistance. --- # Bridging and Withdrawals Source: https://basehub.org/network/bridging-and-withdrawals/ Description: How deposits into Base and standard withdrawals back to Ethereum work, why withdrawals take seven days, and the faster bridge options available. import { Aside } from '@astrojs/starlight/components'; Bridging is the movement of assets or messages across chains. Moving value into Base is a deposit; moving it out is a withdrawal. The two directions have very different timing because each leans on a different part of the rollup protocol. A deposit originates on another chain and lands as a transaction on Base. A standard withdrawal originates on Base, then has to be proven on Ethereum and can only be finalized once a challenge window has closed. ## Deposits to Base A deposit is kicked off somewhere other than Base and surfaces as a Base transaction. When the source is Ethereum, the L1 transaction logs data, and Base nodes read that data to construct the corresponding L2 deposit transaction. These deposit transactions are baked into the protocol rather than submitted like ordinary user transactions. They skip the usual signature, nonce, and fee fields, since authorization comes from the L1 deposit event and the L2 gas was already paid on L1. From a user's point of view it is simple: confirm and process the transaction on the source chain, and the asset or message arrives on Base shortly after. The [deposits specification](https://docs.base.org/base-chain/specs/protocol/bridging/deposits) covers the protocol mechanics. ## Standard withdrawals A standard withdrawal is a single cross-domain action that opens on Base and closes on Ethereum. Through it you can move ETH, bridge supported ERC-20 tokens, or pass a message from Base to a contract on L1. It proceeds in three stages: 1. **Initiate on Base** — submitting the withdrawal on Base writes its message into the `L2ToL1MessagePasser` contract. 2. **Prove on Ethereum** — after Base has published the state that contains it, anyone may hand the `OptimismPortal` contract a proof that the message is present on Base. 3. **Finalize on Ethereum** — when the 7-day challenge window closes, anyone can finalize on Ethereum, which hands over the assets or delivers the message to its destination. Whoever proves or finalizes does not have to be whoever started the withdrawal, so wallets, bridge front-ends, and other services can shepherd users through the steps. ## Why standard withdrawals take 7 days Withdrawals are backed by Base's fault proofs. Every withdrawal proof is anchored to an output root that commits to Base state, and the week-long window exists so that participants can challenge a bad output root before anything resting on it is allowed to finalize. Absent a successful challenge, the withdrawal finalizes when the window expires. Should an output root prove invalid, the chain itself is unaffected — Base does not reorg — but any withdrawal anchored to that root is blocked and has to be re-proven against a sound one. The [Transaction Finality](/network/transaction-finality/#finality-for-withdrawals) page explains how this differs from finality for everyday Base transactions. ## Faster withdrawal options A number of bridge providers advertise withdrawals quicker than the native protocol allows. As a rule they leave the underlying challenge period untouched; what they do instead is advance users their funds on the far chain — backed by liquidity, relayers, or market makers — ahead of the standard withdrawal actually settling. The convenience comes with trade-offs: each provider layers on its own trust assumptions and charges its own fees. Vet a provider's terms, asset support, pricing, liquidity, and security design before depending on it. The [Bridges](/network/bridges/) page lists the available providers and routes. ## Standard bridge contracts Cross-domain ETH and ERC-20 transfers run through the standard bridges, which wrap the cross-domain messenger contracts in a uniform deposit-and-withdrawal interface. On the L2 side, the `L2StandardBridge` is a predeploy at `0x4200000000000000000000000000000000000010`. Contract-level detail lives in the [standard bridges specification](https://docs.base.org/base-chain/specs/protocol/bridging/bridges) and the [withdrawals specification](https://docs.base.org/base-chain/specs/protocol/bridging/withdrawals). --- # Configuration Changelog Source: https://basehub.org/network/configuration-changelog/ Description: A dated log of block-building, fee, and other network parameter changes for Base Mainnet and Base Sepolia. The tables below track network parameter changes — block building, fees, and other operational settings — for Base Mainnet and Base Sepolia. ## Base Mainnet | Date | Change | Documentation | |------|--------|---------------| | May 28, 2026 | Azul: Reduced per-transaction gas maximum to 16,777,216 (2^24) via EIP-7825 | [Per-Transaction Gas Maximum](/network/block-building/#per-transaction-gas-maximum) | | February 19, 2026 | Increased Minimum Base Fee to 5,000,000 wei | [Minimum Base Fee](/network/network-fees/#minimum-base-fee) | | February 4, 2026 | Increased EIP-1559 Denominator to 125 | [EIP-1559 Fee Parameters](/network/network-fees/#eip-1559-fee-parameters) | | February 2, 2026 | Increased Minimum Base Fee to 2,000,000 wei | [Minimum Base Fee](/network/network-fees/#minimum-base-fee) | | January 22, 2026 | Increased Minimum Base Fee to 1,000,000 wei | [Minimum Base Fee](/network/network-fees/#minimum-base-fee) | | December 18, 2025 | Increased Minimum Base Fee to 500,000 wei | [Minimum Base Fee](/network/network-fees/#minimum-base-fee) | | December 4, 2025 | Enabled Minimum Base Fee (200,000 wei) | [Minimum Base Fee](/network/network-fees/#minimum-base-fee) | | September 17, 2025 | Enabled Per-Transaction Gas Maximum | [Per-Transaction Gas Maximum](/network/block-building/#per-transaction-gas-maximum) | | September 11, 2025 | Ended testing Per-Transaction Gas Maximum | [Per-Transaction Gas Maximum](/network/block-building/#per-transaction-gas-maximum) | | September 10, 2025 | Started testing Per-Transaction Gas Maximum | [Per-Transaction Gas Maximum](/network/block-building/#per-transaction-gas-maximum) | | July 7, 2025 | Enabled Flashblocks | [Flashblocks](/network/block-building/#flashblocks) | | May 15, 2025 | Ended testing Flashblocks | [Flashblocks](/network/block-building/#flashblocks) | | May 15, 2025 | Started testing Flashblocks | [Flashblocks](/network/block-building/#flashblocks) | ## Base Sepolia | Date | Change | Documentation | |------|--------|---------------| | April 20, 2026 | Azul: Reduced per-transaction gas maximum to 16,777,216 (2^24) via EIP-7825 | [Per-Transaction Gas Maximum](/network/block-building/#per-transaction-gas-maximum) | | February 19, 2026 | Increased Minimum Base Fee to 5,000,000 wei | [Minimum Base Fee](/network/network-fees/#minimum-base-fee) | | February 10, 2026 | Increased EIP-1559 Denominator to 125 | [EIP-1559 Fee Parameters](/network/network-fees/#eip-1559-fee-parameters) | | February 10, 2026 | Increased Minimum Base Fee to 2,000,000 wei | [Minimum Base Fee](/network/network-fees/#minimum-base-fee) | | November 20, 2025 | Enabled Minimum Base Fee (200,000 wei) | [Minimum Base Fee](/network/network-fees/#minimum-base-fee) | | September 3, 2025 | Enabled Per-Transaction Gas Maximum | [Per-Transaction Gas Maximum](/network/block-building/#per-transaction-gas-maximum) | | February 25, 2025 | Enabled Flashblocks | [Flashblocks](/network/block-building/#flashblocks) | --- # Differences Between Ethereum and Base Source: https://basehub.org/network/diffs-ethereum-base/ Description: Short reference to the protocol-level differences developers encounter when moving apps from Ethereum to Base. Base targets full compatibility with Ethereum, so most apps port over without modification. A small set of behavioral differences are worth knowing before you ship. The notable differences are: - [Flashblocks](/flashblocks/app-integration/) - [Minimum base fee](/network/network-fees/#minimum-base-fee) - [Bridging](https://docs.optimism.io/stack/differences#bridging) - [Opcodes](https://docs.optimism.io/stack/differences#opcodes) - [Address aliasing](https://docs.optimism.io/stack/differences#address-aliasing) - [Transaction costs](https://docs.optimism.io/stack/differences#transaction-fees) - [Chain finality](https://docs.optimism.io/stack/differences#chain-finality) --- # Ecosystem Contracts Source: https://basehub.org/network/ecosystem-contracts/ Description: Deployed addresses for Multicall3, Uniswap v2, and Uniswap v3 on Base Mainnet and Base Sepolia, with block explorer links. import { Aside } from '@astrojs/starlight/components'; The tables below list deployment addresses for widely used onchain apps on Base. ## Base Mainnet ### Multicall3 | Contract | Address | | :--------- | :-------------------------------------------------------------------------------------------------------------------- | | Multicall3 | [0xcA11bde05977b3631167028862bE2a173976CA11](https://basescan.org/address/0xcA11bde05977b3631167028862bE2a173976CA11) | ### Uniswap v3 | Contract | Address | | :----------------------------------- | :-------------------------------------------------------------------------------------------------------------------- | | `Permit2` | [0x000000000022D473030F116dDEE9F6B43aC78BA3](https://basescan.org/address/0x000000000022D473030F116dDEE9F6B43aC78BA3) | | `universal router` | [0x198EF79F1F515F02dFE9e3115eD9fC07183f02fC](https://basescan.org/address/0x198EF79F1F515F02dFE9e3115eD9fC07183f02fC) | | `v3CoreFactory` | [0x33128a8fC17869897dcE68Ed026d694621f6FDfD](https://basescan.org/address/0x33128a8fC17869897dcE68Ed026d694621f6FDfD) | | `multicall` | [0x091e99cb1C49331a94dD62755D168E941AbD0693](https://basescan.org/address/0x091e99cb1C49331a94dD62755D168E941AbD0693) | | `proxyAdmin` | [0x3334d83e224aF5ef9C2E7DDA7c7C98Efd9621fA9](https://basescan.org/address/0x3334d83e224aF5ef9C2E7DDA7c7C98Efd9621fA9) | | `tickLens` | [0x0CdeE061c75D43c82520eD998C23ac2991c9ac6d](https://basescan.org/address/0x0CdeE061c75D43c82520eD998C23ac2991c9ac6d) | | `nftDescriptor` | [0xF9d1077fd35670d4ACbD27af82652a8d84577d9F](https://basescan.org/address/0xF9d1077fd35670d4ACbD27af82652a8d84577d9F) | | `nonfungibleTokenPositionDescriptor` | [0x4f225937EDc33EFD6109c4ceF7b560B2D6401009](https://basescan.org/address/0x4f225937EDc33EFD6109c4ceF7b560B2D6401009) | | `descriptorProxy` | [0x4615C383F85D0a2BbED973d83ccecf5CB7121463](https://basescan.org/address/0x4615C383F85D0a2BbED973d83ccecf5CB7121463) | | `nonfungibleTokenPositionManager` | [0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1](https://basescan.org/address/0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1) | | `v3Migrator` | [0x23cF10b1ee3AdfCA73B0eF17C07F7577e7ACd2d7](https://basescan.org/address/0x23cF10b1ee3AdfCA73B0eF17C07F7577e7ACd2d7) | | `v3Staker` | [0x42bE4D6527829FeFA1493e1fb9F3676d2425C3C1](https://basescan.org/address/0x42bE4D6527829FeFA1493e1fb9F3676d2425C3C1) | | `quoterV2` | [0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a](https://basescan.org/address/0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a) | | `swapRouter` | [0x2626664c2603336E57B271c5C0b26F421741e481](https://basescan.org/address/0x2626664c2603336E57B271c5C0b26F421741e481) | ### Uniswap v2 | Contract | Address | | :-------- | :-------------------------------------------------------------------------------------------------------------------- | | `Factory` | [0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6](https://basescan.org/address/0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6) | | `Router` | [0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24](https://basescan.org/address/0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24) | ## Base Sepolia ### Multicall3 | Contract | Address | | :--------- | :---------------------------------------------------------------------------------------------------------------------------- | | Multicall3 | [0xcA11bde05977b3631167028862bE2a173976CA11](https://sepolia.basescan.org/address/0xcA11bde05977b3631167028862bE2a173976CA11) | ### Uniswap v3 | Contract | Address | | :----------------------------------- | :---------------------------------------------------------------------------------------------------------------------------- | | `Permit2` | [0x000000000022d473030f116ddee9f6b43ac78ba3](https://sepolia.basescan.org/address/0x000000000022d473030f116ddee9f6b43ac78ba3) | | `universal router` | [0x050E797f3625EC8785265e1d9BDd4799b97528A1](https://sepolia.basescan.org/address/0x050E797f3625EC8785265e1d9BDd4799b97528A1) | | `v3CoreFactory` | [0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24](https://sepolia.basescan.org/address/0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24) | | `multicall` | [0xd867e273eAbD6c853fCd0Ca0bFB6a3aE6491d2C1](https://sepolia.basescan.org/address/0xd867e273eAbD6c853fCd0Ca0bFB6a3aE6491d2C1) | | `proxyAdmin` | [0xD7303474Baca835743B54D73799688990f24a79D](https://sepolia.basescan.org/address/0xD7303474Baca835743B54D73799688990f24a79D) | | `tickLens` | [0xedf6066a2b290C185783862C7F4776A2C8077AD1](https://sepolia.basescan.org/address/0xedf6066a2b290C185783862C7F4776A2C8077AD1) | | `nftDescriptor` | [0x4e0caFF1Df1cCd7CF782FDdeD77f020699B57f1a](https://sepolia.basescan.org/address/0x4e0caFF1Df1cCd7CF782FDdeD77f020699B57f1a) | | `nonfungibleTokenPositionDescriptor` | [0xd7c6e867591608D32Fe476d0DbDc95d0cf584c8F](https://sepolia.basescan.org/address/0xd7c6e867591608D32Fe476d0DbDc95d0cf584c8F) | | `nonfungibleTokenPositionManager` | [0x27F971cb582BF9E50F397e4d29a5C7A34f11faA2](https://sepolia.basescan.org/address/0x27F971cb582BF9E50F397e4d29a5C7A34f11faA2) | | `v3Migrator` | [0xCbf8b7f80800bd4888Fbc7bf1713B80FE4E23E10](https://sepolia.basescan.org/address/0xCbf8b7f80800bd4888Fbc7bf1713B80FE4E23E10) | | `v3Staker` | [0x62725F55f50bdE240aCa3e740D47298CAc8d57D5](https://sepolia.basescan.org/address/0x62725F55f50bdE240aCa3e740D47298CAc8d57D5) | | `quoterV2` | [0xC5290058841028F1614F3A6F0F5816cAd0df5E27](https://sepolia.basescan.org/address/0xC5290058841028F1614F3A6F0F5816cAd0df5E27) | | `swapRouter` | [0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4](https://sepolia.basescan.org/address/0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4) | #### Testnet interfaces ### Uniswap v2 | Contract | Address | | :-------- | :---------------------------------------------------------------------------------------------------------------------------- | | `Factory` | [0x7Ae58f10f7849cA6F5fB71b7f45CB416c9204b1e](https://sepolia.basescan.org/address/0x7Ae58f10f7849cA6F5fB71b7f45CB416c9204b1e) | | `Router` | [0x1689E7B1F10000AE47eBfE339a4f69dECd19F602](https://sepolia.basescan.org/address/0x1689E7B1F10000AE47eBfE339a4f69dECd19F602) | --- # Network Incident History Source: https://basehub.org/network/incident-history/ Description: Paraphrased log of Base mainnet and Sepolia incidents and scheduled network events. A running log of substantive incidents and scheduled upgrades affecting Base Mainnet and Base Sepolia. Procedural status pings are omitted. Entries are newest first. ## 2026 ### 2026-06-25 — Mainnet chain stall Beginning at 16:03 UTC on Thursday, June 25th, Base Mainnet block production turned unhealthy. The team traced it to a consensus fault: an invalid block had been sequenced, which kept new blocks from being built past block `47806542`. Sequencing of fresh blocks was restored by around 17:51 UTC, though ecosystem node operators had to restart their Base nodes to resume syncing. Healthy block building was confirmed shortly after, and the Beryl hardfork still activated on schedule at 18:00 UTC. By 19:22 UTC the network was stable with recovery verified across the ecosystem; any nodes still stuck at block `47806542` recover on restart. The root cause was identified and a fix put under verification, with a full post-mortem and root-cause analysis promised to the ecosystem. ### 2026-06-25 — Mainnet Beryl upgrade (scheduled) Base Mainnet is scheduled to activate the Beryl hardfork on Thursday, June 25th at 18:00 UTC (Unix timestamp `1782410400`). Operators of Mainnet nodes must move to the `1.1.1` release or later of `github.com/base/base`, with matching preconfigured binaries published as the [`v1.1.1`](https://github.com/base/node/releases/tag/v1.1.1) release of `github.com/base/node`. Migration steps are documented in the [Beryl Upgrade Overview](/specifications/beryl-overview#required-software). ### 2026-06-18 — Sepolia Beryl upgrade (scheduled) Base Sepolia is scheduled to activate the Beryl hardfork on Thursday, June 18th at 18:00 UTC (`1781805600`). Operators of Sepolia nodes need to move to the `v1.1.0` release or later of `github.com/base/base`, with matching preconfigured builds shipped as the `v1.1.0` release of `github.com/base/node`. The migration details live in the [Beryl Upgrade Overview](/specifications/beryl-overview). ### 2026-06-12 — Mainnet p2p peering instability On Friday, June 12th at 21:28 UTC, the team began investigating peering problems on Base Mainnet where newly started nodes were having trouble finding peers. As a temporary measure, affected operators were pointed at a short list of `--trusted-peers` enode entries to add to their Reth node and restart while a fix was developed. ### 2026-06-02 — Mainnet Flashblocks broadcasting degraded On Tuesday, June 2nd at 22:32 UTC, the team began investigating periodic instability in Flashblocks broadcasting on Base Mainnet. Pre-confirmation delivery was affected, but transaction inclusion in sealed blocks was reported as unaffected throughout. ### 2026-05-29 — Mainnet withdrawal delay (TEE enclave) On Friday, May 29th at 15:55 UTC, Base Mainnet withdrawals began running late after a TEE enclave fault stopped output proposals from being posted. The issue was identified and a fix was reported in progress; withdrawals resume once proposals catch back up. ### 2026-05-28 — Mainnet Azul upgrade (scheduled) Base Mainnet activates the Azul hardfork on Thursday, May 28th at 18:00 UTC. Node operators must run version `0.9.0` or later of `github.com/base/base`, with corresponding preconfigured binaries published as the `v0.16.0` release of `github.com/base/node`. From this upgrade onward, `base-consensus` and `base-reth-node` are the only supported clients — `op-node` and `op-geth` will not follow the new chain. Pending withdrawals will be invalidated by the upgrade; users with in-flight withdrawals will need to submit a second proof transaction afterward to complete them. Migration steps are documented in [Base v1 upgrade](/node-operations/base-v1-upgrade). --- # Network Faucets Source: https://basehub.org/network/network-faucets/ Description: Testnet ETH faucets for Base Sepolia from Coinbase Developer Platform, Alchemy, QuickNode, Chainstack, and other providers. import { Aside } from '@astrojs/starlight/components'; Multiple providers dispense free Base Sepolia testnet ETH (and a few support mainnet drips for contract deployment). Supported assets, claim limits, and authentication requirements vary by provider. ## Coinbase Developer Platform The [Coinbase Developer Platform Faucet](https://portal.cdp.coinbase.com/products/faucet) hands out up to 0.1 testnet ETH per 24 hours on Base Sepolia, and also drips USDC, EURC, and cbBTC. ## thirdweb The [thirdweb Faucet](https://thirdweb.com/base-sepolia-testnet) provides free Base Sepolia ETH with a single claim per 24 hours. ## Alchemy The [Alchemy Faucet](https://basefaucet.com/) requires a free Alchemy account and dispenses Base Sepolia testnet ETH. ## Bware Labs The [Bware Labs Faucet](https://bwarelabs.com/faucets) requires no registration and dispenses Base Sepolia ETH. ## Chainstack The [Chainstack Faucet](https://faucet.chainstack.com/) drips Base ETH against your Chainstack platform API key. ## ethfaucet.com [ethfaucet.com](https://ethfaucet.com/networks/base) provides free Base Sepolia testnet ETH and small mainnet drips for contract deployment. The faucet is operated by [BringID](https://www.bringid.org/). ## QuickNode The [QuickNode Faucet](https://faucet.quicknode.com/drip) is a multi-chain faucet that dispenses Base Sepolia testnet ETH. ## LearnWeb3 The [LearnWeb3 Faucet](https://learnweb3.io/faucets/base_sepolia) is a multi-chain faucet that dispenses Base Sepolia testnet ETH. ## Ethereum Ecosystem The [Base Sepolia Faucet](https://www.ethereum-ecosystem.com/faucets/base-sepolia) is operated by [Ethereum Ecosystem](https://www.ethereum-ecosystem.com) and requires no login. --- # Network Fees Source: https://basehub.org/network/network-fees/ Description: How Base fees are split between L2 execution and L1 data, plus the minimum base fee, EIP-1559 parameters, and the GasPriceOracle. Every Base transaction pays an L2 execution fee plus an L1 security fee — the cost of posting transaction data back to Ethereum. The L1 component is typically the larger of the two. ## How fees work The L1 fee fluctuates with Ethereum gas conditions. If your transaction timing is flexible, submitting during quieter L1 windows (such as weekends) can reduce overall cost. The L2 fee responds to demand on Base itself, using the same EIP-1559 mechanism Ethereum uses. For background, see Coinbase's [EIP-1559 explainer](https://help.coinbase.com/en/coinbase/getting-started/crypto-education/eip-1559). For lower-level details on fee composition, refer to the [OP Stack fee documentation](https://docs.optimism.io/stack/transactions/fees). ## Minimum base fee The [Jovian upgrade](https://docs.optimism.io/notices/upgrade-17) introduced a minimum base fee — a floor that prevents the L2 base fee from collapsing during quiet periods. Base Mainnet's minimum base fee is currently 5,000,000 wei (0.005 gwei). The value may be adjusted over time as data accumulates. As a reference point, a 0.005 gwei base fee costs about $0.002 for a 200,000-gas transaction at an ETH price of $2,000. ### Benefits - **Faster inclusion.** Without a floor, low-activity periods drove the base fee toward zero, and demand spikes could then take a long time to clear the resulting backlog. With a floor, transactions clear faster and most users do not need to manually bump priority fees. - **More predictable fees.** Base fees sit at or near the minimum during normal operation and rise above it during congestion — a surge-pricing pattern. - **Spam mitigation.** Very low fees make spam transactions cheap. The floor prices out that activity while keeping legitimate use affordable. ### Current configuration | Network | Minimum base fee | |--------------|------------------| | Base Mainnet | 5,000,000 wei (0.005 gwei) | | Base Sepolia | 5,000,000 wei (0.005 gwei) | The [configuration changelog](/network/configuration-changelog/) tracks every change to the minimum base fee and other parameters. ## EIP-1559 fee parameters Base uses the OP Stack implementation of EIP-1559 to adjust the L2 base fee in response to demand. Two parameters drive the dynamics. ### Elasticity multiplier The **elasticity multiplier** sets the maximum block gas relative to the target. With elasticity 6, a block can hold up to 6× the target gas, absorbing short demand spikes. ### Base fee change denominator The **base fee change denominator** controls how aggressively the base fee moves between blocks. Larger denominators yield smoother changes; with a denominator of 125, the fee adjusts more gradually than under lower values. ### Maximum rate of change Per-block change is capped at: **Max increase per block = (Elasticity − 1) / Denominator** With the current parameters (Elasticity = 6, Denominator = 125): - Maximum increase per block: (6 − 1) / 125 = **4%** - Minimum time to double the base fee: 18 blocks × 2 seconds = **36 seconds** The smoothed adjustment limits fee volatility during traffic spikes while still letting the network track sustained demand. ### Current configuration | Network | Elasticity | Denominator | Max change per block | |--------------|------------|-------------|----------------------| | Base Mainnet | 6 | 125 | 4% | | Base Sepolia | 6 | 125 | 4% | ## Querying the L1 fee The **GasPriceOracle** predeploy at `0x420000000000000000000000000000000000000F` (see [Contract Addresses](/network/base-contracts/)) lets callers estimate the L1 component before signing. | Method | Returns | | ------ | ------- | | `getL1Fee(bytes)` | Exact L1 fee for a fully serialized (RLP-encoded) transaction | | `getL1FeeUpperBound(uint256 txSize)` | Upper-bound L1 fee estimated from approximate transaction byte length | | `l1BaseFee()` | Current Ethereum L1 base fee as observed by Base | | `blobBaseFee()` | Current EIP-4844 blob base fee | | `baseFeeScalar()` | Scalar applied to the L1 base fee component | | `blobBaseFeeScalar()` | Scalar applied to the blob base fee component | Use `getL1FeeUpperBound` for a quick estimate before the transaction is fully built. Use `getL1Fee` with the complete serialized transaction for an exact pre-sign value. --- # Throughput and Limits Source: https://basehub.org/network/throughput-and-limits/ Description: Gas limits, Flashblock performance figures, fee parameters, data availability throttling, and endpoint limits that shape practical throughput on Base. import { Aside } from '@astrojs/starlight/components'; Several things bound throughput on Base simultaneously: how much gas a block allows, how much data the chain can publish for availability, the parameters of the fee market, and the limits imposed at the RPC endpoint. No single transactions-per-second number describes every workload, since transactions differ widely in the gas and data they each consume. Base has sustained several bursts above 5,000 TPS, and capacity keeps growing as the chain scales. The [Introducing Base Azul](https://blog.base.dev/introducing-base-azul) post has more background. ## Current limits | Limit | Value | |-------|-------| | Full block gas budget | ~400M gas | | First Flashblock gas budget | ~40M gas | | Per-transaction gas maximum | 16,777,216 gas (2^24) | | Deposit transaction limit | Maximum gas includable in an L1 block (20,000,000 gas) | While a block is being assembled, the full gas budget is divided across its Flashblocks: Flashblock 1 can use one tenth of the limit, Flashblock 2 two tenths, and so on up to Flashblock 10, which has the full budget. See [Block Building](/network/block-building/#flashblocks) for how this shapes transaction ordering. ## Flashblock performance Flashblocks emit incremental block updates roughly every 200ms, so apps receive sub-second preconfirmations inside the standard 2-second block. | Metric | Value | |--------|-------| | Flashblock build time (P50) | ~10ms | | Preconfirmation latency | ~200ms | | Full block time | 2 seconds | | Flashblocks per block | 10 | | Reorg rate | < 0.1% | For how reorgs are handled — and other recurring questions — see the [Flashblocks FAQ](/flashblocks/faq/). ## Per-transaction gas maximum Since the [Azul hardfork](/specifications/azul-overview/), any single transaction is held to a protocol-level ceiling of **16,777,216 gas (2^24)**, as set out in [EIP-7825](https://eips.ethereum.org/EIPS/eip-7825). A transaction that declares a gas limit above this value is **rejected during block validation**, and `eth_sendTransaction` or `eth_sendRawTransaction` returns a JSON-RPC error such as `exceeds maximum per-transaction gas limit`. Operators running bundlers for smart contract wallets need to size each bundle to stay under this cap. ## Fee parameters Fees feed back into real-world throughput: as load climbs toward the ceiling, the fee a transaction offers governs whether it makes it into a block. | Parameter | Current value | |-----------|---------------| | Minimum base fee | 5,000,000 wei (0.005 gwei) | | EIP-1559 elasticity | 6 | | EIP-1559 denominator | 125 | | Maximum L2 base fee change per block | 4% | For how Base fees are structured — the L2 execution fee and the L1 security fee — see [Network Fees](/network/network-fees/). ## Data availability throughput Base posts its transaction data to Ethereum for data availability. When DA bandwidth is constrained, the sequencer can throttle L2 throughput while the batcher catches up. While DA throttling is active, even transactions paying a high priority fee may be delayed, and no RPC endpoint estimates priority fees with throttling in mind. The [troubleshooting guide](/network/troubleshooting-transactions/) covers what this means for transaction submission. ## Endpoint limits Public endpoints carry rate limits and are not built for production traffic. A hosted RPC provider may layer on further constraints — caps on requests per second or compute units, restricted method sets, limits on archive data, or ceilings on WebSocket subscriptions. None of that alters the protocol's own capacity, but any of it can turn into the practical ceiling for an app, wallet, indexer, or monitoring system. For production traffic, route through a node provider or [run your own Base node](/getting-started/running/). --- # Transaction Finality Source: https://basehub.org/network/transaction-finality/ Description: The four finality stages for Base L2 transactions and the 7-day window that applies only to withdrawals back to Ethereum. import { Aside } from '@astrojs/starlight/components'; Finality is the point at which a Base transaction becomes irreversible. The behavior differs for transactions that update L2 state versus transactions that withdraw funds from Base L2 to Ethereum L1. ## Finality for Base L2 transactions This section covers every Base transaction except withdrawals back to Ethereum. Finality is layered: each successive stage strengthens the security guarantee. ### Flashblock inclusion: ~200ms After about 200 milliseconds, the Base sequencer includes the transaction in a preconfirmation block (a Flashblock). Reorg probability is below 0.001%. Flashblocks reorg less than 0.001% of the time; the [public stats page](https://base.org/stats) records the reorg history. ### L2 block inclusion: ~2s After roughly 2 seconds, the sequencer has packaged the transaction into a sealed L2 block and propagated it to validator nodes. Reorg probability is effectively 0%. Only one Base L2 block has ever reorged, accounting for 0.0000003% of transactions. The data is visible on [Blockscout](https://base.blockscout.com/blocks?tab=reorgs). ### L1 batch inclusion: ~2m After about 2 minutes, a Base batch containing the transaction has been posted to Ethereum. Reorg probability is effectively 0%. No L2 blocks that have been batched to Ethereum L1 have ever reorged. **An Ethereum L1 reorg does not force a Base L2 reorg.** The sequencer and validator nodes maintain a configurable lag from the L1 tip, so typical L1 reorgs have no impact. If a deeper L1 reorg occurs, Base can resubmit batch data without altering the sequenced L2 blocks. ### L1 batch finality: ~20m The Ethereum batch carrying the transaction is older than 2 epochs (64 L1 blocks). Reorg probability is effectively 0%. L2 blocks past L1 batch finality enjoy the same protection as Ethereum-finalized blocks and are practically impossible to reverse. ## Finality for withdrawals **Withdrawals from Base to Ethereum must wait 7 days before funds can be released to the L1 recipient.** The delay enables Base's Fault Proof system to provide strong security for bridged funds. ### What happens during the 7 days When a withdrawal is initiated, the funds are debited from the Base account immediately. A permissionless "proposer" must then provide Ethereum with proof that Base contains the withdrawal. Ethereum cannot independently confirm Base state — they are separate chains — so a 7-day window opens during which a permissionless "challenger" may dispute a proposal it considers malicious. If no challenge arrives, the withdrawal can be proven against the finalized output root and released on L1. If a challenge is filed, the proposer and challenger play the Fault Dispute Game, escalating bonds until a winner emerges. A winning proposer locks in the output root for use in withdrawal proofs; a winning challenger invalidates the output proposal. The system is secure as long as a single honest party participates. Base will always run an honest proposer and an honest challenger. A successful challenge does not reorg the L2 chain. The output proposal is marked invalid, and any withdrawals proven against it must be re-proven against a different, valid output root. ## FAQ **If Ethereum reorgs, will Base reorg?** Almost never. Base resubmits batch data to Ethereum transparently while the L2 chain continues forward. **How long do deposit transactions take to finalize?** Deposits are initiated on Ethereum and are typically picked up by the Base sequencer within 3 minutes. **If a challenger wins a dispute game, will the L2 chain reorg?** No. The challenged output proposal is marked invalid, and any actions that depended on its output root must use a valid replacement. In practice, withdrawals proven against the invalid proposal must be re-proven against a different one. --- # Troubleshooting Transactions Source: https://basehub.org/network/troubleshooting-transactions/ Description: Diagnose pending, rejected, failed, or slow Base transactions, with fixes for fee, nonce, gas, and Flashblocks issues. import { Aside } from '@astrojs/starlight/components'; This guide walks through the common reasons Base transactions stall, fail, or revert, and explains how to fix each one. ## Pending transactions If a transaction is sitting in the mempool longer than expected, work through the following. ### `maxFeePerGas` too low If your `maxFeePerGas` is below the current base fee, the transaction stays pending until the base fee drops below your value. **Fix:** `maxFeePerGas` must cover both the base fee and the priority fee. Because the base fee can move between blocks, set `maxFeePerGas` high enough to remain valid even if the base fee climbs while the transaction is queued. A common formula: ``` maxFeePerGas = baseFee * 2 + maxPriorityFeePerGas ``` This pattern (used by [ethers.js](https://github.com/ethers-io/ethers.js/blob/98c49d091eb84a9146dfba8476f18e4c3e3d1d31/src.ts/providers/abstract-provider.ts#L945-L950)) provides headroom for the base fee to double before the transaction becomes uneconomical. You only pay the actual base fee at inclusion, not the maximum. ### Priority fee too low When the network is busy, transactions compete for inclusion via priority fees. Submitting a low priority fee against active demand delays inclusion. **Fix:** Most users wait for congestion to ease. For time-sensitive transactions, query `eth_maxPriorityFeePerGas` for an estimate that should outbid recent transactions. ### Nonce gap A pending transaction at nonce N blocks every transaction at nonce N+1 or higher, regardless of fees. **Fix:** Wait for the pending transaction, or replace it by submitting a new transaction with the same nonce and a fee bump of at least 10% on both `maxPriorityFeePerGas` and `maxFeePerGas`. ### Nonce too low Submitting a transaction with a nonce that has already been used results in a rejection. **Fix:** Query the next available nonce with `eth_getTransactionCount` and the `pending` tag. ## Rejected transactions ### Gas limit exceeds the maximum Base enforces a [per-transaction gas maximum](/network/block-building/#per-transaction-gas-maximum) of **16,777,216 gas (2^24)**. Any transaction declaring a higher limit is rejected during block validation. **Error:** `exceeds maximum per-transaction gas limit` **Fix:** Reduce the gas limit to 16,777,216 (2^24) or lower. If the transaction genuinely needs more gas, split it into multiple transactions. ## Included but failed transactions ### Out of gas The transaction exhausted its gas during execution. **Fix:** Increase the gas limit. Estimate with `eth_estimateGas` and add a safety margin (for example, 20%) to absorb variability. ### Reverted by contract Contract logic triggered a revert. **Fix:** Inspect the transaction on [Basescan](https://basescan.org) to find the revert reason. Common causes include failed `require` checks, arithmetic errors, and invalid state transitions. ## Slow confirmations ### Confirmation tiers Base produces an L2 block every 2 seconds, while [Flashblocks](/flashblocks/app-integration/) emit preconfirmations every 200ms. | Confirmation level | Time | Description | |--------------------|------|-------------| | Flashblock preconfirmation | ~200ms | Transaction included in a preconfirmation | | L2 block inclusion | ~2s | Transaction included in a sealed L2 block | | L1 batch inclusion | ~2m | Transaction posted to Ethereum | | L1 finality | ~20m | Ethereum batch is finalized | See [Transaction Finality](/network/transaction-finality/) for the full breakdown. ### Use a Flashblocks-aware endpoint For the fastest possible confirmation, point your client at the Flashblocks RPC endpoint: | Network | Flashblocks RPC | |---------|-----------------| | Mainnet | `https://mainnet.base.org` | | Sepolia | `https://sepolia.base.org` | These endpoints return receipts as soon as the transaction lands in a Flashblock instead of waiting for the full L2 block. ## Debugging tools - **[Basescan](https://basescan.org)** — Inspect status, logs, and revert reasons - **[Tenderly](https://tenderly.co)** — Simulate and debug transactions - **`eth_call`** — Test contract calls without submitting a transaction - **`eth_estimateGas`** — Estimate gas usage before submitting ## Getting help If you are still stuck, ask in the `#developer-chat` channel on the [Base Discord](https://base.org/discord). --- # Azul Node Upgrade Guide Source: https://basehub.org/node-operations/azul-node-upgrade/ Description: Migrate a Base node to base-reth-node and base-consensus for the Azul hardfork, including the op-node to base-consensus environment variable mapping. import { Aside } from '@astrojs/starlight/components'; Azul is only supported by `base-reth-node` (execution) and `base-consensus` (consensus). **Nodes still running `op-node`, `op-geth`, `op-reth`, `nethermind`, or `kona` cannot follow the upgrade and have to be migrated.** Azul went live on mainnet at **May 28, 2026 18:00 UTC** (`1779991200`). The [Azul overview](/specifications/azul-overview/) carries the full activation-timestamp table. ## Required software | Layer | Software | Mainnet | Testnet | |-------|----------|---------|---------| | Execution (EL) | `base-reth-node` | [v0.9.0+](https://github.com/base/base/releases/tag/v0.9.0) | [v0.7.0+](https://github.com/base/base/releases/tag/v0.7.0) | | Consensus (CL) | `base-consensus` | [v0.9.0+](https://github.com/base/base/releases/tag/v0.9.0) | [v0.7.0+](https://github.com/base/base/releases/tag/v0.7.0) | ## Migrating the execution layer ### Coming from OP Reth Operators already on OP Reth via [base/node](https://github.com/base/node) only need to update — the latest version switches the binary to `base-reth-node` automatically, and your current `./reth-data` directory carries over with no re-sync or snapshot restore required. 1. Stop your node: ```bash docker compose down ``` 2. Update to the latest version of [base/node](https://github.com/base/node): ```bash git pull origin main ``` 3. Start your node: ```bash docker compose up ``` 4. Confirm the client version: `web3_clientVersion` should now carry `base` in the string (for example `reth/v1.11.3-.../base/v0.9.0`). ### Coming from another client `op-geth` and `nethermind` are no longer supported, so you will have to start over on `base-reth-node`. 1. Stop your node: ```bash docker compose down ``` 2. Update to the latest version of [base/node](https://github.com/base/node): ```bash git pull origin main ``` 3. Delete the old data directory (for example `./geth-data` or `./nethermind-data`). 4. Adjust `.env.mainnet` or `.env.sepolia` to your preferences. 5. Bootstrap from a [Reth snapshot](/node-operations/snapshots/) so you can skip a full sync. 6. Start your node: ```bash docker compose up ``` ## Migrating the consensus layer Swap `op-node` for `base-consensus` by updating environment variables. 1. Set `USE_BASE_CONSENSUS=true` in your `.env` file. 2. Add the new `BASE_NODE_*` environment variables (mapping below). 3. Restart your node: ```bash docker compose up ``` 4. Verify: - Tail the consensus logs: `docker compose logs -f node` - Confirm sync status: `optimism_syncStatus` still responds ### Environment variable mapping On [base/node](https://github.com/base/node) most of these are already populated in `.env.mainnet` and `.env.sepolia`. If you build from [base/base](https://github.com/base/base), use the table below to translate your `op-node` variables to `base-consensus`. Most are optional; run `base-consensus node --help` for the complete list. | `op-node` | `base-consensus` | |-----------|-------------------| | `OP_NODE_NETWORK` | `BASE_NODE_NETWORK` | | `OP_NODE_ROLLUP_CONFIG` | `BASE_NODE_ROLLUP_CONFIG` | | — | `BASE_NODE_LOG_VERBOSITY` | | — | `BASE_NODE_LOG_FORMAT` | | `OP_NODE_L1_ETH_RPC` | `BASE_NODE_L1_ETH_RPC` | | `OP_NODE_L1_BEACON` | `BASE_NODE_L1_BEACON` | | `OP_NODE_L1_TRUST_RPC` | `BASE_NODE_L1_TRUST_RPC` | | `OP_NODE_L2_ENGINE_RPC` | `BASE_NODE_L2_ENGINE_RPC` | | `OP_NODE_L2_ENGINE_AUTH` | `BASE_NODE_L2_ENGINE_AUTH` | | — | `BASE_NODE_L2_ENGINE_AUTH_ENCODED` | | `OP_NODE_P2P_BOOTNODES` | `BASE_NODE_P2P_BOOTNODES` | | `OP_NODE_P2P_LISTEN_IP` | `BASE_NODE_P2P_LISTEN_IP` | | `OP_NODE_P2P_LISTEN_TCP_PORT` | `BASE_NODE_P2P_LISTEN_TCP_PORT` | | `OP_NODE_P2P_LISTEN_UDP_PORT` | `BASE_NODE_P2P_LISTEN_UDP_PORT` | | `OP_NODE_P2P_ADVERTISE_IP` | `BASE_NODE_P2P_ADVERTISE_IP` | | `OP_NODE_P2P_ADVERTISE_TCP` | `BASE_NODE_P2P_ADVERTISE_TCP_PORT` | | `OP_NODE_P2P_ADVERTISE_UDP` | `BASE_NODE_P2P_ADVERTISE_UDP_PORT` | | `OP_NODE_P2P_PRIV_PATH` | `BASE_NODE_P2P_PRIV_PATH` | | `OP_NODE_P2P_PEER_SCORING` | `BASE_NODE_P2P_SCORING` | | `OP_NODE_P2P_PEER_BANNING` | `BASE_NODE_P2P_BAN_PEERS` | | `OP_NODE_P2P_PEER_BANNING_THRESHOLD` | `BASE_NODE_P2P_BAN_THRESHOLD` | | `OP_NODE_P2P_PEER_BANNING_DURATION` | `BASE_NODE_P2P_BAN_DURATION` | | `OP_NODE_METRICS_ENABLED` | `BASE_NODE_METRICS_ENABLED` | | `OP_NODE_METRICS_ADDR` | `BASE_NODE_METRICS_ADDR` | | `OP_NODE_METRICS_PORT` | `BASE_NODE_METRICS_PORT` | | `OP_NODE_RPC_ADDR` | `BASE_NODE_RPC_ADDR` | | `OP_NODE_RPC_PORT` | `BASE_NODE_RPC_PORT` | | `OP_NODE_RPC_ENABLE_ADMIN` | `BASE_NODE_RPC_ENABLE_ADMIN` | | `OP_NODE_RPC_ADMIN_STATE` | `BASE_NODE_RPC_ADMIN_STATE` | | `OP_NODE_SAFEDB_PATH` | `BASE_NODE_SAFEDB_PATH` | | `OP_NODE_SYNCMODE` | — | | `OP_NODE_VERIFIER_L1_CONFS` | — | | `OP_NODE_L2_ENGINE_KIND` | — | | `OP_NODE_L1_RPC_KIND` | — | | `OP_NODE_L1_BEACON_FETCH_ALL_SIDECARS` | — | | `OP_NODE_L1_BEACON_FALLBACKS` | — | | `OP_NODE_ROLLUP_LOAD_PROTOCOL_VERSIONS` | — | | `OP_NODE_P2P_STATIC` | — | | `OP_NODE_P2P_DISABLE` | — | | `OP_NODE_P2P_NAT` | — | ## FAQ - **Do I have to re-sync?** No, not if you are already on OP Reth — the existing data directory is compatible. - **What if I run `op-geth` or `nethermind`?** Move to `base-reth-node` and bootstrap from a [Reth snapshot](/node-operations/snapshots/). - **Do the OP namespace RPCs still work?** Yes — every existing RPC remains supported. --- # Base Azul Upgrade Source: https://basehub.org/node-operations/base-v1-upgrade/ Description: Migrate Base nodes to base-reth-node and base-consensus ahead of the Azul network upgrade, with environment variable mappings and timelines. import { Aside } from '@astrojs/starlight/components'; The Base Azul upgrade activates Osaka features on Base alongside the TEE/ZK proof systems and requires every node operator to migrate to Base-native clients. The full scope is documented in the [Azul specification](https://specs.base.org/upgrades/azul/overview). ## Activation timeline | Network | Date | Timestamp | |---------|------|-----------| | Mainnet | May 28, 2026 18:00 UTC | `1779991200` | | Sepolia | April 20, 2026 18:00 UTC | `1776708000` | ## Required software | Layer | Software | Mainnet | Testnet | |-------|----------|---------|---------| | Execution (EL) | `base-reth-node` | [v0.9.0+](https://github.com/base/base/releases/tag/v0.9.0) | [v0.7.0+](https://github.com/base/base/releases/tag/v0.7.0) | | Consensus (CL) | `base-consensus` | [v0.9.0+](https://github.com/base/base/releases/tag/v0.9.0) | [v0.7.0+](https://github.com/base/base/releases/tag/v0.7.0) | ## Migrating the execution layer ### From OP Reth If you already run OP Reth via [base/node](https://github.com/base/node), updating to the latest release switches you to `base-reth-node` automatically. Your existing `./reth-data` directory is fully compatible — no resync or snapshot restore required. 1. Stop the node: ```bash docker compose down ``` 2. Pull the latest [base/node](https://github.com/base/node): ```bash git pull origin main ``` 3. Start the node: ```bash docker compose up ``` 4. Confirm the client version with `web3_clientVersion` — the response should include `base` (for example, `reth/v1.11.3-.../base/v0.9.0`). ### From other clients `op-geth` and `nethermind` are no longer supported. You must restart from a clean `base-reth-node` setup. 1. Stop the node: ```bash docker compose down ``` 2. Pull the latest [base/node](https://github.com/base/node): ```bash git pull origin main ``` 3. Remove the existing client data directory (for example, `./geth-data` or `./nethermind-data`). 4. Edit `.env.mainnet` or `.env.sepolia` to match your preferences. 5. Bootstrap from a [Reth snapshot](/node-operations/snapshots/) to skip a full sync. 6. Start the node: ```bash docker compose up ``` ## Migrating the consensus layer Replace `op-node` with `base-consensus` by updating your environment variables. 1. Set `USE_BASE_CONSENSUS=true` in your `.env` file. 2. Update `.env` with the new `BASE_NODE_*` variables (see the mapping below). 3. Restart: ```bash docker compose up ``` 4. Verify: - Tail consensus logs: `docker compose logs -f node` - Confirm sync status: `optimism_syncStatus` continues to work ### Environment variable mapping If you use [base/node](https://github.com/base/node), most variables are already populated in `.env.mainnet` and `.env.sepolia`. If you build from [base/base](https://github.com/base/base), use the table below to translate `op-node` variables to `base-consensus`. Most are optional. Run `base-consensus node --help` for the full list. | `op-node` | `base-consensus` | |-----------|-------------------| | `OP_NODE_NETWORK` | `BASE_NODE_NETWORK` | | `OP_NODE_ROLLUP_CONFIG` | `BASE_NODE_ROLLUP_CONFIG` | | — | `BASE_NODE_LOG_VERBOSITY` | | — | `BASE_NODE_LOG_FORMAT` | | `OP_NODE_L1_ETH_RPC` | `BASE_NODE_L1_ETH_RPC` | | `OP_NODE_L1_BEACON` | `BASE_NODE_L1_BEACON` | | `OP_NODE_L1_TRUST_RPC` | `BASE_NODE_L1_TRUST_RPC` | | `OP_NODE_L2_ENGINE_RPC` | `BASE_NODE_L2_ENGINE_RPC` | | `OP_NODE_L2_ENGINE_AUTH` | `BASE_NODE_L2_ENGINE_AUTH` | | — | `BASE_NODE_L2_ENGINE_AUTH_ENCODED` | | `OP_NODE_P2P_BOOTNODES` | `BASE_NODE_P2P_BOOTNODES` | | `OP_NODE_P2P_LISTEN_IP` | `BASE_NODE_P2P_LISTEN_IP` | | `OP_NODE_P2P_LISTEN_TCP_PORT` | `BASE_NODE_P2P_LISTEN_TCP_PORT` | | `OP_NODE_P2P_LISTEN_UDP_PORT` | `BASE_NODE_P2P_LISTEN_UDP_PORT` | | `OP_NODE_P2P_ADVERTISE_IP` | `BASE_NODE_P2P_ADVERTISE_IP` | | `OP_NODE_P2P_ADVERTISE_TCP` | `BASE_NODE_P2P_ADVERTISE_TCP_PORT` | | `OP_NODE_P2P_ADVERTISE_UDP` | `BASE_NODE_P2P_ADVERTISE_UDP_PORT` | | `OP_NODE_P2P_PRIV_PATH` | `BASE_NODE_P2P_PRIV_PATH` | | `OP_NODE_P2P_PEER_SCORING` | `BASE_NODE_P2P_SCORING` | | `OP_NODE_P2P_PEER_BANNING` | `BASE_NODE_P2P_BAN_PEERS` | | `OP_NODE_P2P_PEER_BANNING_THRESHOLD` | `BASE_NODE_P2P_BAN_THRESHOLD` | | `OP_NODE_P2P_PEER_BANNING_DURATION` | `BASE_NODE_P2P_BAN_DURATION` | | `OP_NODE_METRICS_ENABLED` | `BASE_NODE_METRICS_ENABLED` | | `OP_NODE_METRICS_ADDR` | `BASE_NODE_METRICS_ADDR` | | `OP_NODE_METRICS_PORT` | `BASE_NODE_METRICS_PORT` | | `OP_NODE_RPC_ADDR` | `BASE_NODE_RPC_ADDR` | | `OP_NODE_RPC_PORT` | `BASE_NODE_RPC_PORT` | | `OP_NODE_RPC_ENABLE_ADMIN` | `BASE_NODE_RPC_ENABLE_ADMIN` | | `OP_NODE_RPC_ADMIN_STATE` | `BASE_NODE_RPC_ADMIN_STATE` | | `OP_NODE_SAFEDB_PATH` | `BASE_NODE_SAFEDB_PATH` | | `OP_NODE_SYNCMODE` | — | | `OP_NODE_VERIFIER_L1_CONFS` | — | | `OP_NODE_L2_ENGINE_KIND` | — | | `OP_NODE_L1_RPC_KIND` | — | | `OP_NODE_L1_BEACON_FETCH_ALL_SIDECARS` | — | | `OP_NODE_L1_BEACON_FALLBACKS` | — | | `OP_NODE_ROLLUP_LOAD_PROTOCOL_VERSIONS` | — | | `OP_NODE_P2P_STATIC` | — | | `OP_NODE_P2P_DISABLE` | — | | `OP_NODE_P2P_NAT` | — | ## FAQ - **Do I need to resync?** No, if you already run OP Reth — existing data is compatible. - **What if I'm on `op-geth` or `nethermind`?** Switch to `base-reth-node` and bootstrap from a [Reth snapshot](/node-operations/snapshots/). - **Do OP namespace RPCs still work?** Yes, all existing RPCs remain supported. --- # basectl Source: https://basehub.org/node-operations/basectl/ Description: Configure and inspect Base nodes across mainnet, sepolia, devnet, and custom configs. `basectl` configures and inspects Base nodes across mainnet, Sepolia, devnet, and custom config targets. It ships with the workspace and runs via `cargo` or the `just basectl` recipe. ## Usage ```bash # Build and run with a configuration target cargo run -p basectl --release -- -c ``` Supported configurations: | Config | Network | |--------|---------| | `mainnet` | Base Mainnet | | `sepolia` | Base Sepolia Testnet | | `devnet` | Local Development Network | | `` | Custom configuration file | ## Examples ```bash # Mainnet configuration just basectl mainnet # Sepolia testnet just basectl sepolia # Local devnet just basectl devnet ``` ## Architecture `basectl` is split into two crates: - **`basectl-cli`** ([`crates/infra/basectl`](https://github.com/base/base/tree/main/crates/infra/basectl)) — The library with configuration logic - **`basectl`** ([`bin/basectl`](https://github.com/base/base/tree/main/bin/basectl)) — The binary entry point The binary depends on `basectl-cli`, `clap` for argument parsing, `anyhow` for error handling, and `tokio` for async runtime. --- # Configuration Reference Source: https://basehub.org/node-operations/configuration/ Description: CLI flags, environment variables, and Cargo build profiles for base-reth-node. ## CLI Options The Base Reth Node inherits Reth's comprehensive CLI. View all options: ```bash ./target/release/base-reth-node --help ``` Key option categories: - **Network** — Chain selection, bootnodes, P2P port - **RPC** — HTTP/WS endpoints, CORS, API namespaces - **Database** — Data directory, state pruning - **Metrics** — Prometheus endpoint - **Engine API** — JWT secret, engine API endpoint - **Logging** — Log level, log format ## Environment Variables Configuration can also be set via environment variables. The devnet uses `etc/docker/devnet-env` for its environment. ## Build Profiles The workspace defines several Cargo build profiles: | Profile | Use Case | Key Settings | |---------|---------|-------------| | `dev` | Development | `debug = "line-tables-only"`, `incremental = false` | | `release` | Production | `opt-level = 3`, `lto = "thin"`, `strip = "symbols"` | | `maxperf` | Maximum performance | `lto = "fat"`, `codegen-units = 1` | | `profiling` | Performance analysis | `debug = "line-tables-only"`, `frame-pointers = true` | | `ci` | CI builds | `debug = false`, minimal disk usage | ## basectl The [`basectl`](/node-operations/basectl/) utility provides configuration management for different network targets: ```bash # Mainnet configuration cargo run -p basectl --release -- -c mainnet # Sepolia testnet cargo run -p basectl --release -- -c sepolia # Local devnet cargo run -p basectl --release -- -c devnet ``` --- # Deployment Source: https://basehub.org/node-operations/deployment/ Description: Run base-reth-node in production via the official Docker image or a custom source build. import { Aside } from '@astrojs/starlight/components'; ## Production Images The recommended way to run Base Reth Node in production is via the official Docker image from [base/node](https://github.com/base/node): ```bash docker pull ghcr.io/base/node-reth:latest ``` This image bundles both vanilla Reth and Base Reth and can be toggled with the `NODE_TYPE` environment variable: ```bash # Run Base Reth docker run ghcr.io/base/node-reth -e NODE_TYPE=base # Run vanilla Reth docker run ghcr.io/base/node-reth -e NODE_TYPE=vanilla ``` ## Building from Source for Deployment If you need a custom build: ```bash # Standard release build just build # Maximum performance (LTO fat, single codegen unit, jemalloc) just build-maxperf ``` The `maxperf` profile enables: - Full LTO (`lto = "fat"`) - Single codegen unit (`codegen-units = 1`) - jemalloc memory allocator The resulting binary is at `target/release/base-reth-node` (or `target/maxperf/base-reth-node` for maxperf). ## Hardware Requirements Base Reth Node requires: - **CPU:** Multi-core processor (4+ cores recommended) - **Memory:** 16 GB+ RAM - **Storage:** NVMe SSD with sufficient space for chain state - **Network:** Stable, high-bandwidth connection ## Configuration See the [Configuration Reference](/node-operations/configuration/) for all available options, or run: ```bash ./target/release/base-reth-node --help ``` --- # Docker Operations Source: https://basehub.org/node-operations/docker/ Description: Build and run base-reth-node, the builder, and the websocket proxy with Docker. ## Dockerfiles The repository provides several Dockerfiles in [`etc/docker/`](https://github.com/base/base/tree/main/etc/docker): | Dockerfile | Purpose | |-----------|---------| | `Dockerfile.client` | Main Base Reth Node client | | `Dockerfile.builder` | Block builder service | | `Dockerfile.devnet` | Local development network setup | | `Dockerfile.websocket-proxy` | WebSocket proxy for flashblock streaming | ## Building Images ### Client Node ```bash docker build -t base-reth-node -f etc/docker/Dockerfile.client . ``` ### Builder ```bash docker build -t base-builder -f etc/docker/Dockerfile.builder . ``` ### WebSocket Proxy ```bash docker build -t websocket-proxy -f etc/docker/Dockerfile.websocket-proxy . ``` ## Running Containers ```bash docker run -it --rm \ -p 8545:8545 \ -p 8546:8546 \ -p 30303:30303 \ -p 30303:30303/udp \ -p 9222:9222 \ -p 9222:9222/udp \ -v /path/to/data:/data \ base-reth-node [OPTIONS] ``` Common flags: - `-p 8545:8545` — HTTP JSON-RPC - `-p 8546:8546` — WebSocket JSON-RPC - `-p 30303:30303` (TCP/UDP) — discv4 P2P discovery and RLPx - `-p 9222:9222` (TCP/UDP) — Reth discv5 peer discovery - `-v /path/to/data:/data` — Persistent data volume ### Bootnode egress In addition to the ingress ports above, the node needs outbound access to the Base bootnodes for initial peer discovery. Operators running behind strict egress filtering must allow outgoing traffic to ports `30301` (TCP/UDP) and `9200` (UDP); without these, the node cannot bootstrap into the peer set. If egress is controlled with network ACLs rather than a stateful firewall (for example AWS security groups), open the ephemeral port range **32768–60999** (TCP/UDP) for outbound traffic as well. Stateful firewalls recognize and admit return packets on their own; ACLs do not, so omitting this range causes responses from peers to be dropped without warning. ## Docker Compose The repository includes a Docker Compose configuration for running a local devnet: ```bash docker compose --env-file etc/docker/devnet-env \ -f etc/docker/docker-compose.yml up -d --build ``` See [Local Devnet](/getting-started/devnet/) for the full devnet setup guide. ## Production Images For production use, the official images are published at: ``` ghcr.io/base/node-reth ``` See the [base/node repository](https://github.com/base/node) for release tags and configuration. --- # Monitoring Source: https://basehub.org/node-operations/monitoring/ Description: Prometheus metrics, devnet status commands, log streaming, and profiling for base-reth-node. ## Prometheus Metrics The Base Reth Node exports Prometheus-compatible metrics. The `base-cli-utils` crate sets up a metrics HTTP listener using `metrics-exporter-prometheus`. Key metric categories: - **Process metrics** — CPU, memory, file descriptors (via `metrics-process`) - **RPC metrics** — Request counts, latencies, error rates - **Transaction pool** — Pool size, pending/queued counts - **Sync progress** — Block height, peer count - **Flashblocks** — Flashblock production rate, latency ## Devnet Monitoring The devnet includes built-in monitoring commands: ```bash # Check block numbers and sync status across all nodes just devnet-status # View funded test accounts with balances and nonces just devnet-accounts # Run smoke tests (send test transactions to L1 and L2) just devnet-smoke # Full check suite just devnet-checks ``` ## Log Streaming Stream logs from devnet containers: ```bash # All containers just devnet-logs # Specific containers just devnet-logs l2-node builder ``` ## Profiling For performance profiling, build with the `profiling` profile: ```bash just devnet-profiling ``` This starts the devnet with Pyroscope integration and optimized builds that retain frame pointers for accurate profiling. The profiling profile settings: - Inherits from `release` - LTO disabled for faster builds - Debug info: line tables only - Frame pointers forced on - Symbols not stripped --- # Node Providers Source: https://basehub.org/node-operations/node-providers/ Description: Hosted RPC providers for Base Mainnet and Base Sepolia, including Coinbase Developer Platform, Alchemy, QuickNode, and others. A range of providers offer hosted RPC endpoints for Base Mainnet and Base Sepolia. Capabilities, networks, and pricing tiers vary — links below point to each provider's documentation and pricing pages. ## Coinbase Developer Platform (CDP) [CDP](https://portal.cdp.coinbase.com/) exposes an RPC endpoint that runs on the same node infrastructure powering Coinbase's retail exchange. The free tier is rate-limited and ready for early development. Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## 1RPC [1RPC](https://1rpc.io/) is an on-chain attested, privacy-preserving RPC that minimizes metadata exposure when interacting with chains. Free and [paid plans](https://www.1rpc.io/#pricing) are available. Supported networks: - Base Mainnet ## Alchemy [Alchemy](https://dashboard.alchemy.com/?utm_source=chain_partner&utm_medium=referral&utm_campaign=base) provides a developer platform with SDKs, [JSON-RPC APIs](https://docs.alchemy.com/reference/base-api-quickstart), and hosted Base mainnet and testnet nodes on a free tier. Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## All That Node [All That Node](https://www.allthatnode.com/base.dsrv) is a multi-chain development platform with free and [paid plans](https://www.allthatnode.com/pricing.dsrv). Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## Ankr [Ankr](https://www.ankr.com/rpc/base/) offers private and public Base RPC endpoints backed by a globally distributed node network. Free and [paid plans](https://www.ankr.com/rpc/pricing/) are available. Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## Blast [Blast](https://blastapi.io/public-api/base) provides decentralized blockchain APIs via partnerships with third-party node providers. Users can [generate dedicated Base RPC endpoints](https://blastapi.io/login). Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## Blockdaemon [Blockdaemon](https://www.blockdaemon.com/protocols/base/) hosts Base nodes with a free tier via the Ubiquity Data API Suite. Usage above the free tier incurs additional charges. Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## BlockPI [BlockPI](https://blockpi.io/) provides Base RPC access with [free and paid plans](https://docs.blockpi.io/pricing/pricing-and-rate-limit). Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## Chainstack [Chainstack](https://chainstack.com/build-better-with-base/) provisions elastic Base RPC nodes with personal, geographically diverse endpoints, plus archive nodes for full history queries. See the [free and paid plans](https://chainstack.com/pricing/). Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## dRPC NodeCloud [dRPC NodeCloud](https://drpc.org/nodecloud-multichain-rpc-management) offers Base Mainnet and Base Sepolia endpoints alongside 180+ networks, with smart routing, analytics, key management, and front-end protection. Backed by 40 providers across 9 geoclusters. Free tier with flat-rate pricing from $10. Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## GetBlock [GetBlock](https://getblock.io/nodes/base/) is a Blockchain-as-a-Service platform offering instant API access to full Base nodes with free, pay-per-use, and unlimited tiers. Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## NodeReal [NodeReal](https://nodereal.io/) supplies Base node API access for blockchain infrastructure use cases. Supported networks: - Base Mainnet ## Nodies DLB [Nodies DLB](https://nodies.app) operates high-performance RPC for Base and other OP Stack chains, with public free endpoints, pay-as-you-go, and enterprise plans. Supported networks: - Base Mainnet - Base Testnet (available on request) ## NOWNodes [NOWNodes](https://nownodes.io/nodes/basechain-base) provides shared and dedicated Base RPC access without rate limits. Supported networks: - Base Mainnet ## OnFinality [OnFinality](https://onfinality.io) offers high-performance archive access to Base Mainnet and Base Sepolia. The free tier carries generous limits, and Trace and Debug APIs are available on [paid plans](https://onfinality.io/pricing). Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## QuickNode [QuickNode](https://www.quicknode.com/chains/base) supports Base with Flashblocks and archive data, plus trace/debug capabilities for transaction analysis. Streams and Webhooks cover event-driven and data-streaming workloads, and the Marketplace includes add-ons such as the [Aerodrome Swap API](https://marketplace.quicknode.com/add-on/aerodrome-swap-api). See the [QuickNode Base docs](https://www.quicknode.com/docs/base) for setup. Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## RockX [RockX](https://access.rockx.com) operates a global node network with developer tooling. The free [Base RPC](https://access.rockx.com/product/base-blockchain-api-for-web3-builders) tier provides institutional-grade access. Supported networks: - Base Mainnet ## Stackup [Stackup](https://www.stackup.sh/) is an ERC-4337 infrastructure platform offering hosted Base nodes plus [account abstraction tooling](https://docs.stackup.sh/docs) such as bundlers and paymasters. Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## SubQuery [SubQuery](https://subquery.network/rpc) is a decentralized RPC network with public free endpoints and Flex Plans for higher access. Supported networks: - Base Mainnet ## Tenderly Web3 Gateway [Tenderly Web3 Gateway](https://tenderly.co/web3-gateway) is a hosted node solution with developer tooling spanning the full lifecycle — develop, test, deploy, and monitor on Base. [Free and paid plans](https://tenderly.co/pricing) are available. Supported networks: - Base Mainnet - Base Sepolia (Testnet) ## Unifra [Unifra](https://www.unifra.io) provides Web3 developer tooling, APIs, and Base node infrastructure. Supported networks: - Base Mainnet ## Validation Cloud [Validation Cloud](https://app.validationcloud.io/) offers low-latency Base RPC with 50 million compute units available on the free tier (no credit card required) and a scale tier without rate limits. Supported networks: - Base Mainnet --- # Performance Tuning Source: https://basehub.org/node-operations/performance-tuning/ Description: Hardware, storage, and client recommendations for high-performance Base nodes, including production configurations and sync guidance. import { Aside } from '@astrojs/starlight/components'; This guide covers the hardware, storage, and client choices that yield the best Base node performance. ## Hardware A performant Base node needs adequate hardware. Recommended minimums: 1. A modern multi-core CPU with strong single-core performance. 2. At least 32 GB of RAM (64 GB preferred). 3. A locally attached NVMe SSD. RAID 0 layouts can boost throughput. 4. Sufficient capacity, sized as: ``` (2 × [current chain size] + [snapshot size] + 20% buffer) ``` The formula reserves room for chain growth and snapshot restoration. See [Base Stats](https://base.org/stats) for the current chain size and [Base Snapshots](https://snapshots.base.org) for snapshot size. ### Production hardware Base production nodes use the following hardware: - **Reth archive node** - Instance: AWS `i7i.12xlarge` or larger - Storage: RAID 0 across all local NVMe drives (`/dev/nvme*`) - Filesystem: ext4 ## Initial sync Restore from a recent [snapshot](/node-operations/snapshots/) to dramatically shorten initial sync. ## Client software The [Base Node](https://github.com/base/node) repository carries the current stable client configurations and run instructions. Reth is currently the most performant Base client, and ongoing optimization work is concentrated there. Background on the move to Reth: [Scaling Base with Reth](https://blog.base.dev/scaling-base-with-reth). --- # Release Process Source: https://basehub.org/node-operations/release-process/ Description: How base/base releases are produced — Start Release, automatic RC builds on push, and Publish Release for the final tag. Releases of `base/base` are produced through two manual workflows and one automatic trigger: - **Start Release** — choose a bump type (major / minor / patch) and the release branch is created automatically. - **Publish Release** — choose a version and the final tag, Docker images, and binaries are published. - **Auto-RC** — every commit to a `releases/v*` branch (after the version sync PR is merged) automatically creates an RC tag and builds Docker images and binaries. ## Step by step ### 1. Start a release Run the **Start Release** workflow (`Actions → Start Release → Run workflow`): - Select the bump type: `minor` (new feature release), `patch` (bug fixes), or `major` (breaking changes). - The workflow computes the next version from the latest final tag and creates the `releases/vX.Y.Z` branch. - For `patch` bumps, the base is the most recent `releases/vX.Y.*` branch; for `major` and `minor`, the base is `main`. Once the branch is created, the **Release Version Sync** workflow fires automatically and opens a PR to update `Cargo.toml` to the new version. ### 2. Merge the version sync PR Review and merge the auto-generated version sync PR targeting the release branch. Merging unblocks auto-RC creation. ### 3. Build release candidates (automatic) Every commit pushed to the release branch triggers the **Create RC** workflow, which: - Skips silently if `Cargo.toml` is still `0.0.0` (the version sync PR has not been merged yet). - Otherwise creates the next RC tag (for example, `v0.6.0-rc.1`, `v0.6.0-rc.2`, …). - Builds multi-arch Docker images and native binaries. - Pushes the Docker image tagged with the RC tag only (not `latest`). To produce additional RCs, push more commits — bug fixes or backports — to the release branch. ### 4. Publish the final release Once you are satisfied with an RC, run the **Publish Release** workflow (`Actions → Publish Release → Run workflow`): - Enter the version number (for example, `0.6.0` — no `v` prefix, no `releases/` prefix). - The workflow validates that the release branch exists and `Cargo.toml` is not `0.0.0`. - Creates the final tag `vX.Y.Z` on the release branch. - Builds Docker images tagged as `vX.Y.Z`, `X.Y`, `X`, and `latest`. - Creates a draft GitHub release with auto-generated changelog and uploads binaries. - Review and publish the draft release on GitHub. ## Auto-RC behavior The **Create RC** workflow runs on every push to any `releases/v*` branch. It is safe to push before the version sync PR is merged — the workflow detects `0.0.0` and skips with a notice rather than failing. RC tags follow the pattern `vX.Y.Z-rc.N`, where `N` increments automatically based on existing tags. ## Quick reference | Action | Workflow | Trigger | Output | |--------|----------|---------|--------| | Create release branch | **Start Release** | Manual (bump type) | `releases/vX.Y.Z` branch | | Sync `Cargo.toml` version | **Release Version Sync** | Automatic on branch creation | PR targeting release branch | | Build RC | **Create RC** | Automatic on push to `releases/v*` | RC tag + Docker image + binaries | | Publish final release | **Publish Release** | Manual (version number) | Final tag + Docker images + draft GitHub release | ## Workflows - **Start Release** — Creates the release branch from a bump-type dropdown. - **Release Version Sync** — Opens a PR to update `Cargo.toml` when a release branch is created. - **Create RC** — Triggered on push to `releases/v*`; creates RC tags and builds artifacts. - **Build Release** — Reusable workflow (called by Create RC and Publish Release) that builds Docker images and binaries. - **Publish Release** — Manual workflow to create the final tag and publish the release. --- # Releases Source: https://basehub.org/node-operations/releases/ Description: Versioning, the pinned Reth tag, production image registry, and the GitHub Actions release flow. import { Aside } from '@astrojs/starlight/components'; ## Versioning The workspace version is `0.0.0` (development). All crates share the workspace version defined in the root `Cargo.toml`: ```toml [workspace.package] version = "0.0.0" edition = "2024" rust-version = "1.88" ``` ## Reth Version Base Reth Node tracks a specific Reth release. The current pinned version: ``` reth v1.11.0 ``` This is specified in the workspace dependencies as a git dependency with a tag: ```toml reth-db = { git = "https://github.com/paradigmxyz/reth", tag = "v1.11.0" } ``` ## Production Docker Images Production images are published at: ``` ghcr.io/base/node-reth ``` These are managed in the [base/node](https://github.com/base/node) repository and include both vanilla Reth and Base Reth, selectable via `NODE_TYPE`. ## Release Process Release branches follow the `releases/v` naming convention. The process uses GitHub Actions workflows: 1. **Release candidates** are created via the "Create Release" workflow with `rc` type. RC versions are auto-numbered (e.g., `v1.0.0-rc.1`, `v1.0.0-rc.2`). 2. **Final releases** are created with `final` type, which tags Docker images as `v1.0.0`, `1.0`, `1`, and `latest`. ### Workflows | Workflow | Trigger | Purpose | |----------|---------|---------| | Release Version Sync | Automatic | Updates `Cargo.toml` versions on release branches | | Create Release | Manual | Creates RC or final release tags | | Release | Tag push | Builds Docker images, creates GitHub release | ## Default Build Targets The workspace defines default members that are built by `cargo build`: - `bin/based` - `bin/node` (produces `base-reth-node`) - `bin/basectl` - `bin/builder` - `bin/consensus` - `bin/ingress-rpc` - `bin/audit-archiver` - `bin/mempool-rebroadcaster` --- # Snapshots Source: https://basehub.org/node-operations/snapshots/ Description: Restore Base nodes from official Reth and proofs snapshots for Base Mainnet and Base Sepolia to skip the initial sync window. import { Aside } from '@astrojs/starlight/components'; Restoring from a snapshot drastically reduces initial Base node sync time. Snapshots are refreshed regularly. ## Restoring from a snapshot These steps assume you are inside the cloned `node` directory (the one containing `docker-compose.yml`). 1. **Install aria2c.** Snapshot downloads rely on `aria2c` — a resumable downloader that gracefully recovers from the periodic connection drops Cloudflare imposes on long transfers. If it isn't already on your machine: ```bash # macOS brew install aria2 # Ubuntu / Debian sudo apt-get install aria2 ``` 2. **Prepare the data directory.** - **Before launching Docker for the first time**, create the host directory that will be mapped into the container. The path must match the `volumes` entry in `docker-compose.yml`: ```bash mkdir ./reth-data ``` - If a previous run left an existing data directory, **stop the node first** (`docker compose down`), wipe the contents (for example, `rm -rf ./reth-data/*`), then continue. 3. **Download the snapshot.** Pick the row that matches your network and client, and run the command from inside the `node` directory. | Network | Snapshot type | Download command | | -------- | -------------------- | ---------------- | | Testnet | Archive (recommended)| `aria2c -c -x 16 -s 16 "https://sepolia-reth-archive-snapshots.base.org/$(curl -s https://sepolia-reth-archive-snapshots.base.org/latest)"` | | Testnet | Pruned | `aria2c -c -x 16 -s 16 "https://sepolia-reth-pruned-snapshots.base.org/$(curl -s https://sepolia-reth-pruned-snapshots.base.org/latest)"` | | Mainnet | Archive (recommended)| `aria2c -c -x 16 -s 16 "https://mainnet-reth-archive-snapshots.base.org/$(curl -s https://mainnet-reth-archive-snapshots.base.org/latest)"` | | Mainnet | Pruned | `aria2c -c -x 16 -s 16 "https://mainnet-reth-pruned-snapshots.base.org/$(curl -s https://mainnet-reth-pruned-snapshots.base.org/latest)"` | 4. **Extract the snapshot.** Replace `snapshot-filename` with the actual download: ```bash tar -xzvf # For .tar.zst tar -I zstd -xvf ``` 5. **Move the data into place.** Extraction typically produces a `reth/` directory. - Move its contents into the data directory created in step 2: ```bash mv ./reth/* ./reth-data/ rm -rf ./reth # remove the now-empty extracted folder ``` - The chain data subdirectories (`chaindata`, `nodes`, `segments`, etc.) must sit directly under `./reth-data` — not in a nested folder. 6. **Start the node.** Return to the root of your Base node folder and bring it up: ```bash cd .. docker compose up --build ``` The node should pick up syncing from the last block recorded in the snapshot. 7. **Verify and clean up.** Tail the logs (`docker compose logs -f `) or use the [sync monitoring](/getting-started/running/) command to confirm the node starts at the snapshot's block height. Once verified, delete the downloaded archive (`.tar.gz`) to reclaim disk space. ## Proofs snapshots If you run the [historical proofs ExEx](/getting-started/running/), proof database snapshots are available so you can skip the 24-48 hour backfill. | Network | Download command | | ------- | ---------------- | | Testnet | `aria2c -c -x 16 -s 16 "https://sepolia-reth-proofs-snapshots.base.org/$(curl -s https://sepolia-reth-proofs-snapshots.base.org/latest)"` | | Mainnet | `aria2c -c -x 16 -s 16 "https://mainnet-reth-proofs-snapshots.base.org/$(curl -s https://mainnet-reth-proofs-snapshots.base.org/latest)"` | The restore process matches the steps above — follow [Restoring from a snapshot](#restoring-from-a-snapshot) using this archive. ## FAQ **Why does Base ship a Pruned snapshot instead of a Full node snapshot?** In Reth, "full" is not a separate node type — it is just a pruned node configured with a specific preset. Reth's `--full` preset keeps the most recent **10,064 blocks**, which is roughly 1.4 days on Ethereum but only about 5-6 hours on Base because of the faster block cadence. Base's _pruned_ snapshot instead uses a 31-day rolling retention window. If you want a smaller on-disk footprint, you can override `reth.toml` to match the 10,064-block preset. --- # Node Troubleshooting Source: https://basehub.org/node-operations/troubleshooting/ Description: Diagnose setup, sync, performance, snapshot, and networking issues for Base nodes running the official Docker setup. This guide covers the most common issues operators hit when running a Base node from the official [Docker setup](https://github.com/base/node), with steps to diagnose and resolve each one. ## General troubleshooting Before chasing specific issues, work through these baseline checks. 1. **Inspect container logs.** Tail the relevant container with `docker compose logs -f `: - L2 client (Reth): `docker compose logs -f execution` - Rollup node: `docker compose logs -f node`. Look for errors, warnings, or repeated messages. 2. **Check container status.** Confirm the right containers are running with `docker compose ps`. Restart loops or exited containers warrant a log review. 3. **Watch resource usage.** CPU, RAM, disk I/O, and network usage are common bottlenecks. `htop`, `iostat`, and `iftop` are useful starting points. 4. **Verify RPC endpoints.** Use `curl` to confirm the L2 client's RPC is responding (see [running a Base node](/getting-started/running/)). Confirm L1 endpoints are correct and reachable from the node host. 5. **Check the L1 node.** Ensure the configured L1 execution and consensus clients are fully synced, healthy, and accessible. L1 issues block L2 sync. ## Common issues ### Setup and configuration - **Issue:** The Docker command (`docker compose up ...`) fails. - **Check:** Is Docker (and Docker Compose) installed and the daemon running? - **Check:** Are you in the correct directory (the cloned `node` directory containing `docker-compose.yml`)? - **Check:** Command syntax — typos in `NETWORK_ENV` or `CLIENT` are common. - **Issue:** A container fails to start with `.env` or environment variable errors. - **Check:** Did you set the L1 endpoints (`OP_NODE_L1_ETH_RPC`, `OP_NODE_L1_BEACON`) in the right `.env` file (`.env.mainnet` or `.env.sepolia`)? - **Check:** Is `OP_NODE_L1_BEACON_ARCHIVER` set if your configuration or L1 node requires it? - **Check:** Is `OP_NODE_L1_RPC_KIND` set correctly for your L1 provider? - **Check:** (Reth) Are `RETH_CHAIN` and `RETH_SEQUENCER_HTTP` correctly configured in `.env`? - **Issue:** JWT secret or auth errors between `op-node` and the L2 client. - **Check:** Avoid hand-editing `OP_NODE_L2_ENGINE_AUTH` or the JWT file path (`$OP_NODE_L2_ENGINE_AUTH`) unless you know what you are doing — `docker-compose` handles this automatically. - **Issue:** Permission errors against the data volume (`./reth-data`). - **Check:** The user running `docker compose` must have write access to the cloned repo so Docker can write into `./reth-data`. Avoid `sudo`-running Docker commands; instead, add your user to the `docker` group. ### Syncing - **Issue:** The node never starts syncing or block height stalls. - **Check:** `op-node` logs for L1 endpoint or L2 client connection errors. - **Check:** Execution client logs for Engine API issues on port `8551` or P2P errors. - **Check:** L1 node health — is it accessible and fully synced? - **Check:** System time. Run `ntp` or `chrony`; significant clock drift breaks P2P. - **Issue:** Sync is unusually slow. - **Check:** Hardware against the [performance tuning](/node-operations/performance-tuning/) recommendations, especially RAM and **NVMe SSD**. Disk I/O is usually the bottleneck. - **Check:** L1 RPC responsiveness. A slow L1 node throttles L2 sync. - **Check:** Network bandwidth and connection quality. - **Check:** `op-node` and L2 client logs for performance warnings or errors. - **Issue:** `optimism_syncStatus` (port `7545` on `op-node`) reports a large time delta or errors. - **Action:** Inspect rollup node and L2 execution client logs around the time of the check to identify the cause (often L1 connectivity or L2 client problems). - **Issue:** `Error: nonce has already been used` when sending transactions. - **Cause:** The node is still catching up to the chain head. - **Action:** Wait for full sync. Track progress via `optimism_syncStatus` or logs. ### Performance - **Issue:** High CPU, RAM, or disk I/O usage. - **Check:** Hardware against the [performance tuning](/node-operations/performance-tuning/) recommendations, and upgrade if needed. Local NVMe SSDs are critical. - **Check:** Client logs for specific bottlenecks or errors. ### Snapshot restoration Refer to the [Snapshots](/node-operations/snapshots/) guide for the canonical procedure. - **Issue:** `wget` fails or the snapshot download is corrupted. - **Check:** Network connectivity. - **Check:** Available disk space. - **Action:** Retry the download and verify the URL. - **Issue:** `tar` extraction fails. - **Check:** Downloaded file integrity. - **Check:** Available disk space — extraction needs much more than the download itself. - **Check:** `tar` command syntax. - **Issue:** The node fails to start after restoring a snapshot, with database errors or missing files. - **Check:** Did you run `docker compose down` before touching the data directory? - **Check:** Did you remove the contents of the old data directory (`./reth-data/*`) before extracting and moving the new data? - **Check:** Is the chain data directly inside `./reth-data` rather than in a nested folder (for example, `./reth-data/reth/...`)? Verify the structure. - **Issue:** Out of disk space during download or extraction. - **Action:** Free space or attach a larger volume. Use the storage formula: ``` (2 * chain_size + snapshot_size + 20% buffer) ``` ### Networking and connectivity - **Issue:** RPC or WS connection refused (e.g., `curl localhost:8545` fails). - **Check:** Is the L2 client container running (`docker compose ps`)? - **Check:** Are you using the correct port (`8545` HTTP, `8546` WS by default)? - **Check:** L2 client logs — did the RPC server fail to start? - **Check:** Are `--http.addr` and `--ws.addr` set to `0.0.0.0` in the client config or entrypoint so external connections work within the Docker network? - **Issue:** Low peer count, or the node cannot connect to any peers at all. - **Check:** Ingress ports `30303` (TCP/UDP) and `9222` (TCP/UDP for Reth discv5) are reachable from the public internet through any host firewall and upstream network filtering. - **Check:** Outbound traffic to ports `30301` (TCP/UDP) and `9200` (UDP) is permitted. These are the Base bootnode endpoints — if egress to them is blocked, the node has no entry point into peer discovery and will report zero peers even when ingress rules look correct. - **Check:** Node logs for P2P errors. - **Action:** Behind NAT, set `--nat=extip:` via `ADDITIONAL_ARGS` in `.env` (see [advanced configuration](/getting-started/running/)). - **Issue:** Port conflicts in logs or `docker compose up` fails. - **Check:** Other host services using the default ports (`8545`, `8546`, `8551`, `6060`, `7545`, `30303`): ```bash sudo lsof -i -P -n | grep LISTEN sudo netstat -tulpn | grep LISTEN ``` - **Action:** Stop the conflicting service or remap the Base container ports in `docker-compose.yml`, then update the related variables (`$RPC_PORT`, `$WS_PORT`, etc.) in `.env`. ## Getting more help If you have worked through this guide and the issue persists: - **Discord:** Join the [Base Discord](https://discord.gg/buildonbase) and post in `#node-operators` with details of your setup, the issue, and relevant logs. - **GitHub:** Browse the [Base Node repository issues](https://github.com/base/node/issues), or open a new one if you suspect a bug. --- # Adding a New Network Upgrade Source: https://basehub.org/node-operations/upgrades/ Description: Every code change required to land a new Base network upgrade — config, traits, RollupConfig methods, EVM spec resolution, and precompile routing. import { Aside } from '@astrojs/starlight/components'; This guide walks through each code change needed to land a new network upgrade in [`base/base`](https://github.com/base/base). The work falls into two groups: edits required for every upgrade, and extra edits that apply only when the upgrade changes EVM execution rules. The Azul upgrade is the running example. Substitute `Azul`, `azul`, and `BASE_AZUL` with the actual upgrade name. For broader context on the codebase, see the [architecture overview](/architecture/overview/). ## Architecture overview Activation is wired through three layers: 1. **Config layer** — an optional per-upgrade activation timestamp lives in `UpgradeConfig`, which `RollupConfig` embeds; `RollupConfig` in turn surfaces the `is_X_active(timestamp)` helpers. 2. **Trait layer** — typed, generic activation checks come from the `BaseUpgrade` enum and the `BaseUpgrades` trait, shared alike by the consensus and execution layers. 3. **Execution layer** — an active upgrade is mapped to its EVM spec by `BaseSpecId`; `spec_by_timestamp_after_bedrock` and `RollupConfig::spec_id` decide which spec applies, and `BasePrecompiles` picks the matching precompile set. ## Part 1 — Required for every upgrade ### 1. Add the variant to the `BaseUpgrade` enum **File:** [`crates/common/chains/src/upgrade.rs`](https://github.com/base/base/blob/main/crates/common/chains/src/upgrade.rs) Within the `upgrade!` macro, add the new variant just after the current final entry: ```rust upgrade!( #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Default)] BaseUpgrade { // ... existing variants ... /// Jovian: Base network upgrade. Jovian, /// Azul: First Base-specific network upgrade. Azul, // <-- add here } ); ``` Then bump each chain config array method's length from `[(Self, ForkCondition); N]` to `N+1`, appending the new entry. Leave mainnet and sepolia on `ForkCondition::Never` until activation is scheduled; the generic devnet takes `ForkCondition::ZERO_TIMESTAMP`: ```rust pub const fn mainnet() -> [(Self, ForkCondition); 10] { [ // ... existing entries ... (Self::Azul, ForkCondition::Never), ] } pub const fn devnet() -> [(Self, ForkCondition); 10] { [ // ... existing entries ... (Self::Azul, ForkCondition::ZERO_TIMESTAMP), ] } ``` Finally, extend `check_base_upgrade_from_str` in the test module to cover the new upgrade variant. ### 2. Add the `BaseChainUpgrades` index arm **File:** [`crates/common/chains/src/chain.rs`](https://github.com/base/base/blob/main/crates/common/chains/src/chain.rs) Add `Azul` to the `use BaseUpgrade::{...}` import and add a match arm to `Index`: ```rust use BaseUpgrade::{ Azul, Bedrock, Canyon, Ecotone, Fjord, Granite, Holocene, Isthmus, Jovian, Regolith, }; impl Index for BaseChainUpgrades { fn index(&self, hf: BaseUpgrade) -> &Self::Output { match hf { // ... existing arms ... Jovian => &self.forks[Jovian.idx()].1, Azul => &self.forks[Azul.idx()].1, // <-- add } } } ``` ### 3. Add the config field and nested struct **File:** [`crates/common/genesis/src/chain/upgrade.rs`](https://github.com/base/base/blob/main/crates/common/genesis/src/chain/upgrade.rs) For standard upgrades (a flat timestamp field), add the field directly to `UpgradeConfig`: ```rust /// `azul_time` sets the activation time for the Base Azul network upgrade. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub azul_time: Option, ``` For namespaced upgrades that take the `{ "base": { "azul": } }` JSON shape, declare a sub-struct and embed it: ```rust /// Upgrade configuration for Base-specific upgrades. #[derive(Debug, Copy, Clone, Default, Hash, Eq, PartialEq)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] pub struct BaseUpgradeConfig { #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub azul: Option, } pub struct UpgradeConfig { // ... existing fields ... #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub base: Option, } ``` Add the new entry to `UpgradeConfig::iter()` as well, and re-export any newly added public types from `crates/common/genesis/src/chain/mod.rs` and `crates/common/genesis/src/lib.rs`. ### 4. Add activation methods to `RollupConfig` **File:** [`crates/common/genesis/src/rollup.rs`](https://github.com/base/base/blob/main/crates/common/genesis/src/rollup.rs) Add `is_X_active` and `is_first_X_block` right after the previous upgrade's methods. The pattern you follow turns on whether the new upgrade is **standalone** or **cascading**. **Standalone** (e.g. `pectra_blob_schedule`, `Azul`) — turned on in isolation, never inferred from a later upgrade being active. Reach for this pattern when the upgrade touches only protocol-level behavior and does not gate the upgrade that follows it: ```rust /// Returns true if Base Azul is active at the given timestamp. pub fn is_base_azul_active(&self, timestamp: u64) -> bool { self.upgrades.base.as_ref().and_then(|b| b.azul).is_some_and(|t| timestamp >= t) } /// Returns true if the timestamp marks the first Base Azul block. pub fn is_first_base_azul_block(&self, timestamp: u64) -> bool { self.is_base_azul_active(timestamp) && !self.is_base_azul_active(timestamp.saturating_sub(self.block_time)) } ``` Leave the previous terminal upgrade's `is_X_active` method as-is — no cascade is added. **Cascading** (e.g. `Canyon`, `Ecotone`, `Isthmus`) — whenever the new upgrade is active, the prior one counts as active too. Update the earlier terminal upgrade's method, then add the new method beside it: ```rust /// Returns true if Jovian is active at the given timestamp. pub fn is_jovian_active(&self, timestamp: u64) -> bool { self.upgrades.jovian_time.is_some_and(|t| timestamp >= t) || self.is_next_active(timestamp) // <-- cascade to next fork } /// Returns true if Next is active at the given timestamp. pub fn is_next_active(&self, timestamp: u64) -> bool { self.upgrades.next_time.is_some_and(|t| timestamp >= t) } ``` Add the new arm to `upgrade_activation` in `impl BaseUpgrades for RollupConfig` too. For a **standalone** upgrade, the prior arm retains `unwrap_or(ForkCondition::Never)`: ```rust BaseUpgrade::Jovian => self .upgrades .jovian_time .map(ForkCondition::Timestamp) .unwrap_or(ForkCondition::Never), // standalone: no cascade BaseUpgrade::Azul => self .upgrades .base .as_ref() .and_then(|b| b.azul) .map(ForkCondition::Timestamp) .unwrap_or(ForkCondition::Never), _ => ForkCondition::Never, // required: BaseUpgrade is #[non_exhaustive] ``` For a **cascading** upgrade, swap the prior arm's `unwrap_or(ForkCondition::Never)` for `.unwrap_or_else(|| self.upgrade_activation(BaseUpgrade::Next))`. ### 5. Add the trait method **File:** [`crates/common/chains/src/upgrades.rs`](https://github.com/base/base/blob/main/crates/common/chains/src/upgrades.rs) ```rust /// Returns `true` if [`Azul`](BaseUpgrade::Azul) is active at given block timestamp. fn is_azul_active_at_timestamp(&self, timestamp: u64) -> bool { self.upgrade_activation(BaseUpgrade::Azul).active_at_timestamp(timestamp) } ``` ### 6. Update timestamp constants and test fixtures **Files:** - [`crates/common/chains/src/upgrade.rs`](https://github.com/base/base/blob/main/crates/common/chains/src/upgrade.rs) (mainnet, sepolia, devnet constants) - [`crates/common/chains/src/lib.rs`](https://github.com/base/base/blob/main/crates/common/chains/src/lib.rs) - [`crates/common/chains/src/test_utils.rs`](https://github.com/base/base/blob/main/crates/common/chains/src/test_utils.rs) Add named constants once an activation timestamp is confirmed: ```rust // mainnet.rs /// Base Azul mainnet activation timestamp. pub const BASE_MAINNET_BASE_AZUL_TIMESTAMP: u64 = ; // sepolia.rs /// Base Azul sepolia activation timestamp. pub const BASE_SEPOLIA_BASE_AZUL_TIMESTAMP: u64 = ; ``` Re-export them from `lib.rs` next to the existing timestamp constants. Then update the `UpgradeConfig` literal in both registry fixture files: ```rust upgrades: UpgradeConfig { // ... existing fields ... jovian_time: Some(BASE_MAINNET_JOVIAN_TIMESTAMP), base: Some(BaseUpgradeConfig { azul: Some(BASE_MAINNET_BASE_AZUL_TIMESTAMP) }), }, ``` While the activation timestamp is still unconfirmed, keep `base: None` and hold the chain arrays at `ForkCondition::Never`. ### 7. Update the default rollup config **File:** [`crates/common/chains/src/test_utils.rs`](https://github.com/base/base/blob/main/crates/common/chains/src/test_utils.rs) The `default_rollup_config()` function activates every upgrade at genesis for dev use. Add the new upgrade: ```rust upgrades: UpgradeConfig { // ... existing fields ... jovian_time: Some(0), base: Some(BaseUpgradeConfig { azul: Some(0) }), }, ``` ### 8. Verify the upgrade consistency tests **File:** [`crates/common/chains/tests/upgrade_consistency.rs`](https://github.com/base/base/blob/main/crates/common/chains/tests/upgrade_consistency.rs) For every `BaseUpgrade` variant, these tests check that `BaseChainConfig::mainnet().upgrade_activation(fork)` agrees with `BaseChainUpgrades::mainnet().upgrade_activation(fork)`. No edits should be needed as long as both sides return `ForkCondition::Never` for an unscheduled upgrade — or, once scheduled, the same timestamp. If there is a known discrepancy (say, the cascade produces a mismatch for an unset upgrade), insert a skip with an explanatory comment, following the `Regolith` precedent: ```rust if *fork == BaseUpgrade::Azul { continue; // explanation of why the two sides differ } ``` ## Part 2 — Required when the upgrade changes EVM execution You can skip this part when the upgrade is purely protocol-level (batch decoding, derivation rules, system config) and adds no new EVM opcodes, precompile addresses, or gas-rule changes. ### 9. Add the `BaseSpecId` variant **File:** [`crates/common/evm/src/spec.rs`](https://github.com/base/base/blob/main/crates/common/evm/src/spec.rs) ```rust pub enum BaseSpecId { // ... existing variants ... JOVIAN, AZUL, // <-- add OSAKA, } ``` Touch `BaseSpecId::into_eth_spec()` only if the new Base upgrade shifts the paired Ethereum EL upgrade. Because `BaseSpecId` wraps `BaseUpgrade`, the variant must be added to `BaseUpgrade` first: ```rust BaseUpgrade::Isthmus | BaseUpgrade::Jovian => SpecId::PRAGUE, BaseUpgrade::Azul | BaseUpgrade::Beryl => SpecId::OSAKA, ``` Add the new `BaseUpgrade` variant with its canonical string name: ```rust /// Beryl upgrade. Beryl, ``` `BaseSpecId` parsing and display delegate to `BaseUpgrade`. ### 10. Route precompiles **File:** [`crates/common/precompiles/src/provider.rs`](https://github.com/base/base/blob/main/crates/common/precompiles/src/provider.rs) When the upgrade brings new precompiles, define a new method on `BasePrecompiles`; when it reuses the prior set, just widen the existing arm in `new_with_spec`: ```rust // Reuse previous precompile set BaseUpgrade::Azul | BaseUpgrade::Beryl => Self::azul(), // Or add a new set BaseUpgrade::Beryl => Self::beryl(), ``` ### 11. Update spec resolution **File:** [`crates/common/chains/src/upgrade.rs`](https://github.com/base/base/blob/main/crates/common/chains/src/upgrade.rs) Place the new upgrade as the leading check, since the newest active upgrade takes precedence: ```rust pub fn from_timestamp(chain_spec: impl Upgrades, timestamp: u64) -> Self { if chain_spec.is_beryl_active_at_timestamp(timestamp) { Self::Beryl } else if chain_spec.is_azul_active_at_timestamp(timestamp) { Self::Azul } else if chain_spec.is_jovian_active_at_timestamp(timestamp) { Self::Jovian } // ... remaining checks unchanged } ``` ### 12. Update the reth upgrade schedule builder **File:** [`crates/common/chains/src/chain.rs`](https://github.com/base/base/blob/main/crates/common/chains/src/chain.rs) Add the new upgrade to `to_chain_upgrades()`. Where it pairs with a new Ethereum upgrade (as Canyon does with Shanghai), push both entries; otherwise push only the Base upgrade entry: ```rust // No paired Ethereum upgrade forks.push((BaseUpgrade::Jovian.boxed(), self[BaseUpgrade::Jovian])); forks.push((BaseUpgrade::Azul.boxed(), self[BaseUpgrade::Azul])); // <-- add ``` ## Checklist ### Always required - [ ] `BaseUpgrade` variant added in `upgrade.rs`; all four chain arrays updated - [ ] `Index` arm added in `chain.rs` - [ ] Config field (flat or nested struct) added to `UpgradeConfig` in `upgrade.rs`; `iter()` updated; new types re-exported - [ ] `is_X_active` + `is_first_X_block` added to `RollupConfig`; `upgrade_activation` arm added; previous terminal upgrade cascades to new one (unless standalone) - [ ] `is_X_active_at_timestamp` added to `BaseUpgrades` trait - [ ] Timestamp constants added to chain config modules and re-exported from `lib.rs` - [ ] Registry fixtures (`test_utils/mod.rs`) updated - [ ] Default rollup config updated (`defaults.rs`) - [ ] Upgrade consistency tests pass ### Required when EVM execution changes - [ ] `BaseSpecId` variant added with `into_eth_spec` mapping and `#[strum(serialize = "...")]` attribute - [ ] Precompile match arm updated (or new precompile set added) - [ ] `spec_by_timestamp_after_bedrock` updated (`common/evm/src/spec.rs`) - [ ] `RollupConfig::spec_id` updated (`common/genesis/src/rollup.rs`) - [ ] `to_chain_upgrades` updated (`execution/chainspec/src/upgrades.rs`) ``` --- # Avoid Malicious Flags Source: https://basehub.org/security/avoid-malicious-flags/ Description: Practical steps to keep your Base app from being flagged as malicious — verify contracts, request verification, and follow UX best practices. Keep your app from being flagged as malicious by verifying contracts, requesting verification with trusted ecosystem sources, and following standard UX practices. ## 1. Verify and reduce the risk of your smart contract - **Verify the contract source.** Publish verified source code for your contracts on [block explorers](/network/block-explorers/). For example, verification is available on [Etherscan](https://etherscan.io/verifyContract) and [Basescan](https://basescan.org/verifyContract) under "Verify Contract". - **Limit user-fund exposure.** Design contracts to keep user funds out of unnecessary risk paths. Request only the minimum amount needed to complete each transaction. ## 2. Submit a verification request After verifying your contract source, submit a [verification request](https://report.blockaid.io/). Verification helps ecosystem signal providers recognize your app as safe. ## 3. Follow app best practices - **Accessibility across regions.** Avoid geo-blocking or other access restrictions that lock out specific regions or countries. If legal or compliance requirements force restrictions, note them in the verification request. - **Consistent behavior.** Avoid sudden or unexplained UI changes that erode user trust in your app's reliability. - **Transparent onchain interactions.** The onchain action should match the UI. A "Mint" button should clearly emit a mint transaction. - **Standard sign-in methods.** Offer the standard wallet connection options — WalletConnect, Coinbase Wallet SDK, and the popular browser extension wallets. - **Audit your contracts.** Have your contracts audited by a reputable firm, publish the report, and link to it from your app so users can find it. An audit signals that you have invested in securing the codebase. Following these recommendations significantly reduces the chance of your app being flagged as malicious and helps maintain a secure, trustworthy environment for users. --- **Still flagged?** Coinbase Wallet may produce false positives. Confirm you have completed the actions above first. If your app remains flagged as suspicious or malicious, [report it to Blockaid](https://report.blockaid.io/mistake). --- # Bug Bounty Source: https://basehub.org/security/bug-bounty/ Description: Coinbase's million-dollar HackerOne bug bounty program covers Base, the Base bridge contracts, and Base infrastructure. The Coinbase HackerOne bug bounty program covers the Base network, the Base bridge contracts, and Base infrastructure, with vulnerability reports triaged around the clock. ## Program coverage In line with Base's strategy of being the safest way for users to access crypto, Coinbase extends its [best-in-industry](https://www.coinbase.com/blog/celebrating-10-years-of-our-bug-bounty-program) million-dollar [HackerOne bug bounty program](https://hackerone.com/coinbase?type=team) to: - The Base network - The Base bridge contracts - Base infrastructure Coinbase's program runs alongside Optimism's existing [Immunefi Bedrock bounty program](https://immunefi.com/bounty/optimism/), which covers the open-source [Bedrock](https://docs.optimism.io/stack/getting-started) OP Stack framework. ## Submitting a report All potential vulnerabilities should be submitted via the [HackerOne platform](https://hackerone.com/coinbase). Reports are triaged by Coinbase engineers with relevant domain expertise, ensuring rapid SLAs and high-quality review. For program scope, eligibility, and reward details, see the [security program policies](https://hackerone.com/coinbase?view_policy=true). ## Related - [Reporting Vulnerabilities](/security/report-vulnerability/) — full procedure for submitting a vulnerability report. - [Security Council for Base](/security/security-council/) — the multi-party group that signs off on Base contract upgrades. --- # Reporting Vulnerabilities Source: https://basehub.org/security/report-vulnerability/ Description: How to report Base vulnerabilities — HackerOne for offchain services and infrastructure, Cantina for deployed smart contracts. Base routes vulnerability reports through two separate platforms so that each report reaches reviewers with the right domain expertise. Offchain components and services flow through Coinbase's long-running HackerOne program, while findings in deployed smart contracts go to a dedicated Cantina program. ## Bug bounty programs Consistent with Base's goal of being the safest way for users to access crypto, two complementary bounty programs cover the surface: - The Coinbase [HackerOne bug bounty program](https://hackerone.com/coinbase?type=team) — a [best-in-industry](https://www.coinbase.com/blog/celebrating-10-years-of-our-bug-bounty-program) million-dollar program — has been extended to cover the Base network and Base infrastructure. - A separate [Cantina bug bounty program](https://cantina.xyz/code/55316f42-3c5e-4746-9bd0-0f18dcbc344b), with a 5 million-dollar reward pool, covers every deployed smart contract used by Base and by Coinbase products and services. ## Where to submit a report Pick the platform that matches what you found: 1. [**HackerOne**](https://hackerone.com/coinbase) — for offchain components and services. Submissions are triaged around the clock by Coinbase engineers with the relevant domain expertise. The program's full [security policies](https://hackerone.com/coinbase?view_policy=true) describe scope, eligibility, and reward details. 2. [**Cantina**](https://cantina.xyz/bounties/55316f42-3c5e-4746-9bd0-0f18dcbc344b) — for deployed smart contracts. The covered contracts are enumerated in the program's [Tier 0](https://cantina.xyz/code/55316f42-3c5e-4746-9bd0-0f18dcbc344b/overview?overviewTab=1&assetGroup=0) and [Tier 1](https://cantina.xyz/code/55316f42-3c5e-4746-9bd0-0f18dcbc344b/overview?overviewTab=1&assetGroup=1) scope guides. For any other security-related question that does not fit either bounty program, reach the team at [security@coinbase.com](mailto:security@coinbase.com). --- # Security Council for Base Source: https://basehub.org/security/security-council/ Description: Composition, quorum rules, member criteria, and responsibilities of the Security Council that approves Base contract upgrades. The Security Council for Base is the multi-party group of independent entities and individuals whose signatures are required to approve Base Chain contract upgrades. ## Purpose Base's mission is to build a global onchain economy that increases innovation, creativity, and freedom. That mission is only possible on a decentralized platform. Base is therefore (1) built on Ethereum, the most secure and decentralized L1; (2) built in the [open](https://github.com/base/base); and (3) committed to a clear set of [Neutrality Principles](https://www.coinbase.com/blog/coinbases-neutrality-principles-for-base). As part of the ongoing decentralization roadmap, Base launched [permissionless fault proofs](https://base.mirror.xyz/eOsedW4tm8MU5OhdGK107A9wsn-aU7MAb8f3edgX5Tk) in October 2024, [decentralized control of contract upgrades](https://base.mirror.xyz/tWDMlGp48fF0MeADcLQruUBq1Qxkou4O5x3ax8Rm3jA) via a Security Council in April 2025, and has now reached Stage 1 Decentralization. Reaching **Stage 1** means Base offers stronger security guarantees and fewer trust assumptions. Builders gain infrastructure certainty (no surprise rule changes), and there is no single point of failure — anyone can participate in verifying and securing the network. ## Structure **Composition and quorum** Stage 1 Decentralization requires that the group approving Base Chain contract upgrades contain: - at least 8 participants - a ≥75% quorum - a quorum-blocking group outside the main rollup operator (Base) In addition to Coinbase, the Security Council includes 11 independent entities and individuals from geographically diverse regions. To meet the ≥75% quorum, 9 of the 12 entities (the 11 Security Council members plus Coinbase) must approve any Base upgrade. Upgrades cannot take effect until that quorum signs and approves them. This composition meets all requirements. **Member selection criteria** - Representation across diverse geographic regions and international territories - Strong alignment with [Base's mission and values](https://base.mirror.xyz/jjQnUq_UNTQOk7psnGBFOsShi7FlrRp8xevQUipG_Gk) - Diverse organizations — each member represents a separate entity - Proven track record in the Base and Ethereum ecosystem and good standing in upholding professional and ethical standards in the community - Technical competency and good security practices — completed screening processes including background checks, with demonstrated ability to securely store and use sensitive key material **Current roster** This list stays current with membership. As of February 2026, the Security Council includes the following entities and individuals, based in the listed jurisdictions: - [Entity] Aerodrome — signer based in Japan - [Aerodrome](https://aerodrome.finance/) is a decentralized exchange on Base where users swap, earn rewards, and participate in the onchain economy. - `0xa5959a39cA67b9fb473E4A3A898C611EEAc9CB73` - [Entity] Moonwell — signer based in Brazil - [Moonwell](https://moonwell.fi/) is a decentralized lending and borrowing platform built on Base. - `0x21C7D1e6A81Daca071bA94839ab74C39A25f851F` - [Entity] Blackbird — signer based in USA - [Blackbird](https://www.blackbird.xyz/) is a loyalty and payments platform built specifically for the restaurant industry, powered by Base. - `0xA5657B88A0130a626fcDd6aAA59522373438CdFE` - [Entity] ChainSafe — signer based in Canada - [ChainSafe](https://chainsafe.io/) is a blockchain R&D firm focused on decentralized infrastructure. - `0x1C56A6d2A6Af643cea4E62e72B75B9bDe8d62e2B` - [Entity] Talent Protocol — signer based in Portugal - [Talent Protocol](https://app.talentprotocol.com/) brings professional reputation onchain to help Base builders showcase their skills and get the recognition they deserve. - `0x5ff5C78ff194acc24C22DAaDdE4D639ebF18ACC6` - [Entity] Moshicam — signer based in USA - [Moshicam](https://moshi.cam/) is a community-based photo editing app built on Base. - `0xa8ee754FD1d069fb4B5d652730A0ca5e07a3fb06` - [Individual] Seneca — based in USA - Seneca is the co-founder of [Rounds](https://rounds.wtf/), a social platform that has [powered](https://x.com/jessepollak/status/1781069700652523725) grant distribution to Base builders. - `0x82C80F34C4b5c153dB76122a11AaD2F77C99E766` - [Individual] Juan Suarez — based in USA - Juan is an active member of the Base ecosystem and has advised a number of key Base projects. He is a former member of the Coinbase Legal Team. - `0x99DB5BbA0db16e9aD05e3ff53310683CC3C971D2` - [Individual] Toady Hawk — based in Canada - [Toady Hawk](https://farcaster.xyz/toadyhawk.eth) is the founder of [Zero Rights Media](https://farcaster.xyz/zerorightsmedia), an open-source onchain media org on Base (producers of ZEROPOD), and [The Yellow Collective](https://farcaster.xyz/basedandyellow), an onchain culture club for artists and creators on Base. - `0x0E8A99738a50D523871739c6d676554b0E34252f` - [Individual] Roberto Bayardo — based in USA - [Roberto Bayardo](https://farcaster.xyz/bayardo.eth) is an engineer at Commonware, building a framework for high-performance blockchains. He is a former core Base contributor. - `0x18e982274f8C5B548D5aAc7aBef44D61504e1b3E` - [Individual] Yele Bademosi — based in the UK - [Yele Bademosi](https://x.com/YeleBademosi) is the co-founder of [Onboard](https://www.onboard.xyz/) and a longtime builder in the Base ecosystem, previously founding Bundle and investing in early-stage technology companies via Microtraction. The individual representatives for each entity are not published, to protect personal privacy and improve security. **Member terms** The Security Council for Base operates on a staggered "cohort" model: - Cohort 1 — current term lasts until October 2026 - Cohort 2 — current term lasts until January 2027 ## Roles and responsibilities **Review and approve changes** - Members are notified of proposed upgrades and must verify, approve, and sign them. - Members must verify, approve, and sign role changes (key rotations for lost devices, member rotations, and similar). Any key rotations are designed not to disrupt quorum or security. **Maintain availability and lines of communication** - Be reachable for scheduled signings, coordination calls, and emergencies. - Coordinate with other members to resolve urgent issues. **Preserve key security** - Generate and store key material securely. - Use the keys only for activities directly tied to the Security Council role (upgrades and ownership changes). - Report any suspected loss of access or compromise immediately. - Complete onchain safety and security training at the start of each term. - Participate in periodic liveness checks by signing a message to confirm continued control of the key. **Act in good faith** - Avoid conflicts of interest and disclose any potential conflicts. - Help remove or replace dysfunctional signers without compromising security. ## The future The Security Council for Base is a meaningful step toward a more decentralized, resilient, and secure Base network. Distributing key responsibilities across trusted, independent participants — combined with permissionless fault proofs — reduces reliance on any single entity and strengthens guarantees for users, builders, and the broader ecosystem. This is the start, not the end. As Base evolves, the Security Council's role will be progressively minimized, opening the path to even more trustless infrastructure beyond Stage 1 — Stage 2 — and broader community control. The mission stays the same: build a global onchain economy that empowers innovation, creativity, and freedom on a foundation everyone can rely on. Base is for everyone. --- # Access Lists Specification Source: https://basehub.org/specifications/access-lists/ Description: Flashblock-Level Access Lists (FAL) — adapting EIP-7928 for flashblock chains. Flashblock-Level Access Lists (FAL) adapt [EIP-7928](https://eips.ethereum.org/EIPS/eip-7928) Block-Level Access Lists for chains that emit flashblocks, including Base. This specification defines their data structures, recording semantics, and validation rules. :::note The canonical source is [`docs/specs/access-lists.md`](https://github.com/base/base/blob/main/docs/specs/access-lists.md) in the base/base repository. ::: ## Abstract FAL records every account and storage location accessed during flashblock execution, along with post-execution values. This enables parallel disk reads, parallel transaction validation, and state reconstruction without full re-execution. ## Motivation Standard BAL (EIP-7928) targets L1 Ethereum where blocks are atomic units. Base produces **flashblocks** -- incremental sub-block deltas streamed at 200ms intervals. FAL adapts the access-list concept to this architecture by: - Operating on incremental flashblock deltas rather than complete blocks. - Handling OP Stack-specific transaction types (L1 attributes, L1-to-L2 deposits). - Tracking three separate fee vaults instead of a single `COINBASE`. - Omitting beacon chain features absent from the OP Stack (EIP-4895 withdrawals, EIP-4788 beacon root, EIP-7002/7251 validator operations). ## Key Differences from BAL | Aspect | BAL (EIP-7928) | FAL | |--------|----------------|-----| | Unit of operation | Complete block | Flashblock delta (incremental) | | Hash storage | Block header | Flashblock metadata | | Fee recipient | Single `COINBASE` | Three OP Stack fee vaults | | Transaction types | Standard + withdrawals | Standard + L1 attributes + L1-to-L2 deposits | | Beacon chain features | Included | Omitted | ### OP Stack Fee Vaults Instead of recording balance changes against `COINBASE`, FAL tracks three separate fee vault addresses: | Vault | Address | Purpose | |-------|---------|---------| | Sequencer Fee Vault | `0x4200000000000000000000000000000000000011` | Priority fees | | Base Fee Vault | `0x4200000000000000000000000000000000000019` | Base fees | | L1 Fee Vault | `0x420000000000000000000000000000000000001a` | L1 data fees | ### L1 Attributes Transaction The first transaction in each block (index 0) is the L1 attributes transaction. It updates the L1Block contract (`0x4200000000000000000000000000000000000015`) storage slots: | Slot | Content | |------|---------| | 0 | L1 block number | | 1 | L1 base fee | | 2 | L1 timestamp | | 3 | Batcher hash | | 4 | Sequence number | | 5 | Blob base fee | | 6 | L1 block hash | ### L1-to-L2 User Deposits L1-to-L2 deposits are treated as regular transactions with block access indices `1..n`. The FAL records sender and recipient addresses with their balance changes. --- ## Specification ### Data Structures #### FlashblocksPayloadV1 ```rust pub struct FlashblocksPayloadV1 { pub payload_id: PayloadId, pub index: u64, pub base: Option, pub diff: ExecutionPayloadFlashblockDeltaV1, pub metadata: Value, // Contains "flashblock_access_list" } ``` #### FlashblockAccessList ```rust pub struct FlashblockAccessList { pub min_tx_index: u64, // Inclusive starting transaction index pub max_tx_index: u64, // Exclusive ending transaction index pub account_changes: Vec, // All account state changes pub fal_hash: B256, // Keccak-256 of RLP-encoded changes } ``` ### RLP Data Structures The following Python-style type aliases define the RLP encoding: ```python Address = bytes # 20-byte Ethereum address StorageKey = bytes # 32-byte storage slot key StorageValue = bytes # 32-byte storage value CodeData = bytes # Variable-length bytecode BlockAccessIndex = uint16 Balance = uint256 Nonce = uint64 # Constants (OP Stack adapted) MAX_TXS = 30_000 MAX_SLOTS = 300_000 MAX_ACCOUNTS = 300_000 MAX_CODE_SIZE = 24_576 MAX_CODE_CHANGES = 1 # Change structures StorageChange = [BlockAccessIndex, StorageValue] BalanceChange = [BlockAccessIndex, Balance] NonceChange = [BlockAccessIndex, Nonce] CodeChange = [BlockAccessIndex, CodeData] SlotChanges = [StorageKey, List[StorageChange]] AccountChanges = [ Address, List[SlotChanges], # storage_changes List[StorageKey], # storage_reads List[BalanceChange], # balance_changes List[NonceChange], # nonce_changes List[CodeChange] # code_changes ] FlashblockAccessList = List[AccountChanges] ``` ### Scope and Inclusion Rules The following must be included in the `FlashblockAccessList`: - **Addresses with state changes** -- any account whose storage, balance, nonce, or code was modified. - **Inspection targets** -- targets of `BALANCE`, `EXTCODESIZE`, `EXTCODECOPY`, `EXTCODEHASH`. - **Call targets** -- targets of `CALL`, `CALLCODE`, `DELEGATECALL`, `STATICCALL`. - **Creation targets** -- addresses produced by `CREATE` and `CREATE2`. - **Transaction parties** -- senders and recipients of every transaction. - **Fee vault addresses** -- all three OP Stack fee vaults listed above. - **`SELFDESTRUCT` beneficiaries** -- addresses receiving self-destructed balances. - **System contracts** -- L1Block contract (`0x4200...0015`) and EIP-2935 block hash contract. - **L1-to-L2 deposit recipients** -- recipients of cross-chain deposits. - **Precompiled contracts** -- when accessed during execution. - **L1Block during L1 attributes execution** -- always included in the first flashblock. Addresses without state changes must still appear with empty change lists. ### Ordering and Determinism To ensure deterministic encoding: - **Addresses** are sorted lexicographically (bytewise ascending). - **Storage keys** are sorted lexicographically within each account. - **Block access indices** are sorted in ascending order within each change list. ### BlockAccessIndex Assignment | Index Value | Assigned To | |-------------|-------------| | `0` | Pre-execution system contracts and L1 attributes transaction (first flashblock only, where `min_tx_index = 0`). | | `min_tx_index` ... `max_tx_index - 1` | Transactions within the flashblock, including L1-to-L2 deposits and regular transactions. | | `n + 1` | Post-execution system contracts (final flashblock only). | ### Recording Semantics #### Storage **Writes** (recorded in `storage_changes`): - Any value change where the post-execution value differs from the pre-execution value. - Zeroing a slot (pre-execution value exists, post-execution value is zero). **Reads** (recorded in `storage_reads`): - `SLOAD` without a corresponding write. - No-op writes where `SSTORE` is called but the post-execution value equals the pre-execution value. #### Balance Changes Post-transaction balances are recorded for: - **Senders** -- after gas cost, transferred value, and L1 data fee are deducted (including gas refunds). - **Recipients** -- only when transferred value is greater than zero. - **Fee vaults** -- updated balance after each transaction. - **`SELFDESTRUCT` / `SENDALL` beneficiaries** -- balance after receiving funds. - **Deposit recipients** -- balance after the deposit is credited. For zero-value transfers, the address is included but omitted from `balance_changes`. #### Code Changes Post-transaction runtime bytecode is recorded for: - Contracts deployed via `CREATE` or `CREATE2`. - Contracts modified via `SELFDESTRUCT` (if applicable). - EIP-7702 delegation indicators. #### Nonce Changes Post-transaction nonces are recorded for: - EOA senders (incremented by transaction execution). - Successful `CREATE` performers (nonce incremented). - Newly deployed contracts (nonce set to 1). - EIP-7702 authorities (nonce incremented on successful delegation). ### OP Stack Edge Cases - **Precompiles**: Include when accessed. Record a balance change if the precompile receives value. - **`SENDALL`**: Record both sender (balance to zero) and beneficiary (balance increased). - **`SELFDESTRUCT`**: Include without nonce/code changes. Record balance-to-zero if pre-transaction balance was positive. Include storage reads. - **Gas refunds**: The final sender balance (after refunds) is the recorded value. - **Exceptional halts (reverts/out-of-gas)**: Record the final nonce and balance of the sender and fee vaults. Include all accessed addresses even if execution was reverted. - **EIP-7702**: The authority address must be included with nonce/code changes after a successful delegation operation, and with empty changes if the delegation fails. ### Empty State When no state changes exist in a flashblock: - `account_changes` is an empty list. - `fal_hash` equals the Keccak-256 of an empty RLP list: `0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347`. ### Validation The following pseudocode describes FAL validation: ```python def validate_fal(flashblock, provided_fal): # 1. Extract the FAL from flashblock metadata fal = extract_fal(flashblock.metadata) # 2. Verify the provided hash matches the encoded account_changes encoded = rlp_encode(fal.account_changes) expected_hash = keccak256(encoded) assert fal.fal_hash == expected_hash, "FAL hash mismatch" # 3. Execute the flashblock and collect actual accesses actual_accesses = execute_flashblock(flashblock) actual_encoded = rlp_encode(actual_accesses) actual_hash = keccak256(actual_encoded) # 4. Verify actual execution matches the provided FAL assert actual_hash == fal.fal_hash, "Execution mismatch" return True ``` --- ## Performance FAL enables the following optimizations: - **Parallel disk reads** -- storage slots and account data can be prefetched across transactions. - **Parallel transaction validation** -- independent transactions can be validated concurrently. - **State reconstruction without execution** -- post-state can be derived from the access list alone. - **Compact representation** -- expected size is approximately 40--50 KiB compressed on average, comparable to BAL. ## Source Canonical specification: [docs/specs/access-lists.md](https://github.com/base/base/blob/main/docs/specs/access-lists.md) --- # Azul Execution Engine Source: https://basehub.org/specifications/azul-exec-engine/ Description: Execution-engine changes in the Azul hardfork — the EIP-7825 transaction gas cap, Osaka-era EVM and precompile updates, eth/69 networking, a slimmed Flashblocks payload, and the eth_config RPC method. import { Aside } from '@astrojs/starlight/components'; Azul layers a batch of Osaka-era execution rules onto Base while keeping the chain in lockstep with L1 pricing and behavior. The changes fall into three buckets: EVM-level adjustments (a per-transaction gas cap, MODEXP limits, a new opcode, and precompile repricing), networking updates (the eth/69 wire protocol and a leaner Flashblocks message), and RPC additions (a fixed Engine API method set and the new `eth_config` endpoint). ## EVM Changes ### Transaction Gas Limit Cap [EIP-7825](https://eips.ethereum.org/EIPS/eip-7825) sets a protocol-enforced ceiling of 16,777,216 (2^24) gas on any single transaction. Anything asking for more than that is turned away at validation time. Base matches the L1 ceiling so the two layers stay equivalent. ### Upper-Bound MODEXP [EIP-7823](https://eips.ethereum.org/EIPS/eip-7823) limits each MODEXP precompile input field to at most 1024 bytes; calls that exceed that size are rejected. ### MODEXP Gas Cost Increase [EIP-7883](https://eips.ethereum.org/EIPS/eip-7883) lifts the MODEXP precompile floor from 200 to 500 gas and triples the result of the general cost formula. ### CLZ Opcode [EIP-7939](https://eips.ethereum.org/EIPS/eip-7939) adds the `CLZ` opcode, which counts the leading zero bits in a 256-bit word and returns 256 when the input is zero. ### secp256r1 Precompile Gas Cost [EIP-7951](https://eips.ethereum.org/EIPS/eip-7951) places the secp256r1 precompile at address `0x100` with a gas cost of 3,450. Base has shipped this functionality since Fjord, where the `p256Verify` precompile was added at the same `0x100` address via [RIP-7212](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md), also priced at 3,450 gas. Starting with Azul the cost rises to 6,900 to match the L1 figure in EIP-7951, keeping Base's precompile pricing strictly aligned with Ethereum. ## Networking Changes ### eth/69 [EIP-7642](https://eips.ethereum.org/EIPS/eip-7642) bumps the Ethereum wire protocol to version 69. It strips legacy fields out of the `Status` message and streamlines the handshake. ### Discovery protocol now uses `basev0` protocol ID The execution-layer discovery protocol now advertises `basev0` as its protocol ID. Scoping discovery this way lets Base nodes locate one another faster, which matters most on smaller networks such as Sepolia where peers are scarcer. ### Remove Account Balances & Receipts Azul trims the `FlashblocksMetadata` payload sent over the Flashblocks WebSocket. The `new_account_balances` and `receipts` fields are dropped. The `access_list` field stays in the schema but is left unpopulated in Azul. **Before:** ```json { "block_number": 43403718, "new_account_balances": { "0x4200000000000000000000000000000000000006": "0x35277a9715c6df1c99de" }, "receipts": { "0x1ef9be45b3f7d44de9d98767ddb7c0e330b21777b67a3c79d469be9ffab091dd": { "cumulativeGasUsed": "0x177d7bd", "logs": [], "status": "0x1", "type": "0x2" } }, "access_list": null } ``` **After:** ```json { "block_number": 43403718, "access_list": null } ``` ## RPC Changes ### Engine API Usage From Azul activation onward, block production and import flow through a fixed set of Engine API methods: - `engine_forkchoiceUpdatedV3` to kick off block builds and synchronize forkchoice. - `engine_getPayloadV5` to retrieve built payloads. - `engine_newPayloadV4` to import payloads into the execution engine. `engine_getPayloadV5` hands back a V5 envelope, but the execution payload inside it keeps its V4 shape. Because of that, payloads are still inserted through `engine_newPayloadV4` — Base Azul clients never use an `engine_newPayloadV5` path. The flow operates under these Azul constraints: - Blob-related Engine API inputs are pinned to empty values: - `expectedBlobVersionedHashes` MUST be an empty array. - `blobsBundle` in `engine_getPayloadV5` responses is expected to be empty. - `executionRequests` in `engine_newPayloadV4` MUST be an empty array. ### eth_config RPC Method [EIP-7910](https://eips.ethereum.org/EIPS/eip-7910) adds the `eth_config` JSON-RPC method, a way to surface a chain's configuration — fork activation timestamps among other parameters. Base Azul answers `eth_config` calls with the standard EIP-7910 response schema, subject to a few Base-specific details: - `blobSchedule` always comes back with `current`, `next`, and `last` zeroed out. Since Base carries no native blob transactions, it deliberately avoids reporting Ethereum's synthetic blob-schedule defaults. - `precompiles` lists the active EVM precompile set for that fork — the standard Ethereum precompiles plus any Base-specific additions. - `systemContracts` only contains entries representable under EIP-7910. On Base that means: - `BEACON_ROOTS_ADDRESS` appears once Ecotone is active. - `HISTORY_STORAGE_ADDRESS` appears once Isthmus is active. - `DEPOSIT_CONTRACT_ADDRESS`, `CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS`, and `WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS` are omitted. Base-specific predeploys and the other OP Stack system contracts are not serialized into `eth_config` unless the EIP-7910 schema covers them. --- # Azul Upgrade Overview Source: https://basehub.org/specifications/azul-overview/ Description: Summary of the Azul hardfork — Osaka EVM features, a streamlined Flashblocks websocket format, and a multi-proof system for L2 checkpoints. import { Aside } from '@astrojs/starlight/components'; Azul is the first Base-specific hardfork. It folds Osaka-era EVM changes onto the Base execution layer, trims the Flashblocks websocket payload, and turns on a multi-proof system that shortens withdrawal latency and lays the groundwork for stronger decentralization. It also drops support for non-Base clients. ## What Azul ships - Osaka EVM support on Base. - A simplified Flashblocks websocket message format. - A multi-proof system that unlocks faster withdrawals and a credible path toward decentralization. - Base-native clients (`base-reth-node` + `base-consensus`) as the only supported stack. ## Activation timestamps | Network | Activation timestamp | |---------|----------------------| | `mainnet` | `1779991200` (2026-05-28 18:00:00 UTC) | | `sepolia` | `1776708000` (2026-04-20 18:00:00 UTC) | Both timestamps are now confirmed: Azul shipped to Sepolia first and activated on mainnet on 2026-05-28. Operators who have not yet moved to the Base-native stack should follow the [Azul node upgrade guide](/node-operations/azul-node-upgrade/) to stay on the chain. ## Required software | Layer | Software | Mainnet | Sepolia | |-------|----------|---------|---------| | Execution (EL) | `base-reth-node` | [v0.9.0+](https://github.com/base/base/releases/tag/v0.9.0) | [v0.7.0+](https://github.com/base/base/releases/tag/v0.7.0) | | Consensus (CL) | `base-consensus` | [v0.9.0+](https://github.com/base/base/releases/tag/v0.9.0) | [v0.7.0+](https://github.com/base/base/releases/tag/v0.7.0) | | - | `base/node` | [v0.16.0+](https://github.com/base/node/releases/tag/v0.16.0) | [v0.15.0+](https://github.com/base/node/releases/tag/v0.15.0) | ## Execution layer changes The [Azul execution engine](/specifications/azul-exec-engine/) page covers each of these in full; the list below links straight to the relevant section: - [EIP-7823: Upper-Bound MODEXP](/specifications/azul-exec-engine/#upper-bound-modexp) - [EIP-7825: Transaction Gas Limit Cap](/specifications/azul-exec-engine/#transaction-gas-limit-cap) - [EIP-7883: MODEXP Gas Cost Increase](/specifications/azul-exec-engine/#modexp-gas-cost-increase) - [EIP-7939: CLZ Opcode](/specifications/azul-exec-engine/#clz-opcode) - [EIP-7951: secp256r1 Precompile](/specifications/azul-exec-engine/#secp256r1-precompile-gas-cost) - [EIP-7642: eth/69](/specifications/azul-exec-engine/#eth69) - [EIP-7910: eth_config RPC Method](/specifications/azul-exec-engine/#eth_config-rpc-method) - [Remove Account Balances & Receipts](/specifications/azul-exec-engine/#remove-account-balances--receipts) - [Discovery protocol now uses `basev0`](/specifications/azul-exec-engine/#discovery-protocol-now-uses-basev0-protocol-id) ## Proof system Azul introduces the components that make multi-proof withdrawals possible. The [Azul proof system](/specifications/azul-proofs/) page covers the design in full — the `AggregateVerifier` dispute game, the TEE and ZK proof paths, and the finality windows. The individual roles are documented separately: - [Azul proof system](/specifications/azul-proofs/) — AggregateVerifier, finality model, and why the proof system changed - [Proposer](/specifications/proposer/) - [Challenger](/specifications/challenger/) - [TEE provers](/specifications/tee-prover/) - [ZK provers](/specifications/zk-prover/) - [Prover registrar](/specifications/registrar/) - [Proof contracts](/specifications/contracts/) --- # Azul Proof System Source: https://basehub.org/specifications/azul-proofs/ Description: Specification of the Azul multi-proof design — the AggregateVerifier dispute game, TEE and ZK proof paths, and the finality windows that govern L2 checkpoint withdrawals. import { Aside } from '@astrojs/starlight/components'; Azul moves Base from a single-proof checkpoint model to a multi-proof one. A checkpoint is a fixed interval of L2 blocks reduced to a single output root, and every proposal about that checkpoint is filed against `AggregateVerifier` — an L1 dispute game that can accept one or two independent proofs for the same claimed root before any withdrawal is allowed to depend on it. The common case is driven by a TEE prover, which produces the first proof attached to a proposal. A permissionless ZK prover can subsequently reinforce the same proposal or dispute one that is wrong. `AggregateVerifier` hands each proof off to a dedicated verifier contract and combines the results, while a prover registrar keeps the onchain list of trusted TEE signer identities current. ## Why the proof system changed The pre-Azul fault-proof system is optimistic and interactive: a proposal stands unless someone steps in to challenge it. Two constraints follow from that design: - Every withdrawal carries the full challenge window, so finalization takes at least 7 days. - Each invalid proposal has to be actively disputed. If challengers lack the funds to contest every bad game, an incorrect state can slip through to finality. Centralized guardrails patch that gap today, but they are not a durable answer for Stage 2 decentralization. Azul swaps that model for a multi-proof design anchored on TEE and ZK provers. TEE proofs carry the common path, ZK proofs serve as a permissionless backstop, and the structure leaves room to layer in stronger proving systems later. ## Finality model A proposal on Ethereum can settle along three paths depending on which proofs are present: | Proofs present | Settlement path | Target window | What it means | | -------------- | --------------- | ------------- | ---------------------------------------- | | TEE only | Long window | 7 days | Common path, still overridable by ZK | | ZK only | Long window | 7 days | Permissionless path without TEE reliance | | TEE + ZK | Short window | 1 day | Faster finality when both systems agree | The long window exists so independent provers have time to recheck a claim and dispute it. The short window opens only when both proof systems endorse the same proposal. A ZK prover may also overturn an invalid TEE-backed claim and collect the TEE prover's bond as a reward. In Azul that delay is held inside `AggregateVerifier` itself. `OptimismPortal2` and `AnchorStateRegistry` no longer impose their own separate 3.5 day delay — leaving either legacy delay in place would shut off the fast path even when both proofs are already present. ## Security and decentralization - The TEE path is permissioned and tuned for the common case. - The ZK path is permissionless and can override an invalid TEE-backed claim. - The proof layer stays modular, so it can move toward stronger TEE implementations, alternate ZK systems, or multi-ZK designs over time. ## New and changed onchain components - `AggregateVerifier` — Azul's dispute-game contract for checkpoint proposals. A proposal starts with one proof; a second proof for the same claimed root can be appended later. The contract invokes the proof-specific verifier contracts and aggregates their verdicts to decide how the proposal resolves. The Azul finality delay also lives here. - `TEEVerifier` and `ZKVerifier` — the proof-specific verifier contracts that `AggregateVerifier` calls. Their addresses are immutable on the `AggregateVerifier` implementation, giving each deployment an explicit, fixed verifier set. - `DelayedWETH` — still escrows the proposal bond for each game, but Azul cuts its withdrawal delay to 1 day. That is enough because the only bonds at risk here are proposer bonds. - `OptimismPortal2` — no longer applies the separate 3.5 day proof-maturity delay to these proposals; that timing now sits in `AggregateVerifier` so the 1 day path stays reachable. - `AnchorStateRegistry` — like `OptimismPortal2`, it drops the 3.5 day finalization delay for proposals, which is what makes fast finality possible. ## Proof flow 1. The proposer picks the next canonical checkpoint range and requests a TEE proof. 2. The TEE prover re-executes that L2 block range inside an AWS Nitro Enclave and signs the resulting output root. 3. The proposer checks the result against canonical Base L2 state and submits a new `AggregateVerifier` game to L1. 4. A challenger independently recomputes the same checkpoint roots and, on finding an invalid claim, obtains the ZK proof needed to dispute it. The result keeps the normal path simple, preserves a permissionless dispute path, and enables faster settlement whenever both proof systems are available. ## Proof roles - **Proposer** — converts canonical L2 checkpoints into new `AggregateVerifier` games on L1. See [Proposer](/specifications/proposer). - **Challenger** — recomputes in-progress games against canonical L2 state and disputes incorrect claims. Anyone can run one; the permissionless dispute path is a ZK proof challenge. Base operates a challenger as a security backstop, with access to a TEE nullification path for invalid TEE-backed proposals. See [Challenger](/specifications/challenger). - **TEE provers** — AWS Nitro Enclave-backed services used in the common proposal path. The host collects witness data from RPCs, the enclave re-executes the requested L2 block range in isolation, and it signs the resulting checkpoint outputs with a key that never leaves the enclave. See [TEE Prover](/specifications/tee-prover). - **ZK provers** — the permissionless proving backend, used when a dispute requires a ZK proof — particularly to challenge an invalid TEE-backed proposal or invalidate a bad ZK claim. The proposer does not depend on them to create new games today; a future integration could let new roots carry both proof paths from the start, unlocking faster finality across the board. See [ZK Prover](/specifications/zk-prover). - **Prover registrar** — keeps the onchain `TEEProverRegistry` aligned with the live set of Nitro prover signers: it discovers active provers, attests their signer identities onchain, and prunes orphaned signers with safeguards against transient outages. See [Registrar](/specifications/registrar). - `AggregateVerifier` and its verifier contracts confirm claims before any L1 withdrawal can rely on them. See [Proof Contracts](/specifications/contracts). --- # B20 Native Token Standard Source: https://basehub.org/specifications/b20/ Description: Base's native token standard, serving creators of long-tail tokens, stablecoin issuers, and issuers of real-world assets (RWAs) and equities. import { Aside } from '@astrojs/starlight/components'; B20 is Base's in-house take on [ERC-20](https://eips.ethereum.org/EIPS/eip-20). Compliance tooling is baked in from the start: role-based access control, transfer policies, supply caps, memos, and a freeze-and-seize path. The complete interface definitions live in the [Base Standard Library](https://github.com/base/base-std) repository. Two variants are available: | Variant | Decimals | Additional Features | |---------|----------|---------------------| | **Asset** | 6–18 (configurable) | Rebase multiplier, onchain announcements, batched issuance | | **Stablecoin** | 6 (fixed) | Self-declared fiat currency code | ## ERC-20 compatibility Rather than living as EVM smart contracts, B20 tokens run as **Rust precompiles**, which makes them faster, cheaper, and closer to the chain itself. Every token is deployed through the singleton B20Factory precompile. Beyond that base surface, B20 layers on methods ERC-20 lacks: memos, mint/burn, policy gating, granular pause, and ERC-2612 `permit`. The containment runs one way only — anything ERC-20 defines is present on B20, but the additions don't flow back. Full ABIs are published as [interface definitions](https://github.com/base/base-std/tree/main/src/interfaces) in the Base Standard Library. ## Roles model B20's access control builds on OpenZeppelin `AccessControl`, adding a fixed role set plus one behavioral tweak to how the admin role is renounced. | Role | Gates | |------|-------| | `DEFAULT_ADMIN_ROLE` | All admin operations: role grants, policy updates, supply-cap changes | | `MINT_ROLE` | `mint`, `mintWithMemo` | | `BURN_ROLE` | Caller-side burns: `burn`, `burnWithMemo` | | `BURN_BLOCKED_ROLE` | Third-party burns against policy-blocked accounts: `burnBlocked` | | `PAUSE_ROLE` | `pause` | | `UNPAUSE_ROLE` | `unpause` | | `METADATA_ROLE` | `updateName`, `updateSymbol`, `updateContractURI` | You can define your own roles through `setRoleAdmin` and `grantRole`, but they carry no behavior on their own — only the seven base-surface roles listed above are actually gated. One more, `OPERATOR_ROLE`, is introduced by the Asset variant (see [Variants](#variants)). ### Admin renunciation The final holder of `DEFAULT_ADMIN_ROLE` can't be dropped through `renounceRole` or `revokeRole`; both revert with `LastAdminCannotRenounce`. The only way to make a token permanently admin-less is the purpose-built `renounceLastAdmin()`. Tokens meant to be admin-less from day one are created with `initialAdmin == address(0)`, which skips granting the role and bypasses the `renounceLastAdmin` step altogether. Once `renounceLastAdmin()` has run (or for tokens deployed with `initialAdmin == address(0)`): - Anything gated by `DEFAULT_ADMIN_ROLE` is permanently uncallable. - Roles already held by other addresses (`MINT_ROLE`, `BURN_ROLE`, and so on) keep working on their own. - The admin can't be resurrected: `grantRole`, `revokeRole`, and `setRoleAdmin` all revert with `AccessControlUnauthorizedAccount`, even for a caller holding a custom role. ## Policy registry The PolicyRegistry is one singleton precompile holding allowlists and blocklists. B20 tokens point at policies by `uint64` ID, and anyone can create a policy and name its admin. ### Policy types | Type | Default | Behavior | |------|---------|----------| | `BLOCKLIST` | Authorized | All accounts authorized by default; explicitly listed accounts are denied. | | `ALLOWLIST` | Denied | All accounts denied by default; explicitly listed accounts are authorized. | ### Policy IDs A policy ID is a `uint64`. Its top byte holds the `PolicyType`; the remaining 56 bits are a global counter that begins at `2`. Two reserved IDs work without being created first: | Constant | ID | Behavior | |----------|----|----------| | `ALWAYS_ALLOW` | `0` | Authorizes every account unconditionally. Default scope value on new B20 tokens. | | `ALWAYS_BLOCK` | `(uint64(ALLOWLIST) << 56) \| 1` | Denies every account unconditionally. | `isAuthorized` won't revert when handed a policy ID that doesn't exist — it falls back to empty-set behavior, so a missing `BLOCKLIST` authorizes everyone and a missing `ALLOWLIST` denies everyone. ### Admin model Every policy has exactly one admin, and transfers happen in two steps: the sitting admin calls `stageUpdateAdmin(policyId, newAdmin)`, then the nominee calls `finalizeUpdateAdmin(policyId)`. Calling `renounceAdmin(policyId)` freezes the policy for good — its membership can never change again. ### Creating and managing policies ```solidity // Create a policy (admin first, then type) uint64 policyId = policyRegistry.createPolicy(adminAddress, PolicyType.BLOCKLIST); // Or seed the initial member set in one call: // uint64 policyId = policyRegistry.createPolicyWithAccounts(adminAddress, PolicyType.BLOCKLIST, accounts); // Update membership (batched). The setter is type-specific; the bool sets membership state. policyRegistry.updateBlocklist(policyId, true, accounts); // block these accounts policyRegistry.updateBlocklist(policyId, false, accounts); // unblock these accounts // For ALLOWLIST policies: policyRegistry.updateAllowlist(policyId, allowed, accounts) ``` ### Read interface | Method | Description | |--------|-------------| | `isAuthorized(policyId, account)` | Whether `account` is authorized under `policyId`. Never reverts. | | `policyExists(policyId)` | Whether a policy with this ID has been created. | | `policyAdmin(policyId)` | Current admin address. | | `pendingPolicyAdmin(policyId)` | Pending admin during a two-step transfer. | ## Policy integration B20 exposes a fixed set of policy scopes, each holding a `uint64` policy ID into the PolicyRegistry. Each gated operation triggers an `isAuthorized` check on the matching scope; an unauthorized account makes the call revert with `PolicyForbids`. | Scope | Gates | |-------|-------| | `TRANSFER_SENDER_POLICY` | The `from` of `transfer` / `transferFrom` | | `TRANSFER_RECEIVER_POLICY` | The `to` of `transfer` / `transferFrom` | | `TRANSFER_EXECUTOR_POLICY` | The `msg.sender` of `transferFrom`, when distinct from `from` (not checked on `transfer`) | | `MINT_RECEIVER_POLICY` | The `to` of `mint` | Note that `approve` is never policy-gated — only real balance movement through `transfer` / `transferFrom` is checked. Scopes are read with `policyId(scope)` and set with `updatePolicy(scope, policyId)`. `updatePolicy` is admin-gated and reverts on an unrecognized scope. ## Mint Fresh supply comes from `mint` / `mintWithMemo`, both gated by `MINT_ROLE`. The recipient is checked against `MINT_RECEIVER_POLICY`, and the call reverts with `SupplyCapExceeded` if it would carry `totalSupply` past the cap. ## Burn There are two ways to burn: - **`burn` / `burnWithMemo`** — a holder destroys supply from their own balance, gated by `BURN_ROLE`. - **`burnBlocked`** — destroys supply held by a third party, gated by `BURN_BLOCKED_ROLE`. That account first has to be denied under `TRANSFER_SENDER_POLICY`, giving regulated issuers their freeze-and-seize mechanism. ## Supply cap Capping supply is opt-in. The sentinel `type(uint128).max` signals no cap, which is what a token starts with; it doubles as the highest permitted cap, so `totalSupply` can never climb past it. `updateSupplyCap(newCap)` is admin-gated and emits `SupplyCapUpdated`, reverting with `InvalidSupplyCap` if `newCap` falls beneath the current `totalSupply` or rises above `type(uint128).max`. ## Memos A memo is an optional `bytes32` tag carried alongside an operation for off-chain reference. Any operation carrying one emits `Memo(address indexed caller, bytes32 indexed memo)` immediately after its primary event, and indexers join the two on `(transactionHash, logIndex − 1)`. The memo-capable entrypoints are `transferWithMemo`, `transferFromWithMemo`, `mintWithMemo`, and `burnWithMemo`. ## Pause Pausing is fine-grained: the `PausableFeature` enum splits the token surface into operations that pause independently — `TRANSFER`, `MINT`, and `BURN`. That enum only ever grows. By design, `pause(features)` and `unpause(features)` answer to different roles (`PAUSE_ROLE` and `UNPAUSE_ROLE`). ## ERC-2612 permit / EIP-712 B20 supports ERC-2612 signed approvals over an EIP-712 domain of `(name, version, chainId, verifyingContract)`, with `version` pinned to `"1"`. A `updateName` call rolls the domain separator and fires `EIP712DomainChanged` (ERC-5267). Only ECDSA signatures count — ERC-1271 contract signatures are rejected. ## Contract URI (ERC-7572) `contractURI()` hands back a string pointing at off-chain metadata in the ERC-7572 style. `updateContractURI(newUri)` is gated by `METADATA_ROLE`. ## Metadata updates `METADATA_ROLE` controls: - `updateName(newName)` — changes `name` and cycles the EIP-712 domain separator, emitting `NameUpdated` alongside `EIP712DomainChanged`. - `updateSymbol(newSymbol)` — changes only `symbol`. Emits `SymbolUpdated`. ## Factory Every B20 token is minted through the singleton B20Factory precompile via `createB20(variant, salt, params, initCalls)`. Within `base-std` the factory is reachable as `StdPrecompiles.B20_FACTORY`. | Parameter | Description | |-----------|-------------| | `variant` | `ASSET` or `STABLECOIN` | | `salt` | Caller-chosen entropy for address derivation | | `params` | ABI-encoded, variant-specific creation struct (versioned by leading byte) | | `initCalls` | Optional array of ABI-encoded calls dispatched post-creation; calls originating from the factory bypass role gates and transfer-side policy gates during this window | If the chain has not yet switched on the feature backing the requested variant, `createB20` reverts with `IActivationRegistry.FeatureNotActivated`. ### Address derivation A B20 address is deterministic and carries its variant inline: ```text [10-byte B20 prefix][1-byte variant][9-byte keccak256(deployer, salt)] ``` Because the variant lives in the address, it's recoverable without an RPC call — read byte 10 (zero-indexed) to learn the token type. The factory also exposes `getB20Address(variant, deployer, salt)`, `isB20(addr)`, and `isB20Initialized(addr)`. ### initCalls semantics `initCalls` run after creation. While this bootstrap window is open, anything the factory dispatches skips two enforcement layers at once: the token's role checks, plus the sender/receiver/executor transfer policies (`TRANSFER_SENDER_POLICY`, `TRANSFER_RECEIVER_POLICY`, `TRANSFER_EXECUTOR_POLICY`) — letting a deployer apply admin-gated setup (policies, role grants) and seed opening balances all inside the deploy transaction. That relaxation is deliberately partial: - `MINT_RECEIVER_POLICY` is enforced even inside `initCalls`. - Pause state is never bypassed. - Token invariants (the supply cap and so on) are never bypassed. ## Variants Each variant has a 1-byte identifier written directly into the token's address (see [Address derivation](#address-derivation)): | Variant | Byte | |---------|------| | `ASSET` | `0x00` | | `STABLECOIN` | `0x01` | ### Asset The general-purpose variant, suitable for any kind of asset. Decimals are chosen between 6 and 18 at deployment and locked thereafter. On top of everything in the base surface, the Asset variant layers on extra capabilities. The newly added `OPERATOR_ROLE` guards the multiplier and announcements, while batch mint stays under `MINT_ROLE` and extra metadata under `METADATA_ROLE`. #### Multiplier A WAD-precision rebase multiplier applied across all balance reads. Stored balances stay as-is; the multiplier only scales the value callers see. | Method | Description | |--------|-------------| | `multiplier()` | Current WAD-precision multiplier | | `scaledBalanceOf(account)` | Raw balance × multiplier | | `toScaledBalance(raw)` | Convert raw amount to scaled | | `toRawBalance(scaled)` | Convert scaled amount to raw | | `updateMultiplier(newMultiplier)` | Update the multiplier. Gated by `OPERATOR_ROLE`. | #### Announcements A way to bracket sensitive operations (batch mints, multiplier changes) inside a publicly visible disclosure window, gated by `OPERATOR_ROLE`. `announce(internalCalls, id, description, uri)` emits an `Announcement` event, runs `internalCalls`, then emits `EndAnnouncement`. The `id` has to be unique and is enforced forever, and any inner-call revert is wrapped in `InternalCallFailed`. #### Batch mint `batchMint(recipients, amounts)` mints to several recipients at once, gated by `MINT_ROLE`. It should be wrapped in `announce()` for transparency. #### Extra metadata An open-ended store of issuer-set on-chain metadata, addressed by key. | Method | Description | |--------|-------------| | `extraMetadata(key)` | Read a value by key | | `updateExtraMetadata(key, value)` | Write a value. Gated by `METADATA_ROLE`. Setting an empty value removes the entry. | ### Stablecoin A fiat-backed carveout with locked decimals. The decimal count is fixed at `6` and isn't configurable. It adds `currency()`, returning an ISO-style currency code (`"USD"`, `"EUR"`, and the like). That code is set once at creation through `B20StablecoinCreateParams.currency`, limited to the characters `A`-`Z`. It's self-declared and isn't checked against any outside registry. ## Precompile addresses The supporting precompiles sit at the same fixed addresses on every network where B20 is live — Mainnet, Base Sepolia, Vibenet, and a local `base-anvil` instance alike. | Precompile | Address | |---|---| | [B20Factory](#factory) | `0xB20f000000000000000000000000000000000000` | | Activation Registry | `0x8453000000000000000000000000000000000001` | | [Policy registry](#policy-registry) | `0x8453000000000000000000000000000000000002` | ## Roadmap Running in Rust beyond the EVM gives the node unusually deep control over how B20 tokens behave. The following capabilities are slated for later upgrades. ### Frontier features - **Pay transaction fees with B20** — cover gas with a custom asset alone, holding no ETH at all. - **Virtual addresses** — mint one-off deposit addresses that funnel into a single shared account. - **Indexed data** — pull balances, transfer history, and token metadata straight from Base Node RPCs in aggregated form, with no external indexer in the loop. ### Performance optimizations - **Lower fees** — transfers roughly 50% cheaper than an ERC-20 contract. - **Higher throughput** — about 2× the transfers per block versus an ERC-20 contract. More tuning is planned specifically for trading and payment workloads. --- # Beryl Upgrade Overview Source: https://basehub.org/specifications/beryl-overview/ Description: Summary of the Beryl hardfork — the B20 native token standard, a shorter single-proof withdrawal window, and the Reth V2 node release. import { Aside } from '@astrojs/starlight/components'; Beryl is the next Base hardfork after Azul. It folds together three unrelated changes: a chain-native token standard called [B20](/specifications/b20), a shorter wait before withdrawals finalize, and the [Reth V2](/specifications/reth-v2) node release that trims disk usage and accelerates state-root work. ## What Beryl ships - **[B20](/specifications/b20)** — a native token standard built for issuers of stablecoins, real-world assets (RWAs), and long-tail tokens. - **Faster withdrawals** — a single-proof proposal now finalizes after 5 days instead of the previous 7, returning bridge liquidity sooner. - **[Reth V2](/specifications/reth-v2)** — as much as 50% less disk and a rebuilt state-root pipeline worth roughly +33% throughput. ## Activation timestamps | Network | Timestamp | Date | |---------|-----------|------| | `mainnet` | `1782410400` | 2026-06-25 18:00:00 UTC | | `sepolia` | `1781805600` | 2026-06-18 18:00:00 UTC | ## Required software Builds for both networks are now published — the Mainnet versions have been confirmed ahead of its activation date. | Layer | Software | Mainnet | Sepolia | |-------|----------|---------|---------| | Execution (EL) | `base-reth-node` | [v1.1.1+](https://github.com/base/base/releases/tag/v1.1.1) | [v1.1.0+](https://github.com/base/base/releases/tag/v1.1.0) | | Consensus (CL) | `base-consensus` | [v1.1.1+](https://github.com/base/base/releases/tag/v1.1.1) | [v1.1.0+](https://github.com/base/base/releases/tag/v1.1.0) | | - | `base/node` | [v1.1.1+](https://github.com/base/node/releases/tag/v1.1.1) | [v1.1.0+](https://github.com/base/node/releases/tag/v1.1.0) | ## B20: native token standard B20 is Base's own token standard: tokens that keep ERC-20 compatibility but run as Rust precompiles instead of EVM contracts, aimed at stablecoin, RWA, and long-tail issuers. The full surface is documented in [B20 Native Token Standard](/specifications/b20), covering the [roles model](/specifications/b20#roles-model), [policy integration](/specifications/b20#policy-integration), the [factory](/specifications/b20#factory), the [policy registry](/specifications/b20#policy-registry), [mint and burn](/specifications/b20#mint), and the asset/stablecoin [variants](/specifications/b20#variants). ## Withdrawals Beryl shortens how long a single-proof proposal sits in its dispute game before finalizing — that wait drops to 5 days, down from 7. The TEE + ZK dual-proof path that Azul introduced is untouched and still resolves in a single day. Returning capital to fast-bridge liquidity providers sooner is the point: shorter lockups mean lower fees and steadier third-party bridging for the people using it. ## Reth V2 Reth V2 ships as a node-software update alongside Beryl — it needs no separate network upgrade. It targets the two ceilings on a high-throughput chain, disk footprint and state-root speed, through [Storage V2](/specifications/reth-v2#storage-v2) and a rewritten [state-root computation](/specifications/reth-v2#state-root-computation) path. Full details are in [Reth V2](/specifications/reth-v2). --- # Challenger Source: https://basehub.org/specifications/challenger/ Description: Offchain service that detects invalid AggregateVerifier games and submits dispute transactions on L1 to nullify them. The challenger guards the proof system by independently checking in-progress `AggregateVerifier` games against canonical L2 state. When it finds a checkpoint root that does not match, it obtains the proof material the game contract demands and sends a dispute transaction on L1. The ZK path of the challenger is permissionless — anyone with L1 and L2 RPC access, a ZK proving service, and an L1 transaction signer can run one. Base may additionally run a challenger that has access to a TEE proof endpoint so invalid TEE-backed games can be nullified on the faster TEE path before any ZK fallback. ## Responsibilities A conforming challenger: 1. Scans recent `DisputeGameFactory` games. 2. Selects games that are still `IN_PROGRESS` and whose proof state may need action. 3. Recomputes the relevant checkpoint output roots from an L2 node. 4. Identifies the first invalid checkpoint, or determines whether a ZK challenge was aimed at a correct checkpoint. 5. Sources a TEE or ZK proof for the interval that must be proven. 6. Submits `nullify()` or `challenge()` to the game contract. 7. Tracks the bond lifecycle that follows, when bond claiming is configured. The challenger never decides canonical L2 state by trusting the game. It rebuilds roots from L2 headers and account proofs and treats the game itself purely as an input to be verified. ## Game selection The challenger reads the current `AnchorStateRegistry.anchorGame()`, locates that game's slot in the factory index, and scans every higher index. If the registry is still at the original anchor, or if the anchor game cannot be found in the factory, the scan starts at index `0`. Games seen `IN_PROGRESS` stay tracked until they resolve or are completely nullified, so live metrics describe the full post-anchor set. Each scan re-evaluates that whole range so games can shift categories as new proofs, challenges, or nullifications appear onchain. Individual game query failures are logged and retried on the next scan rather than aborting the run. A game is only selected when `status() == IN_PROGRESS`. The challenger then reads: - `teeProver()` - `zkProver()` - `counteredByIntermediateRootIndexPlusOne()` - `rootClaim()` - `l2SequenceNumber()` - `startingBlockNumber()` - `l1Head()` - `INTERMEDIATE_BLOCK_INTERVAL()` from the game implementation for that game type The `(teeProver, zkProver, countered index)` tuple selects the candidate category: | TEE prover | ZK prover | Countered index | Category | Challenger action | | ---------- | --------- | --------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------- | | non-zero | zero | `0` | Invalid TEE proposal | Validate all checkpoint roots. If invalid, prefer TEE nullification and fall back to ZK `challenge()`. | | non-zero | non-zero | `> 0` | Fraudulent ZK challenge | Validate only the challenged checkpoint. If the challenged root is correct, submit ZK `nullify()`. | | zero | non-zero | `0` | Invalid ZK proposal | Validate all checkpoint roots. If invalid, submit ZK `nullify()`. | | non-zero | non-zero | `0` | Invalid dual proposal | Validate all checkpoint roots. If invalid, nullify the TEE proof first, then rescan to handle the remaining ZK proof. | Games with both prover addresses zeroed are already fully nullified and are skipped. TEE-only or ZK-only games with a non-zero countered index are anomalous states and are also skipped. ## Output root validation For an unchallenged proposal, the challenger validates the submitted intermediate roots. The checkpoint block for index `i` is: ```text startingBlockNumber + INTERMEDIATE_BLOCK_INTERVAL * (i + 1) ``` The number of submitted roots must equal: ```text (l2SequenceNumber - startingBlockNumber) / INTERMEDIATE_BLOCK_INTERVAL ``` The interval must be non-zero, and the starting block must be lower than the proposed L2 sequence number. Arithmetic overflow or a checkpoint-count mismatch causes validation to fail for that scan tick. For each checkpoint block, the challenger derives the expected output root as follows: 1. Fetch the L2 block header by block number. 2. Verify that the RPC-provided header hash matches the hash computed from the consensus header. 3. Fetch an `eth_getProof` account proof for `L2ToL1MessagePasser` at that block hash. 4. Verify the account proof against the header state root. 5. Build the output root from the L2 state root, the `L2ToL1MessagePasser` storage root, and the L2 block hash. 6. Compare the computed root to the root stored in the game. Intermediate roots are validated concurrently, but results are consumed in checkpoint order. The first mismatch sets the `intermediateRootIndex` and `intermediateRootToProve` used by the dispute transaction. `intermediateRootToProve` is the locally computed correct root for the invalid checkpoint. If the requested L2 block is not yet available, the challenger skips that game for the tick. The game stays eligible and is retried on the next scan. ## Fraudulent ZK challenge validation When a TEE proposal has already been challenged by a ZK proof, the game stores a 1-based countered index. The challenger converts it to a 0-based checkpoint index and validates only that one checkpoint. If the onchain root at the challenged index does not match the locally computed root, the ZK challenge was legitimate and the challenger does nothing. If the onchain root does match, the ZK challenge targeted a correct checkpoint and is fraudulent — the challenger then obtains a ZK proof for that checkpoint interval and submits `nullify()`. The validation here is intentionally local to the challenged index. Earlier invalid roots do not legitimize a challenge against a later valid root. ## Proof sourcing The challenger proves only the interval that contains the invalid checkpoint. The trusted anchor is the prior checkpoint root, or the game's `startingBlockNumber` state when the invalid checkpoint is index `0`. A ZK proof request specifies: - `start_block_number` — the start of the invalid checkpoint interval. - `number_of_blocks_to_prove` — `INTERMEDIATE_BLOCK_INTERVAL`. - `proof_type` — Groth16 SNARK. - `session_id` — deterministic from `(game address, invalid checkpoint index)`. - `prover_address` — the L1 address that will submit the transaction. - `l1_head` — the L1 head hash stored in the game at creation. The deterministic session ID makes proof requests idempotent across retries. When a TEE proof source is configured and the game has a TEE prover, the challenger tries the TEE path first for invalid TEE proposals and invalid dual proposals. The TEE request uses the game's stored `l1Head`, its corresponding L1 block number, the locally computed agreed L2 output at the start of the interval, and the expected output root at the invalid checkpoint. The TEE result is only accepted if the enclave's output root equals the locally computed expected root; the challenger then encodes the TEE dispute proof bytes for `nullify()`. If the TEE request fails or times out, the challenger falls back to ZK. If a TEE proof is obtained but the TEE `nullify()` transaction fails, the pending entry transitions to a ZK proof request rather than retrying the same TEE transaction indefinitely. ## Dispute transactions The challenger submits one of two game calls: | Intent | Contract call | Used when | | --------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | Nullify | `nullify(proofBytes, intermediateRootIndex, intermediateRootToProve)` | Removing an invalid TEE proof, removing an invalid ZK proof, or refuting a fraudulent ZK challenge. | | Challenge | `challenge(proofBytes, intermediateRootIndex, intermediateRootToProve)` | Challenging an invalid TEE proposal with a ZK proof. | TEE proofs always target `nullify()`. ZK proofs may target either `challenge()` or `nullify()` depending on the candidate category. Before submitting or retrying a failed proof, the challenger re-reads the game status and prover slots. If the game has already resolved, has already been challenged, or the targeted prover slot has been zeroed, the pending proof is dropped. This prevents duplicate transactions when another actor has already handled the game. ## Pending proof lifecycle Each pending proof is keyed by game address and tracks: - proof kind: TEE or ZK - invalid checkpoint index - expected root for that checkpoint - dispute intent - retry count - phase The phase machine is: ```mermaid flowchart TB ZkStart([ZK job accepted]) --> AwaitingProof[AwaitingProof] TeeStart([TEE proof ready]) --> ReadyToSubmit[ReadyToSubmit] AwaitingProof -->|ZK success| ReadyToSubmit AwaitingProof -->|ZK failed| NeedsRetry[NeedsRetry] NeedsRetry -->|retry accepted| AwaitingProof NeedsRetry -->|exhausted/no fallback| Dropped[Dropped] ReadyToSubmit -->|submitted/stale| Dropped ReadyToSubmit -->|ZK fallback| AwaitingProof ``` ZK proofs are polled from the proving service until the job succeeds, fails, or remains pending. Successful ZK receipts are prefixed with the ZK proof-type byte before submission. Failed proof jobs are retried up to three times. A TEE proof enters `ReadyToSubmit` immediately on arrival; if its transaction fails, the challenger requests the pre-built ZK fallback proof when one is available. If no fallback request exists, the entry is dropped; if the fallback `prove_block` call fails, the entry stays in `NeedsRetry` until the next tick. A proof that stays pending, a failed ZK transaction, or a failed `prove_block` retry leaves the proof in its current phase until the next tick. A pending proof produces no contract reads for that game on that tick. ## Bond claiming Bond claiming is optional and gated on configuring claim addresses. When enabled, the challenger tracks games whose `bondRecipient()` or pre-resolution `zkProver()` matches one of those addresses, which lets a challenger pick up claimable games after a restart and discover games other actors have handled. The bond lifecycle is: 1. `NeedsResolve` — wait for `gameOver()`, then call `resolve()`. 2. `NeedsUnlock` — submit the first `claimCredit()` to unlock the `DelayedWETH` credit. 3. `AwaitingDelay` — wait out the `DelayedWETH` delay. 4. `NeedsWithdraw` — submit the second `claimCredit()` to withdraw the credit. After resolution the challenger re-reads `bondRecipient()` and drops tracking if the bond is no longer claimable by a configured address. For games that resolve as `DEFENDER_WINS`, it also tries a best-effort `AnchorStateRegistry.setAnchorState(game)` update. That registry call is permissionless and self-validating; premature or ineligible calls can revert and be retried. ## Service lifecycle At startup the challenger: 1. Creates L1 and L2 RPC clients. 2. Creates the L1 transaction manager from the configured signer. 3. Creates `DisputeGameFactory` and `AggregateVerifier` clients. 4. Creates the ZK proof client and optional TEE proof client. 5. Starts the health server. 6. Starts the driver loop. Each driver tick: 1. Polls pending proof sessions and submits any disputes that are ready. 2. Discovers claimable bonds and advances tracked bond claims. 3. Scans for in-progress candidate games. 4. Validates and starts proofs for new candidates. The health endpoint reports ready only after the first successful driver step. Shutdown is driven by a cancellation token so the driver and the health server stop together. ## Operator inputs A challenger needs: - L1 RPC endpoint. - L2 execution RPC endpoint. - `DisputeGameFactory` address. - `AnchorStateRegistry` address. - ZK proof RPC endpoint. - L1 transaction signer. - Poll interval. Optional inputs: - TEE proof RPC endpoint and timeout, enabling TEE-first nullification for TEE-backed games. - Bond claim addresses, bond discovery interval, and bond discovery lookback window, enabling automatic bond recovery and claiming. - Metrics and health server settings. ## Safety requirements A challenger implementation must preserve these safety properties: - Do not dispute a game from the game's own claimed roots alone; recompute roots from L2 headers and verified `L2ToL1MessagePasser` account proofs. - Use the game's stored L1 head when requesting dispute proofs so proof journals match the game context that the contract verifies. - For fraudulent ZK challenges, validate the challenged checkpoint itself rather than the first invalid checkpoint in the entire proposal. - Recheck game state before submitting a ready proof, because another challenger or prover may have already changed the game. - Treat unavailable L2 blocks and transient RPC failures as retryable scan conditions, not as final validation results. --- # Configuration Source: https://basehub.org/specifications/configuration/ Description: Reference for Base Chain configuration parameters and the roles authorized to change them, grouped into consensus parameters, policy parameters, admin roles, and service roles. Base configuration is organized into four groups, distinguished by how — and by whom — each value can be altered: - **Consensus Parameters** — set at genesis, or only movable through privileged accounts or a protocol upgrade. - **Policy Parameters** — adjustable within protocol-enforced bounds without breaking consensus. - **Admin Roles** — accounts permitted to upgrade contracts, reassign role ownership, or alter protocol parameters. Usually held in cold/multisig wallets. - **Service Roles** — accounts that drive everyday operations. Usually hot wallets. ## Consensus Parameters | Parameter | Description | Administrator | |-----------|-------------|---------------| | Batch Inbox Address | L1 address where batcher transactions are posted | Static | | Batcher Hash | Versioned hash of the authorized batcher sender(s) | [System Config Owner](#admin-roles) | | Chain ID | Unique chain ID for transaction signature validation | Static | | Proof Maturity Delay | Time between proving and finalizing a withdrawal. 1 day. | [L1 Proxy Admin](#admin-roles) | | Respected Game Type | Game type `OptimismPortal` accepts for withdrawal finalization. Aggregate verifier (`621`). | [Guardian](#service-roles) | | Bond Withdrawal Delay | Time before dispute game bonds can be withdrawn. 1 day. | Static | | Fee Scalar | Markup on transactions relative to raw L1 data cost. Fee margin between 0%–50%. | [System Config Owner](#admin-roles) | | Gas Limit | L2 block gas limit. ≤ 200,000,000 gas. | [System Config Owner](#admin-roles) | | Genesis State | Initial chain state including all predeploy code and storage. Standard predeploys and preinstalls only. | Static | | L2 Block Time | Interval at which L2 blocks are produced via derivation. 1 or 2 seconds. | [L1 Proxy Admin](#admin-roles) | | Sequencing Window Size | Max batch submission gap before L1 fallback triggers. 3,600 L1 blocks (12 hours at 12s L1 block time). | Static | | Start Block | L1 block where `SystemConfig` was first initialized | [L1 Proxy Admin](#admin-roles) | | Superchain Target | `SuperchainConfig` and `ProtocolVersions` addresses for cross-L2 config. Mainnet or Sepolia. | Static | | Governance Token | OP governance token. Disabled. | n/a | | Operator Fee Params | Operator fee scalar and constant for fee calculation. Standard values are 0; non-zero for non-standard configurations such as op-succinct. | [System Config Owner](#admin-roles) | | DA Footprint Gas Scalar | Scalar for DA footprint calculation | [System Config Owner](#admin-roles) | | Minimum Base Fee | Minimum base fee on L2 | [System Config Owner](#admin-roles) | ## Policy Parameters | Parameter | Description | Administrator | |-----------|-------------|---------------| | Data Availability Type | Whether the batcher posts data as blobs or calldata. Ethereum (Blobs or Calldata); Alt-DA not supported. | [Batch Submitter](#service-roles) | | Batch Submission Frequency | Frequency of batcher transaction submissions to L1. ≤ 1,800 L1 blocks (6 hours at 12s L1 block time). | [Batch Submitter](#service-roles) | | Output Frequency | Frequency of output root submissions to L1. ≤ 43,200 L2 blocks (24 hours at 2s L2 block time); must be non-zero. Deprecated once fault proofs are enabled. | [L1 Proxy Admin](#admin-roles) | ## Admin Roles | Role | Description | Administers | |------|-------------|-------------| | L1 Proxy Admin | `ProxyAdmin` from the latest `op-contracts` release, authorized to upgrade L1 contracts | L1 contracts | | L1 ProxyAdmin Owner | Authorized to update the L1 Proxy Admin. [0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A](https://etherscan.io/address/0x5a0Aae59D09fccBdDb6C6CcEB07B7279367C3d2A) | L1 Proxy Admin | | L2 Proxy Admin | `ProxyAdmin` at `0x4200000000000000000000000000000000000018`, authorized to upgrade L2 contracts | Predeploys | | L2 ProxyAdmin Owner | Aliased L1 ProxyAdmin Owner; upgrades L2 contracts via `ProxyAdmin`. [0x6B1BAE59D09fCcbdDB6C6cceb07B7279367C4E3b](https://optimistic.etherscan.io/address/0x6B1BAE59D09fCcbdDB6C6cceb07B7279367C4E3b) | L2 Proxy Admin | | System Config Owner | Authorized to change values in the `SystemConfig` contract | Batch Submitter, Sequencer P2P Signer, Fee Scalar, Gas Limit | ## Service Roles | Role | Description | Administrator | |------|-------------|-------------| | Batch Submitter | Authenticates batches submitted to L1 | [System Config Owner](#admin-roles) | | Challenger | Interacts with permissioned dispute games. [0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A](https://etherscan.io/address/0x9BA6e03D8B90dE867373Db8cF1A58d2F7F006b3A) | [L1 Proxy Admin](#admin-roles) | | Guardian | Pauses L1 withdrawals, blacklists dispute games, sets respected game type in `OptimismPortal`. [0x09f7150D8c019BeF34450d6920f6B3608ceFdAf2](https://etherscan.io/address/0x09f7150D8c019BeF34450d6920f6B3608ceFdAf2) | [L1 Proxy Admin](#admin-roles) | | Proposer | Creates permissioned dispute games on L1. | [L1 Proxy Admin](#admin-roles) | | Sequencer P2P Signer | Signs unsafe/pre-submitted blocks at the P2P layer | [System Config Owner](#admin-roles) | --- # Proof Contracts Source: https://basehub.org/specifications/contracts/ Description: Onchain contracts that verify proof material, track game state, and release withdrawals for the Azul proof system. The proof contracts are what turn offchain proof material into onchain checkpoint games. A game asserts an L2 output root for a fixed block interval. The contracts verify the initial proof, accept an optional second proof, let invalid proof material be challenged or nullified, resolve the game after the applicable delay, advance the anchor state, and release the initialization bond. The contracts in this spec: - `AnchorStateRegistry` - `DelayedWETH` - `DisputeGameFactory` - `AggregateVerifier` - `ZKVerifier` - `TEEVerifier` - `TEEProverRegistry` - `NitroEnclaveVerifier` ## Contract graph ```mermaid flowchart TB Factory[DisputeGameFactory] -->|clones| Game[AggregateVerifier game] Game -->|validates parent and finality| ASR[AnchorStateRegistry] Game -->|escrows and releases bond| WETH[DelayedWETH] Game -->|TEE proofs| TEEVerifier[TEEVerifier] Game -->|ZK proofs| ZKVerifier[ZKVerifier] TEEVerifier -->|signer and proposer checks| Registry[TEEProverRegistry] Registry -->|attestation proof| Nitro[NitroEnclaveVerifier] Registry -->|current TEE_IMAGE_HASH| Factory ZKVerifier -->|SP1 proof| SP1[SP1 verifier gateway] Nitro -->|RISC Zero or SP1 proof| Coprocessor[ZK verifier contract] ``` `DisputeGameFactory`, `AnchorStateRegistry`, and `DelayedWETH` are proxied system contracts. `AggregateVerifier` is deployed as an implementation and cloned by the factory with immutable arguments. `TEEVerifier`, `ZKVerifier`, `TEEProverRegistry`, and `NitroEnclaveVerifier` are standalone verifier and registry contracts that the game implementation references. ## Data model The contracts share the same dispute-game types: | Type | Meaning | | ------------ | ----------------------------------------------------------------------------------------- | | `GameType` | A `uint32` identifier for a dispute game implementation. | | `Claim` | A 32-byte root claim. In this proof system it is an L2 output root. | | `Hash` | A 32-byte hash wrapper. | | `Timestamp` | A `uint64` timestamp wrapper. | | `Proposal` | `(root, l2SequenceNumber)`, where `l2SequenceNumber` is the L2 block number for the root. | | `GameStatus` | `IN_PROGRESS`, `CHALLENGER_WINS`, or `DEFENDER_WINS`. | | `ProofType` | `TEE` or `ZK` inside `AggregateVerifier`. | The `AggregateVerifier` game has two block intervals: ```text BLOCK_INTERVAL INTERMEDIATE_BLOCK_INTERVAL ``` `BLOCK_INTERVAL` is the distance between a parent output root and a proposed output root. `INTERMEDIATE_BLOCK_INTERVAL` is the spacing between intermediate roots inside that range. Both must be non-zero, and `BLOCK_INTERVAL` must be divisible by `INTERMEDIATE_BLOCK_INTERVAL`. The number of intermediate roots in every game is: ```text BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL ``` The final intermediate root must equal the game's `rootClaim`. ## Game lifecycle 1. The factory owner configures a game type with an `AggregateVerifier` implementation and an initialization bond. 2. TEE operators register enclave signer addresses in `TEEProverRegistry` using ZK-verified Nitro attestation. 3. A proposer creates a game through `DisputeGameFactory.createWithInitData()`, paying the exact initialization bond and supplying an initial TEE or ZK proof. 4. The game validates its parent, L2 block number, intermediate roots, L1 origin, and proof journal. The bond is deposited into `DelayedWETH`. 5. A second proof can be submitted through `verifyProposalProof()`. If the proposal is invalid, challengers can call `challenge()` or `nullify()` with proof material for an intermediate root. 6. After the expected resolution time, anyone can call `resolve()`. The result is `DEFENDER_WINS` for a valid unchallenged game and `CHALLENGER_WINS` for a successful challenge or invalid parent. 7. After resolution and the registry finality delay, anyone can call `closeGame()` to make a best-effort anchor update. 8. The bond recipient calls `claimCredit()` twice: once to unlock the `DelayedWETH` credit, then again after the `DelayedWETH` delay to withdraw and receive ETH. ## DisputeGameFactory `DisputeGameFactory` creates and indexes dispute-game clones. Each game has a unique identifier: ```text keccak256(abi.encode(gameType, rootClaim, extraData)) ``` The factory stores that UUID in `_disputeGames` and also appends a packed `GameId` to `_disputeGameList` for index-based discovery. Offchain services find games via `DisputeGameCreated`, `gameAtIndex()`, and `findLatestGames()`. ### Configuration Only the factory owner can: - set a game implementation with `setImplementation(gameType, impl)` - set a game implementation plus opaque implementation args with `setImplementation(gameType, impl, args)` - set the exact required creation bond with `setInitBond(gameType, initBond)` Creation reverts when the implementation is unset, the paid value disagrees with `initBonds`, or a game with the same UUID already exists. ### Clone arguments With no implementation args configured, the clone-with-immutable-args payload is: | Bytes | Description | | -------------- | ------------------------------------- | | `[0, 20)` | Game creator address | | `[20, 52)` | Root claim | | `[52, 84)` | Parent L1 block hash at creation time | | `[84, 84 + n)` | Opaque game `extraData` | With implementation args configured, the payload is: | Bytes | Description | | ---------------------- | ------------------------------------- | | `[0, 20)` | Game creator address | | `[20, 52)` | Root claim | | `[52, 84)` | Parent L1 block hash at creation time | | `[84, 88)` | Game type | | `[88, 88 + n)` | Opaque game `extraData` | | `[88 + n, 88 + n + m)` | Opaque implementation args | `AggregateVerifier` uses the standard layout. Its `extraData` is specified in the `AggregateVerifier` section below. ## AnchorStateRegistry `AnchorStateRegistry` is the source of truth for whether a dispute game can be trusted by the proof system. It stores: - the `SystemConfig` - the `DisputeGameFactory` - the starting anchor root - the current anchor game, if one has been accepted - the current respected game type - a game blacklist - a retirement timestamp - a dispute-game finality delay The initial retirement timestamp is set during first initialization. Games created at or before the retirement timestamp are retired. ### Game predicates The registry exposes these predicates: | Predicate | True when | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `isGameRegistered(game)` | The factory maps the game's `(gameType, rootClaim, extraData)` back to the same address, and the game points at this registry. | | `isGameRespected(game)` | The game reports that its game type was respected when it was created. | | `isGameBlacklisted(game)` | The guardian has blacklisted the game address. | | `isGameRetired(game)` | `game.createdAt() <= retirementTimestamp`. | | `isGameResolved(game)` | The game has a non-zero `resolvedAt` and ended with `DEFENDER_WINS` or `CHALLENGER_WINS`. | | `isGameProper(game)` | The game is registered, not blacklisted, not retired, and the system is not paused. | | `isGameFinalized(game)` | The game is resolved and more than `disputeGameFinalityDelaySeconds` have elapsed since `resolvedAt`. | | `isGameClaimValid(game)` | The game is proper, respected, finalized, and resolved with `DEFENDER_WINS`. | `isGameProper()` is not a statement about the correctness of the root claim. It only certifies that the game has not been invalidated by registry-level controls. Consumers that need claim validity must use `isGameClaimValid()`. ### Guardian controls The `SystemConfig.guardian()` can: - set the respected game type - update the retirement timestamp to the current block timestamp - blacklist individual games These controls are the onchain safety valves for invalidating games before they can become valid claims. ### Anchor updates `getAnchorRoot()` returns the starting anchor root until an anchor game has been accepted. After that, it returns the root claim and L2 block number of `anchorGame`. `setAnchorState(game)` accepts a new anchor game only when: - `isGameClaimValid(game)` is true - the game's L2 sequence number is greater than the current anchor root's sequence number The update is permissionless and self-validating. ## DelayedWETH `DelayedWETH` is WETH with delayed withdrawals. It escrows game bonds and forces a two-step credit claim: 1. The game calls `unlock(subAccount, amount)` for the bond recipient. 2. After `delay()` seconds, the game calls `withdraw(subAccount, amount)` and sends ETH to the recipient. Unlocks are keyed by: ```text withdrawals[msg.sender][subAccount] ``` For proof games, `msg.sender` is the `AggregateVerifier` game contract and `subAccount` is the current `bondRecipient`. Withdrawals revert while the system is paused. The proxy admin owner also holds emergency recovery powers: - `recover(amount)` sends up to `amount` ETH from the contract to the owner. - `hold(account)` or `hold(account, amount)` pulls WETH from an account into the owner address. ## AggregateVerifier `AggregateVerifier` is the dispute-game implementation for checkpoint proofs. Every factory-created game is a clone with immutable game data. The implementation itself owns no per-game storage beyond the clone's storage. ### Constructor configuration An implementation fixes these values for all clones of that game type: | Value | Purpose | | ----------------------------- | -------------------------------------------------------- | | `GAME_TYPE` | The dispute-game type served by this implementation. | | `ANCHOR_STATE_REGISTRY` | Parent validation, claim validity, and anchor updates. | | `DISPUTE_GAME_FACTORY` | Read from the registry during construction. | | `DELAYED_WETH` | Bond escrow. | | `TEE_VERIFIER` | Verifier for TEE signatures. | | `TEE_IMAGE_HASH` | Expected TEE image hash committed into TEE journals. | | `ZK_VERIFIER` | Verifier for ZK proofs. | | `ZK_RANGE_HASH` | Range-program hash committed into ZK journals. | | `ZK_AGGREGATE_HASH` | Aggregate-program hash passed to the ZK verifier. | | `CONFIG_HASH` | Rollup configuration hash committed into proof journals. | | `L2_CHAIN_ID` | L2 chain the game argues about. | | `BLOCK_INTERVAL` | Distance from parent block to proposed block. | | `INTERMEDIATE_BLOCK_INTERVAL` | Distance between intermediate checkpoint roots. | | `PROOF_THRESHOLD` | Number of proofs required to resolve, either `1` or `2`. | `PROOF_THRESHOLD` governs resolution, not proof submission. The game can store one TEE proof, one ZK proof, or both. ### Game extra data `AggregateVerifier.extraData()` is encoded as: | Bytes | Description | | ------------------- | ---------------------------------------------------------------------- | | `[0, 32)` | Proposed L2 block number. | | `[32, 52)` | Parent address. The first game uses the `AnchorStateRegistry` address. | | `[52, 52 + 32 * n)` | Ordered intermediate output roots. | where: ```text n = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL ``` The final intermediate output root must equal `rootClaim`. ### Initialization `initializeWithInitData(proof)` runs exactly once. It checks the calldata size so unused bytes cannot fabricate multiple factory UUIDs for the same logical proposal. During initialization the game: 1. Checks that the final intermediate root matches `rootClaim`. 2. Resolves the starting root. If `parentAddress` is the registry address, the starting root is `AnchorStateRegistry.getStartingAnchorRoot()`. Otherwise the parent must be a valid registered game. 3. Requires: ```text l2SequenceNumber == startingL2SequenceNumber + BLOCK_INTERVAL ``` 4. Records `createdAt`, `wasRespectedGameTypeWhenCreated`, and an initial `expectedResolution`. 5. Verifies the claimed L1 origin hash in the initialization proof against either `blockhash()` or EIP-2935 history. 6. Verifies the supplied TEE or ZK proof. 7. Records the initial prover, sets `bondRecipient` to `gameCreator`, and deposits the bond into `DelayedWETH`. The initialization proof format is: | Bytes | Description | | ----------- | -------------------------------------- | | `[0, 1)` | `ProofType`: `0` for TEE, `1` for ZK. | | `[1, 33)` | L1 origin hash. | | `[33, 65)` | L1 origin block number. | | `[65, end)` | Proof bytes for the selected verifier. | The L1 origin block must lie in the past. Native `blockhash()` is used for block ages up to 256 blocks. EIP-2935 history is used up to 8191 blocks. Older or unavailable L1 origin blocks revert. ### Additional proofs `verifyProposalProof(proofBytes)` adds the missing proof type while a game is in progress and not over. It does not re-read a new L1 origin from calldata; instead it uses the `l1Head()` captured by the factory at clone creation. The additional proof format is: | Bytes | Description | | ---------- | -------------------------------------- | | `[0, 1)` | `ProofType`: `0` for TEE, `1` for ZK. | | `[1, end)` | Proof bytes for the selected verifier. | A game cannot store more than one proof of the same type. ### Proof journals TEE and ZK proofs commit to the same transition shape: ```text proposer l1OriginHash startingRoot startingL2SequenceNumber endingRoot endingL2SequenceNumber intermediateRoots CONFIG_HASH proof-system-specific hash ``` For TEE proofs the final field is `TEE_IMAGE_HASH` and the journal is checked by `TEEVerifier`. The game calls: ```text TEE_VERIFIER.verify(proposer || signature, TEE_IMAGE_HASH, keccak256(journal)) ``` For ZK proofs the final field is `ZK_RANGE_HASH` and the proof is checked by `ZKVerifier`. The game calls: ```text ZK_VERIFIER.verify(proofBytes, ZK_AGGREGATE_HASH, keccak256(journal)) ``` ### Resolution delay `expectedResolution` is derived from the number of currently accepted proofs: | Proof count | Delay | | ----------- | ------------------------------------------- | | `0` | Never resolvable. | | `1` | `SLOW_FINALIZATION_DELAY`, fixed at 7 days. | | `2` | `FAST_FINALIZATION_DELAY`, fixed at 1 day. | Adding a proof can only push `expectedResolution` earlier. Nullifying a proof can push it later. A challenge with a ZK proof sets `expectedResolution` to 7 days from the challenge so the challenge can itself be nullified. ### Challenge `challenge(proofBytes, intermediateRootIndex, intermediateRootToProve)` challenges a TEE-backed proposal with a ZK proof for one intermediate interval. The call is accepted only when: - the game is still `IN_PROGRESS` - the game itself is valid according to the registry - the parent has not resolved with `CHALLENGER_WINS` - the game has a TEE proof - the game does not already have a ZK proof - the supplied proof type is ZK - the challenged index is in range - the supplied root differs from the currently proposed intermediate root When the ZK proof verifies, the game records the ZK prover, increments `proofCount`, stores the 1-based countered intermediate index, and emits `Challenged`. On resolution the challenger receives the bond and the game status becomes `CHALLENGER_WINS`. ### Nullification `nullify(proofBytes, intermediateRootIndex, intermediateRootToProve)` removes an already accepted proof by proving a contradictory intermediate root. For an unchallenged game, the target root must differ from the proposed intermediate root. For a challenged game, only the challenged index can be nullified, only with a ZK proof, and the supplied root must equal the original proposed intermediate root. After a successful nullification: - the prover slot for that proof type is deleted - `proofCount` decreases - `expectedResolution` is recalculated - the countered index is cleared if the ZK challenge was nullified - the corresponding verifier contract is nullified Verifier nullification is a global safety stop. Once `TEE_VERIFIER.nullify()` or `ZK_VERIFIER.nullify()` succeeds, future proof verification through that verifier reverts until the system is upgraded or reconfigured. ### Resolve, close, and bonds `resolve()` is permissionless. The parent must be resolved unless the parent is the registry itself. If the parent resolved with `CHALLENGER_WINS`, or later became blacklisted or retired, the child also resolves with `CHALLENGER_WINS`. Otherwise the game must be over and must hold at least `PROOF_THRESHOLD` accepted proofs. If the game was challenged, `resolve()` sets `CHALLENGER_WINS` and moves the bond recipient to the ZK prover. Otherwise it sets `DEFENDER_WINS`. `closeGame()` is permissionless. It reverts while the registry is paused, requires the game to be resolved and finalized by the registry, and then attempts `AnchorStateRegistry.setAnchorState()`. The anchor update is best-effort: if the registry rejects the game because it is no longer the newest valid claim, `closeGame()` swallows that registry revert. `claimCredit()` has two phases: 1. Unlock the bond in `DelayedWETH`. 2. After the `DelayedWETH` delay, withdraw WETH and send ETH to `bondRecipient`. If accepted proofs have been nullified and `expectedResolution` is reset to the never-resolvable sentinel, `claimCredit()` is blocked until 14 days after `createdAt`. This stops a stuck game from locking the bond forever. ## ZKVerifier `ZKVerifier` adapts the Succinct SP1 verifier gateway to the common `IVerifier` interface used by `AggregateVerifier`. The call: ```text verify(proofBytes, imageId, journal) ``` runs: ```text SP1_VERIFIER.verifyProof(imageId, abi.encodePacked(journal), proofBytes) ``` and returns `true` if the SP1 gateway does not revert. `imageId` is the aggregate program verification key supplied by the game, and `journal` is the hash of the public inputs assembled by the game. `ZKVerifier` inherits verifier nullification. Once a proper respected game nullifies the verifier, all future `verify()` calls revert. ## TEEVerifier `TEEVerifier` verifies TEE proof signatures against the `TEEProverRegistry`. The proof bytes passed to `TEEVerifier` are: | Bytes | Description | | ---------- | ------------------------ | | `[0, 20)` | Proposer address. | | `[20, 85)` | 65-byte ECDSA signature. | The signature is recovered over the journal hash directly. It is not wrapped with the Ethereum signed-message prefix. A TEE proof is valid only when: - the proof is at least 85 bytes - the signature recovers cleanly - the proposer is allowlisted in `TEEProverRegistry` - the recovered signer is registered in `TEEProverRegistry` - the signer's registered image hash equals the `imageId` supplied by the calling game The image-hash check stops an enclave registered for one image from producing accepted proofs for a game type or upgrade that expects another image. `TEEVerifier` also inherits verifier nullification. ## TEEProverRegistry `TEEProverRegistry` manages TEE signer registration and proposer allowlisting. The registry holds: - an owner - a manager - a `NitroEnclaveVerifier` - a `DisputeGameFactory` - a configurable `gameType` - registered signer state - proposer allowlist state The owner can set proposer addresses and change the `gameType`. The owner or manager can register and deregister signers. ### Expected image hash The registry reads the expected TEE image hash from the current game implementation: ```text DisputeGameFactory.gameImpls(gameType).TEE_IMAGE_HASH() ``` `setGameType()` checks that this call succeeds and returns a non-zero hash. `isValidSigner()` returns true only when the signer is registered and its stored image hash matches the current expected hash. Signer registration is itself PCR0-agnostic. This lets operators pre-register signers for a future image before a game-type migration. Those signers do not become valid for proof submission until the game implementation's `TEE_IMAGE_HASH` matches their registered image hash. ### Signer registration `registerSigner(output, proofBytes)` calls: ```text NITRO_VERIFIER.verify(output, ZkCoProcessorType.RiscZero, proofBytes) ``` The returned journal must have `VerificationResult.Success`. The attestation timestamp must not be older than `MAX_AGE`, which is fixed at 60 minutes. The public key must be exactly 65 bytes in uncompressed ANSI X9.62 form: ```text 0x04 || x || y ``` The registry derives the signer address as: ```text address(uint160(uint256(keccak256(x || y)))) ``` It pulls PCR0 out of the journal and stores: ```text signerImageHash[signer] = keccak256(pcr0.first || pcr0.second) ``` It then marks the signer as registered and adds it to an enumerable signer set. ### Deregistration `deregisterSigner(signer)` deletes the signer's registration and image hash, removes it from the enumerable set, and emits `SignerDeregistered`. `getRegisteredSigners()` returns the current enumerable set. Ordering is not guaranteed. ## NitroEnclaveVerifier `NitroEnclaveVerifier` verifies ZK proofs of AWS Nitro Enclave attestation documents. It is the attestation verifier used by `TEEProverRegistry`. The contract supports: - single-attestation verification - batch attestation verification - RISC Zero and Succinct SP1 proof systems - root certificate configuration - trusted intermediate certificate caching - certificate revocation - route-specific verifier selection - permanently frozen proof routes ### Roles and configuration The owner controls: - `rootCert` - `maxTimeDiff` - `proofSubmitter` - `revoker` - ZK verifier configuration - verifier program IDs - aggregator program IDs - route-specific verifier overrides - route freezing The `revoker` can also revoke trusted intermediate certificates. `proofSubmitter` is the only address allowed to call `verify()` or `batchVerify()`. `zkConfig[zkCoProcessor]` stores: | Field | Purpose | | -------------- | ----------------------------------------------- | | `verifierId` | Program ID for single-attestation verification. | | `aggregatorId` | Program ID for batch verification. | | `zkVerifier` | Default verifier contract address. | Route-specific verifier overrides are keyed by `(zkCoProcessor, selector)`, where `selector` is the first four bytes of `proofBytes`. If a route is frozen, verification through that route permanently reverts. ### Single verification `verify(output, zkCoprocessor, proofBytes)`: 1. Requires `msg.sender == proofSubmitter`. 2. Resolves the verifier route from the proof selector. 3. Verifies the ZK proof against `zkConfig[zkCoprocessor].verifierId`. 4. Decodes `output` as a `VerifierJournal`. 5. Validates the journal. 6. Emits `AttestationSubmitted`. 7. Returns the journal with its final verification result. For RISC Zero, proof verification uses: ```text IRiscZeroVerifier.verify(proofBytes, programId, sha256(output)) ``` For Succinct, proof verification uses: ```text ISP1Verifier.verifyProof(programId, output, proofBytes) ``` ### Batch verification `batchVerify(output, zkCoprocessor, proofBytes)`: 1. Requires `msg.sender == proofSubmitter`. 2. Verifies the ZK proof against `zkConfig[zkCoprocessor].aggregatorId`. 3. Decodes `output` as a `BatchVerifierJournal`. 4. Requires `batchJournal.verifierVk == getVerifierProofId(zkCoprocessor)`. 5. Validates every embedded `VerifierJournal`. 6. Emits `BatchAttestationSubmitted`. 7. Returns the validated journals. ### Journal validation A successful journal stays successful only when: - the trusted certificate prefix length is non-zero - the first certificate equals `rootCert` - every trusted intermediate certificate is still trusted and unexpired - every newly supplied certificate is unexpired - the attestation timestamp is not too old - the attestation timestamp is not in the future Attestation timestamps are provided in milliseconds and converted to seconds. The timestamp is valid only when: ```text timestamp + maxTimeDiff > block.timestamp timestamp < block.timestamp ``` Certificates beyond the trusted prefix are cached with their expiry timestamps after successful validation. A revoked certificate can become trusted again only if it shows up in a later successful attestation proof and is cached again. ## Cross-contract safety properties The proof contracts rely on these cross-contract properties: - Factory uniqueness: a logical `(gameType, rootClaim, extraData)` can create at most one game. - Parent validity: non-anchor games can only start from a registered, respected, non-retired, non-blacklisted parent that has not lost. - Monotonic checkpoints: each child game must advance exactly `BLOCK_INTERVAL` L2 blocks from its starting root. - Intermediate accountability: every proposal commits to all intermediate roots, so challengers can target the first invalid checkpoint interval. - Verifier separation: TEE and ZK proofs use different verifier contracts and different journal domain separators (`TEE_IMAGE_HASH` versus `ZK_RANGE_HASH`). - Fast finality requires diversity: a game with two accepted proof types can resolve after one day, while a game with one proof waits seven days. - Registry finality is separate from game resolution: a game can resolve before the `AnchorStateRegistry` accepts it as a valid claim. - Safety controls fail closed: pause, blacklist, retirement, verifier nullification, route freezing, and certificate revocation all prevent acceptance rather than expanding trust. ## Administrative surfaces | Contract | Privileged role | Privileged actions | | ---------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------- | | `DisputeGameFactory` | Owner | Set game implementations, implementation args, and initialization bonds. | | `AnchorStateRegistry` | Guardian from `SystemConfig` | Set respected game type, blacklist games, update retirement timestamp. | | `DelayedWETH` | Proxy admin owner | Recover ETH and hold WETH from accounts. | | `TEEProverRegistry` | Owner | Set proposers, update game type, transfer ownership or management. | | `TEEProverRegistry` | Owner or manager | Register and deregister TEE signers. | | `NitroEnclaveVerifier` | Owner | Configure root certificate, time tolerance, proof submitter, revoker, ZK routes, and program IDs. | | `NitroEnclaveVerifier` | Owner or revoker | Revoke trusted intermediate certificates. | These surfaces are intentionally narrow but high impact. Operational changes here can affect which games are respected, which proofs verify, and which attestations can register new TEE signers. --- # Flashblocks Specification Source: https://basehub.org/specifications/flashblocks/ Description: JSON-RPC methods, parameters, and return types exposed by the Flashblocks RPC provider. The Flashblocks RPC provider extends standard Ethereum JSON-RPC methods to query preconfirmed flashblock state via the `pending` tag. The methods, parameters, and return types are specified below. :::note This document serves as a reference for the current implementation and may differ from the full Flashblocks specification in terms of scope and methods implemented. The canonical source is [`docs/specs/flashblocks.md`](https://github.com/base/base/blob/main/docs/specs/flashblocks.md) in the base/base repository. ::: ## Type Definitions All types used in these RPC methods are identical to the standard OP Stack RPC types. No modifications have been made to the existing type definitions. ## Modified Ethereum JSON-RPC Methods The following standard Ethereum JSON-RPC methods are enhanced to support the `pending` tag for querying preconfirmed state. ### `eth_getBlockByNumber` Returns block information for the specified block number. #### Parameters | Name | Type | Description | |------|------|-------------| | `blockNumber` | `String` | Block number (hex) or tag. Use `"pending"` for preconfirmed state. | | `fullTransactions` | `Boolean` | If `true`, returns full transaction objects; if `false`, returns transaction hashes. | #### Returns `Object` -- Block object, or `null` if the block does not exist. #### Example ```json // Request { "method": "eth_getBlockByNumber", "params": ["pending", false], "id": 1, "jsonrpc": "2.0" } // Response { "id": 1, "jsonrpc": "2.0", "result": { "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", "parentHash": "0x...", "stateRoot": "0x...", "transactionsRoot": "0x...", "receiptsRoot": "0x...", "number": "0x123", "gasUsed": "0x5208", "gasLimit": "0x1c9c380", "timestamp": "0x...", "extraData": "0x", "mixHash": "0x...", "nonce": "0x0", "transactions": ["0x..."] } } ``` #### Response Fields | Field | Description | |-------|-------------| | `hash` | Block hash calculated from the current flashblock header. | | `parentHash` | Hash of the parent block. | | `stateRoot` | Current state root from the latest flashblock. | | `transactionsRoot` | Transactions trie root. | | `receiptsRoot` | Receipts trie root. | | `number` | Block number being built. | | `gasUsed` | Cumulative gas used by all transactions. | | `gasLimit` | Block gas limit. | | `timestamp` | Block timestamp. | | `extraData` | Extra data bytes. | | `mixHash` | Mix hash value. | | `nonce` | Block nonce value. | | `transactions` | Array of transaction hashes or full transaction objects. | --- ### `eth_getTransactionReceipt` Returns the receipt for a transaction. When the transaction exists only in the preconfirmed (flashblock) state, the receipt is still returned -- enabling callers to confirm inclusion before the block is finalized. #### Parameters | Name | Type | Description | |------|------|-------------| | `transactionHash` | `String` | Hash of the transaction. | #### Returns `Object` -- Transaction receipt, or `null` if the transaction is not found. #### Example ```json // Request { "method": "eth_getTransactionReceipt", "params": ["0x..."], "id": 1, "jsonrpc": "2.0" } // Response { "id": 1, "jsonrpc": "2.0", "result": { "transactionHash": "0x...", "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "blockNumber": "0x123", "transactionIndex": "0x0", "from": "0x...", "to": "0x...", "gasUsed": "0x5208", "cumulativeGasUsed": "0x5208", "effectiveGasPrice": "0x...", "status": "0x1", "contractAddress": null, "logs": [], "logsBloom": "0x..." } } ``` #### Response Fields | Field | Description | |-------|-------------| | `transactionHash` | Hash of the transaction. | | `blockHash` | Zero hash (`0x000...000`) for preconfirmed transactions. | | `blockNumber` | Block number containing the transaction. | | `transactionIndex` | Index of the transaction in the block. | | `from` | Sender address. | | `to` | Recipient address. | | `gasUsed` | Gas used by this transaction. | | `cumulativeGasUsed` | Total gas used up to and including this transaction. | | `effectiveGasPrice` | Effective gas price paid. | | `status` | `0x1` for success, `0x0` for failure. | | `contractAddress` | Address of created contract (for contract creation transactions), otherwise `null`. | | `logs` | Array of log objects emitted by the transaction. | | `logsBloom` | Bloom filter for the logs. | --- ### `eth_getBalance` Returns the balance of an account. #### Parameters | Name | Type | Description | |------|------|-------------| | `address` | `String` | Address to query. | | `blockNumber` | `String` | Block number (hex) or tag. Use `"pending"` for preconfirmed state. | #### Returns `String` -- Account balance in wei, hex-encoded. #### Example ```json // Request { "method": "eth_getBalance", "params": ["0x...", "pending"], "id": 1, "jsonrpc": "2.0" } // Response { "id": 1, "jsonrpc": "2.0", "result": "0x1bc16d674ec80000" } ``` --- ### `eth_getTransactionCount` Returns the number of transactions sent from an address (the account nonce). #### Parameters | Name | Type | Description | |------|------|-------------| | `address` | `String` | Address to query. | | `blockNumber` | `String` | Block number (hex) or tag. Use `"pending"` for preconfirmed state. | #### Returns `String` -- Transaction count (nonce), hex-encoded. #### Example ```json // Request { "method": "eth_getTransactionCount", "params": ["0x...", "pending"], "id": 1, "jsonrpc": "2.0" } // Response { "id": 1, "jsonrpc": "2.0", "result": "0x5" } ``` --- ## Behavior Notes ### Pending Tag Usage - When `"pending"` is used as the block tag, the method queries preconfirmed state from the flashblocks cache. - If no preconfirmed data is available, the method falls back to the latest confirmed state. - For non-pending queries (e.g., `"latest"`, `"finalized"`, or a specific block number), the method behaves identically to standard Ethereum JSON-RPC. ### Error Handling - Standard JSON-RPC error responses are returned for invalid requests. - `null` is returned for non-existent transactions or blocks. - When flashblocks are disabled or unavailable, the RPC falls back to standard behavior transparently. ## Source Canonical specification: [docs/specs/flashblocks.md](https://github.com/base/base/blob/main/docs/specs/flashblocks.md) --- # Glossary Source: https://basehub.org/specifications/glossary/ Description: Definitions for the terms used throughout the Base Chain protocol specification — from L1/L2 basics to sequencing, deposits, withdrawals, batch submission, derivation, and the execution engine. This page collects the vocabulary used across the Base Chain protocol specs. Terms are grouped by topic; many definitions cross-reference each other, so the same word may appear in more than one section. ## General terms ### Layer 1 (L1) Shorthand for the Ethereum blockchain. It is the counterpart to [layer 2](#layer-2-l2), which here means Base. ### Layer 2 (L2) Shorthand for Base Chain — the rollup described by this specification. It sits on top of [layer 1](#layer-1-l1), the Ethereum settlement layer. ### Block A term that applies to both L1 and L2 blocks, which share the same overall shape. A block bundles an ordered list of transactions together with header fields. Some of those fields are *inputs* — known before the block's transactions run — while others are *outputs* derived only after execution. The output fields include several [Merkle Patricia Trie](#merkle-patricia-trie) roots that commit to the post-execution L2 state and to the logs emitted while the block ran. The Ethereum yellow paper (section 4.3) documents the full set of header properties. ### EOA Short for "Externally Owned Account." It denotes an address controlled directly by a user's key, in contrast to a contract address. ### Merkle Patricia Trie A Merkle Patricia Trie (MPT) is a sparse, tree-shaped map from keys to values. Its root hash commits to everything stored in the tree, which means you can produce a Merkle proof for any key-value pair it contains and check that proof against the root. ### Chain re-organization A re-organization — "re-org" for short — happens when the head of a chain switches, according to the [fork choice rule](#fork-choice-rule), to a block that is not a direct child of the previous head. On L1 these arise from network conditions or attacks. On L2 they are downstream effects of L1 re-orgs, propagated through [L2 chain derivation](#l2-chain-derivation). ### Predeployed contract ("predeploy") A contract written into the L2 genesis state, so it exists from the very first block of the chain. The full set lives in the predeploys specification. ### Preinstalled contract ("preinstall") Like a predeploy, a preinstall is seeded into the L2 genesis state. Unlike predeploys, preinstalls do not carry the same security guarantees; they are general-purpose contracts included to improve L2 user experience. The full set lives in the preinstalls specification. ### Precompiled contract ("precompile") A contract implemented natively inside the EVM that runs a particular operation faster than an equivalent bytecode (for example, Solidity) version would. Precompiles live at fixed addresses and are introduced or changed only through network upgrades. The full set lives in the precompiles specification. ### Receipt A receipt is what a transaction emits as a record of its execution: a status code, the gas it consumed, its log entries, and a [bloom filter](https://en.wikipedia.org/wiki/Bloom_filter) over those entries. Log entries are the mechanism behind Solidity events. Blocks do not embed receipts directly; instead each block stores a [Merkle Patricia Trie](#merkle-patricia-trie) root over the tree of every transaction receipt in that block. Receipts are defined in the yellow paper, section 4.3.1. ### Transaction type [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) defines a scheme for distinguishing transaction types. Each type may carry a different payload and be processed under different rules by the protocol. ### Fork choice rule The rule that picks which block counts as the head of a chain. On L1 the choice follows the proof-of-stake rules. L2 has its own fork choice logic, and which block it selects depends on whether you are asking for the [safe](#safe-l2-head), [unsafe](#unsafe-l2-head), or [finalized](#finalized-l2-head) L2 head. ### Priority gas auction Ethereum orders transactions by the price they pay for inclusion. A Priority Gas Auction (PGA) breaks out when several parties race to occupy the first slot in a block, each repeatedly bumping the gas price on their transaction. PGAs appear wherever being first carries value — being the earliest deposit, say, or landing a deposit before the guaranteed gas runs out. Because they generate a flood of transactions in a tiny time window, PGAs impose negative externalities on the network. ## Sequencing A rollup transaction reaches the chain by one of two routes: - As a [deposited transaction](#deposited-transaction), which the system itself enforces. - As an ordinary transaction packed into a [sequencer batch](#sequencer-batch). Routing transactions through a batch trims overhead and lets the sequencer pre-confirm them ahead of L1 posting the underlying data. ### Sequencer The sequencer is either a [rollup node](#rollup-node) operating in sequencer mode, or the party running that node. It is a privileged role: it takes L2 transactions from users, assembles them into L2 blocks, and forwards those blocks to a [data availability provider](#data-availability-provider) by way of a [batcher](#batcher). It also posts [output roots](#l2-output-root) to L1. ### Sequencing window A sequencing window is the span of L1 blocks that a single [sequencing epoch](#sequencing-epoch) is derived from. If a window opens at L1 block `N`, it holds the [batcher transactions](#batcher-transaction) for epoch `N` and spans blocks `[N, N + SWS)`, with `SWS` the sequencer window size. The default `sws` is 3600 epochs. The window's first block also fixes the [depositing transactions](#depositing-transaction) that decide which [deposits](#deposits) go into the epoch's first L2 block. ### Sequencing epoch A sequencing epoch is the consecutive run of L2 blocks derived from one [sequencing window](#sequencing-window) of L1 blocks. Its epoch number equals the block number of the window's first L1 block. Epochs may vary in length within defined limits; the L2 chain derivation spec covers the constraints. ### L1 origin The L1 origin of an L2 block is the L1 block tied to that block's [sequencing epoch](#sequencing-epoch). ## Deposits Broadly, a deposit is an L2 transaction that the [rollup driver](#rollup-driver) derives from an L1 block. Deposits are commonly — though not exclusively — used to bridge ETH and tokens onto L2, but the word should be read as "a transaction *deposited* to L2 from L1." Because these "transactions" surface at several layers, the term is ambiguous; the entries below pin down the variants. A *deposit* can mean: - A [deposited transaction](#deposited-transaction) on L2 belonging to a deposit block. - A [depositing call](#depositing-call) that triggers a deposited transaction to be derived. - The log/event data produced by that depositing call, which the rollup driver reads in order to derive the deposited transaction. The phrase *user deposit* is similar but deliberately leaves out [L1 attributes deposited transactions](#l1-attributes-deposited-transaction). ### Deposited transaction An L2 transaction derived from L1 and placed into an L2 block. Two kinds exist: - The [L1 attributes deposited transaction](#l1-attributes-deposited-transaction), which records the L1 block's attributes into the [L1 Attributes Predeployed Contract](#l1-attributes-predeployed-contract). - [User-deposited transactions](#user-deposited-transaction), derived from an L1 call into the [deposit contract](#deposit-contract). ### L1 attributes deposited transaction A [deposited transaction](#deposited-transaction) that writes the attributes of an L1 block — its number, timestamp, and so on — onto L2 by calling the [L1 Attributes Predeployed Contract](#l1-attributes-predeployed-contract). Other contracts can then read those attributes for the L1 block matching the current L2 block. ### User-deposited transaction A [deposited transaction](#deposited-transaction) produced from an L1 [depositing call](#depositing-call) into the [deposit contract](#deposit-contract). ### Depositing call An L1 call into the [deposit contract](#deposit-contract) that the [rollup driver](#rollup-driver) turns into a [user-deposited transaction](#user-deposited-transaction). The call supplies every field the resulting deposited transaction needs — destination, value, calldata, and the rest. ### Depositing transaction An L1 transaction that issues one or more [depositing calls](#depositing-call). ### Depositor The L1 account — contract or [EOA](#eoa) — that is the `msg.sender` of the [depositing call](#depositing-call). The depositor is explicitly **not** the originator (`tx.origin`) of the surrounding depositing transaction. ### Deposited transaction type The [EIP-2718](#transaction-type) transaction type that defines the input fields and processing rules for a [deposited transaction](#deposited-transaction). ### Deposit contract An [L1](#layer-1-l1) contract that [EOAs](#eoa) and contracts send [deposits](#deposits) to. It emits each deposit as a log record — a Solidity event — for [rollup nodes](#rollup-node) to consume. One subtlety: deposits are not carried in calldata, because a deposit can originate from a contract. In that case the calldata is part of *internal* contract-to-contract execution and never lands in any of the [Merkle Patricia Trie](#merkle-patricia-trie) roots committed by the L1 block. ## Withdrawals In general terms, a withdrawal is a transaction sent from L2 to L1 that can move data, value, or both. As with deposits, the word spans multiple layers and is therefore imprecise. To keep the L1 and L2 halves distinct: - A *withdrawal initiating transaction* is the L2 transaction sent to the withdrawals predeploy. - A *withdrawal finalizing transaction* is the L1 transaction that finalizes and relays that withdrawal. ### Relayer An L1 EOA that finalizes a withdrawal by submitting whatever data is needed to prove the withdrawal's inclusion on L2. ### Finalization period The finalization period — also called the *withdrawal delay* — is the minimum number of seconds that must pass before a [withdrawal](#withdrawals) can be finalized. The delay exists to give [validators](#validator) enough time to mount a [fault proof](#fault-proof). ## Configuration ### Batch inbox The **Batch Inbox** is the address that sequencer transaction batches are posted to. A sequencer publishes to it by naming it as the `to` address of a transaction whose batched L2 transactions ride in calldata or in blobdata. ### Batcher hash The **Batcher Hash** marks which sender(s) the L2 clients of a given Base chain will accept transactions from at the [Batch Inbox](#batch-inbox). Its first byte is a version tag. The V0 layout is a 32-byte hash structured as: | 1 byte | 11 bytes | 20 bytes | | -------------- | -------- | -------- | | version (0x00) | empty | address | Equivalently: ```solidity bytes32(address(batcher)) ``` Here `batcher` is the address that submits to the Batch Inbox. In short, the V0 hash names a *single* address whose batches the L2 clients will honor. Versioning the hash leaves room to later reinterpret it — for example as a commitment to an allowlist of accounts or some other batcher-identification scheme. ### Fee scalars The **Fee Scalars** are the parameters that feed the L1 data fee charged on L2 transactions. They are also known as Gas Price Oracle (GPO) parameters. #### Pre-Ecotone parameters Prior to the Ecotone upgrade: - **Scalar**: a multiplier over the L1 base fee, read as a big-endian `uint256`. - **Overhead**: a fixed gas overhead, read as a big-endian `uint256`. #### Post-Ecotone parameters After Ecotone: - **Scalar** packs extra scalar information using a versioned encoding. - **Overhead** is disregarded and has no bearing on the L2 state-transition output. #### Post-Ecotone scalar encoding The Scalar is a big-endian `uint256` reinterpreted as `bytes32` and laid out as: - Byte `0`: scalar-version byte. - Bytes `[1, 32)`: contents depend on the scalar version: - Scalar-version `0`: - Bytes `[1, 28)`: padding, expected to be zero. - Bytes `[28, 32)`: big-endian `uint32`, the L1-fee `baseFeeScalar`. - Implies `blobBaseFeeScalar` is 0. - If the padding region holds any non-zero byte, `baseFeeScalar` must be set to MaxUint32. - Scalar-version `1`: - Bytes `[1, 24)`: padding, must be zero. - Bytes `[24, 28)`: big-endian `uint32`, the `blobBaseFeeScalar`. - Bytes `[28, 32)`: big-endian `uint32`, the `baseFeeScalar`. `baseFeeScalar` captures the user transaction's per-byte share of the total regular L1 EVM gas the batch-submitter's data transaction consumes; for blob transactions this is the L1 transaction's fixed intrinsic gas cost. `blobBaseFeeScalar` captures the user transaction's per-byte share of the blobdata that the batch-submitter's data transaction introduces. ### Unsafe block signer The **Unsafe Block Signer** is an Ethereum address whose private key signs "unsafe" blocks before they go to L1. The signature lets P2P-network nodes treat those blocks as the canonical unsafe blocks, which guards the P2P layer against denial-of-service attacks. So that the value can be read with a storage proof regardless of storage layout, it lives at a dedicated slot keyed by `keccak256("systemconfig.unsafeblocksigner")`. Unlike the other system-config values, this one is purely a blockchain-policy setting and is not a consensus-level parameter. ### L2 gas limit The **L2 Gas Limit** caps the gas a single L2 block may spend, keeping blocks small enough to process and prove. A change takes full effect in the first L2 block whose L1 origin carried the change. The limit may not exceed the protocol's maximum gas limit, which keeps L2 blocks provable and processable by consensus and execution clients. ## Batch submission ### Data availability Data availability is the assurance that some data stays *retrievable* over a reasonably long window. For Base, that data is the [sequencer batches](#sequencer-batch) that [validators](#validator) need in order to check the sequencer's work and validate the L2 chain. The [finalization period](#finalization-period) is the lower bound on this window, since availability matters most then — that is when the data is required to run a [fault proof](#fault-proof). Note that "available" is **not** a promise of long-term storage. ### Data availability provider A service that makes data available, in the sense described under [data availability](#data-availability). A good provider backs that availability with strong, *verifiable* guarantees. Today the supported providers are Ethereum calldata and blob data. ### Sequencer batch A sequencer batch is a list of L2 transactions (submitted to a sequencer) tagged with a [epoch number](#sequencing-epoch) and an L2 block timestamp — which converts straight to a block number because the block time is fixed. Batches are part of the [L2 derivation inputs](#l2-derivation-inputs). Each batch carries the inputs to build exactly **one** L2 block on top of existing L2 state — except for an epoch's first block, which additionally needs deposit information. ### Channel A channel is a run of [sequencer batches](#sequencer-batch) for consecutive blocks, compressed together. Grouping batches improves the compression ratio and so lowers data-availability cost. A channel can be cut into [frames](#channel-frame) for transmission inside [batcher transactions](#batcher-transaction), which matters when the channel is too big to fit in one transaction. Each channel is identified by its creation timestamp (UNIX time) plus a random value. From the [rollup node's](#rollup-node) perspective, a channel is *open* until its explicitly-marked final frame has been read, and closed thereafter. ### Channel frame A channel frame is a slice of a [channel](#channel). A single [batcher transaction](#batcher-transaction) may carry one or several frames. Splitting a channel into frames is what allows oversized channels to be transmitted across multiple transactions. ### Batcher The batcher is a standalone program tasked with publishing channels to a data-availability provider. It pulls channels from the rollup node and makes them available through [batcher transactions](#batcher-transaction). ### Batcher transaction A batcher transaction is the transaction a [batcher](#batcher) sends to a data-availability provider to publish channels. Each one carries one or more complete frames, which may come from different channels, and a single channel's frames may be spread across several batcher transactions. When posted to Ethereum calldata, the transaction's receiver must be the sequencer inbox address and it must be signed by a recognized batch submitter account. That account is recorded in the [System Configuration](#system-configuration). ### Batch submission frequency As long as it stays within the [sequencing window](#sequencing-window), the batcher may post L2 blocks for data availability whenever it likes; the batcher software lets operators tune this policy dynamically. The rollup's safety and liveness guarantees are enforced through the sequencing window if data fails to arrive in time. Posting in smaller, more frequent chunks shortens the delay before L2 block inputs are confirmed, lets verifiers establish safety sooner, and reduces the time to L1 finality (and thus to L2 input-finality). Posting in larger, less frequent chunks aggregates more L2 data per submission, spreading the fixed overhead of batch submission over more data and lowering cost — especially for lower-throughput chains that take a while to fill a data transaction (typically 128 KB of calldata or 800 KB of blobdata). ### Channel timeout The channel timeout is a window, measured in L1 blocks, during which a channel's [frames](#channel-frame) may arrive on L1 inside [batcher transactions](#batcher-transaction). A [channel's](#channel) frames are accepted over the time range `[channel_id.timestamp, channel_id.timestamp + CHANNEL_TIMEOUT]`; any L1 block whose timestamp lands in that range is eligible. (The `channel_id.timestamp` must be earlier than the L1 block timestamp of every L1 block carrying the channel's frames, or those frames are dropped.) Channel timeouts serve two purposes: they stop old, never-closed channels from lingering indefinitely, and they bound how far back through L1 blocks a node must scan to decode [sequencer batches](#sequencer-batch) — which is especially useful during L1 re-orgs. ## L2 output root proposals ### Proposer The proposer builds and submits output roots — commitments to L2 state — to the L2OutputOracle contract on the L1 settlement layer. It periodically asks the rollup node for the latest output root derived from the most recent finalized L1 block, then posts that root to the L2OutputOracle. ## L2 chain derivation L2 chain derivation is the process of reading [L2 derivation inputs](#l2-derivation-inputs) from L1 and using them to reconstruct the L2 chain. The L2 chain derivation specification has the full details. ### L2 derivation inputs The data found in L1 blocks that the [rollup node](#rollup-node) reads to assemble [payload attributes](#payload-attributes). These inputs are: - L1 block attributes - block number - timestamp - basefee - blob base fee - [deposits](#deposits) (carried as log data) - [sequencer batches](#sequencer-batch) (carried as transaction data) - [System configuration](#system-configuration) updates (carried as log data) ### System configuration The set of dynamically adjustable rollup parameters held by the `SystemConfig` contract on L1 and read by the L2 [derivation](#l2-chain-derivation) process. Keeping them in a contract lets keys be rotated and external cost parameters be retuned without going through a hardfork. The Configuration reference has the full overview. ### Payload attributes An object derived from the [L2 derivation inputs](#l2-derivation-inputs) on L1 and handed to the [execution engine](#execution-engine) to build L2 blocks. In effect it encodes [a block stripped of its output properties](#block). The object originates in the Ethereum Engine API spec and is extended by the Execution Engine specification. ### L2 genesis block The first block of the L2 chain in its current version. Its state is made up of: - State carried over from the previous version of the L2 chain — possibly altered by "state surgeries" (for instance, the Bedrock migration changed how native ETH balances were held in the storage trie). - [Predeployed contracts](#predeployed-contract-predeploy). The genesis block's timestamp must be a multiple of the [block time](#block-time) — an even number, since the block time is 2 seconds. When the rollup moves to a new protocol version, it may perform a *squash fork*, which mints a fresh L2 genesis block numbered `X + 1`, where `X` is the last L2 block before the upgrade. This differs from a *re-genesis*, an older approach that reset L2 block numbers so the new genesis block was number 0; re-genesis will not be used again. Squash forks win out because they avoid reusing L2 block numbers, which would break many external tools. ### L2 chain inception The L1 block number at which the output roots for the [genesis block](#l2-genesis-block) were first proposed to the output oracle contract. In the current implementation that is the L1 block where the output oracle was deployed or upgraded. ### Safe L2 block An L2 block that a [rollup node](#rollup-node) can derive in full from L1. Which block qualifies can differ between nodes depending on each node's view of the L1 chain. ### Safe L2 head The highest [safe L2 block](#safe-l2-block) a [rollup node](#rollup-node) is aware of. ### Unsafe L2 block An L2 block a [rollup node](#rollup-node) knows about but has not derived from L1. In sequencer mode this is a block the sequencer produced itself; in validator mode it is a block pulled from the sequencer via [unsafe sync](#unsafe-sync). ### Unsafe L2 head The highest [unsafe L2 block](#unsafe-l2-block) a [rollup node](#rollup-node) is aware of. ### Unsafe block consolidation The process by which a [rollup node](#rollup-node) advances the [safe L2 head](#safe-l2-head) by one block, promoting the oldest [unsafe L2 block](#unsafe-l2-block) to be the new safe head. To do so the node checks that the [payload attributes](#payload-attributes) derived from L1 match that oldest unsafe block exactly. ### Finalized L2 head The highest L2 block derivable from *finalized* L1 blocks — that is, L1 blocks older than two L1 epochs (64 L1 [time slots](#time-slot)). ## Other L2 chain concepts ### Address aliasing When a contract sends a [deposit](#deposits) from L1 to L2, the address reported by `ORIGIN` and `CALLER` on L2 is an *aliased* variant of the originating contract's address rather than the address itself. ### Rollup node The rollup node [derives the L2 chain](#l2-chain-derivation) from the L1 chain — from L1 [blocks](#block) and their associated [receipts](#receipt). It runs in either *validator* or *sequencer* mode. In sequencer mode it takes L2 transactions from users, builds L2 blocks, and posts them to a [data availability provider](#data-availability-provider) through [batch submission](#batch-submission); derivation then doubles as a sanity check and a way to spot L1 [re-orgs](#chain-re-organization). In validator mode it derives the chain as usual but can also "run ahead" of L1 by fetching blocks straight from the sequencer, with derivation serving to verify the sequencer's behavior. A validator-mode node is sometimes called a *replica*. ### Rollup driver The [rollup node](#rollup-node) component responsible for [deriving the L2 chain](#l2-chain-derivation) from the L1 chain and its [receipts](#receipt). ### L1 attributes predeployed contract A [predeployed contract](#predeployed-contract-predeploy) on L2 for looking up the attributes of an L1 block by its number or hash. ### L2 output root A 32-byte value committing to the current state of the L2 chain. ### L2 output oracle contract An L1 contract that the [sequencer](#sequencer) posts [L2 output roots](#l2-output-root) to. ### Validator An individual or organization running a [rollup node](#rollup-node) in validator mode. Doing so brings benefits much like running an Ethereum node — for example simulating L2 transactions locally without rate limits. It also lets the validator audit the [sequencer](#sequencer) by re-deriving [output roots](#l2-output-root) and comparing them to the submitted ones; on a mismatch the validator can run a [fault proof](#fault-proof). ### Fault proof An on-chain, *interactive* proof that [validators](#validator) run to show that a [sequencer](#sequencer) submitted incorrect [output roots](#l2-output-root). ### Time slot L2 produces a block every 2 seconds — this interval is the [block time](#block-time). A "time slot" occurs at every 2-second multiple after the [L2 genesis block's](#l2-genesis-block) timestamp. On post-[merge](https://ethereum.org/en/eth2/merge/) L1, time slots come every 12 seconds, though an L1 block need not be produced for every slot, even under benign consensus hiccups. ### Block time The L2 block time is 2 seconds, so there is an L2 block at each 2-second [time slot](#time-slot). Post-merge, the L1 block time can be described as 12 seconds — its [time slot](#time-slot) length — but it is effectively variable because some slots are skipped. Pre-merge, L1 block time was variable too, averaging around 13 seconds. ### Unsafe sync The process by which a [validator](#validator) learns of [unsafe L2 blocks](#unsafe-l2-block) directly from the [sequencer](#sequencer). These blocks still have to be confirmed against the L1 chain later, via [unsafe block consolidation](#unsafe-block-consolidation). ## Execution engine concepts ### Execution engine The execution engine runs the transactions in a block and computes the resulting state root, receipts root, and block hash. Both post-[merge](https://ethereum.org/en/eth2/merge/) L1 and L2 have one. On L1, the blocks it executes either come from synchronizing the L1 chain or are freshly minted from the L1 mempool at the consensus layer's request. On L2, blocks are always freshly minted at the [rollup node's](#rollup-node) request, from transactions [derived from L1 blocks](#l2-chain-derivation). Throughout these specifications, "execution engine" means the L2 execution engine unless stated otherwise. --- # Isthmus L2 Execution Engine Source: https://basehub.org/specifications/isthmus-exec-engine/ Description: Execution-engine changes introduced by the Isthmus upgrade — the L2ToL1MessagePasser storage root in the block header, deposit-request handling, BLS precompiles, and the new operator fee. Isthmus reworks several execution-layer rules. The headline change is that the block header's `withdrawalsRoot` field now carries the storage root of the `L2ToL1MessagePasser` predeploy. The upgrade also adjusts deposit-request handling, adds BLS precompiles, and introduces a configurable operator fee. ## Timestamp Activation Like every Base network upgrade, Isthmus turns on at a timestamp. The new L2 block execution rules take effect once `L2 Timestamp >= activation time`. ## `L2ToL1MessagePasser` Storage Root in Header From Isthmus activation onward, the L2 block header's `withdrawalsRoot` field holds the 32-byte `L2ToL1MessagePasser` account storage root, taken from the world state identified by the header's `stateRoot`. That root is the same value `eth_getProof` returns for the account at the given block number. ### Header Validity Rules Before Isthmus activates: - the L2 block header's `withdrawalsRoot` field must be: - `nil` if Canyon has not been activated. - `keccak256(rlp(empty_string_code))` if Canyon has been activated. - the L2 block header's `requestsHash` field must be omitted. After Isthmus activates, an L2 block header is valid if and only if: 1. The `withdrawalsRoot` field 1. Is 32 bytes in length. 1. Matches the `L2ToL1MessagePasser` account storage root, as committed to in the `storageRoot` within the block header 1. The `requestsHash` field is equal to `sha256('') = 0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`, indicating no requests in the block. ### Header Withdrawals Root | Byte offset | Description | | ----------- | -------------------------------------------- | | `[0, 32)` | `L2ToL1MessagePasser` account storage root | #### Rationale Generating L2 output roots for historical blocks otherwise requires an archive node, which pushes real cost onto the people who keep the system running. In a world secured by fault proofs that means a proposer needs an archive node to propose an output root at the safe head, and a user proving a withdrawal needs one to confirm that the output root they are proving against is genuinely valid and part of the safe chain. Committing the `L2ToL1MessagePasser` storage root directly into the `withdrawalsRoot` field removes that requirement, so both proposers and verifiers can do their work at lower operating cost. #### Genesis Block If Isthmus is active at the genesis block, the genesis header's `withdrawalsRoot` is set to the `L2ToL1MessagePasser` account storage root. #### State Processing While transactions are being validated, the header's `withdrawalsRoot` must not be exposed to the EVM or the application layer. #### P2P During sync the withdrawals list in the block body is expected to be empty (OP Stack does not use it), so its hash is the MPT root of an empty list. When the synced header chain is verified against the final synced header, the header timestamp decides whether Isthmus is active at that block. If it is, the header's `withdrawalsRoot` MPT hash may be any non-null value, since it is expected to hold the `L2ToL1MessagePasser` storage root. #### Backwards Compatibility Considerations From Canyon (which brings in Shanghai support) up until Isthmus activation, `withdrawalsRoot` is set to the MPT root of an empty withdrawals list — the same value as an empty storage root. Withdrawals are captured in the L2 state during this window but are not reflected in `withdrawalsRoot`, so even when a header carries a non-trivial MPT root in that field before Isthmus, it must not be relied on. Any output-root calculation has to avoid using the header `withdrawalsRoot` in this range. There is always nonzero storage in the `L2ToL1MessagePasser`, because it is a proxied predeploy and stores an implementation address and owner address from genesis. As a result, from Isthmus the `withdrawalsRoot` is always non-nil and never equal to the MPT root of an empty list. #### Forwards Compatibility Considerations The `withdrawalsRoot` field is otherwise unused in Base's header consensus format and is not slated for any other use. Pointing it at the withdrawal account's storage root fits Base cleanly and reuses a field that already exists in the L1 header format. #### Client Implementation Considerations Execution clients keep historical account state in different ways. In a contrived case where Base produced no outbound withdrawal for a long stretch, a node might not retain the `L2ToL1MessagePasser` account storage root and would then be unable to stay in consensus. In practice most modern clients can reconstruct an account's storage root at a given block on demand even when they do not persist it directly. ##### Transaction Simulation For RPC methods such as `eth_simulateV1` that simulate arbitrary transactions across one or more blocks, the header of a simulated block should carry an empty withdrawals root. The same applies whenever the real withdrawals-root value is not readily available. ## Deposit Requests [EIP-6110] moves deposits to the execution layer and defines a new [EIP-7685] deposit request of type `DEPOSIT_REQUEST_TYPE`, which would otherwise show up in the [EIP-7685] requests list. Base ignores these: requests generation is modified to leave out [EIP-6110] deposit requests. Because the [EIP-6110] request type did not exist before Pectra on L1 and Isthmus on L2, no activation time is needed — these deposit-type requests can always be excluded. [EIP-6110]: https://eips.ethereum.org/EIPS/eip-6110 [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685 ## Block Body Withdrawals List The withdrawals list in the block body is encoded as an empty RLP list. ## EVM Changes ### BLS Precompiles As with the `bn256Pairing` precompile in the Granite hardfork, [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537) adds a BLS precompile that short-circuits based on input size in the EVM. The input-size limits for the BLS precompile contracts are: - G1 multiple-scalar-multiply: `input_size <= 513760 bytes` - G2 multiple-scalar-multiply: `input_size <= 488448 bytes` - Pairing check: `input_size <= 235008 bytes` The remaining BLS precompiles are fixed-size operations with a fixed gas cost. ## Block Sealing On Base, `EIP-7685` is no-op'd and `requestsHash` is always set to `sha256('')`, as covered in the [header validity rules](#header-validity-rules). That also means [EIP-6110](https://eips.ethereum.org/EIPS/eip-6110), [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002), and [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) are not enabled. After Isthmus activation, the Base execution layer must not run the post-block deposit-contract event filtering (EIP-6110) or the `EIP-7002` + `EIP-7251` system calls during block sealing. Users remain free to deploy these contracts permissionlessly, but the Base execution layer gives them no special treatment, and the system calls L1 introduced in Pectra are not considered. ## Engine API Updates ### Update to `ExecutionPayload` After Isthmus, `ExecutionPayload` carries an additional `withdrawalsRoot` field. ### `engine_newPayloadV4` API From Isthmus onward, `engine_newPayloadV4` is used. The `executionRequests` parameter MUST be an empty array. ## Fees Different OP Stack variants consume resources differently and need a more flexible pricing model. To allow more customizable fee structures, Isthmus adds a new term to the fee calculation — the `operatorFee` — parameterized by two scalars, the `operatorFeeScalar` and the `operatorFeeConstant`. ### Operator Fee The operator fee is wired directly into the EVM, alongside the standard gas fee and Base's L1 data fee. It behaves like the EVM's existing fees, just with a different beneficiary account. #### Fee Formula ```text operatorFee = (gas * operatorFeeScalar / 10^6) + operatorFeeConstant ``` Where: - `gas` is the amount of gas that the transaction used. When calculating the amount of gas that is bought at the beginning of the transaction, this should be the `gas_limit`. When determining how much gas should be refunded, based off of how much of the `gas_limit` the transaction used, this should be the `gas_used`. - `operatorFeeScalar` is a `uint32` scalar set by the chain operator, scaled by `1e6`. - `operatorFeeConstant` is a `uint64` scalar set by the chain operator. The operator fee's maximum value fits in 77 bits, which follows from the maximum input parameters: ```text operatorFee_max = (uint64_max * uint32_max / 10^6) + uint64_max ≈ 7.924660923989131 * 10^22 ``` So implementations that compute it with `uint256` types do not need to check for overflow. #### Deposit Operator Fees Deposit transactions are never charged an operator fee: whatever the operator fee parameters are, it is **zero** for them. They also receive no operator-fee gas refund, since they never bought that gas in the first place. #### EVM Fee Semantics Like the EVM's other fees, the operator fee is charged following this pattern: 1. During pre-execution validation, the account must have enough ETH to cover the existing worst-case gas + L1 data fees _as well as_ the worst-case operator fee (for deposits, the worst-case fee is `0`). To compute this value, use the [fee formula](#fee-formula) with `gas` set to the `gas_limit` of the transaction, and add it to the existing worst-case transaction fee. 1. When buying gas prior to execution, charge the account the worst-case operator fee. To compute this value, use the [fee formula](#fee-formula) with `gas` set to the `gas_limit` of the transaction. 1. After execution, when issuing refunds, transactions that bought operator fee gas should be refunded the operator fee gas that was unused (i.e., the caller should only be charged the _effective_ operator fee.) The refund should be calculated as `opFeeRefund = opFeeWorstCase - opFeeActual`, where: - `opFeeWorstCase` is as described in #1 + #2. - `opFeeActual` is the amount of the operator fee that was actually used. This value is computed using the [fee formula](#fee-formula) with `gas` set to the `gas_limit - gas_used + refunded_gas`. `refunded_gas` is as described in [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529). 1. After execution, when rewarding the fee beneficiaries, send the _spent operator fee_ to the operator fee vault. This value is exactly `opFeeActual` as described above. Implementations must ensure ETH is neither minted nor destroyed as a result of the operator fee. #### Transaction Pool Changes Because this extra fee feeds into transaction validity, the transaction pool must reject any transaction whose balance cannot cover the worst-case cost — and that worst-case cost now includes the worst-case operator fee. #### Configuring Operator Fee Parameters `operatorFeeScalar` and `operatorFeeConstant` are loaded much like the `baseFeeScalar` and `blobBaseFeeScalar` used in the `L1Fee` calculation. They can be read in two interchangeable ways: - read from the deposited L1 attributes (`operatorFeeScalar` and `operatorFeeConstant`) of the current L2 block - read from the L1 Block Info contract (`0x4200000000000000000000000000000000000015`) - using the respective solidity getter functions (`operatorFeeScalar`, `operatorFeeConstant`) - using direct storage-reads: - Operator fee scalar as big-endian `uint32` in slot `8` at offset `0`. - Operator fee constant as big-endian `uint64` in slot `8` at offset `4`. ### Fee Vaults The collected operator fees go to a new vault, the `OperatorFeeVault`. As with the existing vaults, this is a hardcoded address pointing at a pre-deployed proxy, backed by a `FeeVault`-based deployment that routes vault funds to L1 securely. ### Receipts After Isthmus activation, two new fields — `operatorFeeScalar` and `operatorFeeConstant` — are added to transaction receipts whenever at least one of them is non-zero. --- # Jovian L2 Execution Engine Source: https://basehub.org/specifications/jovian-exec-engine/ Description: Execution-engine changes introduced by the Jovian upgrade — a configurable minimum base fee, the DA footprint block limit, the updated operator fee formula, and precompile input-size restrictions. Jovian reworks several execution-layer rules. It adds a configurable minimum base fee, introduces a DA footprint block limit that ties data-availability usage into base fee dynamics, raises the ceiling on the operator fee, and tightens input-size limits on a handful of precompiles. ## Minimum Base Fee Jovian adds a [configurable minimum base fee](https://github.com/ethereum-optimism/design-docs/blob/main/protocol/minimum-base-fee.md) whose purpose is to shorten priority-fee auctions on Base. It is configured in `SystemConfig` (see [System Configuration](../../protocol/consensus/derivation#system-configuration)); the execution engine then enforces it, encoding the value into the block header's `extraData` and threading it through the `PayloadAttributesV3` parameters of the Engine API. ### Minimum Base Fee in Block Header As with [Holocene's dynamic EIP-1559 parameters](../holocene/exec-engine#dynamic-eip-1559-parameters), Jovian packs fee parameters into each L2 block header's `extraData`. The layout grows by one `u64` field holding the minimum base fee in wei. | Name | Type | Byte Offset | | ------------------- | ------------------ | ----------- | | `minBaseFee` | `u64 (big-endian)` | `[9, 17)` | Constraints: - `version` MUST be `1` (raised from Holocene's `0`). - Nothing may appear past these 17 bytes. `minBaseFee` is an absolute floor expressed in wei. When the base fee is computed, a result below `minBaseFee` MUST be clamped up to it: ```javascript if (baseFee < minBaseFee) { baseFee = minBaseFee } ``` Note: `extraData` is capped at 32 bytes (matching the L1 beacon-chain `extraData` type), and future upgrades may extend it. ### Minimum Base Fee in `PayloadAttributesV3` The Engine API [`PayloadAttributesV3`](../../protocol/execution/index#extended-payloadattributesv3) gains a new `minBaseFee` field. The existing `eip1559Params` stays at 8 bytes (the Holocene format). ```text PayloadAttributesV3: { timestamp: QUANTITY prevRandao: DATA (32 bytes) suggestedFeeRecipient: DATA (20 bytes) withdrawals: array of WithdrawalV1 parentBeaconBlockRoot: DATA (32 bytes) transactions: array of DATA noTxPool: bool gasLimit: QUANTITY or null eip1559Params: DATA (8 bytes) or null minBaseFee: QUANTITY or null } ``` `minBaseFee` MUST be `null` before the Jovian fork and non-`null` after it. ### Rationale As with [Holocene's dynamic EIP-1559 parameters](../holocene/exec-engine#rationale), keeping the minimum base fee in the block header means block sealing never has to reach into state. The function that derives the next block's base fee from its parent header stays pure while the parameter remains dynamically configurable — handled much like `gasLimit`, with the derivation pipeline feeding the relevant `SystemConfig` values to the block builder through `PayloadAttributesV3`. ## DA Footprint Block Limit Jovian introduces a _DA footprint block limit_ that caps the total estimated compressed transaction data a block may carry. Each transaction now tracks a new resource — its DA footprint — alongside gas usage. The figure is scaled into the gas dimension so its block total can be bounded by the block gas limit, just like total gas usage. A block's `daFootprint` is defined as: ```python def daFootprint(block: Block) -> int: daFootprint = 0 for tx in block.transactions: if tx.type == DEPOSIT_TX_TYPE: continue daUsageEstimate = max( minTransactionSize, (intercept + fastlzCoef * tx.fastlzSize) // 1e6 ) daFootprint += daUsageEstimate * daFootprintGasScalar return daFootprint ``` Here `intercept`, `minTransactionSize`, `fastlzCoef`, and `fastlzSize` come from the [Fjord specs](../fjord/exec-engine), `DEPOSIT_TX_TYPE` is `0x7E`, and `//` denotes integer floor division. From Jovian onward, each block header's `blobGasUsed` property is set to that block's `daFootprint`. Pre-Jovian (since Ecotone) it was held at 0 because Base has no blob support; the field is now reused to carry the DA footprint. Block building must guarantee — and header validation must check — that a block's `daFootprint` stays under the `gasLimit`, exactly as it does for `gasUsed`. This implies a block may hold at most `gasLimit/daFootprintGasScalar` total estimated DA usage bytes. In addition, from Jovian the base fee update uses `gasMetered := max(gasUsed, blobGasUsed)` in place of the previous `gasUsed`. As a result, blocks with heavy DA usage can push the base fee up in following blocks. ### Scalar loading `daFootprintGasScalar` loads in the same fashion as the `operatorFeeScalar` and `operatorFeeConstant` [introduced](../isthmus/exec-engine#operator-fee) in Isthmus. It can be read two interchangeable ways: - from the current L2 block's deposited L1 attributes (`daFootprintGasScalar`), decoded per the [jovian schema](./l1-attributes) - from the L1 Block Info contract (`0x4200000000000000000000000000000000000015`) - through the Solidity getter `daFootprintGasScalar` - by reading storage directly — a big-endian `uint16` in slot `8` at offset `12`. Its default value is given in the [L1 Attributes](./l1-attributes) section. ### Receipts Once Jovian is active, transaction receipts gain a new `daFootprintGasScalar` field carrying its block's DA footprint gas scalar. The receipt's `blobGasUsed` field is also set to the transaction's DA footprint. ### Rationale Today's L1 fee mechanism prices DA usage from an estimate of each transaction's DA footprint, yet nothing at the protocol level reflects how limited DA throughput on L1 actually is. For example, on Ethereum L1 with Pectra enabled, blob throughput is roughly `~96 kB/s` (target `~64 kB/s`), yet the calldata floor gas price of `40` for calldata-heavy L2 transactions permits more incompressible data than Ethereum blob space could absorb on most Base chains. This is currently handled at the policy level by batcher-sequencer throttling, which artificially constricts block building — and can drag base fees down, costing chain operators and worsening the user experience (inclusion delays, priority-fee auctions). Imposing a hard cap on a block's DA footprint that simultaneously feeds into the base fee resolves these shortcomings of the policy-based approach. ## Operator Fee ### Fee Formula Update Jovian revises the operator fee calculation so larger fees can be charged. From the Jovian activation onward, the operator fee MUST be derived as: ```text operatorFee = (gas * operatorFeeScalar * 100) + operatorFeeConstant ``` In effect, the per-gas scalar applied becomes `100 * operatorFeeScalar`. Every other data type and the operator-fee semantics from the [Isthmus spec](../isthmus/exec-engine#operator-fee) carry over unchanged. ### Maximum value Under the new formula the operator fee's maximum value occupies 103 bits: ```text operatorFee_max = (uint64_max * uint32_max * 100) + uint64_max ≈ 7.924660923989131 * 10^30 ``` Implementations that use `uint256` for intermediate arithmetic need no extra overflow checks. ## EVM Changes ### Precompile Input Size Restrictions A few precompiles have revised input-size limits. The new caps are: - `bn256Pairing`: 81,984 bytes (427 pairs) - `BLS12-381 G1 MSM`: 288,960 bytes (1,806 pairs) - `BLS12-381 G2 MSM`: 278,784 bytes (968 pairs) - `BLS12-381 Pairing`: 156,672 bytes (408 pairs) --- # P2P Networking Stack Source: https://basehub.org/specifications/p2p/ Description: How Base nodes discover peers and gossip blocks across two independent P2P stacks — discv5 plus libp2p on the consensus layer and DevP2P plus RLPx on execution. import { Aside } from '@astrojs/starlight/components'; Base nodes run two independent peer-to-peer networks — one for the consensus layer (CL) and one for the execution layer (EL) — and bridge them locally through the Engine API. This guide walks through both stacks, why they exist, and where the relevant code lives in [`base/base`](https://github.com/base/base). ## Why P2P matters A blockchain is a network of computers that must coordinate without a central server. Peer-to-peer networking provides that coordination. Each node connects to a handful of peers, and those peers connect to others, forming a mesh — a web of overlapping links where every node can reach every other node by some path. When a node sees a new block or transaction, it relays it to its neighbors, and within seconds the entire network has converged. This propagation pattern is gossip. P2P networking solves three problems: 1. **Discovery** — how a fresh node finds peers to connect to. 2. **Transport** — how two nodes establish a secure session and exchange data. 3. **Gossip** — how connected nodes broadcast new information efficiently without flooding the network. ## Why Ethereum has two P2P layers Before The Merge in September 2022, Ethereum ran a single P2P network. Every node ran one execution client (such as Geth) that handled discovery, transaction gossip, block propagation, and proof-of-work consensus. The networking stack was DevP2P, a custom protocol suite designed for Ethereum starting in 2014. The Merge introduced a second binary: the consensus client. Consensus clients follow the beacon chain — a separate chain coordinating proof-of-stake — and decide which blocks are canonical. The beacon chain was designed from scratch on libp2p rather than DevP2P because libp2p is more modular and better suited to the publish/subscribe patterns required for attestation and block gossip in proof-of-stake. Merging the stacks would have been a massive undertaking with little practical benefit, so Ethereum kept them separate and bridged them with a local API. After The Merge, every Ethereum node runs two processes with two separate P2P networks. The execution layer continues to use DevP2P over RLPx (an encrypted transport described later) for transaction and historical block exchange. The consensus layer uses libp2p with gossipsub (a broadcast protocol described later) for beacon blocks and attestations. The two layers communicate locally through the Engine API — JSON-RPC over HTTP or WebSocket on localhost. A rollup like Base inherits the same two-layer architecture but ships different payloads. Where Ethereum L1 gossips beacon blocks and attestations from hundreds of thousands of validators, Base's CL gossips L2 execution payloads — the new blocks produced by the sequencer (the single designated node that orders and produces L2 blocks). The sequencer signs each block with its private key so other nodes can verify it came from the authorized producer rather than from an attacker. It then publishes the signed block to the CL gossip network, and every Base node picks it up from there. This is a latency optimization, not a security mechanism. Rollup security comes entirely from L1: batch data posted to Ethereum L1 is the canonical source of truth, and the derivation pipeline (software that reconstructs L2 state from L1 batch data) can rebuild the entire L2 chain from L1 alone. P2P gossip lets nodes see new L2 blocks several minutes before the corresponding batch lands on L1, reducing user-visible latency. The EL still runs a DevP2P network for transaction pool gossip and historical sync, though in practice the sequencer often receives transactions directly rather than through EL gossip. ## How the CL and EL communicate: the Engine API Before walking through each stack, it helps to know how they connect. The consensus and execution clients do not share a P2P network — they share a local RPC bridge called the Engine API: a JSON-RPC interface exposed by the execution client (typically on port 8551) and secured with JWT authentication, a shared-secret mechanism that proves the caller is authorized. Only the co-located consensus client can issue these calls. The Engine API has three core methods. Think of it as a three-step exchange: "start building a block," "give me what you built," and "here's a block someone else built — check it." `engine_forkchoiceUpdated` tells the execution client which block is the current head, the latest safe block, and the latest finalized block. When the consensus client also passes optional payload attributes (timestamp, fee recipient, and other parameters describing the block to build), the call additionally instructs the execution client to begin assembling a new block. `engine_getPayload` retrieves the block the execution client has been assembling from its mempool. `engine_newPayload` sends an execution payload received from gossip to the execution client for validation and execution. The execution client re-executes all transactions, verifies the state root (a cryptographic fingerprint of the entire blockchain state after executing the block's transactions), and responds with VALID, INVALID, or SYNCING (meaning the node has not caught up to the chain tip yet and cannot validate the block). For block production on Base, the flow is: the sequencer's CL calls `engine_forkchoiceUpdated` with payload attributes to start building, waits, calls `engine_getPayload` to retrieve the built payload, signs it, and publishes it to the CL gossip network. For block validation, the flow is reversed: the CL receives a signed block from gossip, validates the signature, sends the execution payload to the EL via `engine_newPayload`, and uses the validity response to accept or reject the block. This separation lets each layer evolve its networking independently. It also means implementations are swappable — any CL and EL that speak the Engine API can be paired. ## The consensus layer P2P stack The Base consensus P2P stack lives under [`crates/consensus/`](https://github.com/base/base/tree/main/crates/consensus) and is composed of four crates that layer on top of one another: peers, disc, gossip, and the network actor in the service crate. ### Peers — the foundation The [`base-consensus-peers`](https://github.com/base/base/tree/main/crates/consensus/peers) crate provides the fundamental types for identifying and managing peers on the consensus network. The central concept is the ENR (Ethereum Node Record), defined in [EIP-778](https://eips.ethereum.org/EIPS/eip-778). An ENR is a compact, self-describing identity document a node uses to advertise itself. It is encoded with RLP (Recursive Length Prefix, Ethereum's standard binary serialization format) and contains a cryptographic signature, a monotonically increasing sequence number (so newer records always rank higher), and a sorted set of key-value pairs. Predefined keys include `id` (the identity scheme — currently "v4" for secp256k1, the elliptic curve Ethereum uses for public-key cryptography), `secp256k1` (the node's compressed 33-byte public key), `ip` (IPv4 address), and `tcp`/`udp` (port numbers). When a node's information changes (for example, its IP address rotates), it bumps the sequence number and re-signs the record; the signature ensures records cannot be tampered with. ENRs are deliberately small (300 bytes maximum) because they propagate frequently and may need to fit in constrained transports such as DNS TXT records. Different layers of the Ethereum stack use ENR extension keys for chain-specific metadata. Ethereum L1's execution layer carries an `eth` key with fork ID information. The L1 consensus layer carries an `eth2` key with the fork digest and attestation subnet bitfield. On Base — and OP Stack chains in general — every ENR includes an `opstack` key encoding the L2 chain ID and a version number. This is how nodes on different chains (Base Mainnet versus Base Sepolia) tell each other apart during discovery. ENRs are typically displayed as a base64-encoded string prefixed with `enr:`. The [`BaseEnr`](https://github.com/base/base/blob/main/crates/consensus/peers/src/enr.rs) struct handles this encoding: ```rust /// The unique L2 network identifier pub struct BaseEnr { /// Chain ID pub chain_id: u64, /// The version. Always set to 0. pub version: u64, } impl BaseEnr { /// The ENR key literal string for the consensus layer. pub const OP_CL_KEY: &str = "opstack"; /// Constructs a BaseEnr from a chain id. pub const fn from_chain_id(chain_id: u64) -> Self { Self { chain_id, version: 0 } } } ``` When a node discovers another node's ENR, [`EnrValidation`](https://github.com/base/base/blob/main/crates/consensus/peers/src/enr.rs) checks that the `opstack` key is present, decodes correctly, and that the chain ID matches. If a Base Mainnet node (chain ID 8453) sees an ENR for a different chain, it ignores it. The peers crate also provides a [`BootStore`](https://github.com/base/base/blob/main/crates/consensus/peers/src/store.rs), a JSON file that persists discovered ENRs to disk so the node does not need to bootstrap discovery from scratch on every restart. The store caps at 2048 entries and prunes the oldest when full. ### Discovery — finding peers with discv5 The [`base-consensus-disc`](https://github.com/base/base/tree/main/crates/consensus/disc) crate implements peer discovery using discv5. Discv5 is a UDP-based protocol that maintains a distributed hash table (DHT) of node records. It succeeds discv4 (used by the EL) and was designed for the consensus layer's needs. Discv5 is inspired by Kademlia, an academic DHT protocol from 2002. A DHT is a system where many computers collectively maintain a lookup directory without a central coordinator. The Kademlia insight is using XOR as a distance metric. Every node has a 256-bit ID derived from its public key; the "distance" between two nodes is the XOR of their IDs interpreted as a number. The metric is symmetric (distance A→B equals B→A) and unrelated to physical or network distance. Two nodes with similar IDs can sit on opposite sides of the planet. Think of comparing two phone numbers digit by digit: numbers sharing a long prefix are "close" in Kademlia space even if the people holding them live in different countries. Each node maintains a routing table organized into 256 "k-buckets" — one per possible bit-length of XOR distance. Bucket 0 holds nodes whose IDs differ from the local ID only in the least significant bit; bucket 255 holds nodes that differ in the most significant bit. Each bucket holds up to k entries (typically k=16). Imagine sorting your contacts into folders by how their IDs differ from yours — most contacts end up in "close" folders and fewer in the "far" folders, which is the right shape for efficient lookups. To discover new peers, a node performs an iterative lookup: it picks a target ID (often random, to populate its routing table), finds the closest nodes it already knows, and sends them FINDNODE requests. Unlike discv4, which asks for nodes "near" a specific ID, discv5 FINDNODE requests specify a logarithmic distance — a number from 0 to 256 representing the bit position where IDs first differ ("give me all nodes at XOR distance 2^n from you"). It is more efficient and harder to abuse. The node receives lists of nodes at that distance, queries those newly learned nodes, and gets progressively closer to the target with each round. Fresh nodes bootstrap by connecting to a small set of well-known bootnodes whose addresses are hardcoded into the client. The bootnode responds to FINDNODE requests, giving the new node its first set of peers. From there the new node performs a few random lookups to fill its routing table and within minutes has a healthy set of diverse peers. The [`Discv5Driver`](https://github.com/base/base/blob/main/crates/consensus/disc/src/driver.rs) orchestrates discovery. On startup it initializes the discv5 UDP service with exponential backoff retries (waiting progressively longer between attempts — for example 1s, 2s, 4s, 8s). Once that succeeds, it starts the event stream, retrying up to 10 times with 2-second delays if initialization fails. Then it bootstraps by loading the boot store from disk and adding hardcoded boot nodes. Each ENR is validated against the expected chain ID before being added to the routing table: ```rust let validation = EnrValidation::validate(&enr, chain_id); if validation.is_invalid() { trace!(target: "discovery::bootstrap", enr = ?enr, validation = ?validation, "Ignoring Invalid Bootnode ENR"); continue; } if let Err(e) = disc.add_enr(enr.clone()) { debug!(target: "discovery::bootstrap", error = ?e, "Failed to add enr"); continue; } store.add_enr(enr); ``` After bootstrapping, the driver enters its main loop, doing several things concurrently via `tokio::select!` (a Rust macro that waits on multiple async operations and runs the code for whichever completes first). It runs periodic random node queries (every 5 seconds by default) to keep discovering peers. It listens for discv5 events such as `Discovered`, `SessionEstablished`, and `UnverifiableEnr`, and when it sees a valid ENR it forwards it through an `mpsc` (multi-producer, single-consumer) channel to the gossip layer so it can dial the peer. An `mpsc` channel is a thread-safe queue where multiple senders push to a single receiver — the primary way components communicate in async Rust. The driver also persists the current set of known ENRs to the boot store every 60 seconds. The driver communicates with the rest of the system through a [`Discv5Handler`](https://github.com/base/base/blob/main/crates/consensus/disc/src/handler.rs), a thin wrapper around an `mpsc::Sender`. Other parts of the system can request metrics, peer lists, the local ENR, or ask discovery to ban specific addresses. This channel-based design avoids shared mutable state across async boundaries. ### Gossip — broadcasting blocks with libp2p and gossipsub The [`base-consensus-gossip`](https://github.com/base/base/tree/main/crates/consensus/gossip) crate is where the action happens — this is the layer that actually receives and broadcasts L2 blocks across the network. Gossipsub is a publish/subscribe protocol on libp2p that solves the problem of distributing messages efficiently in a decentralized network where no node can be trusted. The naive approach (floodsub) forwards every message to every peer — it works but consumes huge bandwidth because every node receives each message from multiple sources. Gossipsub instead builds a sparse "mesh" overlay per topic. When a node subscribes to a topic, it announces the subscription to its peers. It then selects D peers (the "desired mesh degree") that are also subscribed to the topic and establishes mesh links by sending GRAFT control messages. Mesh links are symmetric: if A grafts B, B also considers A part of its mesh. When a message is published, it flows eagerly through these links — each receiving node forwards the message to all of its mesh peers for that topic (except the sender). The result is a connected overlay where messages propagate in O(log N) hops with bounded bandwidth per node. The protocol layers reliability on top of the mesh. Every heartbeat interval, each node sends IHAVE control messages to a random subset of non-mesh peers, listing recently seen message IDs. If a peer has not seen one of those messages (perhaps a mesh path failed or was slow), it replies with IWANT and the original node sends the full message. This lazy repair ensures messages reach everyone within a few heartbeat rounds even if the mesh is partitioned or a peer drops out. PRUNE messages are the inverse of GRAFT: a node sends PRUNE to remove a peer from its mesh, either because the mesh is too large or the peer's score is poor. Pruned peers are not disconnected — they simply move from the eager-push mesh to the lazy-gossip pool. Gossipsub v1.1, which Base uses, adds a peer scoring system. Each node evaluates its peers based on behavior (whether they deliver valid messages promptly, whether they spam duplicates) and preferentially keeps well-scored peers in the mesh while pruning poorly scored ones. A peer that repeatedly sends invalid messages accumulates a negative score, and once it crosses a configurable threshold it is pruned and eventually disconnected. v1.1 also added flood publishing (where the original publisher sends to all connected peers, not just mesh peers), though Base disables this by default to conserve bandwidth. The gossipsub configuration in Base lives in [`gossip/src/config.rs`](https://github.com/base/base/blob/main/crates/consensus/gossip/src/config.rs): ```rust pub const DEFAULT_MESH_D: usize = 8; // target peers in mesh pub const DEFAULT_MESH_DLO: usize = 6; // minimum before mesh repair pub const DEFAULT_MESH_DHI: usize = 12; // maximum before pruning pub const DEFAULT_MESH_DLAZY: usize = 6; // peers for lazy gossip pub const GOSSIP_HEARTBEAT: Duration = Duration::from_millis(500); pub const MAX_GOSSIP_SIZE: usize = 10 * (1 << 20); // 10 MB ``` In practice, each node aims for 8 peers in its mesh per topic. If it falls below 6 it grafts new peers; above 12 it prunes some. Every 500ms the heartbeat fires and the protocol checks mesh health. The lazy gossip parameter means that for messages a node has seen but did not forward via the mesh, it still tells 6 additional peers via lightweight metadata so they can request them if needed. Messages are compressed with Snappy (a fast compression algorithm prioritizing speed over ratio) before being sent, and each message ID is computed by SHA-256 hashing (a cryptographic hash function that produces a unique fixed-size fingerprint of arbitrary data) the decompressed content with a domain prefix (a few extra bytes prepended before hashing to distinguish valid from invalid encodings). This is how the network deduplicates messages. The gossip topics are where Base's L2-specific design becomes visible. The [`BlockHandler`](https://github.com/base/base/blob/main/crates/consensus/gossip/src/handler.rs) manages four versioned topics, one per protocol version: ```rust blocks_v1_topic: IdentTopic::new(format!("/optimism/{chain_id}/0/blocks")), blocks_v2_topic: IdentTopic::new(format!("/optimism/{chain_id}/1/blocks")), blocks_v3_topic: IdentTopic::new(format!("/optimism/{chain_id}/2/blocks")), blocks_v4_topic: IdentTopic::new(format!("/optimism/{chain_id}/3/blocks")), ``` For Base Mainnet (chain ID 8453), these resolve to `/optimism/8453/0/blocks` through `/optimism/8453/3/blocks`. The version is selected based on the active hardfork (a protocol upgrade that changes network rules, activated at a specific timestamp) at the block's timestamp. V1 covers pre-Canyon blocks, V2 Canyon/Delta, V3 Ecotone, and V4 Isthmus. Each version uses a slightly different encoding for the execution payload envelope. When a node subscribes to gossip, it subscribes to all four topics simultaneously so it can handle blocks from any active version. The `BlockHandler` implements the [`Handler`](https://github.com/base/base/blob/main/crates/consensus/gossip/src/handler.rs) trait, with two methods: `handle()` for processing incoming messages and `topics()` for declaring which topics it cares about. When a gossip message arrives, the handler first checks the topic to choose the decoder, then decodes the payload, then validates it: ```rust fn handle(&mut self, msg: Message) -> (MessageAcceptance, Option) { let decoded = if msg.topic == self.blocks_v1_topic.hash() { NetworkPayloadEnvelope::decode_v1(&msg.data) } else if msg.topic == self.blocks_v2_topic.hash() { NetworkPayloadEnvelope::decode_v2(&msg.data) } else if msg.topic == self.blocks_v3_topic.hash() { NetworkPayloadEnvelope::decode_v3(&msg.data) } else if msg.topic == self.blocks_v4_topic.hash() { NetworkPayloadEnvelope::decode_v4(&msg.data) } else { return (MessageAcceptance::Reject, None); }; match decoded { Ok(envelope) => match self.block_valid(&envelope) { Ok(()) => (MessageAcceptance::Accept, Some(envelope)), Err(err) => (err.into(), None), }, Err(err) => (MessageAcceptance::Reject, None), } } ``` The `MessageAcceptance` result feeds back into gossipsub's peer scoring. `Accept` means the message was valid and the peer earns credit. `Reject` means the message was invalid and the peer's score takes a hit. `Ignore` is used for already-seen blocks and does not penalize the peer. ### Block validation — keeping gossip honest Block validation in [`block_validity.rs`](https://github.com/base/base/blob/main/crates/consensus/gossip/src/block_validity.rs) is one of the most important pieces of the P2P stack because it determines what the node accepts. The checks run in sequence and the order matters. First, the timestamp must fall within an acceptable window — no more than 5 seconds in the future or 60 seconds in the past. This rejects stale blocks and prevents replay attacks (where an attacker re-broadcasts old, legitimate messages to confuse the network). Second, the block hash is recomputed from the payload and compared against the hash in the envelope. A mismatch indicates tampering. Third, version-specific payload constraints are enforced. V3 (Ecotone) and later payloads must include a non-empty parent beacon block root and zero blob gas usage. V4 (Isthmus) payloads must include a withdrawals root. This step catches blocks that are structurally invalid for their version. Fourth, the handler consults its seen-hashes tracking. It maintains a `BTreeMap` keyed by block height with a 1,000-entry cache. If more than 5 different blocks already exist for a given height (a sixth unique block is accepted but a seventh is rejected), the new block is rejected. If this exact block hash has been seen before, it is ignored without penalty. Multiple valid blocks at the same height can occur with a single sequencer when it produces a replacement block — for example, after a reorg (chain reorganization) triggered from L1. Fifth, and most important for rollup security, the signature is verified. The sequencer signs each block with its private key, and every node knows the expected signer's address. The expected signer is read first, then the signature is recovered from the payload hash via ECDSA recovery (a property of elliptic curve signatures that lets you compute the signer's public key from just the signature and the signed message). If the recovered address does not match the expected signer, the block is rejected: ```rust let msg = envelope.payload_hash.signature_message(self.rollup_config.l2_chain_id.id()); let block_signer = *self.signer_recv.borrow(); let Ok(msg_signer) = envelope.signature.recover_address_from_prehash(&msg) else { return Err(BlockInvalidError::Signature); }; if msg_signer != block_signer { return Err(BlockInvalidError::Signer { expected: block_signer, received: msg_signer }); } ``` Only after the signature passes does the handler insert the block hash into the seen-hashes map, marking it as processed. This prevents spam without rejecting legitimate competing blocks. ### Connection gating — controlling who connects The [`ConnectionGater`](https://github.com/base/base/blob/main/crates/consensus/gossip/src/gater.rs) is a rate-limiting layer that controls peer connections. It tracks dial attempts per peer address and enforces a configurable dial period (default: 1 hour). By default, redialing is disabled entirely — a peer can only be dialed once per period. The CLI overrides this to allow up to 500 redials per period via `--p2p.redial`. The gater also supports explicitly blocking peers by ID, IP address, or subnet, and protecting specific peers from disconnection regardless of score. Without connection gating, a misbehaving or misconfigured node could repeatedly attempt to connect, wasting resources. The gater bounds connection attempts and lets known-bad actors be blocked at the connection level rather than just at the gossip level. ### The libp2p Behaviour — combining protocols The [`Behaviour`](https://github.com/base/base/blob/main/crates/consensus/gossip/src/behaviour.rs) struct is a libp2p `NetworkBehaviour` that combines several sub-protocols into a single swarm (libp2p's term for the combination of transport, protocol behaviors, and connection management — essentially the "networking engine"): ```rust #[derive(NetworkBehaviour)] pub struct Behaviour { pub ping: libp2p::ping::Behaviour, pub gossipsub: libp2p::gossipsub::Behaviour, pub identify: libp2p::identify::Behaviour, pub sync_req_resp: libp2p_stream::Behaviour, } ``` `ping` sends periodic keepalive pings and measures round-trip times. `gossipsub` handles block gossip. `identify` exchanges capability information when peers first connect (Base advertises its agent version as `"base"`). `sync_req_resp` supports a legacy request-response protocol called `payload_by_number` that is part of the OP Stack spec. It is being deprecated and the Base implementation responds "not found" to all requests, but it is still present so op-nodes do not penalize Base nodes for missing it. The `GossipDriver` ([`gossip/src/driver.rs`](https://github.com/base/base/blob/main/crates/consensus/gossip/src/driver.rs)) wraps the swarm and provides higher-level operations. Its `start()` method binds the swarm to a TCP address (default `0.0.0.0:9222`), waits for the `NewListenAddr` event confirming the listener is up, and returns. Its `publish()` method takes an execution payload envelope (the signed wrapper around a block's contents — transactions, state root, gas used, and so on), selects the appropriate topic based on the block's timestamp and active hardfork, encodes it with version-appropriate serialization, and publishes it to gossipsub. Its `dial()` method takes an ENR from discovery, validates the chain ID, extracts the TCP multiaddr (a self-describing network address format used by libp2p, e.g. `/ip4/192.168.1.1/tcp/9222`), checks the connection gate, and initiates the connection. ### Putting it together — the network actor The [`NetworkActor`](https://github.com/base/base/blob/main/crates/consensus/service/src/actors/network/actor.rs) in the service crate ties everything together. It follows the actor pattern, a concurrency design where each component runs as an independent task that communicates with others exclusively through message channels, avoiding shared mutable state. The actor is the top-level component the consensus node's main loop interacts with. It is generic over a [`GossipTransport`](https://github.com/base/base/blob/main/crates/consensus/service/src/actors/network/transport.rs) trait, which lets the real networking stack be swapped out for an in-process test transport: ```rust #[async_trait] pub trait GossipTransport: Send + 'static { type Error: std::fmt::Debug + Send + 'static; async fn publish(&mut self, payload: BaseExecutionPayloadEnvelope) -> Result<(), Self::Error>; async fn next_unsafe_block(&mut self) -> Option; fn set_block_signer(&mut self, address: Address); fn handle_p2p_rpc(&mut self, request: P2pRpcRequest); } ``` The production implementation, [`NetworkHandler`](https://github.com/base/base/blob/main/crates/consensus/service/src/actors/network/handler.rs), composes the `GossipDriver` and `Discv5Handler`. It runs a `tokio::select!` loop that simultaneously handles several things: receiving ENRs from discovery and dialing them as new gossip peers, receiving blocks from gossip and forwarding them to the consensus engine, publishing blocks produced locally if the node is the sequencer, inspecting peer scores every 15 seconds and banning low-scoring peers (disconnecting them from gossip and banning their addresses in discovery), and handling administrative RPC requests. The [`NetworkDriver`](https://github.com/base/base/blob/main/crates/consensus/service/src/actors/network/driver.rs) handles the startup sequence. It starts the gossip swarm first, gets back the actual listen address, optionally updates the local ENR with that address (so other nodes discover the correct port), and then starts discovery. Ordering matters because the ENR must contain the real TCP port that gossip is listening on. The `NetworkActor` communicates with the rest of the consensus node through `mpsc` channels bundled in a [`NetworkInboundData`](https://github.com/base/base/blob/main/crates/consensus/service/src/actors/network/actor.rs) struct: ```rust pub struct NetworkInboundData { pub signer: mpsc::Sender
, pub p2p_rpc: mpsc::Sender, pub admin_rpc: mpsc::Sender, pub gossip_payload_tx: mpsc::Sender, } ``` Other actors in the node (such as the sequencer or the admin RPC server) push data to the network actor through these senders. The `signer` channel updates the expected unsafe block signer address. The `gossip_payload_tx` channel lets the sequencer publish newly produced blocks. This message-passing architecture isolates all networking concerns in one actor without shared mutable state. ### CL startup flow from the command line When you launch the Base consensus binary, P2P configuration comes from CLI flags defined in [`base-client-cli`](https://github.com/base/base/tree/main/crates/client/cli). Key flags include `--p2p.listen.tcp` (default 9222) and `--p2p.listen.udp` (default 9223) for local bind addresses, `--p2p.advertise.ip` for NAT (Network Address Translation) scenarios where the node sits behind a router and its public IP differs from its local IP, `--p2p.priv.path` for the node's secp256k1 private key, and the mesh parameters such as `--p2p.gossip.mesh.d` for the target mesh size. The startup flow in the CLI's `exec()` function loads the rollup configuration, parses the P2P arguments into a `NetworkConfig`, constructs a `NetworkBuilder` with both the gossip and discovery builders, and passes it to the `RollupNodeBuilder` which starts the `NetworkActor`. From that point on, the node is live on the consensus P2P network. ## The execution layer P2P stack The execution layer P2P stack is built on Reth, a high-performance Ethereum execution client written in Rust. The Base-specific customizations live under [`crates/execution/`](https://github.com/base/base/tree/main/crates/execution), and the node definition is in [`crates/execution/node/`](https://github.com/base/base/tree/main/crates/execution/node). For an architectural overview of how this fits the rest of the system, see the [architecture overview](/architecture/overview/). ### How Reth handles networking Reth implements the standard Ethereum execution layer networking stack. At the transport level it uses DevP2P with RLPx, a cryptographic transport where peers handshake to establish an encrypted, authenticated session. The mechanics resemble HTTPS: both nodes exchange temporary keys and derive a shared secret, after which all communication is encrypted. (For the curious, the handshake uses ECIES — Elliptic Curve Integrated Encryption Scheme — with ephemeral key shares and Diffie-Hellman key agreement. The session is encrypted with AES-256 and authenticated with keccak256-based MACs. Understanding these details is not necessary to work with the codebase.) Once the encrypted channel is up, the two nodes exchange "Hello" messages negotiating which sub-protocols they both support. On top of this transport, Reth speaks the `eth/68` wire protocol (version 68 of the Ethereum sub-protocol for exchanging chain data between execution clients). It handles block headers, block bodies, receipts, and transaction announcements. For transaction gossip specifically, eth/68 uses a two-phase announcement system: when a node receives a new transaction, it sends the full transaction to a small random fraction of its peers and sends lightweight hash announcements (with type and size metadata) to all other peers, who can then request the full transaction if they do not already have it. Reth also supports the `snap/1` protocol for snapshot-based state synchronization, letting new nodes download state much faster than replaying chain history. For peer discovery, Reth supports both discv4 (the older UDP-based Kademlia protocol) and discv5 (the newer protocol also used by the consensus layer). Nodes can additionally be configured with static boot nodes and DNS-based discovery. ### The BaseNetworkBuilder The [`BaseNetworkBuilder`](https://github.com/base/base/blob/main/crates/execution/node/src/node.rs) configures Reth's network for Base. It exposes two flags: ```rust pub struct BaseNetworkBuilder { pub disable_txpool_gossip: bool, pub disable_discovery_v4: bool, } ``` The `disable_txpool_gossip` flag is particularly important for rollup nodes. When a node is configured with a sequencer endpoint (so it should forward transactions to the sequencer rather than including them itself), transaction pool gossip is disabled. This stops the node from broadcasting pending transactions to the rest of the network, because in a rollup the sequencer is the only entity that orders transactions. The `network_config` method assembles Reth's `NetworkConfig` by applying the discovery settings. The code below builds the configuration step by step: it sets the RLPx socket address, conditionally disables discv4, enables discv5 with boot nodes, and finally sets the transaction gossip flag: ```rust let network_builder = ctx .network_config_builder()? .apply(|mut builder| { let rlpx_socket = (args.addr, args.port).into(); if disable_discovery_v4 || args.discovery.disable_discovery { builder = builder.disable_discv4_discovery(); } if !args.discovery.disable_discovery { builder = builder.discovery_v5( args.discovery.discovery_v5_builder( rlpx_socket, ctx.config() .network .resolved_bootnodes() .or_else(|| ctx.chain_spec().bootnodes()) .unwrap_or_default(), ), ); } builder }); let mut network_config = ctx.build_network_config(network_builder); network_config.tx_gossip_disabled = disable_txpool_gossip; ``` Discv4 is disabled by default for Base (`--rollup.discovery.v4` defaults to false) while discv5 is enabled. Boot nodes resolve from CLI arguments first and fall back to the chain specification. Once the config is built, `build_network` creates a `NetworkManager`, starts it, and logs the local enode record (the DevP2P equivalent of an ENR — a URL-formatted node identifier such as `enode://@:`). ### Transaction pool and gossip The Base transaction pool lives in [`crates/execution/txpool/`](https://github.com/base/base/tree/main/crates/execution/txpool). It extends Reth's standard transaction pool with rollup-specific validation and ordering. The [`OpTransactionValidator`](https://github.com/base/base/blob/main/crates/execution/txpool/src/validator.rs) wraps Reth's `EthTransactionValidator` and adds L1 data gas fee checks. Every transaction on Base pays both an L2 execution gas cost and an L1 data fee (the cost of posting transaction data to Ethereum L1). The validator confirms the sender's balance covers both. It also rejects EIP-4844 blob transactions (a special transaction type used on L1 to carry large data blobs for rollups, which are not meaningful on the L2 itself). The ordering strategy is configurable via `--rollup.txpool-ordering` and defined in [`ordering.rs`](https://github.com/base/base/blob/main/crates/execution/txpool/src/ordering.rs): ```rust pub enum BaseOrdering { CoinbaseTip(CoinbaseTipOrdering), Timestamp(TimestampOrdering), } ``` `CoinbaseTip` is the standard Ethereum priority-fee ordering — higher fees first. `Timestamp` is a rollup-specific FIFO ordering that prioritizes by arrival time regardless of fee, which can produce fairer sequencing. ### Transaction forwarding For non-sequencer nodes, transactions accepted into the mempool need to be forwarded to the sequencer for inclusion. This is handled by the consumer/forwarder pipeline in the txpool crate. The [`SpawnedConsumer`](https://github.com/base/base/blob/main/crates/execution/txpool/src/consumer/mod.rs) polls the transaction pool for new pending transactions and broadcasts them through a `tokio::broadcast` channel. The [`SpawnedForwarder`](https://github.com/base/base/blob/main/crates/execution/txpool/src/forwarder/mod.rs) subscribes to that channel and forwards each transaction via a custom JSON-RPC method (`base_insertValidatedTransactions`) to configured builder endpoints. One forwarder task is spawned per builder URL, so multiple downstream builders can receive transactions simultaneously. The pipeline runs as background tasks on the node's task executor. ### EL P2P configuration flags The execution layer P2P is configured through Reth's standard network flags plus Base-specific rollup flags defined in [`args.rs`](https://github.com/base/base/blob/main/crates/execution/node/src/args.rs). Key flags: `--rollup.sequencer` sets the sequencer endpoint for transaction forwarding. `--rollup.disable-tx-pool-gossip` disables transaction gossip on the DevP2P network (this is a separate flag — setting the sequencer endpoint does not automatically disable gossip, so operators typically set both). `--rollup.discovery.v4` enables the legacy discv4 discovery protocol (disabled by default since Base uses discv5). `--rollup.txpool-ordering` selects between `coinbase-tip` and `timestamp` ordering strategies. Standard Reth network flags still apply: `--network.addr` and `--network.port` control the RLPx bind address (default port 30303), `--network.discovery.disable-discovery` turns off all peer discovery, and `--network.discovery.bootnodes` provides custom boot node addresses. ## How the two stacks interact at runtime When a Base node is running, the consensus and execution processes work in tandem but network independently. Block reception flows like this: The sequencer produces a new L2 block and signs it. The signed block is published as a `NetworkPayloadEnvelope` to the CL gossip network on the appropriate versioned topic (for example, `/optimism/8453/3/blocks` for Isthmus). Every consensus node's `BlockHandler` receives the message, validates it (timestamp, hash, signature, deduplication), and if valid marks it `Accept` and passes it to the consensus engine. The engine sends the execution payload to the local execution client via `engine_newPayloadV*`. The execution client validates the block against the EVM, computes the new state root, and reports the result back. Transaction submission flows in the opposite direction. A user submits a transaction to an EL node's RPC endpoint. If transaction pool gossip is enabled, the transaction is broadcast to other EL peers via the DevP2P `eth/68` protocol. If a sequencer endpoint is configured, the transaction forwarder sends it directly to the sequencer via JSON-RPC. The sequencer includes it in the next block, which then propagates through the CL gossip network as described above. Discovery on each layer is independent. The CL uses discv5 on a UDP port (default 9223) to find other CL peers, validating ENRs by chain ID so it only connects to Base nodes. The EL uses discv5 (and optionally discv4) on its own UDP port to find other EL peers. The two discovery networks are entirely separate and serve different purposes. ## Key files **Consensus layer peers and ENR management:** - [`crates/consensus/peers/src/enr.rs`](https://github.com/base/base/blob/main/crates/consensus/peers/src/enr.rs) — BaseEnr encoding and validation - [`crates/consensus/peers/src/store.rs`](https://github.com/base/base/blob/main/crates/consensus/peers/src/store.rs) — BootStore persistence **Consensus layer discovery:** - [`crates/consensus/disc/src/driver.rs`](https://github.com/base/base/blob/main/crates/consensus/disc/src/driver.rs) — Discv5Driver event loop and bootstrap **Consensus layer gossip:** - [`crates/consensus/gossip/src/config.rs`](https://github.com/base/base/blob/main/crates/consensus/gossip/src/config.rs) — Gossipsub constants and configuration - [`crates/consensus/gossip/src/handler.rs`](https://github.com/base/base/blob/main/crates/consensus/gossip/src/handler.rs) — BlockHandler and topic management - [`crates/consensus/gossip/src/block_validity.rs`](https://github.com/base/base/blob/main/crates/consensus/gossip/src/block_validity.rs) — Block validation rules - [`crates/consensus/gossip/src/behaviour.rs`](https://github.com/base/base/blob/main/crates/consensus/gossip/src/behaviour.rs) — libp2p Behaviour composition - [`crates/consensus/gossip/src/gater.rs`](https://github.com/base/base/blob/main/crates/consensus/gossip/src/gater.rs) — Connection rate limiting - [`crates/consensus/gossip/src/driver.rs`](https://github.com/base/base/blob/main/crates/consensus/gossip/src/driver.rs) — GossipDriver swarm management **Consensus layer orchestration:** - [`crates/consensus/service/src/actors/network/actor.rs`](https://github.com/base/base/blob/main/crates/consensus/service/src/actors/network/actor.rs) — NetworkActor definition - [`crates/consensus/service/src/actors/network/handler.rs`](https://github.com/base/base/blob/main/crates/consensus/service/src/actors/network/handler.rs) — Production NetworkHandler transport - [`crates/consensus/service/src/actors/network/driver.rs`](https://github.com/base/base/blob/main/crates/consensus/service/src/actors/network/driver.rs) — Network startup sequence - [`crates/consensus/service/src/actors/network/transport.rs`](https://github.com/base/base/blob/main/crates/consensus/service/src/actors/network/transport.rs) — GossipTransport trait **Execution layer node and networking:** - [`crates/execution/node/src/node.rs`](https://github.com/base/base/blob/main/crates/execution/node/src/node.rs) — BaseNetworkBuilder and network configuration - [`crates/execution/node/src/args.rs`](https://github.com/base/base/blob/main/crates/execution/node/src/args.rs) — Rollup-specific CLI arguments **Execution layer transaction pool:** - [`crates/execution/txpool/src/validator.rs`](https://github.com/base/base/blob/main/crates/execution/txpool/src/validator.rs) — OpTransactionValidator with L1 data gas checks - [`crates/execution/txpool/src/ordering.rs`](https://github.com/base/base/blob/main/crates/execution/txpool/src/ordering.rs) — BaseOrdering (fee-based vs FIFO) - [`crates/execution/txpool/src/consumer/`](https://github.com/base/base/tree/main/crates/execution/txpool/src/consumer) — Transaction pool consumer - [`crates/execution/txpool/src/forwarder/`](https://github.com/base/base/tree/main/crates/execution/txpool/src/forwarder) — Transaction forwarder to sequencer ## Glossary **Attestation** — A validator's vote that a particular block is valid. **Beacon chain** — The chain introduced with Ethereum's proof-of-stake upgrade that coordinates consensus. **Bootnode** — A well-known node whose address is hardcoded into client software, used to bootstrap new nodes into the network. **CL (Consensus Layer)** — The part of an Ethereum node responsible for consensus (deciding which blocks are canonical). **DevP2P** — The execution layer's P2P protocol suite, including RLPx transport and the eth wire protocol. **DHT (Distributed Hash Table)** — A system where many computers collectively maintain a lookup directory without any central coordinator. **Discv4 / Discv5** — Node Discovery Protocol versions 4 and 5. UDP-based protocols for finding peers. **EL (Execution Layer)** — The part of an Ethereum node responsible for executing transactions and maintaining state. **Engine API** — The JSON-RPC interface that the consensus and execution layers use to communicate locally. **ENR (Ethereum Node Record)** — A signed, self-describing identity document that a node uses to advertise itself (IP, ports, public key, chain metadata). **EVM (Ethereum Virtual Machine)** — The runtime environment that executes smart contract bytecode. **Gossipsub** — A publish/subscribe protocol built on libp2p that uses a mesh overlay for efficient message distribution. **GRAFT / PRUNE** — Gossipsub control messages for adding or removing a peer from the mesh. **Hardfork** — A protocol upgrade that changes the rules of the network, activated at a specific timestamp. **IHAVE / IWANT** — Gossipsub control messages for the lazy repair mechanism (advertising and requesting missed messages). **k-bucket** — A fixed-size list of peers at a particular XOR distance in a Kademlia routing table. **L1 (Layer 1)** — The base Ethereum chain. **L2 (Layer 2)** — A chain that runs on top of L1 for scalability (e.g. Base). **libp2p** — A modular networking framework used by the consensus layer, originally developed for IPFS. **Mempool** — The pool of pending transactions waiting to be included in a block. **mpsc** — Multi-producer, single-consumer channel. A thread-safe queue used for async communication in Rust. **Multiaddr** — A self-describing network address format used by libp2p (e.g. `/ip4/192.168.1.1/tcp/9222`). **NAT (Network Address Translation)** — When a node is behind a router and its public IP differs from its local IP. **Reorg** — A chain reorganization where the network switches to a different sequence of blocks. **RLP (Recursive Length Prefix)** — Ethereum's standard binary serialization format. **RLPx** — The execution layer's encrypted TCP transport protocol. **Rollup** — A type of blockchain that executes transactions on its own chain but posts data back to Ethereum for security. **Sequencer** — The single designated node that orders and produces L2 blocks. **Snappy** — A fast compression algorithm used for gossip message compression. **State root** — A cryptographic fingerprint of the entire blockchain state after executing a block's transactions. **Swarm** — libp2p's term for the combination of transport, protocol behaviors, and connection management. **XOR distance** — The distance metric used in Kademlia, computed by XOR-ing two node IDs. Has nothing to do with geographic distance. --- # Proof System Overview Source: https://basehub.org/specifications/proofs-overview/ Description: Roles of the offchain services and onchain contracts that make Azul L2 checkpoint proposals verifiable from Ethereum. The proof system is the collection of offchain services and onchain contracts that lets Ethereum verify Base's L2 checkpoint proposals. Every proposal asserts an output root for a fixed L2 block range; independent proof actors then recompute that claim, supply proof material, and dispute the game when the claim does not match canonical L2 state. This section covers the components used by the Azul proof system. The legacy interactive fault-proof construction is documented separately. ## Component roles - [Challenger](/specifications/challenger) — watches in-flight games, compares them to canonical L2 state, and disputes claims that disagree. - [Proposer](/specifications/proposer) — turns a canonical L2 checkpoint range into a new dispute game on L1. - [Registrar](/specifications/registrar) — keeps the onchain registry of accepted TEE signer identities in sync with the running prover fleet. - [TEE Prover](/specifications/tee-prover) — produces Nitro Enclave–backed proofs for the common, low-latency proposal path. - [ZK Prover](/specifications/zk-prover) — produces permissionless proofs that can be used for both proposing and disputing. - [Proof Contracts](/specifications/contracts) — verify submitted proof material, track game state, and release withdrawals and bonds according to each game's outcome. --- # Proposer Source: https://basehub.org/specifications/proposer/ Description: Offchain service that turns canonical L2 checkpoint ranges into AggregateVerifier dispute games on L1. The proposer is the offchain service that promotes a canonical L2 checkpoint range into an `AggregateVerifier` game on L1. Each cycle it reads the latest onchain parent state, picks the next checkpoint, asks a TEE prover for a proof over that range, revalidates the proof against canonical L2 state, and opens a new dispute game through `DisputeGameFactory`. The production proposer is gated by the L1 signer it is configured with. Its output remains self-validating end to end: a game is uniquely keyed by game type, claimed output root, parent, L2 block number, and the ordered intermediate output roots, and the proof itself can be re-checked by the onchain verifier and by independent challengers. ## Responsibilities A conforming proposer is expected to: 1. Load the active `AggregateVerifier` implementation and proposal parameters from L1. 2. Recover the latest onchain parent state from `AnchorStateRegistry` and `DisputeGameFactory`. 3. Pick the next checkpoint block that is at or before the configured safe head. 4. Build a `prover_prove` request covering that checkpoint range. 5. Accept only TEE proof results for proposal creation. 6. Re-check the aggregate output root and every intermediate root against canonical L2 state right before L1 submission. 7. Optionally pre-validate the TEE signer against `TEEProverRegistry`. 8. Submit `DisputeGameFactory.createWithInitData()` with the required bond. 9. Retry transient proof, RPC, and transaction failures without ever creating games out of order. The proposer does not challenge games, resolve them, claim bonds, or decide withdrawal finality — those duties live with the challenger and the proof contracts. ## Startup Configuration At startup the proposer connects to: - an L1 execution RPC for contract reads and transaction submission - an L2 execution RPC for agreed L2 block headers - a rollup RPC for sync status and output roots - a prover RPC that implements `prover_prove` - `AnchorStateRegistry` - `DisputeGameFactory` - an optional `TEEProverRegistry` The game implementation address comes from: ```text DisputeGameFactory.gameImpls(gameType) ``` That address must be non-zero. From there the proposer reads: ```text AggregateVerifier.BLOCK_INTERVAL() AggregateVerifier.INTERMEDIATE_BLOCK_INTERVAL() DisputeGameFactory.initBonds(gameType) ``` `BLOCK_INTERVAL` must be at least `2`, `INTERMEDIATE_BLOCK_INTERVAL` must be non-zero, and `BLOCK_INTERVAL % INTERMEDIATE_BLOCK_INTERVAL` must be `0`. The number of intermediate roots a proposal carries is: ```text BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL ``` By default the proposer targets finalized L2 state. Operators may opt into safe-but-not-finalized proposals, in which case the rollup node's safe L2 head is used instead. ## Parent recovery Before planning new work, the proposer walks forward from L1 to find the current tip of the proposal chain. Parent state is captured as: ```text parentAddress parentOutputRoot parentL2BlockNumber ``` If no games have been created yet, the parent is the anchor root from `AnchorStateRegistry`: ```text parentAddress = AnchorStateRegistry address parentOutputRoot = AnchorStateRegistry.getAnchorRoot().root parentL2BlockNumber = AnchorStateRegistry.getAnchorRoot().l2BlockNumber ``` Otherwise the proposer performs a deterministic forward walk from the anchor root (or from a cached recovered tip when the cache is still fresh). At each step it: 1. Computes: ```text expectedBlock = parentL2BlockNumber + BLOCK_INTERVAL ``` 2. Fetches the canonical output root for every intermediate checkpoint at: ```text parentL2BlockNumber + INTERMEDIATE_BLOCK_INTERVAL * i ``` for `i` in `1..=BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL`. 3. Takes the final intermediate root as the canonical root claim for `expectedBlock`. 4. Builds `extraData` from `expectedBlock`, `parentAddress`, and the ordered intermediate roots. 5. Looks the expected game up: ```text DisputeGameFactory.games(gameType, rootClaim, extraData) ``` 6. If the lookup returns `address(0)`, walk stops and the current parent is the recovered tip. 7. Otherwise it advances the parent to the returned game proxy and repeats. This recovery does not scan the factory for a "best" game. It uses each game's unique factory key, so only the canonical next game for the recovered parent can advance the parent chain. Any game that disagrees on root, parent, L2 block number, or intermediate roots has a different key and is invisible to recovery. ## Checkpoint selection After recovery, the next proposal target is: ```text targetBlock = parentL2BlockNumber + BLOCK_INTERVAL ``` The proposer must not request or submit a proof for `targetBlock` unless: ```text targetBlock <= safeHead ``` where `safeHead` is either: - `finalized_l2.number`, by default, or - `safe_l2.number`, only when non-finalized proposals are explicitly enabled. When parallel proving is on, multiple proof requests can be in flight for future checkpoints, but L1 submissions are still strictly sequential — at most one proposal transaction is pending at a time, and the next one waits until earlier checkpoint games are recovered or confirmed. ## Proof request For a checkpoint range the proposer builds a `ProofRequest` with: | Field | Value | | ----------------------------- | ---------------------------------------------------------- | | `l1_head` | Hash of the latest L1 block at request construction time | | `l1_head_number` | Number of the latest L1 block at request construction time | | `agreed_l2_head_hash` | L2 block hash at `parentL2BlockNumber` | | `agreed_l2_output_root` | Parent output root recovered from L1 | | `claimed_l2_output_root` | Rollup RPC output root at `targetBlock` | | `claimed_l2_block_number` | `targetBlock` | | `proposer` | L1 address that will submit the proposal transaction | | `intermediate_block_interval` | `INTERMEDIATE_BLOCK_INTERVAL` | | `image_hash` | Expected TEE image hash | The prover RPC method is: ```text prover_prove(ProofRequest) -> ProofResult ``` Only `ProofResult::Tee` is accepted by the proposer. ZK proof results are not valid input on this path. ## TEE proposal journal The TEE prover returns: - one aggregate proposal that covers the entire checkpoint range - per-block proposals for each block inside that range The aggregate proposal carries: ```text outputRoot signature l1OriginHash l1OriginNumber l2BlockNumber prevOutputRoot configHash ``` The TEE signature is taken over: ```text keccak256(journal) ``` where `journal` is packed as: ```text proposer(20) || l1OriginHash(32) || prevOutputRoot(32) || startingL2Block(8) || outputRoot(32) || endingL2Block(8) || intermediateRoots(32 * N) || configHash(32) || teeImageHash(32) ``` For aggregate proposals: ```text startingL2Block = parentL2BlockNumber endingL2Block = targetBlock prevOutputRoot = parentOutputRoot outputRoot = claimed root at targetBlock ``` `intermediateRoots` are sampled every `INTERMEDIATE_BLOCK_INTERVAL` blocks and include the final target block root. ## Pre-submission validation Right before sending to L1, the proposer re-validates the proof against canonical L2 state: 1. Fetch the rollup output root at `targetBlock`. 2. Require it to equal the aggregate proposal's `outputRoot`. 3. Extract the intermediate roots from the per-block proposals. 4. Fetch the canonical output root for every intermediate checkpoint. 5. Require every proposed intermediate root to equal its canonical counterpart. If the aggregate root or any intermediate root no longer matches canonical state, the proposer drops the pending work and restarts recovery. This is the guard against stale proof results after L1 or L2 reorgs. When a `TEEProverRegistry` is configured, the proposer should recover the TEE signer from the aggregate proposal signature and call: ```text TEEProverRegistry.isValidSigner(signer) ``` A `false` return means the proof must not be submitted; discard it and request a new one. If the registry call itself fails for RPC or deployment reasons, the proposer may continue to submission and let the onchain verifier enforce signer validity. ## Game creation The proposer creates the game with: ```solidity DisputeGameFactory.createWithInitData{value: initBond}( gameType, rootClaim, extraData, initData ) ``` where: ```text rootClaim = aggregateProposal.outputRoot ``` `extraData` is packed, not ABI-encoded: ```text l2BlockNumber(32) || parentAddress(20) || intermediateRoots(32 * N) ``` `l2BlockNumber` is a 32-byte big-endian integer. `parentAddress` is the recovered parent game proxy, or the `AnchorStateRegistry` address for the first game after the anchor. `initData` is the TEE proof bytes for `AggregateVerifier.initializeWithInitData()`: ```text proofType(1) || l1OriginHash(32) || l1OriginNumber(32) || signature(65) ``` For TEE proofs: ```text proofType = 0 ``` The ECDSA `v` value must be normalized to `27` or `28` before submission. `initBond` is read from `DisputeGameFactory.initBonds(gameType)` at startup and is sent as the transaction value. The L1 transaction manager owns nonce management, fee bumping, signing, and resubmission. ## Duplicate games A game's factory key is: ```text gameType || rootClaim || extraData ``` If `createWithInitData()` reverts with `GameAlreadyExists`, the proposer treats the target as already submitted, refreshes recovery from L1, and continues from the new tip. This covers both the case where a prior transaction succeeded but the receipt was missed, and the case where another valid proposer beat us to the same game. ## Retry behavior Transient failures are retried on later ticks: | Failure | Required behavior | | ------------------------------------- | ----------------------------------------------------------- | | Recovery RPC or contract read failure | Skip the current tick and retry recovery on the next tick | | Proof request failure | Retry the target on a later tick | | Repeated proof failure | Reset pipeline state and recover from L1 | | L1 submission failure | Keep the proved result and retry submission on a later tick | | L1 submission timeout | Treat as a submission failure and retry after recovery | | `GameAlreadyExists` | Treat as success, refresh recovery, and continue | | Canonical root mismatch | Reset pipeline state and re-prove from recovered L1 state | | Invalid TEE signer | Discard the proof and request a new one | The current implementation retries a single proof target up to three times before resetting pipeline state, and bounds proposal submission with a ten-minute timeout. ## Admin interface The proposer may expose an optional JSON-RPC admin interface. When enabled, it provides: | Method | Result | | ----------------------- | --------------------------------------- | | `admin_startProposer` | Starts the proving pipeline | | `admin_stopProposer` | Stops the proving pipeline | | `admin_proposerRunning` | Returns whether the pipeline is running | Starting a proposer that is already running, or stopping one that is already stopped, are both errors. ## Dry run mode In dry run mode the proposer still performs recovery, checkpoint selection, proof sourcing, and pre-submission validation, but it does not send L1 transactions — it just logs the game that would have been created. Useful for validating prover and RPC behavior, but it does not advance the onchain proposal chain. --- # TEE Prover Registrar Source: https://basehub.org/specifications/registrar/ Description: Offchain service that discovers TEE prover enclaves, attests them with a ZK proof, and maintains the onchain TEEProverRegistry of accepted signer identities. The registrar is an offchain service that keeps the onchain set of accepted TEE signer identities in sync with the actual running prover fleet. It locates live TEE prover instances, pulls each enclave's AWS Nitro attestation document, produces a ZK proof that the attestation is well-formed, and writes the resulting signer registration to [`TEEProverRegistry`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEProverRegistry.sol) on L1. It also removes signers whose backing instances have disappeared and revokes intermediate certificates that AWS has marked as withdrawn. The Base team operates a single registrar. The proof system trusts only signers that this registrar has admitted, which makes registrar correctness a prerequisite for accepting TEE proofs onchain. The output is still self-validating end-to-end: the attestation ZK proof, the enclave PCR0 measurement, and the signer public key are all re-checked by `TEEProverRegistry` and [`NitroEnclaveVerifier`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/NitroEnclaveVerifier.sol) before the signer is treated as valid. ## Responsibilities A conforming registrar performs the following work each cycle: 1. Discover the current set of TEE prover instances behind the production load balancer. 2. Fetch per-enclave signer public keys and Nitro attestation documents from each instance. 3. Optionally check the attestation certificate chain against AWS-published CRLs and against the onchain durable revocation set. 4. Generate a ZK proof of attestation correctness for every enclave that is not yet registered. 5. Submit `TEEProverRegistry.registerSigner()` for newly attested signers. 6. Submit `TEEProverRegistry.deregisterSigner()` for onchain signers whose instances are no longer reachable. 7. Submit `NitroEnclaveVerifier.revokeCert()` for any intermediate certificate found to be revoked. 8. Resume in-flight proof requests after a process restart without re-spending proving work. The registrar deliberately does not gate which PCR0 measurements are admitted. Registration is PCR0-agnostic so the next image's signers can be pre-registered ahead of a hardfork. Whether a proof produced by a given signer is actually accepted is decided onchain by [`TEEVerifier`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEVerifier.sol), which compares against the active game implementation's current `TEE_IMAGE_HASH`. The registrar also does not build proposals, generate proof material for proposals or disputes, or challenge invalid state transitions. Those duties live with the proposer, the TEE provers, and the challenger. ## Startup configuration At startup, the registrar connects to: - an L1 execution RPC for contract reads and transaction submission - AWS APIs for ELBv2 target health and EC2 instance metadata - a JSON-RPC endpoint on each discovered TEE prover instance - a proving backend (Boundless marketplace or a self-hosted RISC Zero prover) - `TEEProverRegistry` - an optional `NitroEnclaveVerifier`, required only when CRL checking is enabled Beyond the registry and verifier addresses passed in by the operator, the registrar reads no contract configuration at startup. Any onchain signer that is not present in its own instance set is treated as an orphan candidate, so a given `TEEProverRegistry` must be written by exactly one registrar. ## Driver loop The service runs a single driver loop: 1. Discover the current instance set. 2. Process every instance concurrently, bounded by `max_concurrency`. 3. Read the onchain signer set. 4. Deregister orphan signers. 5. Sleep `poll_interval` seconds, or exit on cancellation. `step()` runs once at startup before the first sleep. Cancellation is observed promptly between ticks and inside long-running transaction retries, so the service can shut down without leaving partial state behind. ## Instance discovery Discovery is driven exclusively by AWS ALB target group polling — DNS, SRV, and Kubernetes discovery are not supported. Each discovery cycle: 1. Calls `elasticloadbalancingv2.DescribeTargetHealth(target_group_arn)`. 2. Drops non-instance targets (IDs that do not start with `i-`). 3. Deduplicates instance IDs that appear on more than one port. 4. Calls `ec2.DescribeInstances(instance_ids)` to read each instance's private IP and launch time. 5. Builds JSON-RPC endpoint URLs shaped like `http://{private_ip}:{prover_port}` and pairs each one with its ALB-reported health state. ALB health states map to internal states as follows: | AWS state | Internal state | `should_register()` | | ------------ | -------------- | ------------------- | | `initial` | `Initial` | true | | `healthy` | `Healthy` | true | | `draining` | `Draining` | false | | anything else| `Unhealthy` | false | An `Unhealthy` instance is still allowed to register if it is within `unhealthy_registration_window` seconds of its `launch_time`. This is a warm-up grace period: it gives a freshly launched instance — whose JSON-RPC endpoint may be briefly slow — enough time to complete enclave attestation and registration before the next ALB health check would deregister it. The window must be smaller than the Boundless proving timeout, so any proof that has already started can finish before the instance becomes ineligible. A discovery failure aborts the current tick and skips orphan cleanup. It never deregisters a live signer. ## Per-instance processing For each discovered instance, the registrar: 1. Calls `enclave_signerPublicKey` to fetch the per-enclave SEC1 public keys. A single instance can host multiple enclaves, and each enclave has its own signer key. 2. Derives the Ethereum signer address from each public key as the last 20 bytes of `keccak256(uncompressed_pubkey_xy)`. 3. Returns immediately when no signers were reported — that instance contributes nothing to the active set, and the call becomes a no-op. 4. Decides whether the instance is currently registerable: `Initial` and `Healthy` instances proceed; `Unhealthy` instances inside the warm-up window also proceed; every other state contributes its addresses to the active set but does not generate new proofs or transactions. 5. Generates a single 32-byte random nonce and calls `enclave_signerAttestation` once with that nonce. The nonce binds every per-enclave attestation in the returned batch to a single freshness commitment. 6. Runs CRL checks once per batch when CRL checking is enabled. Each enclave signs with its own key, but AWS Nitro attestations are signed by the parent EC2 instance's Nitro Hypervisor, whose signing key is endorsed by a per-instance AWS-issued certificate chain. Every enclave on a given instance therefore lives under the same parent chain, so one CRL check per instance is sufficient. 7. Runs the registration pipeline for each signer address. All reachable instances contribute to the active signer set, including `Draining` and `Unhealthy` ones. This prevents an instance that is rotating in or out from being deregistered prematurely. ## Attestation proof generation For each signer that is not yet onchain, the registrar calls an `AttestationProofProvider` to produce proof material. The provider returns: ```text output // ABI-encoded VerifierJournal (PCRs, public key, timestamp, cert hashes) proofBytes // Groth16 seal ``` `output` is the `VerifierJournal` consumed by `NitroEnclaveVerifier.verify()` during `registerSigner()`. `proofBytes` is the Groth16 SNARK proving that the journal corresponds to a valid Nitro attestation document. Two backends are supported: | Backend | Description | | ----------- | ------------------------------------------------------------------------------------------------------------ | | `boundless` | Submits the proving job to the Boundless marketplace using a dedicated wallet. | | `direct` | Loads the guest ELF locally and proves via `risc0_zkvm::default_prover()`, routing to Bonsai or a local prover according to RISC Zero environment variables. | Both are valid production paths. `boundless` is the primary production backend; `direct` is used for local development and tests, and it is also suitable as a production fallback when an operator needs to bypass the marketplace — for instance during a Boundless incident or in a private deployment. For Boundless, the registrar submits a `RequestParams` carrying the program URL, the attestation input, the expected `image_id`, and a `prefix_match(image_id)` requirement so that a fulfilled request cannot be replayed against a different program. Onchain Boundless submissions are serialized behind a mutex to avoid wallet-nonce races. ### Restart recovery The registrar process is ephemeral. Across restarts, it must not re-spend proving work and must not submit stale proofs. Boundless `RequestId` slots are derived deterministically: ```text request_index(signer, attempt) = u32::from_be_bytes(keccak256(signer || attempt)[..4]) ``` For each signer, the registrar probes `max_recovery_attempts` consecutive deterministic slots before submitting a fresh request. The action depends on what each slot looks like: | Slot status | Registrar action | | ------------- | --------------------------------------------------------------------------------- | | `Unknown` | Record the first such slot as the candidate fresh-submission slot; keep scanning. | | `Locked` | Resume `wait_for_request_fulfillment` and use the resulting receipt. | | `Fulfilled` | Fetch the receipt and check journal freshness before accepting it. | | `Expired` | Skip the slot permanently; continue scanning. | A `RequestIsNotLocked` revert encountered mid-scan is treated as in-flight and short-circuits to waiting on that slot. If a recovered receipt's attestation timestamp is older than `max_attestation_age`, the registrar discards it and submits a fresh request in the candidate slot. The default freshness window is 3300 seconds, deliberately kept under the onchain `MAX_AGE` of 3600 seconds so that a recovered proof can still be submitted before it ages out onchain. After an `ExecutionReverted` from `registerSigner()`, the signer is added to a per-process `recovery_blocked` set. On the next cycle, the recovery scan is skipped for that signer and a fresh request is submitted instead, so a known-bad recovered proof is never tried twice. The set clears on restart, which gives each process one fresh attempt even for previously blocked signers. ## Registration transactions For each unregistered signer, the registrar: 1. Calls `TEEProverRegistry.isRegisteredSigner(signer)` and skips the signer if it returns true. 2. Generates or recovers proof material as described above. 3. ABI-encodes `registerSigner(output, proofBytes)`. 4. Submits the transaction through the L1 transaction manager. 5. Retries failed submissions according to the rules below. 6. Increments the registration counter on a successful receipt. The transaction retry rules are: | Failure | Required behavior | | ----------------------------- | ------------------------------------------------------------------------------------------------------- | | Retryable error | Sleep `tx_retry_delay`, then retry, up to `max_tx_retries` total attempts. | | `ExecutionReverted` revert | Block recovery for this signer so the next cycle generates a fresh proof, then return the error. | | Insufficient funds, fee cap | Treat as non-retryable. Surface the error and stop attempting this signer for the current cycle. | | Reverted receipt | Treat as a transaction failure even when submission succeeded. | | Reported error after mining | Re-read `isRegisteredSigner(signer)`. If true, treat the attempt as success. | The post-error reconciliation matters because fee-bumping and nonce races can surface errors even when the underlying transaction has already been mined. Without the recheck, the registrar would burn proving work generating a fresh proof for a signer that is already registered. Transaction submission is cancellation-aware: both the active send and the inter-attempt sleep abort cleanly on shutdown, so the next process starts from a clean nonce state without committing a partial transaction. ## Orphan deregistration After every instance has been processed, the registrar reconciles the onchain signer set against the active set: 1. Skip cleanup if discovery failed during this tick. 2. Skip cleanup if cancellation was requested. 3. Compare reachable instances against total discovered instances. If `reachable_instances * 2 <= total_instances`, skip cleanup. 4. Read the onchain set with `TEEProverRegistry.getRegisteredSigners()`. 5. Compute `orphans = onchain_signers \ active_signers`. 6. For each orphan, in order: 1. Recheck `isRegisteredSigner(signer)`. Skip if it returns false. 2. ABI-encode `deregisterSigner(signer)` and submit it through the transaction manager. The majority-reachable guard exists so that a transient AWS or VPC outage cannot deregister most of the prover fleet at once. The per-orphan `isRegisteredSigner` recheck is a race guard: the set returned by `getRegisteredSigners()` is read once per cycle, and a different writer might have deregistered the signer between that read and this transaction. Skipping signers that are already gone avoids wasted gas on a no-op transaction. The procedure assumes a single registrar per `TEEProverRegistry`. Two registrars sharing a registry would each treat the other's signers as orphans. ## Certificate revocation When the operator turns on CRL checking, the registrar enforces revocation through two layers, applied in order. Both are required to make CRL handling safe. ### Layer 1 — onchain durable revocation pre-check For each intermediate certificate in the attestation chain, the registrar reads `NitroEnclaveVerifier.revokedCerts(certPathDigest)`. Any hit blocks registration for that batch and skips Layer 2 entirely. This layer guards against a known attack on the cached-cert path: an intermediate that was once revoked onchain could be reintroduced through a later `_cacheNewCert` write if its CRL entry has since been pruned by AWS. Consulting the durable mapping first ensures a revoked cert cannot be silently rehabilitated. RPC errors against `revokedCerts` fail open and fall through to Layer 2, but they are counted as revocation check errors. `RegistrationDriver::new` requires a `NitroEnclaveVerifier` client whenever CRL checking is enabled and rejects misconfiguration at startup. ### Layer 2 — AWS CRL distribution points For intermediates that pass Layer 1, the registrar: 1. Parses each CRL distribution point from the chain. 2. Validates the URL host against an allowlist that requires the `.amazonaws.com` suffix and the `nitro-enclave` keyword. HTTP redirects are disabled and responses are capped at 10 MiB. 3. Fetches the CRL with a configurable timeout. 4. Looks up the certificate's serial number. 5. Submits `NitroEnclaveVerifier.revokeCert(certPathDigest)` for any revoked intermediate found. 6. Returns true if any intermediate is revoked, which blocks registration for the batch. A `revokeCert` failure is counted but does not abort registration for other instances on the same tick. Submitted revocations flip Layer 1 to a hit on the next cycle, so subsequent registrations can short-circuit without re-fetching the CRL. ## Pending registration lifecycle Each per-signer pipeline is keyed by Ethereum signer address. The Boundless proof slot for a signer transitions through: ```mermaid flowchart TB Start([process_instance]) --> Recover[Recovery scan] Recover -->|Locked slot| Wait[wait_for_request_fulfillment] Recover -->|Fulfilled slot| Fresh{Journal fresh?} Recover -->|All slots Unknown/Expired| Submit[Submit fresh request] Recover -->|Blocked recovery| Submit Fresh -->|yes| Receipt[Use recovered receipt] Fresh -->|no| Submit Wait --> Receipt Submit --> Wait Receipt --> Send[tx_manager.send registerSigner] Send -->|Ok| Done([Registered]) Send -->|Retryable| Send Send -->|ExecutionReverted| Block[Block recovery for signer] Block --> Done ``` A pending recovery state, a fulfilled-but-stale receipt, and an `ExecutionReverted` revert all funnel back to a fresh submission on the next tick rather than wedging the signer. ## Onchain interactions The registrar issues the following contract calls. `TEEProverRegistry.isValidSigner()` is intentionally never called by the registrar — that predicate is enforced by `TEEVerifier` at proof-submission time and includes an image-hash match that the registrar itself cannot satisfy. | Contract | Method | Caller path | | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | --------------------------------------------------- | | [`TEEProverRegistry`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEProverRegistry.sol) | `registerSigner(output, proof)` | Per-signer registration transaction. | | [`TEEProverRegistry`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEProverRegistry.sol) | `deregisterSigner(signer)` | Per-orphan deregistration transaction. | | [`TEEProverRegistry`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEProverRegistry.sol) | `isRegisteredSigner(signer)` | Pre-check, post-error reconciliation, orphan race guard. | | [`TEEProverRegistry`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/TEEProverRegistry.sol) | `getRegisteredSigners()` | Once per cycle for orphan computation. | | [`NitroEnclaveVerifier`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/NitroEnclaveVerifier.sol) | `revokeCert(certHash)` | When AWS CRL revokes an intermediate. | | [`NitroEnclaveVerifier`](https://github.com/base/contracts/blob/main/src/L1/proofs/tee/NitroEnclaveVerifier.sol) | `revokedCerts(certHash)` | Layer-1 onchain durable revocation pre-check. | PCR0 enforcement happens onchain at proof submission, not at registration. The registrar registers any enclave whose Nitro attestation verifies, regardless of PCR0 value. That makes it possible to bring up the next image's fleet and pre-register its signers before a hardfork; those signers cannot produce accepted proposals until the active game implementation's `TEE_IMAGE_HASH` matches their registered image hash. ## Service lifecycle At startup, the registrar: 1. Parses CLI configuration and validates it. 2. Initializes tracing and installs the `rustls` ring crypto provider. 3. Installs a signal handler that triggers a cancellation token. 4. Initializes Prometheus metrics, including L1 wallet and Boundless wallet balance monitoring. 5. Builds the L1 provider, transaction manager, AWS SDK clients, and discovery client. 6. Builds the registry client and the optional Nitro verifier client. 7. Builds the proof provider for the configured backend. 8. Starts the health server and marks readiness. 9. Starts the driver loop. The health endpoint reports ready as soon as wiring completes. Connectivity gating is intentionally omitted because the registrar is outbound-only. Each driver tick: 1. Discovers instances. 2. Processes instances concurrently. 3. Computes orphans subject to the majority-reachable guard. 4. Submits deregistration transactions for any confirmed orphans. Shutdown is driven by a cancellation token. The driver loop exits, in-flight per-instance futures are dropped, the readiness flag clears, the `up` metric is set to zero, and the health server is joined. ## Operator inputs A registrar needs: - L1 RPC endpoint and chain ID. - `TEEProverRegistry` address. - AWS region and ALB target group ARN. - Prover JSON-RPC port shared by the fleet. - L1 transaction signer (local key, or remote signing endpoint plus expected address). - Proving backend selection: `boundless` or `direct`. - For `boundless`: marketplace RPC URL, dedicated wallet key, guest program URL, polling interval, prove timeout, recovery attempt limit, and attestation freshness window. - For `direct`: path to the guest ELF. - Poll interval, prover JSON-RPC timeout, max concurrency, max transaction retries, transaction retry delay, and the unhealthy registration warm-up window. Optional inputs: - CRL checking enable flag. - `NitroEnclaveVerifier` address, required when CRL checking is enabled. - CRL fetch timeout. - Health server bind address and port. - Logging filter and Prometheus metrics settings. ## Safety requirements Any registrar implementation must preserve these safety properties: - Never deregister live signers because of a transient AWS or VPC outage. Apply the majority-reachable guard before any deregistration. - Treat `Draining` and `Unhealthy` instances as part of the active set as long as their JSON-RPC endpoint responds, so rotations do not race deregistration. - Use a fresh random nonce per instance batch and pass it to the enclave attestation request so the verifier journal carries an unguessable freshness commitment. - Derive Boundless request slots deterministically from the signer address so a restarted process can recover in-flight proving work without paying again for proofs. - Reject recovered proofs whose attestation timestamp is older than `max_attestation_age`, keeping recovered proofs strictly inside the onchain `MAX_AGE` window. - Block recovery for a signer after an `ExecutionReverted` so the next cycle proves freshly rather than resubmitting the same bad proof. - Recheck `isRegisteredSigner` after a transaction error to absorb fee-bump and nonce-race false negatives. - Recheck `isRegisteredSigner` for every orphan candidate immediately before submitting a deregistration, so a concurrent writer or an earlier in-flight transaction cannot cause a redundant deregistration. - When CRL checking is enabled, run the onchain durable revocation pre-check before fetching network CRLs so that a previously revoked intermediate cannot be silently rehabilitated. - Restrict CRL fetches to allowlisted hosts and bound the response size to defeat SSRF and resource-exhaustion attacks. - Treat unavailable AWS APIs, unreachable prover endpoints, transient RPC errors, and Boundless polling failures as retryable conditions for the next tick — not as deregistration or failure signals. --- # Reth V2 Source: https://basehub.org/specifications/reth-v2/ Description: The Reth V2 node release — up to 50% smaller on disk, with a rebuilt state-root pipeline that raises throughput and lowers per-block execution latency. import { Aside } from '@astrojs/starlight/components'; Base's execution client is Reth, a high-performance Ethereum client. Its next major release, Reth V2, goes straight at the two things that bound a high-throughput chain: how much disk a node burns and how fast it can compute state roots. ## Storage V2 Storage V2 reworks the on-disk layout to bring node storage requirements down sharply: | Node type | Disk reduction | |-----------|----------------| | Full | ~30% | | Minimal | ~50% | | Archive | ~23% | That lifts a disk-growth ceiling that would otherwise cap how far Base can push block throughput. ## State root computation Reth V2 rebuilds the state-root pipeline around three changes: - **Warm sparse-trie caching across blocks** — skips recomputing subtrees that didn't change from one block to the next. - **Parallel trie** — spreads state-root work across CPU cores at once. - **Proof V2** — a reworked proof-generation path that trims per-proof overhead. ## Performance Across Reth's published benchmarks, those changes add up to: - **+33% throughput** - **~25% lower average block-execution latency** `base-reth-node` picks up these gains directly, adding headroom on the way toward 1 gigagas/s. --- # TEE Prover Source: https://basehub.org/specifications/tee-prover/ Description: Offchain service that re-executes L2 block ranges inside AWS Nitro Enclaves to produce signed proof material for AggregateVerifier games. The TEE prover is the offchain service that produces signed proof material for `AggregateVerifier` games by re-deriving and re-executing an L2 block range inside an AWS Nitro Enclave. The same service powers both proposal creation and dispute nullification: callers (proposer or challenger) submit a block range, the host gathers witness data, the enclave re-executes the range, and a per-instance key that lives only inside the enclave signs the resulting journal. Verification of the signature is self-contained onchain. `TEEVerifier` recovers the signer from each proposal and checks it against `TEEProverRegistry` for the active game implementation's `TEE_IMAGE_HASH`. A signer from a different enclave image, or one that has been deregistered, cannot satisfy verification. The Nitro hypervisor's per-instance attestation binds the signer's public key to a specific PCR0, which the [registrar](/specifications/registrar) certifies separately. ## Responsibilities A conforming TEE prover stack: 1. Serve `prover_prove` for proposal and dispute ranges over JSON-RPC. 2. Collect witness data from canonical L1, L1 beacon, and L2 RPCs on the host. 3. Forward content-verified preimages to the enclave over vsock. 4. Re-derive and re-execute the L2 range inside the enclave and validate the claimed output root against the re-executed one before signing anything. 5. Sign per-block journals and an aggregate journal with a secp256k1 key generated inside the enclave. 6. Expose `enclave_signerPublicKey` and `enclave_signerAttestation` for the registrar. 7. Optionally gate every request on registry signer validity to fail closed against deregistered enclaves. 8. Support multi-enclave deployment on a single EC2 parent so different PCR0 images can run side-by-side across rotations. The TEE prover never decides whether a proposal or dispute is correct. It re-executes the range, signs the result when the re-execution matches the claim, and returns. Callers still recheck game state before submitting onchain. ## Architecture The service runs as two processes on a Nitro-capable EC2 parent: - A **host** binary (`base-prover-nitro-host`) that terminates JSON-RPC, gathers witness data over HTTP, and proxies requests to one or more enclaves. - An **enclave** binary (`base-prover-nitro-enclave`) packed into an EIF that holds the signing key, exposes a vsock listener, and runs the proof pipeline. The two processes talk only over vsock. The enclave has no network interface; all external RPC connectivity sits on the host side. ```mermaid flowchart LR caller[Proposer / Challenger] host[NitroProverServer\nbase-prover-nitro-host] rpc[L1 EL / L1 beacon / L2 EL] enclave[NitroEnclave\nbase-prover-nitro-enclave] nsm[NSM\nPCR0 / RNG / attestation] caller -- "prover_prove\nenclave_*" --> host host -- "witness build" --> rpc host -- "vsock\nlength-prefixed bincode" --> enclave enclave --> nsm ``` Each vsock connection handles exactly one request before closing. The enclave keeps no per-request state between connections; the only persistent state inside is the signer key and the boot-time PCR0 measurement. Vsock frames use a `u32` big-endian length prefix followed by a bincode payload, with a 5-minute read timeout. The transport caps write chunks at 28 KiB to dodge a Linux kernel `virtio_vsock` SKB corruption bug. ## JSON-RPC interface The host serves two JSON-RPC namespaces on a single HTTP listener, plus an HTTP `GET /healthz` proxy that routes to the JSON-RPC `healthz` method. | Method | Purpose | | ---------------------------- | ------------------------------------------------------------------------ | | `prover_prove` | Produce per-block and aggregate signed proposals for a block range. | | `enclave_signerPublicKey` | Return the 65-byte uncompressed secp256k1 public key for each enclave. | | `enclave_signerAttestation` | Return the COSE_Sign1 attestation document for each enclave. | | `healthz` / `GET /healthz` | Liveness, plus optional onchain signer validity (latching) when enabled. | The `enclave_*` calls are all-or-nothing across multiple enclaves: a transport failure or an error from any enclave fails the whole response. Callers register every signer together, so a partial response would be unusable. ### prover_prove request `ProofRequest` fields: | Field | Meaning | | ------------------------------ | ------------------------------------------------------------------------------------------------------ | | `l1_head` | L1 head block hash anchoring the derivation window. | | `l1_head_number` | L1 head block number. | | `agreed_l2_head_hash` | L2 block hash at the parent of the range. | | `agreed_l2_output_root` | Output root at the parent. Used as the starting state. | | `claimed_l2_output_root` | Claimed output root at the target. Trust-critical: the enclave only signs if re-execution matches it. | | `claimed_l2_block_number` | Target L2 block number (ending block of the range). | | `proposer` | L1 address that will submit the proof. Committed into the journal so onchain `msg.sender` must match. | | `intermediate_block_interval` | Sampling stride for intermediate roots in the aggregate proposal. | | `image_hash` | `keccak256(PCR0)` the caller expects. Currently informational; routing uses onchain signer validity. | ### prover_prove response `ProofResult::Tee` carries: | Field | Meaning | | -------------------- | --------------------------------------------------------------------------------------------- | | `aggregate_proposal` | One `Proposal` covering the full range with sampled intermediate roots. | | `proposals` | Per-block `Proposal`s in order, each chaining `prev_output_root` to the previous block's root.| Each `Proposal`: | Field | Meaning | | ------------------ | ---------------------------------------------------------------------------------- | | `output_root` | Output root at this proposal's ending block. | | `signature` | 65-byte secp256k1 ECDSA signature (`r || s || v`) over `keccak256(journal)`. | | `l1_origin_hash` | L1 head hash used during derivation. | | `l1_origin_number` | L1 head block number. | | `l2_block_number` | Ending L2 block number for this proposal. | | `prev_output_root` | Output root before this proposal's range. | | `config_hash` | Per-chain config hash hardcoded into the enclave. | A single-block range collapses the aggregate proposal into the lone per-block proposal. For multi-block ranges the aggregate carries its own signature over a journal whose `prev_output_root` is the request's `agreed_l2_output_root`, whose `intermediate_roots` are sampled at `intermediate_block_interval`, and whose `ending_l2_block` is the final block in the range. ### enclave_signerAttestation Takes optional `user_data` and `nonce` byte arguments. The NSM hardware caps each at 512 bytes, and oversize values are rejected at the host RPC layer before reaching vsock. The host returns one raw `COSE_Sign1` document per configured enclave, in the same order as `enclave_signerPublicKey`. The registrar uses this endpoint to bind each enclave's signer to a fresh attestation before submitting it onchain. ## Proof pipeline A single `prover_prove` request flows host → vsock → enclave → host: 1. **Host**: `ProverService::prove_block` constructs a `Host` from the prover config, then calls `Host::build_witness` to walk L1 EL, L1 beacon, and L2 EL and populate an `Oracle` with hash-keyed preimages. 2. **Host**: `NitroBackend::prove` flattens the oracle into `(PreimageKey, Vec)` pairs and `NitroTransport::prove` sends them over vsock as one `EnclaveRequest::Prove(...)` frame. 3. **Enclave**: `Oracle::new` content-verifies every `Keccak256`- or `Sha256`-keyed preimage so the stored value actually hashes to its key. 4. **Enclave**: `BootInfo::load` extracts the proposer, L1 head, agreed/claimed roots, intermediate-block interval, and chain ID from local preimages. 5. **Enclave**: `config_hash_for_chain` looks up a hardcoded per-chain config hash from `CONFIG_HASHES` (computed at first access from `ChainConfig::all()`). Unknown chain IDs return `UnsupportedChain` and refuse to prove. 6. **Enclave**: the proof prologue drives derivation and execution via `driver.execute_with_intermediates()`. The epilogue's `validate()` is the trust-critical gate — it confirms the re-executed final output root matches the request's `claimed_l2_output_root`. Signing only happens after this check passes. 7. **Enclave**: for each block result, build a `ProofJournal` with empty `intermediate_roots` and sign it; chain `prev_output_root` through the loop. Then build and sign the aggregate journal with sampled intermediate roots. 8. **Enclave**: return `EnclaveResponse::Prove(ProofResult::Tee { aggregate_proposal, proposals })`. 9. **Host**: return the result to the JSON-RPC caller, applying the configured proof request timeout (default 1740 s, ~29 minutes). The proposer consumes both the aggregate and per-block proposals: per-block roots feed `proposeOutputRoots` and the aggregate signature satisfies `AggregateVerifier`. The challenger only uses the aggregate signature, repacking it for `nullify()` via `ProofEncoder::encode_dispute_proof_bytes`. The enclave neither knows nor cares which caller it is serving. ## Signed journal Each signature is `secp256k1.sign(keccak256(journal))`, serialized as 65 bytes (`r || s || v`). The journal is packed (not ABI-encoded), `196 + 32·N` bytes where `N` is the number of intermediate roots: ```text proposer(20) || l1OriginHash(32) || prevOutputRoot(32) startingL2Block(8) || outputRoot(32) || endingL2Block(8) intermediateRoots(32 × N) || configHash(32) teeImageHash(32) ``` Per-block proposals have `N == 0` and `startingL2Block == endingL2Block - 1`. Aggregate proposals have `startingL2Block == firstBlock - 1`, `endingL2Block == lastBlock`, and `N == lastBlock / intermediate_block_interval`. `teeImageHash` is `keccak256(PCR0)` taken at enclave boot. Embedding it into every journal means a signature recovered onchain transitively commits to the exact EIF measurement that produced it. In local mode (no NSM, development and test only), `teeImageHash` is zero. The signature `v` byte is encoded as the secp256k1 recovery id (`0` or `1`); callers normalize it to whichever EIP-155 form they need before L1 submission. ## Multi-enclave routing `--vsock-cid` accepts one or more CIDs, so a single host process can attach to multiple enclaves running on the same EC2 parent. Each CID is an independent vsock endpoint that can run a different EIF — a different PCR0, a different `tee_image_hash`, and a different registered signer. `--tee-prover-registry-address` is required whenever more than one CID is configured. Without the registry there is no deterministic way to pick between enclaves, so multi-enclave deployments are fail-closed-only. Per-request routing walks configured CIDs in order and picks the first enclave whose signer is currently valid in `TEEProverRegistry`: 1. Fetch the signer public key from the enclave (skip the transport if this fails). 2. Call `isValidSigner(signer)` on `TEEProverRegistry`. 3. If valid, route the request to this enclave. If not, log and continue. 4. If no enclave in the list has a valid signer, fail the request with `NoValidSigner`. The common operational case is image rotation. Run the old and new EIFs side-by-side; both signers stay registered for the active game implementation's `TEE_IMAGE_HASH` during the overlap window; after the registry switches to the new image hash only the new enclave's signer is valid and every new request routes to it. `enclave_*` calls fan out to every configured enclave so the registrar can register every signer in one cycle. ## Registration gating and health Setting `--tee-prover-registry-address` enables two registry-backed behaviors on the host: - `GET /healthz` only reports healthy after at least one enclave's signer has been confirmed valid onchain. The health flag latches: once an enclave has been seen valid, `/healthz` continues to report healthy even if the registry RPC later fails or the signer is deregistered. This keeps load balancers stable through short outages. - Every `prover_prove` request consults `RegistrationChecker::select_valid_enclave` before forwarding. A deregistered enclave, or one whose key fetch fails, is skipped. If no enclave is valid the request is rejected with JSON-RPC error code `-32001`. Without the registry flag, the host is permissive: `/healthz` reports healthy as long as the server is up, and `prover_prove` routes to the first configured enclave. ## Attestation The signer key is generated inside the enclave at startup and never leaves the enclave process. The `Server::new_enclave` constructor: 1. Opens an NSM session (`nsm_init`). 2. Reads PCR0 (48-byte SHA-384). A wrong-length PCR0 aborts startup. 3. Computes `tee_image_hash = keccak256(PCR0)` and stores it for inclusion in every signed journal. 4. Generates a secp256k1 ECDSA key with `NsmRng`, which calls `nsm_process_request(Request::GetRandom)`. 5. Logs the signer address (no key material). There is no startup or periodic attestation. Attestations are produced only when the registrar calls `enclave_signerAttestation`. Each call: 1. Opens a fresh NSM session. 2. Calls `nsm_process_request(Request::Attestation { public_key, user_data, nonce })`. 3. Returns the raw COSE_Sign1 bytes. The attestation document embeds the 65-byte uncompressed public key, all populated PCRs, the AWS-issued certificate chain, the timestamp, and the supplied `user_data`/`nonce`, all signed by the per-instance Nitro hypervisor key. Only PCR0 is consumed by this system — it is the value bound into every signed journal via `teeImageHash = keccak256(PCR0)`. See the [registrar](/specifications/registrar) spec for how attestations are verified and submitted onchain. ## Service lifecycle The host startup sequence (`ServerArgs::run`): 1. Parse CLI; initialize logging and metrics via `base_cli_utils`. 2. Resolve the `RollupConfig` and L1 chain config from `--l2-chain-id`. Fail on unknown chains. 3. Build one `NitroTransport::vsock(cid, 8000)` per `--vsock-cid`. 4. Construct `NitroProverServer::new_multi(prover_config, transports, timeout)` and, if `--tee-prover-registry-address` is set, wrap with `RegistrationHealthConfig`. 5. Build a jsonrpsee HTTP server with a `/healthz` proxy layer, merge `ProverApiServer`, `EnclaveApiServer`, and one of the healthz modules, and start the server. 6. Block on the server handle; exit on ctrl-C. The enclave startup sequence (`NitroEnclave::new`): 1. `Server::new()` opens NSM, derives `tee_image_hash`, and generates the signer key. 2. Bind a `VsockListener` on `VMADDR_CID_ANY:8000`. 3. For each connection, spawn a handler that reads one framed `EnclaveRequest`, dispatches to `Server::prove`, `signer_public_key`, or `signer_attestation`, writes the response, and closes the connection. Per-request flow on the host: 1. (Optional) `select_valid_enclave` chooses a registered enclave. 2. `tokio::time::timeout(proof_request_timeout, enclave.service.prove_block(request))`. 3. On timeout, return JSON-RPC `-32000` with the offending L2 block number. 4. On error from the enclave, return JSON-RPC `-32000` with the underlying error message. Shutdown is driven by ctrl-C handled by `RuntimeManager`. The jsonrpsee server stops, in-flight requests drain, and the runtime exits. The enclave has no graceful shutdown path; process termination drops NSM file descriptors via `Drop`. ## Operator inputs A TEE prover host needs: - L1 execution RPC URL. - L1 beacon RPC URL. - L2 execution RPC URL. - L2 chain ID (used to select the rollup config and per-chain config hash). - JSON-RPC listen address. - One or more vsock CIDs, each backed by a Nitro Enclave running the prover EIF. - Proof request timeout (default 1740 seconds). - Logging filter and Prometheus metrics settings. Optional: - `TEEProverRegistry` address. Required when more than one vsock CID is configured. Enables registration-gated health and per-request signer validation. - Experimental witness endpoint flag for hosts that expose `debug_executePayload`. The enclave needs no operator inputs beyond the EIF image and the vsock channel. PCR0 is read at boot from NSM; the signer key is generated from the hardware RNG. ## Safety requirements A conforming TEE prover preserves these safety properties: - Generate the signing key inside the enclave from the NSM hardware RNG and never serialize it out of the enclave process. - Validate the re-executed final output root against the request's `claimed_l2_output_root` before any signing, and refuse to sign if the check fails. - Embed `tee_image_hash = keccak256(PCR0)` in every signed journal so signatures bind to one EIF measurement. - Content-verify every hash-keyed preimage as it enters the enclave so derivation cannot consume preimages whose values disagree with their keys. - Refuse to prove for chain IDs not present in the hardcoded `CONFIG_HASHES` table. - Cap `user_data` and `nonce` at the NSM 512-byte limit at the host RPC boundary so oversize attestation requests cannot reach the enclave. - Serve at most one request per vsock connection and keep no mutable state between requests so a malformed request cannot influence a later one. - When `--tee-prover-registry-address` is configured, fail closed on per-request signer validity and reject the request if no configured enclave's signer is currently valid onchain. --- # Transaction Event Journal Source: https://basehub.org/specifications/transaction-events/ Description: The transaction-event/v1 contract — a shared JSONL business-event journal for Base transaction observability. The `transaction-event/v1` contract gives Base components a common way to record business-level transaction events. Each producer appends newline-delimited JSON to a dedicated journal file rather than mixing these records into ordinary application output. The usual stdout/stderr path (and its Kubernetes-to-Datadog pipeline) is reserved for logs and must not double as this journal. A Vector agent follows those JSONL files and forwards each record to `audit-archiver`. Because the audit ingest endpoint is collector-facing, it reads exactly one event object per line and does not accept a wrapped JSON batch. :::note The canonical source is [`docs/transaction-events/README.md`](https://github.com/base/base/blob/main/docs/transaction-events/README.md) in the base/base repository. ::: ## Configuration Fields Rust producers are expected to surface the following settings, either at the top level or behind a producer-specific prefix: | Field | Type | Meaning | | --- | --- | --- | | `enabled` | boolean | Enables transaction event journal writes. | | `file_path` | string | Dedicated JSONL file path tailed by Vector. | | `queue_capacity` | integer | Upper bound on the in-process event queue. Under backpressure a producer discards events rather than stalling its transaction-serving path. | | `required` | boolean | When true, startup fails if the writer cannot open the file. A failure to write at runtime stays non-fatal and is still surfaced for observability. | | `producer` | string | One of the producer identities below. | | `network` | string | Network label, for example `base-mainnet` or `base-sepolia`. | Go and proxyd builds carry the identical field names in TOML: ```toml [transaction_events] enabled = true file_path = "/var/log/base/transaction-events.jsonl" queue_capacity = 16384 required = false producer = "base-routing/proxyd" network = "base-mainnet" ``` ## Envelope Every line is a single JSON object: ```json { "schema_version": "transaction-event/v1", "event_id": "0x7d5c4f...", "event_time": "2026-06-02T00:00:00.000000000Z", "producer": "base-reth-node", "event_type": "TXPOOL_PENDING", "network": "base-mainnet", "tx_hash": "0x1111111111111111111111111111111111111111111111111111111111111111", "block_hash": null, "block_number": null, "payload_id": null, "request_id": null, "data": { "pool": "pending" } } ``` These fields are mandatory: - `schema_version` - `event_id` - `event_time` - `producer` - `event_type` In normal operation at least one join key should be set — either `tx_hash`, the `block_hash`/`block_number` pair, or `payload_id`. The `request_id` field is not required but helps correlate records across proxies and ingress. Producers should avoid emitting journal records for aggregate operational states that cannot be attributed to one of those join keys: broadcast lag, for instance, stays in logs and metrics because the receiver only sees a skipped count, whereas `INGRESS_METERING_SEND_DROPPED` fires once per dropped transaction precisely because ingress still holds the original `tx_hash`. Anything producer-specific goes inside `data`. Never journal raw transaction bytes, calldata, full request bodies, API keys, secrets, private keys, bearer tokens, authorization headers, raw forwarding headers, or raw client IP forwarding chains. A collector sidecar may attach deployment-specific origin metadata under `data.observability_source` before handing events off to `audit-archiver`. The archiver persists that object alongside the rest of `data`, but the shared contract places no schema constraints on it. On the Rust side, the `TransactionEvent::validate` helper rejects a mismatched schema version, an empty `event_id`, and a short, exact denylist of unsafe `data` keys — among them `raw_tx`, `calldata`, `request_body`, `authorization`, `api_key`, `headers`, and `x-forwarded-for`. Vector collector pipelines are expected to catch wider case and delimiter variants — such as `rawTransaction`, `requestBody`, `secret_key`, and `privateKey` — before anything is ingested. ## Local Devnet Verification The ingress devnet brings up a local Postgres, a Vector shipper, and a Postgres-backed `audit-archiver` ingest path: ```bash just devnet ingress just devnet tx-observability-smoke ``` To exercise proxyd transaction events before that code ships in the default proxyd image, point `BASE_ROUTING_CONTEXT` at a local `protocols/base-routing` checkout: ```bash BASE_ROUTING_CONTEXT=/path/to/base-routing just devnet ingress just devnet tx-observability-smoke ``` The smoke test pushes a single transaction through ingress, waits for Vector to deliver the JSONL records from the ingress, proxyd, txpool-tracing, and builder producers, and then confirms `audit-archiver` can read the stored events back out of Postgres by transaction hash. To keep an eye on the local shipper, alert on or inspect the `component_discarded_events_total` metric. Two transforms account for what it counts: `parse_transaction_events` sheds malformed JSONL lines, while `validate_transaction_events` discards already-parsed records whose `data` keys are flagged unsafe. ## Producer Values - `base-reth-node` - `base-builder` - `ingress-rpc` - `base-routing/proxyd` ## Txpool Tracing Example When `--enable-transaction-event-journal` and `--transaction-event-journal-path` are supplied, `base-reth-node` txpool tracing can route its existing live LRU events into the durable journal: ```json {"schema_version":"transaction-event/v1","event_id":"0x4d6d...","event_time":"2026-06-02T00:00:00Z","producer":"base-reth-node","event_type":"TXPOOL_PENDING","network":"base-mainnet","tx_hash":"0x1111111111111111111111111111111111111111111111111111111111111111","block_hash":null,"block_number":null,"payload_id":null,"request_id":null,"data":{"event_source":"txpool-tracing","txpool_event":"pending","event_index":0,"node_role":"mempool","pool":"pending"}} ``` ## Event Vocabulary Edge/proxy: - `PROXY_RECEIVED` - `PROXY_REJECTED` - `PROXY_VALIDATION_ACCEPTED` - `PROXY_VALIDATION_REJECTED` - `PROXY_ROUTED_TO_BACKEND` - `PROXY_BACKEND_SUCCESS` - `PROXY_BACKEND_FAILURE` - `PROXY_INGRESS_RPC_ATTEMPT` - `PROXY_INGRESS_RPC_SUCCESS` - `PROXY_INGRESS_RPC_FAILURE` Ingress/audit: - `INGRESS_RECEIVED` - `SIMULATION_STARTED` - `SIMULATION_SUCCEEDED` - `SIMULATION_FAILED` - `INGRESS_METERING_SEND_ATTEMPT` - `INGRESS_METERING_SEND_SUCCESS` - `INGRESS_METERING_SEND_FAILURE` - `INGRESS_METERING_SEND_DROPPED` Mempool/node: - `TXPOOL_PENDING` - `TXPOOL_QUEUED` - `TXPOOL_PENDING_TO_QUEUED` - `TXPOOL_QUEUED_TO_PENDING` - `TXPOOL_DROPPED` - `TXPOOL_REPLACED` - `TXPOOL_TRACKING_OVERFLOWED` - `TXPOOL_BLOCK_INCLUDED` - `TXPOOL_FLASHBLOCK_INCLUDED` Forwarding: - `TXPOOL_BUILDER_FORWARD_ATTEMPT` - `TXPOOL_BUILDER_FORWARD_SUCCESS` - `TXPOOL_BUILDER_FORWARD_FAILURE` - `TXPOOL_BUILDER_FORWARD_DROPPED` - `TXPOOL_VALIDATED_INSERT_ACCEPTED` - `TXPOOL_VALIDATED_INSERT_REJECTED` `TXPOOL_BUILDER_FORWARD_DROPPED` is reserved for transaction-scoped drops where the forwarding task still has the `tx_hash` in hand, such as a final RPC failure after the retries are exhausted. Broadcast lag is deliberately kept out of the journal and stays visible only through logs and metrics. Builder: - `BUILDER_CONSIDERED` - `BUILDER_ACCEPTED` - `BUILDER_REJECTED` - `BUILDER_INCLUDED` - `BUILDER_PAYLOAD_FINALIZED` - `BUILDER_FLASHBLOCK_STARTED` - `BUILDER_FLASHBLOCK_PUBLISHED` - `BUILDER_FLASHBLOCK_BUILD_STOPPED` Builder caveat: `BUILDER_CONSIDERED`, `BUILDER_ACCEPTED`, and `BUILDER_REJECTED` fire once per payload-building attempt and, where relevant, carry `payload_id`, `block_number`, and `flashblock_index`. As a result, one transaction can generate several decision events across consecutive flashblocks. `BUILDER_INCLUDED` is recorded once the builder finalizes the payload it can serve through `engine_getPayload`, and it sets `data.inclusion_signal = "builder_finalized_payload"`. Nonce and validation skips are recorded by the payload loop as `BUILDER_REJECTED` rather than fabricating replacement relationships. `BUILDER_PAYLOAD_FINALIZED` is written once for every built payload — even an empty one with no user transactions — and ties the `payload_id` to the builder's block hash and number; it includes `data.parent_hash`, `data.transaction_count`, `data.gas_used`, `data.gas_limit`, and `data.timestamp`. `BUILDER_FLASHBLOCK_STARTED`, `BUILDER_FLASHBLOCK_PUBLISHED`, and `BUILDER_FLASHBLOCK_BUILD_STOPPED` are scoped to a payload/flashblock and carry top-level `payload_id` and `block_number` plus `data.parent_hash`, `data.flashblock_index`, and `data.target_flashblock_count`. Published events additionally set top-level `block_hash` and `data.transaction_count`, `data.byte_size`, and `data.build_duration_ms`. Build-stopped events use `data.reason` to label control-flow stops, for example payload resolution winning the race before publish. Canonicality caveat: builder events describe local payload construction, not canonical-chain or consensus-finality state. A builder event that carries `block_hash` means the builder computed or published that payload shape — it is not, on its own, evidence that the block became canonical. Tie records back to canonical history through canonical-state observers such as txpool tracing by matching `block_hash`, `block_number`, and transaction hashes. ## Event ID Guidance Prefer deterministic `event_id` values whenever the source has stable inputs. Reasonable components to combine include: - `producer` - `event_type` - source timestamp bucket or source sequence - `tx_hash` - `request_id` - backend/node identifier when applicable - attempt index when applicable When a source genuinely cannot produce a deterministic ID, record the reason in the producer implementation and pack enough fields into `data` for `audit-archiver` to enforce uniqueness on the database side. ## proxyd Examples Received raw transaction request: ```json { "schema_version": "transaction-event/v1", "event_id": "0x1f3f...", "event_time": "2026-06-02T00:00:00.000000000Z", "producer": "base-routing/proxyd", "event_type": "PROXY_RECEIVED", "network": "base-mainnet", "tx_hash": "0x2222222222222222222222222222222222222222222222222222222222222222", "block_hash": null, "block_number": null, "payload_id": null, "request_id": "req-abc", "data": { "rpc_method": "eth_sendRawTransaction" } } ``` Validation rejection: ```json { "schema_version": "transaction-event/v1", "event_id": "0x2a4b...", "event_time": "2026-06-02T00:00:00.000000000Z", "producer": "base-routing/proxyd", "event_type": "PROXY_VALIDATION_REJECTED", "network": "base-mainnet", "tx_hash": "0x2222222222222222222222222222222222222222222222222222222222222222", "block_hash": null, "block_number": null, "payload_id": null, "request_id": "req-abc", "data": { "rpc_method": "eth_sendRawTransaction", "validation_service": "tx-validation", "fail_open": false } } ``` Routed to node: ```json { "schema_version": "transaction-event/v1", "event_id": "0x3b5c...", "event_time": "2026-06-02T00:00:00.000000000Z", "producer": "base-routing/proxyd", "event_type": "PROXY_ROUTED_TO_BACKEND", "network": "base-mainnet", "tx_hash": "0x2222222222222222222222222222222222222222222222222222222222222222", "block_hash": null, "block_number": null, "payload_id": null, "request_id": "req-abc", "data": { "backend": "reth-mainnet-0", "attempt_index": 0 } } ``` ## TIPS S3/Postgres Parity Checklist The journal feeds the audit-archiver/Postgres query path that backs the TIPS UI. Before an environment flips `TIPS_UI_QUERY_BACKEND=audit`, the Postgres route should return the same answers the existing S3-backed route does. Keep S3 serving traffic until the samples below match against audit-archiver, then cut over. **Sample inputs.** Pick a representative set and reuse it across both backends: - A recent block hash that includes at least one non-system transaction, plus that same block addressed by number. - A transaction hash from that block carrying a `SIMULATION_SUCCEEDED` event. - A bundle hash or bundle id drawn from that transaction's event data. - A rejected transaction from the trailing 31 days, if one exists. **Route comparisons.** Run each sample through both backends and check that they line up: - On `GET /api/block/`, the two paths should agree on block hash, number, gas limits, the ordering of transactions, and the transaction hashes themselves. - Addressing the block by number via `GET /api/block/` should land on that same canonical block. - Per sampled non-system transaction, both backends should concur on whether simulation data is present; where it is, the bundle hash/id, state block number, total gas used, total execution time, state-root time, and the per-transaction gas/time summaries should all match. - `GET /api/txn/` should hand back a matching transaction hash along with at least one bundle id/hash join key. - Events from `GET /api/bundle/` should arrive in event-time order, with the accepted simulation event for the sample among them. - Where a rejected sample exists, `GET /api/rejected` should echo its transaction hash, block number, rejection reason, timestamp, and metering summary. **Operational checks:** - On `audit-archiver`, `TIPS_AUDIT_POSTGRES_URL` is configured, and the audit service port answers both JSON-RPC and transaction-event HTTP ingest — that same port is where Vector delivers NDJSON, at `/v1/transaction-events/batch`. - `ingress-rpc` carries `TIPS_INGRESS_TRANSACTION_EVENTS_ENABLED=true` in the test environment alone, its JSONL path aligned with the Vector sidecar mount. - On the UI side, `TIPS_UI_QUERY_BACKEND=audit` is set with `TIPS_UI_AUDIT_RPC_URL` aimed at the audit service. - Writes to S3 keep flowing across the whole comparison window. - Nothing in the rollout drops a Kafka, S3, or existing RPC persistence path. --- # ZK Prover Source: https://basehub.org/specifications/zk-prover/ Description: Offchain service that drives SP1 programs to produce permissionless proofs for AggregateVerifier proposals and disputes. The ZK prover is an offchain service that takes block-range proof requests, drives SP1 programs to completion, and returns receipts a caller can hand to `AggregateVerifier`. The service persists request state across restarts, submits the underlying work to its configured SP1 backend, and exposes proof status and receipt retrieval over gRPC. The ZK path is permissionless: any operator with canonical L1 and L2 RPC access, a working SP1 backend, and an L1 transaction signer can both request proofs and submit valid proof material onchain. ## Responsibilities A conforming ZK prover stack: 1. Accept proving requests for L2 block ranges. 2. Generate witness input from canonical L1, L2, and beacon RPCs. 3. Prove the range program with SP1. 4. For Groth16 requests, aggregate the completed range proof into an onchain-verifiable SNARK. 5. Persist proof request and backend session state so work can recover across process restarts. 6. Expose proof status and receipt retrieval over gRPC. 7. Encode receipts in the format expected by challengers, proposers, and `ZKVerifier`. The ZK prover never decides whether a game is valid. Proposers and challengers pick the range, recompute canonical roots themselves, and recheck game state before submitting proof material onchain. ## Proving service API The proving service exposes: ```text ProveBlock(ProveBlockRequest) -> ProveBlockResponse GetProof(GetProofRequest) -> GetProofResponse ``` `ProveBlock` enqueues a proof request and returns a `session_id`. `GetProof` returns the current status and, once complete, the requested receipt bytes. ### ProveBlock request `ProveBlockRequest` carries: | Field | Meaning | | --------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `start_block_number` | L2 block whose output root is the trusted starting state for the range. | | `number_of_blocks_to_prove` | Number of L2 blocks to prove after `start_block_number`. | | `sequence_window` | Optional L1 block lookahead used when deriving an L1 head for witness generation. | | `proof_type` | `PROOF_TYPE_COMPRESSED` or `PROOF_TYPE_SNARK_GROTH16`. | | `session_id` | Optional caller-supplied UUID used for idempotent requests. | | `prover_address` | L1 address committed into the Groth16 journal so a proof cannot be replayed by another sender. Required for Groth16. | | `l1_head` | Optional 32-byte hex L1 block hash used for witness generation. | A caller that supplies the same `session_id` twice receives the existing session in response. Challengers exploit this by deriving the UUID deterministically from `(game address, invalid checkpoint index)` so retries across process restarts are safe. Pass `l1_head` when the proof journal must match an L1 head already committed onchain — dispute proofs against an existing game, for example. With `l1_head` omitted, the service picks one from the L2 block's L1 origin plus the request or service sequence window, which is the right behavior for fresh proposals where the caller has not yet committed to an L1 head. For `PROOF_TYPE_SNARK_GROTH16` the `prover_address` is mandatory: the aggregation program embeds it into the journal digest and `AggregateVerifier` re-checks the same digest, so a Groth16 receipt is bound to the L1 sender that requested it. ### Proof types Two proof types are supported: | Proof type | Backend sessions | Result | | ----------------------------- | ---------------- | ---------------------------------------------------------------------- | | `PROOF_TYPE_COMPRESSED` | `STARK` | A compressed SP1 range proof. | | `PROOF_TYPE_SNARK_GROTH16` | `STARK`, `SNARK` | A range proof plus a Groth16 aggregation proof suitable for onchain use. | A `PROOF_TYPE_SNARK_GROTH16` request runs in two stages: the range program is submitted as a compressed STARK session, and once that session lands the aggregation program is submitted as a Groth16 SNARK session. ## Request lifecycle Each proof request starts in `CREATED` once the request and its outbox entry are durably stored. A worker claims the outbox task and the request moves to `PENDING` while it prepares and submits backend work. The state advances to `RUNNING` as soon as at least one backend session exists. The terminal states are `SUCCEEDED` — every session required by the proof type completed and the receipt bytes are stored — or `FAILED`, set when validation, witness generation, backend submission, backend execution, receipt download, or retry recovery gives up. Backend sessions advance through `RUNNING`, `COMPLETED`, or `FAILED` independently of the proof request itself. A compressed request lands when its STARK session lands. A Groth16 request lands only after both STARK and SNARK sessions complete. Any failed session fails the parent request. ## Receipt retrieval `GetProofRequest` carries: | Field | Meaning | | -------------- | ----------------------------------------------------------- | | `session_id` | UUID returned by `ProveBlock`. | | `receipt_type` | Optional receipt selector. Defaults to `RECEIPT_TYPE_STARK`. | Receipt selectors: | Receipt type | Response bytes | | ----------------------------- | ----------------------------------------------------------------------------------------- | | `RECEIPT_TYPE_STARK` | Serialized SP1 proof-with-public-values for the range proof. | | `RECEIPT_TYPE_SNARK` | Serialized SP1 proof-with-public-values for the aggregation proof. | | `RECEIPT_TYPE_ON_CHAIN_SNARK` | Onchain proof bytes extracted from the stored SNARK receipt for the SP1 Groth16 verifier. | While a request is `CREATED`, `PENDING`, or `RUNNING`, `GetProof` returns empty receipt bytes. A failed request returns `STATUS_FAILED` and the stored error message. A success always carries non-empty bytes; if the stored request is `Succeeded` but the requested receipt kind is missing, `GetProof` returns gRPC `NOT_FOUND` rather than an empty success. Wrapping returned receipt bytes in the `AggregateVerifier` proof format is the caller's job. For challenge, nullification, and additional-proof submission the caller prefixes the ZK proof-type byte. For game initialization the caller also adds the L1 origin fields required by `initializeWithInitData()`. See [Proof Contracts](/specifications/contracts) for the verifier-side framing. ## Backend modes Three backend modes are available: | Mode | Purpose | | --------- | ----------------------------------------------------------------------- | | `mock` | Produces fake receipts for local tests without witness generation. | | `cluster` | Submits work to a self-hosted SP1 cluster with Redis or S3 artifacts. | | `network` | Submits work to the SP1 Network with the configured fulfillment policy. | The `cluster` and `network` backends share the same witness generation; only submission, polling, and artifact retrieval differ. The `mock` backend skips witness generation entirely. ## SP1 range program The range program proves a Base L2 state transition over a contiguous block range. Its stdin contains: ```text rkyv(DefaultWitnessData) intermediateRootInterval ``` The program reconstructs the preimage oracle and beacon blob provider from the witness, runs the Ethereum DA witness executor, and commits a `BootInfoStruct`. The committed boot info contains: | Field | Meaning | | -------------------------- | --------------------------------------------------------------- | | `l2PreRoot` | Output root for the trusted starting L2 block. | | `l2PreBlockNumber` | Starting L2 block number. | | `l2PostRoot` | Output root after executing the requested range. | | `l2BlockNumber` | Ending L2 block number. | | `l1Head` | L1 block hash used for derivation data. | | `rollupConfigHash` | Hash of the rollup configuration used during execution. | | `intermediateRoots` | Ordered output roots sampled every intermediate-root interval. | The final intermediate root must correspond to the ending L2 block for the range being proven. ## SP1 aggregation program The aggregation program collapses completed range proofs into the journal digest used by onchain verification. Inputs: ```text AggregationInputs (sp1_zkvm::io::read) L1 headers (CBOR-encoded) (sp1_zkvm::io::read_vec) compressed range proofs (SP1 proof-input channel) ``` The compressed range proofs travel over SP1's proof-input mechanism rather than plain stdin bytes, and the program verifies them internally with `sp1_lib::verify::verify_sp1_proof`. `AggregationInputs` bundles the range boot infos, the latest L1 checkpoint head, the range-program verification key, and the prover address. The aggregation program checks: 1. At least one range boot info is present. 2. Adjacent range boot infos are sequential: ```text previous.l2PostRoot == next.l2PreRoot previous.l2BlockNumber == next.l2PreBlockNumber ``` 3. Every range uses the same `rollupConfigHash`. 4. Every compressed range proof verifies against the supplied range verification key. 5. The supplied L1 headers form a linked chain ending at `latest_l1_checkpoint_head`. 6. Every range `l1Head` appears in that header chain. After verification, the program concatenates all intermediate roots and builds one aggregate output: ```text proverAddress l1Head l2PreRoot startingL2SequenceNumber l2PostRoot endingL2SequenceNumber intermediateRoots rollupConfigHash imageHash ``` `imageHash` is the range-program verification key commitment. The aggregation program commits: ```text keccak256(abi.encodePacked(AggregationOutputs)) ``` That digest matches the journal hash `AggregateVerifier` assembles for ZK proof verification. In [Proof Contracts](/specifications/contracts) terminology, `imageHash` is `ZK_RANGE_HASH` and the aggregation verification key configured on `ZKVerifier` is `ZK_AGGREGATE_HASH`. ## ELF reproducibility SP1 ELF binaries are built on demand rather than committed. The repository pins expected ELF SHA-256 hashes in `crates/proof/succinct/elf/manifest.toml`. Any code change touching either SP1 program must rebuild the ELFs and update `manifest.toml` in the same change. The range verification key commitment (`ZK_RANGE_HASH`) and aggregation verification key hash (`ZK_AGGREGATE_HASH`) are onchain security parameters. Operators must deploy or configure verifier contracts with values derived from the same ELFs the proving service runs. ## Retry behavior Transient conditions are retried without changing the logical proof request: | Condition | Required behavior | | -------------------------------------------------- | ------------------------------------------------------------------------------ | | Outbox task already claimed | Skip the duplicate worker. | | Stuck `PENDING` request without an active session | Reset to `CREATED` with a new outbox entry until the retry limit is exhausted. | | Backend status polling error | Leave the request `RUNNING` and retry on a later poll. | | Proof artifact unavailable after backend success | Leave the session `RUNNING` or retry download on a later poll. | | Backend reports failed or unfulfillable work | Mark the session and proof request `FAILED`. | | Groth16 stage-two submission fails after STARK | Mark the proof request `FAILED`. | Callers should treat `FAILED` as terminal for the stored request. If the proof is still wanted, resubmit or retry the same logical request using its deterministic `session_id`. ## Service lifecycle At startup, the proving service: 1. Connects to Postgres. 2. Optionally starts rate-limited local proxies for L1, L2, and beacon RPCs. 3. Loads rollup configuration from the rollup RPC. 4. Computes the range and aggregation proving and verifying keys. 5. Initializes the configured backend. 6. Starts the outbox processor. 7. Starts the status poller. 8. Starts the gRPC server and reflection service. The outbox processor converts persisted requests into backend sessions. The status poller keeps running sessions in sync, downloads receipts, triggers Groth16 stage two when needed, and either retries or fails stuck requests. ## Operator inputs A ZK prover service needs: - L1 execution RPC endpoint. - L1 beacon RPC endpoint. - L2 execution RPC endpoint. - Rollup RPC endpoint. - Postgres connection settings. - SP1 backend configuration. - Artifact storage configuration for cluster mode. - Poll intervals, stuck-request timeout, and retry limits. - Metrics and logging configuration. Network mode also needs an SP1 Network signer or KMS requester configuration. Cluster mode also needs an SP1 cluster endpoint and exactly one artifact storage backend. ## Onchain expectations ZK proof bytes are submitted to `AggregateVerifier` with proof type `ZK`. The game assembles the expected journal from proposal or dispute context and calls `ZKVerifier.verify()` with the configured aggregation verification key. A valid Groth16 receipt proves the aggregation program committed the expected journal digest. It does not substitute for caller-side state checks. Proposers and challengers must still recompute canonical roots and recheck game state before submitting proof material. ## Safety requirements A conforming ZK prover preserves these safety properties: - Use the caller-provided `l1_head` when present so dispute proofs match the game context stored onchain. - Require `prover_address` for Groth16 proofs because the aggregation journal commits it. - Keep request creation idempotent for deterministic `session_id` values. - Never return onchain SNARK bytes unless the stored SNARK receipt deserializes successfully. - Persist backend session metadata before relying on asynchronous backend completion. - Pin ELF hashes so verification keys and onchain configuration cannot silently drift. - Treat unavailable RPC data, backend polling failures, and artifact download failures as retryable service conditions, not proof validity results. ---