Skip to main content


Nitro makes minimal modifications to geth in hopes of not violating its assumptions. This document will explore the relationship between geth and ArbOS, which consists of a series of hooks, interface implementations, and strategic re-appropriations of geth's basic types.

We store ArbOS's state at an address inside a geth statedb. In doing so, ArbOS inherits the statedb's statefulness and lifetime properties. For example, a transaction's direct state changes to ArbOS are discarded upon a revert.

The fictional account representing ArbOS


Arbitrum uses various hooks to modify geth's behavior when processing transactions. Each provides an opportunity for ArbOS to update its state and make decisions about the tx during its lifetime. Transactions are applied using geth's ApplyTransaction function.

Below is ApplyTransaction's callgraph, with additional info on where the various Arbitrum-specific hooks are inserted. Click on any to go to their section. By default, these hooks do nothing so as to leave geth's default behavior unchanged, but for chains configured with EnableArbOS set to true, ReadyEVMForL2 installs the alternative L2 hooks.

What follows is an overview of each hook, in chronological order.


A call to ReadyEVMForL2 installs the other transaction-specific hooks into each geth EVM right before it performs a state transition. Without this call, the state transition will instead use the default DefaultTxProcessor and get exactly the same results as vanilla geth. A TxProcessor object is what carries these hooks and the associated arbitrum-specific state during the transaction's lifetime.


The StartTxHook is called by geth before a transaction starts executing. This allows ArbOS to handle two arbitrum-specific transaction types.

If the transaction is ArbitrumDepositTx, ArbOS adds balance to the destination account. This is safe because the L1 bridge submits such a transaction only after collecting the same amount of funds on L1.

If the transaction is an ArbitrumSubmitRetryableTx, ArbOS creates a retryable based on the transaction's fields. If the transaction includes sufficient gas, ArbOS schedules a retry of the new retryable.

The hook returns true for both of these transaction types, signifying that the state transition is complete.


This fallible hook ensures the user has enough funds to pay their poster's L1 calldata costs. If not, the tx is reverted and the EVM does not start. In the common case that the user can pay, the amount paid for calldata is set aside for later reimbursement of the poster. All other fees go to the network account, as they represent the tx's burden on validators and nodes more generally.

If the user attempts to purchase compute gas in excess of ArbOS's per-block gas limit, the difference is set aside and refunded later via ForceRefundGas so that only the gas limit is used. Note that the limit observed may not be the same as that seen at the start of the block if ArbOS's larger gas pool falls below the MaxPerBlockGasLimit while processing the block's previous txes.


These hooks track the callers within the EVM callstack, pushing and popping as calls are made and complete. This provides ArbSys with info about the callstack, which it uses to implement the methods WasMyCallersAddressAliased and MyCallersAddressWithoutAliasing.


In arbitrum, the BlockHash and Number operations return data that relies on underlying L1 blocks intead of L2 blocks, to accomendate the normal use-case of these opcodes, which often assume ethereum-like time passes between different blocks. The L1BlockHash and L1BlockNumber hooks have the required data for these operations.


This hook allows ArbOS to add additional refunds to the user's tx. This is currently only used to refund any compute gas purchased in excess of ArbOS's per-block gas limit during the GasChargingHook.


Because poster costs come at the expense of L1 aggregators and not the network more broadly, the amounts paid for L1 calldata should not be refunded. This hook provides geth access to the equivalent amount of L2 gas the poster's cost equals, ensuring this amount is not reimbursed for network-incentivized behaviors like freeing storage slots.


The EndTxHook is called after the EVM has returned a transaction's result, allowing one last opportunity for ArbOS to intervene before the state transition is finalized. Final gas amounts are known at this point, enabling ArbOS to credit the network and poster each's share of the user's gas expenditures as well as adjust the pools. The hook returns from the TxProcessor a final time, in effect discarding its state as the system moves on to the next transaction where the hook's contents will be set afresh.

Interfaces and components


APIBackend implements the ethapi.Bakend interface, which allows simple integration of the arbitrum chain to existing geth API. Most calls are answered using the Backend member.


This struct was created as an arbitrum equivalent to the Ethereum struct. It is mostly glue logic, including a pointer to the ArbInterface interface.


This interface is the main interaction-point between geth-standard APIs and the arbitrum chain. Geth APIs mostly either check status by working on the Blockchain struct retrieved from the Blockchain call, or send transactions to arbitrum using the PublishTransactions call.


RecordingKV is a read-only key-value store, which retrieves values from an internal trie database. All values accessed by a RecordingKV are also recorded internally. This is used to record all preimages accessed during block creation, which will be needed to proove execution of this particular block. A RecordingChainContext should also be used, to record which block headers the block execution reads (another option would be to always assume the last 256 block headers were accessed). The process is simplified using two functions: PrepareRecording creates a stateDB and chaincontext objects, running block creation process using these objects records the required preimages, and PreimagesFromRecording function extracts the preimages recorded.

Transaction Types

Nitro geth includes a few L2-specific transaction types. Click on any to jump to their section.

Tx TypeRepresentsLast Hook Reached  Source
ArbitrumUnsignedTxAn L1 to L2 messageEndTxHookBridge
ArbitrumContractTxA nonce-less L1 to L2 message  EndTxHookBridge
ArbitrumDepositTxA user depositStartTxHookBridge
ArbitrumSubmitRetryableTx  Creating a retryableStartTxHook  Bridge
ArbitrumRetryTxA retryable redeem attemptEndTxHookL2
ArbitrumInternalTxArbOS state updateStartTxHookArbOS

The following reference documents each type.


Provides a mechanism for a user on L1 to message a contract on L2. This uses the bridge for authentication rather than requiring the user's signature. Note, the user's acting address will be remapped on L2 to distinguish them from a normal L2 caller.


These are like an ArbitrumUnsignedTx but are intended for smart contracts. These use the bridge's unique, sequential nonce rather than requiring the caller specify their own. An L1 contract may still use an ArbitrumUnsignedTx, but doing so may necessitate tracking the nonce in L1 state.


Represents a user deposit from L1 to L2. This increases the user's balance by the amount deposited on L1.


Represents a retryable submission and may schedule an ArbitrumRetryTx if provided enough gas. Please see the retryables documentation for more info.


These are scheduled by calls to the redeem precompile method and via retryable auto-redemption. Please see the retryables documentation for more info.


Because tracing support requires ArbOS's state-changes happen inside a transaction, ArbOS may create a tx of this type to update its state in-between user-generated transactions. Such a tx has a Type field signifying the state it will update, though currently this is just future-proofing as there's only one value it may have. Below are the internal tx types.


Updates the L1 block number. This tx is generated whenever a message originates from an L1 block newer than any ArbOS has seen thus far. They are guaranteed to be the first in their L2 block.

Transaction Run Modes and Underlying Transactions

A geth message may be processed for various purposes. For example, a message may be used to estimate the gas of a contract call, whereas another may perform the corresponding state transition. Nitro geth denotes the intent behind a message by means of its TxRunMode, which it sets before processing it. ArbOS uses this info to make decisions about the tx the message ultimately constructs.

A message derived from a transaction will carry that transaction in a field accessible via its UnderlyingTransaction method. While this is related to the way a given message is used, they are not one-to-one. The table below shows the various run modes and whether each could have an underlying transaction.

Run ModeScopeCarries an Underlying Tx?
MessageCommitModestate transition  always
MessageGasEstimationMode  gas estimationwhen created via NodeInterface.sol or when scheduled

Arbitrum Chain Parameters

Nitro's geth may be configured with the following l2-specific chain parameters. These allow the rollup creator to customize their rollup at genesis.


Introduces ArbOS, converting what would otherwise be a vanilla L1 chain into an L2 Arbitrum rollup.


Allows access to debug precompiles. Not enabled for Arbitrum One. When false, calls to debug precompiles will always revert.


Currently does nothing besides indicate that the rollup will access a data availability service for preimage resolution in the future. This is not enabled for Arbitrum One, which is a strict state-function of its L1 inbox messages.

Miscellaneous Geth Changes

ABI Gas Margin

Vanilla Geth's abi library submits txes with the exact estimate the node returns, employing no padding. This means a tx may revert should another arriving just before even slightly change the tx's codepath. To account for this, we've added a GasMargin field to bind.TransactOpts that pads estimates by the number of basis points set.

Conservation of L2 ETH

The total amount of L2 ether in the system should not change except in controlled cases, such as when bridging. As a safety precaution, ArbOS checks geth's balance delta each time a block is created, alerting or panicking should conservation be violated.

MixDigest and ExtraData

To aid with outbox proof construction, the root hash and leaf count of ArbOS's send merkle accumulator are stored in the MixDigest and ExtraData fields of each L2 block. The yellow paper specifies that the ExtraData field may be no larger than 32 bytes, so we use the first 8 bytes of the MixDigest, which has no meaning in a system without miners/stakers, to store the send count.

Retryable Support

Retryables are mostly implemented in ArbOS. Some modifications were required in geth to support them.

  • Added ScheduledTxes field to ExecutionResult. This lists transactions scheduled during the execution. To enable using this field, we also pass the ExecutionResult to callers of ApplyTransaction.
  • Added gasEstimation param to DoCall. When enabled, DoCall will also also executing any retryable activated by the original call. This allows estimating gas to enable retryables.

Added accessors

Added UnderlyingTransaction to Message interface Added GetCurrentTxLogs to StateDB We created the AdvancedPrecompile interface, which executes and charges gas with the same function call. This is used by Arbitrum's precompiles, and also wraps geth's standard precompiles. For more information on Arbitrum precompiles, see ArbOS doc.

WASM build support

The WASM arbitrum executable does not support file oprations. We created fileutil.go to wrap fileutil calls, stubbing them out when building WASM. fake_leveldb.go is a similar WASM-mock for leveldb. These are not required for the WASM block-replayer.


Arbitrum introduces a new signer, and multiple new transaction types.


Geth natively only allows reorgs to a fork of the currently-known network. In nitro, reorgs can sometimes be detected before computing the forked block. We added the ReorgToOldBlock function to support reorging to a block that's an ancestor of current head.

Genesis block creation

Genesis block in nitro is not necessarily block #0. Nitro supports importing blocks that take place before genesis. We split out WriteHeadBlock from gensis.Commit and use it to commit non-zero genesis blocks.