Robinhood Chain block.number vs eth_blockNumber
Last updated:
Endpoint and plans: Robinhood Chain RPC endpoint.
At a glance
| Property | Value |
|---|---|
| block.number in a contract | Estimate of the Ethereum L1 block number, updated periodically |
| eth_blockNumber over RPC | The Robinhood Chain (L2) block number |
| L2 block number in a contract | ArbSys(0x0000000000000000000000000000000000000064).arbBlockNumber() |
| arbBlockNumber() selector | 0xa3b1b31d |
| L1 estimate over RPC | l1BlockNumber field of the block |
| Sync of the L1 estimate (Arbitrum docs) | Every 13 to 15 seconds, occasionally longer |
| Measured on 13 Oct 2026, 17:30 UTC | L2 block 87,691,694; L1 estimate 26,185,074 |
What you see
A contract's block-based logic disagrees with what the explorer and your RPC show. A vesting schedule or a cooldown written in blocks runs far longer than planned, a per-block limit lets many transactions through in what your backend sees as different blocks, a value stored from block.number does not match the blockNumber in the transaction's receipt, or a helper contract's getBlockNumber() returns a number tens of millions lower than eth_blockNumber.
Why this happens
Robinhood Chain is an Arbitrum Nitro Layer-2, and Robinhood's differences page says that block.number "returns an estimate of the L1 (Ethereum) block number, not the Robinhood Chain block number" and "updates only periodically". Arbitrum's reference explains the mechanism: the value is "close to (but not necessarily exactly)" the parent chain's block number and is synced with Ethereum "every 13 to 15 seconds (occasionally longer)", so it is "unreliable in the shorter term (minutes)". JSON-RPC works the other way round: eth_blockNumber, a block's number field and a receipt's blockNumber are all the L2 block number, and the L1 estimate appears in an extra l1BlockNumber field. Inside a contract, the L2 number is available from the ArbSys precompile, ArbSys(0x0000000000000000000000000000000000000064).arbBlockNumber(). The gap is not small: many L2 blocks are produced for every Ethereum block, so in the capture below the L2 counter and the L1 estimate differ by more than 61 million, and block.number stayed on one value for 81 L2 blocks before moving on by two. Local test networks model Ethereum, where both numbers are the same, so the bug survives compilation and unit tests.
Three block numbers, one block
| Value | Where it comes from | Read on 13 Oct 2026 |
|---|---|---|
| eth_blockNumber | RPC: the L2 head the node serves | 0x53a11ae = 87,691,694 |
| ArbSys.arbBlockNumber() | Contract call to the precompile at 0x…64 (eth_call at that block) | 0x…53a11ae = 87,691,694 |
| block.number in Solidity | Estimate of the Ethereum block number, synced periodically | 26,185,074 |
| l1BlockNumber in eth_getBlockByNumber | The same L1 estimate, exposed in the block header over RPC | 0x18f8d72 = 26,185,074 |
| block.timestamp | The sequencer's clock, set per L2 block | 1791912635 = 17:30:35 UTC |
| L1 estimate across blocks 87,691,614–87,691,694 | 81 consecutive L2 blocks, timestamps 8 seconds apart | 26,185,074 in every one; 26,185,072 before |
What to do
- Search the contracts for block.number and classify each use: a time measure, an L2 block counter, or a value you compare with off-chain data.
- Time measures — timelocks, vesting, auction windows, cooldowns: rewrite them with block.timestamp. Arbitrum sets the timestamp per L2 block from the sequencer's clock and it never decreases.
- L2 block counters — per-block limits, "one action per block", IDs or nonces derived from the block: call ArbSys(0x…64).arbBlockNumber() instead of block.number.
- Values compared with off-chain data — anything stored on chain and later matched against a receipt's blockNumber or eth_blockNumber: store arbBlockNumber(), because the RPC side is always the L2 number.
- Confirmation depth: do not count confirmations in the contract with block.number. Count them off chain by comparing the receipt's blockNumber with eth_blockNumber; both are L2 numbers.
- Helper contracts: a multicall's getBlockNumber() returns block.number and therefore the L1 estimate; Arbitrum's own reference warns that such helpers "may exhibit unintended behavior" out of the box.
- Before shipping, check the three values on the live chain with the calls below rather than in a local simulator.
L2 block number inside a contract
interface ArbSys {
function arbBlockNumber() external view returns (uint256);
}
uint256 l2Block = ArbSys(address(100)).arbBlockNumber(); // 0x…64
uint256 l1Estimate = block.number; // Ethereum block estimate, not the L2 blockThe three values over JSON-RPC (one batch)
curl -s "$RH_RPC_URL" -H "Content-Type: application/json" -d '[
{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]},
{"jsonrpc":"2.0","id":2,"method":"eth_call","params":[{"to":"0x0000000000000000000000000000000000000064","data":"0xa3b1b31d"},"latest"]},
{"jsonrpc":"2.0","id":3,"method":"eth_getBlockByNumber","params":["latest",false]}
]'
# id 1: L2 head e.g. "0x53a11ae"
# id 2: arbBlockNumber() e.g. "0x…053a11ae" (same L2 number)
# id 3: result.l1BlockNumber e.g. "0x18f8d72" -> what block.number returnsWhen this is not our problem
The meaning of block.number, the ArbSys precompile and the l1BlockNumber field are defined by Robinhood Chain's Arbitrum Nitro stack; every RPC provider, Triport included, returns the same values for the same block. To run the checks above against Robinhood Chain mainnet on Triport, set RH_RPC_URL to the keyed URL form https://triport.io/r/<YOUR_KEY>/robinhood.
FAQ
- Why does block.number on Robinhood Chain not match the explorer?
- Because block.number in a contract returns an estimate of the Ethereum L1 block number. Explorers and eth_blockNumber show the Robinhood Chain L2 block number, which is a different and much larger counter.
- How do I get the Robinhood Chain block number inside a contract?
- Call ArbSys at 0x0000000000000000000000000000000000000064: arbBlockNumber() returns the L2 block number, the same value eth_blockNumber reports for that block.
- Is eth_blockNumber the L1 or the L2 block number?
- The L2 block number. The L1 estimate is available separately in the l1BlockNumber field returned by eth_getBlockByNumber.
- Can I use block.number for a timelock on Robinhood Chain?
- Not reliably: it moves in irregular steps that follow Ethereum, not the L2. Use block.timestamp for time-based locks.