Skip to main content

A gentle introduction

ArbOS is the child EVM hypervisor for the Arbitrum chain, providing the execution environment for the chain. Acting as a trusted "system glue" component within the STF, ArbOS is responsible for:

1. Managing network resources

It allocates and tracks the resources needed to execute transactions on the child chain.

2. Block production

ArbOS processes incoming sequencer data batches to produce child chain blocks, ensuring the state is updated correctly.

3. Cross-chain messaging

It facilitates communication between the parent and child chains, supporting functionalities like Ether and token deposits and withdrawals.

4. Enhanced EVM execution

ArbOS runs its instrumented Geth instance to execute smart contracts, incorporating additional logic specific to the child chain environment.

5. Stylus-specific tasks in ArbOS

ArbOS manages host I/O calls, memory operations, and execution context for Stylus transactions, ensuring efficient and deterministic processing with the WASM runtime.

By offloading high-cost tasks from the parent chain, ArbOS enables them to be executed quickly and cost-effectively on the child chain. This design reduces computational and storage costs and offers significant flexibility, allowing the child chain code to evolve or be customized more easily than in a parent-chain-enforced architecture.

While Ethereum's STF provides a secure, deterministic basis for state updates, Arbitrum's Nitro stack builds on this foundation with key modifications — ranging from dual gas accounting to cross-chain messaging — to optimize performance and flexibility. These innovations are realized through minimal yet strategic modifications to Geth, integrated seamlessly with ArbOS, forming the "geth sandwich."

With the introduction of Stylus, Arbitrum extends its execution model beyond the EVM, enabling high-performance WASM-based smart contracts. This integration introduces additional modifications to Geth, ensuring compatibility with Stylus transactions while preserving Ethereum-like execution guarantees. These changes include handling Stylus-specific transaction types and ensuring smooth interaction between the EVM and WASM environments.

In the following section, we'll dive deep into these modifications, exploring how Nitro leverages 'Geth at the core' and the custom enhancements provided by ArbOS to deliver an advanced, high-performance STF. Stylus-specific tasks within ArbOS are covered separately to highlight its role in managing execution, host I/O, and memory operations.

How precompiles work

A precompile consists of a Solidity interface in contracts/src/precompiles/ and a corresponding Golang implementation in precompiles/. Using Geth's ABI generator, solgen/gen.go generates solgen/go/precompilesgen/precompilesgen.go, which collects the ABI data of the precompiles. The runtime installer uses this generated file to check the type safety of each precompile's implementer.

The installer uses runtime reflection to ensure each implementer has all the right methods and signatures. This reflection includes restricting access to stateful objects, such as the EVM and statedb, based on their declared purity. Additionally, the installer verifies and populates event function pointers, enabling each precompile to emit logs and determine its gas cost. Additional configurations, such as restricting a precompile's methods to be callable only by the chain owner, are possible by adding precompile wrappers like ownerOnly and debugOnly to their installation entry.

Completion of calling, dispatching, and recording of precompile methods occurs via runtime reflection, which avoids any human error that manually parsing and writing bytes could introduce, and uses Geth's stable APIs for packing and unpacking values.

Each time a transaction calls a method of a child chain-specific precompile, a call context gets created to track and record the gas burnt. For convenience, it also provides access to the public fields of the underlying TxProcessor. Because sub-transactions could revert without updates to this struct, the TxProcessor only makes public what is safe, such as the amount of parent chain calldata paid by the top-level transaction.

For a complete list of precompiles, refer to the precompile references.

Messages

An L1IncomingMessage represents an incoming sequencer message. A message includes one or more user transactions depending on load and is made into a unique child chain block. The child chain block may include additional system transactions while processing the message's user transactions. However, ultimately, the relationship is still bijective: for every L1IncomingMessage, there is a child chain block with a unique child chain block hash, and for every child chain block after chain initialization, there was an L1IncomingMessage that made it. A sequencer batch may contain more than one L1IncomingMessage.

Retryables

A retryable is a special message type for creating an atomic parent-to-child chain; for details, see parent-to-child chain messaging.

ArbOS state

ArbOS's state is viewed and modified via ArbosState objects, which provide convenient abstractions for working with the underlying data of its backingStorage. The backing storage's keyed subspace strategy makes ArbosState's convenient getters and setters possible, minimizing the need to work directly with the specific keys and values of the underlying storage's stateDB.

Because two ArbosState objects with the same backingStorage contain and mutate the same underlying state, different ArbosState objects can provide different views of ArbOS's contents. Burner objects, which track gas usage while working with the ArbosState, provide the internal mechanism. Some are read-only, causing transactions to revert with vm.ErrWriteProtection when a mutating request is issued. Others demand that the caller have elevated privileges. Meanwhile, others dynamically charge users when doing stateful work. This view is chosen for safety when OpenArbosState() creates the object and may never change.

arbosVersion, updgradeVersion and upgradeTimestamp ArbOS upgrades are scheduled to happen when finalizing the first block after the upgradeTimestamp.

Most of ArbOS's state exists to facilitate its precompiles. The parts that aren't are detailed below.

blockhashes

This component maintains the last 256 parent chain block hashes in a circular buffer. This component allows the TxProcessor to implement the BLOCKHASH and NUMBER opcodes and supports the precompile methods that involve the Outbox. To avoid changing the ArbOS state outside of a transaction, blocks made from messages with a new parent chain–block number update this info during an InternalTxUpdateL1BlockNumber ArbitrumInternalTx included as the first transaction of the block.

l1PricingState

In addition to supporting the ArbAggregator precompile, the parent chain pricing state provides tools for determining the parent chain component of a transaction's gas costs. This part of the state tracks the total amount of funds collected from transactions in parent chain gas fees and the funds spent by batch posters to post data breaches on the parent chain.

Based on this information, ArbOS maintains a parent chain data fee, which is also tracked in this state and determines how much the parent chain fee will cost. ArbOS dynamically adjusts this value so that fees collected are approximately equal to batch posting costs. For more details about darent chain pricing, see Parent chain pricing.

l2PricingState

The child chain pricing state tracks child chain resource usage to determine a reasonable child chain gas price. This process considers various factors, including user demand, the state of Geth, and the computational speed limit. The primary mechanism for doing so consists of a pair of pools, one larger than the other, that drain as child chain–specific resources are consumed and filled as time passes. Parent chain-specific resources, such as parent chain calldata, are not accounted for in the pools since they do not directly impact the computational workload of network actors. Instead, the design of the speed limit mechanism regulates execution resources to ensure consistent system performance and synchronization.

While much of this state is accessible through the ArbGasInfo and ArbOwner precompiles, most changes are automatic and happen during block production and the transaction hooks. Each transaction in an incoming message removes the parent chain component of the gas it consumes from the pool. Afterward, the message's timestamp informs the pricing mechanism of the time passed as ArbOS finalizes the block.

ArbOS's larger gas pool determines the per-block gas limit, setting a dynamic upper limit on the amount of compute gas a child chain block may have. This limit is always enforced, though it's done in the GasChargingHook for the first transaction to avoid sharp decreases in the parent chain gas price from over-inflating the compute component purchased to above the gas limit. Enforcing this improves UX by allowing the first transaction to succeed rather than requiring a resubmission. Because the first transaction reduces the space left in the block, subsequent transactions do not use this strategy and may fail due to compute-component inflation. This space is acceptable because such transactions occur only when the system is under heavy load. The result is that the user's transaction is dropped without charges since the state transition fails early. Those trusting the Sequencer can rely on the automatic resubmission of a transaction in such a scenario.

We need a per-block gas limit because arbitrator WAVM execution is much slower than native transaction execution. This limit means there can only be so much gas, roughly translating to wall-block time, in a child chain block. It also allows ArbOS to limit the size of blocks should demand continue to surge even as prices rise.

ArbOS's per-block gas limit is distinct from Geth's block limit, which ArbOS sets sufficiently high never to run out. This approach is safe since Geth's block limit exists to constrain the work done per block, which ArbOS already does via its own per-block gas limit. Though it'll never run out, a block's transactions use the same Geth gas pool to maintain the invariant that the pool decreases monotonically after each transaction. Block headers use the Geth block limit for internal consistency and to ensure gas estimation works. They are distinct from the gasLeft variable, which ephemerally exists outside of the global state to keep child chain blocks from exceeding ArbOS's per-block gas limit and to deduct space where the state transition failed or used negligible amounts of compute gas. ArbOS does not need to persist gasLeft because its pool induces a revert, and transactions use the Geth block limit during EVM execution.

Child chain gas pricing

The child chain gas price on a given Arbitrum chain has a set floor, which is queriable via ArbGasInfo's getMinimumGasPrice method (currently 0.01 gwei on Arbitrum One and 0.01 gwei on Nova).

Estimating child chain gas

Calling an Arbitrum Node's eth_estimateGas RPC gives a value sufficient to cover the full transaction fee at the given child chain gas price, i.e., the value returned from eth_estimateGas multiplied by the child chain gas price tells you how much total Ether is required for the transaction to succeed. Note that this means the value returned by eth_estimateGas for a given operation will change over time (as the parent chain's calldata price fluctuates). See 2-D fees and How to estimate gas in Arbitrum for more information.

Child chain gas fees

Child chain gas fees work very similarly to gas on Ethereum. The amount of gas is multiplied by the current basefee to get the child chain gas fee charged to the transaction.

The child chain basefee is set by a version of the "exponential mechanism" widely discussed in the Ethereum community and shown to be equivalent to Ethereum's EIP-1559 gas pricing mechanism.

The algorithm compares gas usage against the speed limit parameter —the target amount of gas per second that the chain can sustainably handle over time. (The speed limit on Arbitrum One is 7,000,000 gas per second.) The algorithm tracks a gas backlog. Whenever a transaction consumes gas, it gets added to the backlog. Whenever the clock ticks a second, the speed limit is subtracted from the backlog, but the backlog can never go below zero.

Intuitively, if the backlog grows, the algorithm should increase the gas price to slow gas usage because usage is above the sustainable level. If the backlog shrinks, the price should decrease again because usage has been below the sustainable limit, so more gas usage can be welcomed.

More precisely, the basefee is an exponential function of the backlog, F = exp(-a(B-b)), where a and b are suitably chosen constants: a controls how rapidly the price escalates with the backlog, and b allows a small backlog before the basefee escalation begins.

Child chain tips

The Sequencer prioritizes transactions on a first-come, first-served basis. Because tips do not make sense in this model, they are ignored. Arbitrum users always pay the basefee regardless of the tip they choose.

Gas estimating retryables

When a transaction schedules another, the subsequent transaction's execution will be included when estimating gas via the node's RPC. Finding a gas estimate for a transaction is only possible if all the transactions succeed at a given gas limit. This estimation is especially important when working with retryables and scheduling redeem attempts.

Because a call to redeem consumes all the call's gas, doing multiple calls requires limiting the gas provided to each sub-call. Otherwise, the first will take all of the gas and force the second to fail, irrespective of the estimation's gas limit.

Gas estimation for retryable submissions is possible via the NodeInterface and similarly requires the auto-redeem attempt to succeed.

The speed limit

The security of Nitro chains depends on the assumption that when one validator creates an assertion, other validators will check it and respond with a correct assertion and a challenge if it is wrong. This assumption requires that the other validators have the time and resources to check each assertion and issue a timely challenge quickly. The Arbitrum protocol accounts for this when setting deadlines for assertions.

This approach sets an effective speed limit on execution of a Nitro chain: in the long run, the chain cannot make progress faster than a validator can emulate its execution. If assertions are published faster than the speed limit, their deadlines will get farther and farther into the future. Due to the Rollup protocol's limit on how far a deadline can be in the future, this will eventually slow new assertions, thereby enforcing the effective speed limit.

Being able to set the speed limit accurately depends on estimating the time required to validate an assertion with some accuracy. Any uncertainty in estimating validation time will force us to lower the speed limit to be safe. We do not want to lower the speed limit, so we try to enable accurate estimation.

Stylus-specific differences

This section details how Stylus integrates into the State Transition Function (STF), covering execution flow, messaging handling, caching, and interactions with ArbOS and Geth.

1. Execution flow of a Stylus transaction

When a Transaction interacts with a Stylus contract, its execution follows a distinct path compared to EVM transactions:

  • Transaction submission and routing
    • The transaction is included in a child chain block by the Sequencer.
    • Geth processes the transaction and determines its target contract.
    • If the target is a Stylus contract, ArbOS routes execution to the WASM runtime instead of the EVM.
  • Stylus execution within ArbOS
    • ArbOS retrieves the Stylus program from its cache (stylus/src/cache.rs) or loads it from storage if not cached.
    • The WebAssembly System Interface (Go-WASI) initializes a secure execution environment.
    • The WASM module executes within ArbOS, processing instructions efficiently and calling host I/O functions.
  • Host I/O operations for blockchain state access
    • Stylus contracts do not use EVM opcodes. Instead, they interact with the blockchain through host I/O calls handled by ArbOS.
    • These include storage access (TLOAD TSTORE), arithmetic operations (MULMOD, ADDMOD), and context retrieval (GETCALLER, GETCALLVALUE).
    • ArbOS ensures these operations are efficient and compatible with Ethereum's state model.
    • State commitment and finalization
      • Once execution is complete, ArbOS finalizes storage changes and updates logs and receipts.
      • Geth processes the final transaction result and commits it to the state tree.

This process bypasses the EVM interpreter entirely, allowing Stylus contracts to execute significantly faster than their Solidity counterparts.

2. Stylus caching and gas pricing

  • Stylus gas pricing model Unlike standard EVM gas pricing, Stylus pricing follows a multi-dimensional cost model, incorporating:
    • Ink cost (memory and execution cost)
      • Measure in Ink units (Stylus's equivalent of computational gas).
      • Ink pricing varies based on execution complexity, memory usage, and computation steps.
      • Complex WASM operations consume more Ink, directly impacting execution costs.
    • Opcode pricing
      • WASM instructions are assigned individual execution costs similar to EVM opcodes.
      • Heavy computation opcodes are priced higher.
      • Cheap opcodes (e.g., simple arithmetic, bitwise operations) have minimal costs.
    • Host I/O pricing
      • Stylus introduces fine-grained pricing for different I/O calls:
        • Storage read/writes: Priced based on access pattern and data size.
        • Precompile calls: Stylus-specific precompiles have fixed execution costs.
        • External calls to EVM contracts: Encapsulated within ArbOS transaction handling, with additional gas considerations.

Stylus caching

Stylus contracts leverage an advanced caching system to minimize execution overhead within ArbOS:

  • LRU (Least Recently Used) caching: Keeps the most recently accessed Stylus contracts in memory for fast execution.
  • Persistent long-term caching: Caching for selected contracts may occur across blocks based on an economic auction model.
  • Init costs and execution pricing: Instead of a flat gas cost, Stylus contracts have dynamic execution costs based on WASM complexity. ArbOS maintains pricing parameters (initCost, cachedCost) that are adjusted based on future WASM execution optimizations.

3. Interaction with ArbOS and Geth

Execution StageHandled By
Transaction submissionGeth (identifies target contract)
Stylus executionArbOS (switches to WASM runtime)
Host I/O callsArbOS (handles storage, call data, context retrieval)
State commitmentGeth and ArbOS (finalizes updates, commits to state)

4. Go-WASI and co-threads in Stylus execution

ArbOS executes Stylus contracts using Go-WASI, a WASM-compatible runtime with custom optimizations for Arbitrum. Key features include:

  • Memory management: WASM modules execute in a sandboxed environment with strict memory allocation policies.
  • Co-threads for efficient execution: Instead of traditional synchronous execution, Stylus employs co-threading, enabling lightweight task switching and parallelism where possible.
  • Deterministic execution: Ensures that Stylus contracts remain fully deterministic and compatible with Ethereum's consensus model.

These optimizations make Stylus an extremely efficient execution environment, capable of outperforming the EVM while maintaining security and compatibility with Ethereum's state model.