# Arbitrum Documentation > Official documentation for the Arbitrum ecosystem: building apps, bridging tokens, running nodes, launching Arbitrum chains, and developing with Stylus. - [Arbitrum Documentation](/index.md) ## arbitrum-bridge - [Arbitrum bridge transaction traceability](/arbitrum-bridge/bridge-transaction-traceability.md): Learn how to trace transactions happening through the Arbitrum bridge - [Arbitrum embedded bridge widget](/arbitrum-bridge/embedded-bridge-widget.md): Learn how to integrate and configure the Arbitrum bridge widget in your dApp - [Quickstart: Arbitrum bridge](/arbitrum-bridge/quickstart.md): Learn how to use Arbitrum's bridge to transfer ETH or ERC-20 tokens between a parent chain and a child chain - [Troubleshooting: Arbitrum bridge](/arbitrum-bridge/troubleshooting.md): List of questions and answers frequently asked by users - [USDC on Arbitrum One](/arbitrum-bridge/usdc-arbitrum-one.md): Learn about the two different types of USDC supported by Arbitrum One: Arbitrum-Native USDC and Bridged (from Ethereum) USDC - [Monitor withdrawals programmatically](/arbitrum-bridge/withdrawal-monitoring.md): Track child-to-parent withdrawals on Arbitrum chains through their full lifecycle: statuses, expected timelines per chain type, SDK and onchain queries, and how to diagnose stuck withdrawals ## arbitrum-essentials - [Arbitrum essentials](/arbitrum-essentials.md): Core, language-agnostic guidance for building on Arbitrum — bridging, oracles, precompiles, the NodeInterface, RPC and provider references, networks, gas, and how Arbitrum differs from Ethereum. Applies whether you write contracts in Solidity or Stylus. - [Block gas limit, numbers and time](/arbitrum-essentials/arbitrum-vs-ethereum/block-numbers-and-time.md): Understand how Arbitrum handles block gas limits, block numbers, and transaction timing differently from Ethereum. Learn about parent chain gas components and block.number behavior on Arbitrum. - [Differences between Arbitrum and Ethereum: Overview](/arbitrum-essentials/arbitrum-vs-ethereum/comparison-overview.md): Explore the key differences between Arbitrum and Ethereum, including state transition functions, EVM compatibility, gas handling, and block production. Understand what changes when deploying Ethereum dApps to Arbitrum. - [Nonce management](/arbitrum-essentials/arbitrum-vs-ethereum/nonce-management.md): Understand how Arbitrum handles transaction nonces differently from Ethereum. Arbitrum's sequencer keeps only a short retry window for out-of-order nonces instead of a long-lived pending pool, so concurrent submitters must coordinate nonce allocation. - [RPC methods](/arbitrum-essentials/arbitrum-vs-ethereum/rpc-methods.md): Compare Arbitrum and Ethereum RPC method responses, including additional transaction types, fields, and blocks returned by Arbitrum nodes. Covers eth_getTransactionByHash, eth_getBlockByHash, and other JSON-RPC methods. - [Solidity support](/arbitrum-essentials/arbitrum-vs-ethereum/solidity-support.md): Learn how Solidity behaves differently on Arbitrum compared to Ethereum, including blockhash, block.coinbase, block.number, msg.sender, and other EVM opcode differences for smart contract developers. - [Configure custom gateway bridging](/arbitrum-essentials/bridging/configure-token-gateway/custom.md): Guide to creating and deploying a custom gateway for specialized token bridging between Ethereum and Arbitrum. For advanced use cases not covered by the standard or generic-custom gateways. - [Configure generic-custom gateway bridging](/arbitrum-essentials/bridging/configure-token-gateway/generic-custom.md): Guide to configuring Arbitrum's generic-custom gateway for token bridging with custom child chain token functionality. Enables custom ERC-20 behavior while using Arbitrum's built-in bridge infrastructure. - [Configure standard gateway bridging](/arbitrum-essentials/bridging/configure-token-gateway/standard.md): Guide to configuring ERC-20 token bridging using Arbitrum's standard gateway, which automatically creates a standard token representation on the child chain with no custom configuration required. - [Cross-chain messaging](/arbitrum-essentials/bridging/cross-chain-messaging.md): Learn how to send cross-chain messages between Ethereum and Arbitrum using retryable tickets, the ArbSys precompile, and the Outbox contract. Includes a complete Greeter tutorial demonstrating both parent-to-child and child-to-parent messaging. - [SDK support for custom gas token Arbitrum chains](/arbitrum-essentials/bridging/custom-gas-token-chains.md): Guide to using the Arbitrum SDK for custom gas token chains, including APIs for ERC-20 deposits, withdrawals, and bridging on Arbitrum chains configured with non-ETH gas tokens. - [How to bridge from parent chain to child chain](/arbitrum-essentials/bridging/deposit/eth-and-messages.md): Step-by-step guide to programmatically bridge ETH and send messages from Ethereum to an Arbitrum using retryable tickets and the Inbox contract. - [Deposit tokens to Arbitrum](/arbitrum-essentials/bridging/deposit/tokens.md): Step-by-step guide to depositing ERC-20 tokens from Ethereum (parent chain) to an Arbitrum child chain using the token bridge gateway system and Arbitrum SDK. - [Bridge tokens from L1 to an Arbitrum L3](/arbitrum-essentials/bridging/l1-l3-teleportation.md): Learn how to bridge ERC-20 tokens and ETH from Ethereum (L1) directly to an Arbitrum L3 chain using the Arbitrum SDK teleport feature, requiring only a single user transaction. - [Bridging overview](/arbitrum-essentials/bridging/overview.md): Choose the right approach for bridging assets and messages between Ethereum and Arbitrum. - [How to bridge from child chain to parent chain](/arbitrum-essentials/bridging/withdraw/eth-and-messages.md): Step-by-step guide to programmatically withdraw ETH and send messages from an Arbitrum child chain to Ethereum using ArbSys and the Outbox contract. - [Withdraw tokens to parent chain](/arbitrum-essentials/bridging/withdraw/tokens.md): Step-by-step guide to withdrawing ERC-20 tokens from an Arbitrum child chain back to Ethereum (parent chain) using the token bridge gateway system, including the 6.4-day challenge period. - [How to estimate gas in Arbitrum](/arbitrum-essentials/how-to-estimate-gas.md): Learn how to estimate gas costs on Arbitrum using eth_estimateGas, NodeInterface.gasEstimateComponents(), and the Arbitrum SDK. Covers the two-component fee model with L1 data costs and L2 execution costs. - [How to verify child chain state on the parent chain](/arbitrum-essentials/how-to-get-l2block-on-l1.md): Learn how to verify child chain state on its parent chain - [NodeInterface overview](/arbitrum-essentials/nodeinterface/overview.md): Overview of Arbitrum's NodeInterface, a virtual contract at address 0xc8 accessible only via RPCs. Provides gas estimation, proof construction, and other node-level utilities not available onchain. - [NodeInterface reference](/arbitrum-essentials/nodeinterface/reference.md): Complete API reference for Arbitrum's NodeInterface contract methods, including gas estimation, retryable ticket helpers, and outbox proof construction. Available at address 0xc8 via RPC calls only. - [Oracles](/arbitrum-essentials/oracles/overview-oracles.md): Overview of blockchain oracles on Arbitrum — external data providers that supply offchain data to smart contracts. Learn how oracles work, their trust models, and available providers on Arbitrum. - [Precompiles overview](/arbitrum-essentials/precompiles/overview.md): Overview of Arbitrum precompiled contracts — predefined smart contracts executed natively by the Arbitrum client for optimized performance. Covers both Ethereum-standard and Arbitrum-specific precompiles. - [Precompiles reference](/arbitrum-essentials/precompiles/reference.md): Complete reference for all Arbitrum precompiled contracts, including ArbSys, ArbRetryableTx, ArbGasInfo, ArbAggregator, and more. Lists methods, addresses, Solidity interfaces, and Go implementations. - [Arbitrum chains overview](/arbitrum-essentials/public-chains.md): Overview of Arbitrum's public chains including Arbitrum One (Rollup), Arbitrum Nova (AnyTrust), and available testnets. Compare chain features, technology stacks, and use cases for each network. - [Chain parameters](/arbitrum-essentials/reference/chain-params.md): Reference of key system parameters for Arbitrum One and Arbitrum Nova, including chain IDs, block times, gas settings, and confirmation thresholds for public Arbitrum chains. - [Smart contract addresses](/arbitrum-essentials/reference/contract-addresses.md): Complete list of deployed Arbitrum smart contract addresses across Arbitrum One, Arbitrum Nova, and testnets. Includes core protocol contracts, token bridge contracts, and governance contracts. - [Debugging tools](/arbitrum-essentials/reference/debugging-tools.md): Overview of debugging tools for Arbitrum dApp development, including Tenderly and other platforms for transaction simulation, tracing, and smart contract debugging on Arbitrum chains. - [Development frameworks](/arbitrum-essentials/reference/development-frameworks.md): Overview of development frameworks compatible with Arbitrum, including Hardhat, Foundry, and other tools for building, testing, and deploying smart contracts on Arbitrum chains. - [Arbitrum: Understanding the risks](/arbitrum-essentials/reference/mainnet-risks.md): Understand the risks of deploying on Arbitrum mainnet, including the state of progressive decentralization, smart contract upgrade mechanisms, and trust assumptions for Arbitrum One and Nova. - [Monitoring tools and block explorers](/arbitrum-essentials/reference/monitoring-tools-block-explorers.md): List of monitoring tools and block explorers for Arbitrum, including Arbiscan, Dune Analytics, and other platforms for tracking transactions, contracts, and network activity on Arbitrum chains. - [RPC endpoints and providers](/arbitrum-essentials/reference/node-providers.md): List of RPC endpoints and third-party node providers for Arbitrum One, Arbitrum Nova, and Arbitrum testnets. Find public and premium endpoints for connecting your dApp to Arbitrum networks. - [Solidity references](/arbitrum-essentials/reference/solidity-references.md): Discover references and resources to learn and progress as a Solidity developer. - [Web3 libraries and tools](/arbitrum-essentials/reference/web3-libraries-tools.md): Overview of Web3 libraries and tools for interacting with Arbitrum, including ethers.js, web3.js, viem, and other client libraries for building decentralized applications. ## audit-reports - [Security audit reports](/audit-reports.md): A comprehensive list of security audits conducted on Arbitrum protocol components, including Nitro, BoLD, Stylus, and related smart contracts. ## build-decentralized-apps - [Machine Payments Protocol (MPP)](/build-decentralized-apps/machine-payments-protocol.md): This quickstart shows you how to implement the Machine Payments Protocol (MPP) on Arbitrum. - [Create a token using Foundry (Quickstart)](/build-decentralized-apps/quickstart-create-a-token.md): This quickstart walks you through the process of how to setup the Foundry environment, and create a token on Arbitrum. - [Build a decentralized app with Solidity (Quickstart)](/build-decentralized-apps/quickstart-solidity-remix.md): Learn how to build and deploy your first Solidity smart contract on Arbitrum using Remix IDE. This beginner-friendly quickstart guides you from a JavaScript vending machine to a decentralized app deployed on Arbitrum One, covering smart contract basics, wallet setup, and testnet deployment. ## for-devs - [Contribute docs](/for-devs/contribute.md): Learn how to contribute to Arbitrum's open-source documentation. - [Arbitrum chain information](/for-devs/dev-tools-and-resources/chain-info.md): Chains information Arbitrum - [API3](/for-devs/oracles/api3.md): Learn how to query a price feed for your smart contract. - [Chainlink](/for-devs/oracles/chainlink.md): Learn how to integrate oracles into your Arbitrum app - [Chronicle](/for-devs/oracles/chronicle.md): Learn how to integrate oracles into your Arbitrum app - [DIA](/for-devs/oracles/DIA.md): Learn how to integrate DIA oracles into your Arbitrum app - [ORA](/for-devs/oracles/ora.md): Learn how to use ORA Onchain AI Oracle - [Oracles providers](/for-devs/oracles/oracles-content-map.md): Overview of oracle providers available on Arbitrum, including API3, Chainlink, Chronicle, and more. - [Supra, price feed oracle](/for-devs/oracles/supra/supras-price-feed.md): Learn how to use Supra price feed oracle - [Supra, VRF](/for-devs/oracles/supra/supras-vrf.md): Learn how to use Supra VRF - [Trellor](/for-devs/oracles/trellor.md): Learn how to integrate oracles into your Arbitrum app - [Circle Paymaster quickstart](/for-devs/third-party-docs/Circle/usdc-paymaster-quickstart.md): Learn how to integrate with Circle paymaster. - [USDC quick start guide](/for-devs/third-party-docs/Circle/usdc-quickstart-guide.md): Learn how to integrate USDC into your application using the Arbitrum network. - [Codex Quickstart - DeFi Data API for Arbitrum](/for-devs/third-party-docs/Codex.md): Learn how to use the Codex GraphQL API to access real-time and historical DeFi data on Arbitrum, including token prices, DEX trades, liquidity pools, and wallet analytics. - [Contribute third-party docs](/for-devs/third-party-docs/contribute.md): Discover how to contribute third-party resources to Arbitrum's documentation. Share your expertise and help expand the largest Ethereum Layer 2 developer ecosystem. - [Quickstart - Covalent Indexing and Querying API](/for-devs/third-party-docs/Covalent.md): Learn how to use the Covalent APIs to access Arbitrum One, Nova and Arbitrum chain data including balances, transactions, NFTs, DEX and core blockchain data. - [How to deploy an NFT smart contract and enable credit card and cross-chain payments with no-code](/for-devs/third-party-docs/Crossmint.md): Learn how to use Crossmint to create and deploy NFT contracts and enable credit card and cross-chain payments for your customers and users. - [Quickstart: Blazing-fast indexing and data analytics using Envio](/for-devs/third-party-docs/Envio.md): Learn how to create an API for your smart contracts in less than 3 minutes and index Arbitrum data 100x faster than RPC - [Quickstart: Indexing Arbitrum custom data via Flair](/for-devs/third-party-docs/Flair.md): Real-time and historical custom data indexing for any evm chain. - [Getting Started with Gelato VRF](/for-devs/third-party-docs/Gelato/gelato-vrf.md): Learn how to use Gelato VRF - [LayerZero](/for-devs/third-party-docs/LayerZero.md): Omnichain interoperability protocol enabling secure cross-chain communication for Arbitrum applications. Connect to 100+ blockchains, send messages, and deploy omnichain tokens. - [MetaMask Smart Accounts](/for-devs/third-party-docs/MetaMask.md): Learn about MetaMask Smart Accounts and how to use them with Arbitrum - [Moralis Quickstart - Crypto Data APIs for Arbitrum](/for-devs/third-party-docs/Moralis.md): Learn how to use Moralis APIs to get data about wallets, tokens, NFTs, prices and more on Arbitrum. - [OKX - Crypto exchange, app, and wallet](/for-devs/third-party-docs/OKX.md): Learn how to use OKX. - [How to onboard users and make a sponsored transaction](/for-devs/third-party-docs/Openfort.md): Learn how to use Openfort to integrate wallets into your project on Arbitrum - [How to make your Arbitrum dApp chain-agnostic with Universal Accounts](/for-devs/third-party-docs/Particle.md): Guide covering the process of leveraging Particle Network's Universal Accounts SDK to accept deposits from any chain and interact cross-chain. - [QuickNode Backfill Templates](/for-devs/third-party-docs/QuickNode/backfill-templates.md): Learn about QuickNode's Backfill Templates, a tool that simplifies the process of retrieving historical blockchain data. - [Reactive Network](/for-devs/third-party-docs/Reactive.md): Learn how to use the Reactive Network to create reactive dApps - [Reown (prev. known as WalletConnect](/for-devs/third-party-docs/Reown.md): Reown gives developers the tools to build user experiences that make digital ownership effortless, intuitive, and secure. - [The Graph: index and query Arbitrum data](/for-devs/third-party-docs/TheGraph.md): Learn how to use The Graph to index and query smart contract data on Arbitrum using subgraphs and GraphQL. - [Venly Tools <> Arbitrum](/for-devs/third-party-docs/Venly.md): Venly Tools stand on three pillars: Digital Wallets, Digital Assets, and Payments - [What is the Webacy Risk Data Network?](/for-devs/third-party-docs/Webacy.md): Learn about the Webacy Risk Data Network, and how you can use it in your products. - [Quickstart - Zerion API for Arbitrum](/for-devs/third-party-docs/Zerion.md): Learn how to use the Zerion API to access wallet portfolios, token balances, DeFi positions, transactions, and NFT data on Arbitrum. - [ZeroDev Smart Account Integration Guide for Arbitrum](/for-devs/third-party-docs/ZeroDev/zero-dev.md): Learn about integrating smart accounts for Arbitrum and account abstraction. - [Troubleshooting: Building Arbitrum dApps](/for-devs/troubleshooting-building.md): List of questions and answers frequently asked by developers ## get-started - [Arbitrum introduction](/get-started/arbitrum-introduction.md): Frequently asked questions about Arbitrum, a finance-native blockchain platform. ## how-arbitrum-works - [Economics of Disputes in Arbitrum BoLD](/how-arbitrum-works/bold/bold-economics-of-disputes.md): An in-depth explanation on BoLD economic mechanisms. - [BoLD: a technical deep dive](/how-arbitrum-works/bold/bold-technical-deep-dive.md): A technical deep dive into the BoLD protocol. - [Overview of BoLD](/how-arbitrum-works/bold/gentle-introduction.md): An educational introduction that provides a high-level understanding of BoLD, a new dispute protocol to enable permissionless validation for Arbitrum chains. - [How BoLD bisection works](/how-arbitrum-works/bold/how-bold-bisection-works.md): Interactive visualization of the BoLD edge challenge flow, replaying real onchain events from Arbitrum Sepolia. - [AnyTrust Protocol](/how-arbitrum-works/deep-dives/anytrust-protocol.md): Learn the fundamentals of the Arbitrum AnyTrust protocol. - [ArbOS](/how-arbitrum-works/deep-dives/arbos.md): How ArbOS works as the child chain hypervisor, managing resources, block production, cross-chain messaging, and EVM execution. - [Assertions](/how-arbitrum-works/deep-dives/assertions.md): Deep dive information on assertions and how to make an assertion. - [Gas and fees](/how-arbitrum-works/deep-dives/gas-and-fees.md): Learn the fundamentals of how to calculate fees on Arbitrum, including parent chain pricing formulas and child chain gas mechanics. - [Bridging from a parent chain to a child chain](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md): Learn the fundamentals of parent to child chain messaging on Arbitrum. - [Child to parent chain messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md): Learn the fundamentals of child to parent chain messaging on Arbitrum. - [The Sequencer and Censorship Resistance](/how-arbitrum-works/deep-dives/sequencer.md): Learn the fundamentals of the Arbitrum Sequencer. - [Sequencer architecture and transaction flow](/how-arbitrum-works/deep-dives/sequencer-transaction-flow.md): A deep dive into how the Sequencer processes transactions end-to-end: the transaction queue, ordering, block creation timing, and the sequencer feed. - [A gentle introduction](/how-arbitrum-works/deep-dives/stf-gentle-intro.md): Learn the fundamentals of Nitro, Arbitrum stack. - [Token bridging](/how-arbitrum-works/deep-dives/token-bridging.md): Describes how token bridging works on Arbitrum and the architecture of the token bridge - [Transaction lifecycle on Arbitrum](/how-arbitrum-works/deep-dives/transaction-lifecycle.md): Learn how transactions are submitted and processed on Arbitrum, including sequencer and non-sequencer pathways. - [Inside Arbitrum Nitro](/how-arbitrum-works/inside-arbitrum-nitro.md): Follow a transaction's journey through the complete Arbitrum Nitro stack, from submission to finality. - [ArbOS technical reference](/how-arbitrum-works/reference/arbos-reference.md): Technical reference for ArbOS internals: precompile architecture, state storage, child chain pricing state, and Stylus execution details. - [Finality and chain reorganizations](/how-arbitrum-works/reference/finality-and-reorgs.md): How finality works on Arbitrum chains, how and why a child chain can reorganize, how deep a reorg can go, and the confirmation levels an indexer should wait for before treating data as irreversible. - [Geth at the core: modified Geth on Arbitrum Nitro](/how-arbitrum-works/reference/geth.md): Learn the fundamentals of Nitro, Arbitrum stack. - [Inputs to the State Transition Function](/how-arbitrum-works/reference/stf-inputs.md): Reference for the message types and input channels that feed the Arbitrum State Transition Function. - [How Timeboost works](/how-arbitrum-works/timeboost/gentle-introduction.md): Learn how Timeboost works and how it can benefit your Arbitrum-based project. - [How to use Timeboost](/how-arbitrum-works/timeboost/how-to-use-timeboost.md): Learn how to use timeboost - [Frequently Asked Questions (FAQs) about Timeboost](/how-arbitrum-works/timeboost/timeboost-faq.md): Timeboost FAQ - [Troubleshoot Timeboost](/how-arbitrum-works/timeboost/troubleshoot-timeboost.md): A guide on common errors & best practices when using Timeboost ## intro - [Arbitrum glossary](/intro/glossary.md): A list of terms and definitions related to Arbitrum. ## launch-arbitrum-chain - [Additional configuration parameters: Arbitrum chains](/launch-arbitrum-chain/chain-config/additional-configuration-parameters.md): Reference list of additional CLI configuration parameters available when deploying or managing your Arbitrum chain. - [Batch Poster](/launch-arbitrum-chain/chain-config/batch-poster/config-batch-poster.md): Learn how to configure the batch poster. - [Enabling blob transactions for Arbitrum batch poster](/launch-arbitrum-chain/chain-config/batch-poster/enable-4844-blobs.md): How to configure your Arbitrum node to post EIP-4844 blob transactions to the parent chain - [Batch poster fee tuning](/launch-arbitrum-chain/chain-config/batch-poster/fee-tuning.md): Learn how to configure and tune fee-related parameters for batch posting, including blob transaction fees and gas price spike handling. - [chainConfig reference](/launch-arbitrum-chain/chain-config/chainConfig-reference.md): Reference list of chainConfig JSON parameters available when deploying or managing your Arbitrum chain. - [The AEP fee router: introduction](/launch-arbitrum-chain/chain-config/costs/aep-overview.md): Learn what is the AEP fee router. - [How to set up an AEP fee router](/launch-arbitrum-chain/chain-config/costs/aep-router-contracts.md): Learn how to setup an AEP fee router. - [Use native interop token with mint/burn as your Arbitrum chain gas token](/launch-arbitrum-chain/chain-config/costs/configure-native-mint-burn.md): Learn how use a native interop token with mint/burn (such as LayerZero OFT, xERC-20, or native USDC) as your Arbitrum chain gas token - [How to configure a custom gas token for your AnyTrust Arbitrum chain](/launch-arbitrum-chain/chain-config/costs/custom-gas-token-anytrust.md): Learn how to deploy your AnyTrust Arbitrum chain with a custom gas token - [How to configure a custom gas token for your Rollup Arbitrum chain](/launch-arbitrum-chain/chain-config/costs/custom-gas-token-rollup.md): Learn how to deploy your Rollup Arbitrum chain with a custom gas token - [Dynamic Pricing for Arbitrum chains](/launch-arbitrum-chain/chain-config/costs/dynamic-pricing.md): Guidance & best practices for using Dynamic Pricing on Arbitrum chains - [How to manage the fee parameters of your Arbitrum chain](/launch-arbitrum-chain/chain-config/costs/fee-management.md): Learn how to manage the fee parameters of your Arbitrum chain - [Configure and optimize gas](/launch-arbitrum-chain/chain-config/costs/gas-optimization.md): Learn how to configure and optimize gas for your Arbitrum chain - [Guidance for Arbitrum chains gas target](/launch-arbitrum-chain/chain-config/costs/gas-target.md): How to manage your Arbitrum chain gas target - [Reporting on Fees](/launch-arbitrum-chain/chain-config/costs/reporting-on-fees.md): Learn how to setup an AEP fee router. - [Understand network revenue routing on your Arbitrum chain](/launch-arbitrum-chain/chain-config/costs/revenue-routing.md): Learn how transaction fees flow through an Arbitrum chain: which addresses collect each fee component, when funds actually move, and how to withdraw revenue to the parent chain - [Configure data availability](/launch-arbitrum-chain/chain-config/data-availability/config-data-availability.md): Learn how to configure data availability for Rollup, AnyTrust, and alt-DA for your Arbitrum chain. - [How to configure the Data Availability Committee (DAC) in your chain](/launch-arbitrum-chain/chain-config/data-availability/configure-dac.md): This how-to will help you configure the DAC in your chain. - [DAC configuration defaults](/launch-arbitrum-chain/chain-config/data-availability/dac-configuration-defaults.md): Learn the default settings for DAC configuration for your Arbitrum chain. - [DAC/DAS Operations Guide](/launch-arbitrum-chain/chain-config/data-availability/dac-das-operations.md): Learn about DAC and DAS specific operations such as key rotation, bitMask bit-level formatting, quorum mechanics, committee member management for your Arbitrum chain. - [How to configure a Data Availability Committee: Introduction](/launch-arbitrum-chain/chain-config/data-availability/dac-get-started.md): Learn what's needed to configure a Data Availability Committee for your chain - [How to deploy a Data Availability Server (DAS) with Docker](/launch-arbitrum-chain/chain-config/data-availability/das-docker-deployment.md): Deploy a Data Availability Server (DAS) for your AnyTrust chain using Docker and Docker Compose, without Kubernetes: generate BLS keys in a container, run the server, and verify it. - [DAS RPC method reference](/launch-arbitrum-chain/chain-config/data-availability/das-rpc-method-reference.md): Discover and learn about the DAS RPC methods. - [How to deploy a Data Availability Server (DAS)](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md): This how-to will help you deploy a Data Availability Server (DAS) - [How to deploy a mirror Data Availability Server (DAS)](/launch-arbitrum-chain/chain-config/data-availability/deploy-mirror-das.md): This how-to will help you deploy a mirror Data Availability Server (DAS) - [Configure smart contract size limit](/launch-arbitrum-chain/chain-config/execution/smart-contract-size-limit.md): Learn how to configure the smart contract size limits on your Arbitrum chain - [How to configure Delayed Inbox finality](/launch-arbitrum-chain/chain-config/sequencer/chain-finality.md): Learn how to configure Delayed Inbox finality on your Arbitrum chain. - [Compliance filtering](/launch-arbitrum-chain/chain-config/sequencer/compliance-filtering.md): How chain owners configure protocol-level transaction filtering to restrict sanctioned addresses on their Arbitrum chain. - [Configure Sequencer timing adjustments](/launch-arbitrum-chain/chain-config/sequencer/sequencer-timing-adjustments.md): Learn how to configure timing adjustments to the Sequencer for your Arbitrum chain - [Timeboost for Arbitrum chains](/launch-arbitrum-chain/chain-config/sequencer/timeboost.md): Guidance & best practices for using Timeboost on Arbitrum chains - [Configure assertion control](/launch-arbitrum-chain/chain-config/validation/assertion-control.md): Learn how to configure assertion control. - [BoLD for Arbitrum chains](/launch-arbitrum-chain/chain-config/validation/bold.md): Learn how to integrate BoLD with your Arbitrum chain - [Bond and validator configurations](/launch-arbitrum-chain/chain-config/validation/bond-and-validator.md): Learn how to configure your Arbitrum chain with a custom bond and validator configurations - [Customizable challenge period](/launch-arbitrum-chain/chain-config/validation/challenge-period.md): Learn how to customize your Arbitrum chain's challenge period - [Enable fast withdrawals on your Arbitrum chain](/launch-arbitrum-chain/chain-config/validation/fast-withdrawals.md): Learn to deploy Fast Withdrawals - [Canonical factory contracts](/launch-arbitrum-chain/deploy/canonical-factory-contracts.md): Learn how to use the canonical factory contracts for deploying your Arbitrum chain - [How to configure your Arbitrum chain's node using the Chain SDK](/launch-arbitrum-chain/deploy/configure-node.md): Learn how to configure a node using the Chain SDK - [How to deploy an Arbitrum chain using the Chain SDK](/launch-arbitrum-chain/deploy/deploy-chain.md): How to deploy an Arbitrum chain using the Chain SDK - [Deploy a token bridge using the Chain SDK](/launch-arbitrum-chain/deploy/token-bridge.md): How to deploy a token bridge using the Chain SDK - [How to customize ArbOS on your Arbitrum chain](/launch-arbitrum-chain/extend-the-protocol/arbos.md): Learn how to customize your ArbOS version - [How to integrate with the DA API](/launch-arbitrum-chain/extend-the-protocol/da-api-guide.md): Learn how to implement your own custom DA provider for Nitro - [How to customize your Arbitrum chain's precompiles](/launch-arbitrum-chain/extend-the-protocol/precompiles.md): Learn how (and when) to customize your Arbitrum chain's precompiles - [How to customize your Arbitrum chain's behavior](/launch-arbitrum-chain/extend-the-protocol/stf.md): Learn how to customize your Arbitrum chain's behavior (known as the State Transition Function or STF) - [Batch poster: External signing (KMS)](/launch-arbitrum-chain/integrations/bp-kms-signing-services.md): Learn how to integrate external signing—including AWS KMS—for your chain's batch poster. - [How to add your Arbitrum chain to Arbitrum's bridge](/launch-arbitrum-chain/integrations/bridge-ui.md): Learn how to add your Arbitrum chain to Arbitrum's bridge. - [How to adopt the bridged USDC standard on your Arbitrum chain](/launch-arbitrum-chain/integrations/bridged-usdc.md): How to implement Circle bridged USDC standard on Arbitrum chain - [Exchange integration checklist: deposit and withdrawal verification](/launch-arbitrum-chain/integrations/exchange-integration-checklist.md): A version-pinned checklist that helps centralized exchanges reliably detect deposits, process withdrawals, and re-verify their indexing logic across Nitro and ArbOS upgrades on Dedicated Blockchains. - [Third-party Arbitrum chain infrastructure providers](/launch-arbitrum-chain/integrations/infrastructure-providers.md): A high-level overview of third-party Arbitrum chain infrastructure providers for production-grade chains. - [Migrate between RaaSes](/launch-arbitrum-chain/migrate/between-raases.md): Learn how to migrate your chain from a RaaS provider - [Migrate from another stack to an Arbitrum chain](/launch-arbitrum-chain/migrate/from-another-stack.md): Migrate to Arbitrum chain from another stack - [How to upgrade ArbOS on your Arbitrum chain](/launch-arbitrum-chain/operate/arbos-upgrade.md): Learn how to upgrade ArbOS on your Arbitrum chain. - [Batch poster troubleshooting](/launch-arbitrum-chain/operate/batch-poster-troubleshooting.md): Diagnose and resolve common batch poster operational issues including mempool errors, balance management, backlog conditions, and retry failures. - [BoLD upgrade playbook for multisig and security council chains](/launch-arbitrum-chain/operate/bold-upgrade-playbook.md): Sequence a BoLD upgrade when execution requires multisig or security council signatures that take days to collect, without freezing your validators for the entire window. - [Batch poster recovery](/launch-arbitrum-chain/operate/bp-recovery.md): Learn how the batch poster recovers state and the mechanisms that make recovery possible. - [Arbitrum Node Key Rotation Guide](/launch-arbitrum-chain/operate/key-rotation.md): Learn how to rotate keys for the different roles in your Arbitrum chain. - [Monitoring tools and considerations](/launch-arbitrum-chain/operate/monitoring.md): Tools available to monitor an Arbitrum chain, and a component-by-component reference of the metrics, signals, and thresholds to watch for the sequencer, batch poster, validator, and chain fees. - [Ownership structure and access control](/launch-arbitrum-chain/operate/ownership-and-access.md): Overview of the technical architecture of chain ownership affordances on Arbitrum chains. - [Post-launch deployment of deterministic contracts](/launch-arbitrum-chain/operate/post-launch-deployments.md): Learn about deploying contracts like Multicall3 to an Arbitrum chain after launch using sendL2Message, without needing a chain upgrade or predeploy. - [Managing state growth and corresponding issues](/launch-arbitrum-chain/operate/state-growth.md): Learn about state growth and corresponding issues - [Arbitrum chain upgrade runbook](/launch-arbitrum-chain/operate/upgrade-runbook.md): An end-to-end operational checklist for upgrading an Arbitrum chain, covering pre-upgrade validation, execution order, post-upgrade verification, WASM module root troubleshooting, and rollback options. - [Validator troubleshooting](/launch-arbitrum-chain/operate/validator-troubleshooting.md): Diagnose and resolve common validator operational issues, including assertion timing flags, parent-chain read consistency, assertion errors, stuck transactions, and manual assertion confirmation. - [Arbitrum chain FAQ](/launch-arbitrum-chain/overview/faq.md): List of questions and answers frequently asked by developers launching and working on Arbitrum chains - [Overview of Arbitrum chains](/launch-arbitrum-chain/overview/introduction.md): Launch your own Arbitrum chain using the Arbitrum Nitro codebase. Build a completely customizable chain that fits your specific needs. - [Arbitrum chain licensing](/launch-arbitrum-chain/overview/license.md): Learn about the Arbitrum chain license and AEP. - [Public preview: What to expect](/launch-arbitrum-chain/overview/public-preview.md): Arbitrum chains are currently a public preview capability. This concept document explains what this means, and what to expect from public preview capabilities. - [Run an L3 rollup from scratch](/launch-arbitrum-chain/quickstart/l3-rollup-from-scratch.md): A simple A-Z guide to deploy and run a default-configured L3 rollup chain on Arbitrum Sepolia. - [Run testnet infrastructure on your first rollup (product-level testnet)](/launch-arbitrum-chain/quickstart/l3-rollup-testnet.md): Step-by-step guide to run your Arbitrum chain infrastructure as a production-level testnet with high availability. - [Deploy a production chain: an overview](/launch-arbitrum-chain/quickstart/sdk-introduction.md): Learn how to deploy and manage your Arbitrum chain with the Arbitrum chain SDK. - [Run a batch poster](/launch-arbitrum-chain/run-a-node/batch-poster.md): Learn how to run and configure a batch poster. - [How to set up a high-availability sequencer](/launch-arbitrum-chain/run-a-node/high-availability-sequencer.md): Learn how to set up a high-availabilty sequencer for your Arbitrum chain. - [How to run a full node with Helm on Kubernetes](/launch-arbitrum-chain/run-a-node/run-full-node-with-helm.md): Deploy an Arbitrum chain full node on Kubernetes with the community Helm chart, including memory configuration, monitoring, log signals, and network egress. - [Run a split validator node](/launch-arbitrum-chain/run-a-node/split-validator-node.md): How to run a split validator node ## learn-more - [Arbitrum FAQ](/learn-more/faq.md): List of questions and answers frequently asked by users ## node-running - [Frequently asked questions: Run a node](/node-running/faq.md): List of questions and answers frequently asked by node runners - [Sequencer](/node-running/sequencer-content-map.md): Learn how to keep your Arbitrum node in sync with the sequencer, including reading the sequencer feed and running the coordination manager. ## notices - [Upgrade notice for ArbOS 51](/notices/arbos51-upgrade-notice.md): Upgrade notices for ArbOS 51 activation on Arbitrum One, Arbitrum Nova, and Arbitrum Spolia - [Upgrade notice for ArbOS 60](/notices/arbos60-upgrade-notice.md): Upgrade notices for ArbOS 60 activation on Arbitrum One, Arbitrum Nova, and Arbitrum Sepolia - [Upgrade notice for ArbOS 61](/notices/arbos61-upgrade-notice.md): Upgrade notices for ArbOS 61 activation on Arbitrum One, Arbitrum Nova, and Arbitrum Sepolia - [Fusaka Compatibility Notice](/notices/fusaka-upgrade-notice.md): Upgrade notices for the transition to Fusaka ## Offchain-pattern-guide - [Offchain Pattern guide](/Offchain-pattern-guide.md): Offchain Pattern guide ## run-arbitrum-node - [ArbOS 11](/run-arbitrum-node/arbos-releases/arbos11.md): Release notes for ArbOS 11, shipped via Nitro v2.2.0, including Shanghai upgrade EVM changes and upgrade requirements. - [ArbOS 20 Atlas](/run-arbitrum-node/arbos-releases/arbos20.md): Release notes for ArbOS 20 Atlas, shipped via Nitro v2.3.1, adding support for Ethereum Dencun upgrade and blob data. - [ArbOS 32 Bianca](/run-arbitrum-node/arbos-releases/arbos32.md): Release notes for ArbOS 32 Bianca, the canonical Bianca release shipped via Nitro v3.3.1, including Stylus fixes and optimizations. - [ArbOS 40 Callisto](/run-arbitrum-node/arbos-releases/arbos40.md): Release notes for ArbOS 40 Callisto, shipped via Nitro v3.6.5, building upon ArbOS 32 Bianca with new features and requirements. - [ArbOS 51 Dia](/run-arbitrum-node/arbos-releases/arbos51.md): Learn about ArbOS 51 Dia release, including Fusaka upgrade support, new EIPs, bug fixes, and upgrade requirements for Arbitrum chain owners and node operators. - [ArbOS software releases: Overview](/run-arbitrum-node/arbos-releases/overview.md): Overview of ArbOS software releases for Arbitrum, including upgrade requirements and the relationship between Nitro and ArbOS versions. - [How to assign roles to a Nitro node](/run-arbitrum-node/assign-node-roles.md): Understand how a Nitro node's role (RPC node, archive node, sequencer, batch poster, validator — optionally with split validation — or feed relay) is set by configuration flags, and how to convert a node from one role to another. - [Beacon Nodes: Historical Blobs](/run-arbitrum-node/beacon-nodes-historical-blobs.md): Learn more about the impacts of the Fusaka upgrade when running a beacon node. - [Data Availability](/run-arbitrum-node/data-availability.md): Learn how data availability works in arbitrum - [Ethereum beacon chain RPC providers](/run-arbitrum-node/l1-ethereum-beacon-chain-rpc-providers.md): A reference list of Ethereum beacon chain RPC providers that Arbitrum node operators can use to access blob data after the Dencun upgrade. - [How to run an archive node](/run-arbitrum-node/more-types/run-archive-node.md): Learn how to run an Arbitrum archive node on your local machine - [How to run a Classic node](/run-arbitrum-node/more-types/run-classic-node.md): Learn how to run an classic node on your local machine. - [How to run a validator](/run-arbitrum-node/more-types/run-validator-node.md): Learn how to run an Arbitrum validator node - [Nitro support policy](/run-arbitrum-node/nitro-support-policy.md): Learn the details of the support policy for Nitro. - [How to build Nitro locally (Debian, Ubuntu, macOS)](/run-arbitrum-node/nitro/build-nitro-locally.md): This how-to provides step-by-step instructions for building Nitro locally using Docker on Debian, Ubuntu, or macOS. - [CLI flags reference](/run-arbitrum-node/nitro/cli-flags-reference.md): Complete reference of all Nitro node command-line flags with types, defaults, and descriptions - [Nitro configuration system](/run-arbitrum-node/nitro/configuration-system.md): How Nitro loads configuration from files, environment variables, CLI flags, and S3 — including precedence rules and how to dump running config - [Data availability tools reference](/run-arbitrum-node/nitro/da-tools-reference.md): Reference for anytrusttool (keygen, dumpkeyset, client), anytrustserver, and daprovider CLI tools - [Docker images and CLI binaries](/run-arbitrum-node/nitro/docker-and-cli-binaries.md): Nitro Docker image variants, available CLI binaries, entrypoint usage, and Docker Compose examples - [How to convert databases from leveldb to pebble](/run-arbitrum-node/nitro/how-to-convert-databases-from-leveldb-to-pebble.md): Learn how convert your node database from leveldb to pebble - [How to migrate state and history from a classic (pre-Nitro) node to a Nitro node](/run-arbitrum-node/nitro/migrate-state-and-history-from-classic.md): This how-to provides step-by-step instructions for migrating the state and history from a classic (pre-Nitro) node to a Nitro node - [Nitro database snapshots](/run-arbitrum-node/nitro/nitro-database-snapshots.md): This page explains how to supply a snapshot to Nitro and how to create a new snapshot - [Node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md): Memory management, cache configuration, validator tuning, metrics endpoints, and monitoring setup for Nitro nodes - [Arbitrum nodes: an overview](/run-arbitrum-node/overview.md): Learn more about what types of Arbitrum nodes are available to run. - [How to run a feed relay](/run-arbitrum-node/run-feed-relay.md): Learn how to run an Arbitrum feed relay on your local machine. - [How to run a full node for an Arbitrum chain](/run-arbitrum-node/run-full-node.md): Learn how to run an Arbitrum node on your local machine - [How to run a local full chain simulation](/run-arbitrum-node/run-local-full-chain-simulation.md): This page provides instructions for setting up a complete local development environment for testing Arbitrum contracts in a fully simulated environment. - [How to run a local Nitro dev node](/run-arbitrum-node/run-nitro-dev-node.md): This page provides instructions for setting up and running a local Nitro dev node for contract testing and development. - [How to read the sequencer feed](/run-arbitrum-node/sequencer/read-sequencer-feed.md): Learn how to read the sequencer feed - [How to run a Sequencer Coordinator Manager (SQM)](/run-arbitrum-node/sequencer/run-sequencer-coordination-manager.md): Learn how to run a Sequencer Coordinator Manager terminal UI tool on your local machine. - [How to run a normal sequencer node for an Arbitrum chain](/run-arbitrum-node/sequencer/run-sequencer-node.md): Learn how to run an normal Arbitrum chain sequencer node on your local machine - [Prepare to run a node](/run-arbitrum-node/start-here.md): Learn the prerequisites for running an Arbitrum node on your local machine - [Troubleshooting: Run a node](/run-arbitrum-node/troubleshooting.md): Troubleshoot common issues when running an Arbitrum node, with configuration-specific guidance and a report generator for getting help. ## stylus - [Build apps with Stylus](/stylus.md): Learn how to build decentralized applications using Stylus, Arbitrum's next-generation smart contract platform supporting Rust, C, and C++. - [Hostio exports](/stylus/advanced/hostio-exports.md): Learn about low-level hostio functions for direct VM access in Stylus smart contracts - [Minimal entrypoint contracts](/stylus/advanced/minimal-entrypoint-contracts.md): Understanding low-level Stylus contract entrypoints and the Router trait - [Recommended libraries (Rust crates)](/stylus/advanced/recommended-libraries.md): Using public Rust crates - [Rust to Solidity differences](/stylus/advanced/rust-to-solidity-differences.md): Language and syntax differences between Rust Stylus and Solidity smart contracts - [Gas optimization best practices](/stylus/best-practices/gas-optimization.md): Techniques and patterns for optimizing gas costs in Stylus smart contracts - [Security best practices](/stylus/best-practices/security.md): Essential security patterns and guidelines for writing secure Stylus smart contracts - [Check and deploy](/stylus/cli-tools/check-and-deploy.md): Check and deploy Stylus contracts - [cargo-stylus command reference](/stylus/cli-tools/commands-reference.md): Complete reference for all cargo-stylus CLI commands, including flags, defaults, and aliases - [How to debug Stylus transactions](/stylus/cli-tools/debugging-tx.md): Learn how to debug Stylus transactions using cargo-stylus replay, trace, and usertrace commands, as well as the StylusDB debugger for advanced multi-contract debugging. - [Using Stylus CLI](/stylus/cli-tools/overview.md): Get started with Stylus CLI, a Rust toolkit for developing Stylus contracts - [How to verify Stylus contracts](/stylus/cli-tools/verify-contracts.md): Learn how to verify Stylus contracts locally and on Arbiscan - [Activation](/stylus/concepts/activation.md): Understanding Stylus contract deployment and activation - [Gas metering](/stylus/concepts/gas-metering.md): A conceptual overview of gas and ink, the primitives that Stylus uses to measure the cost of WASM activation, compute, memory, and storage. - [VM and execution differences](/stylus/concepts/vm-differences.md): Understand the differences between EVM and WASM execution models in Stylus - [WebAssembly in Nitro](/stylus/concepts/webassembly.md): Understanding WebAssembly compilation, deployment, and execution in Arbitrum Nitro - [Choose your learning path](/stylus/fundamentals/choose-your-path.md): Find the right Stylus learning path based on your background and goals - [Stylus contracts](/stylus/fundamentals/contracts.md): Stylus Rust SDK contracts, methods, and lifecycle - [Stylus compound types](/stylus/fundamentals/data-types/compound-types.md): Stylus Rust SDK compound types - [Conversions between types](/stylus/fundamentals/data-types/conversions-between-types.md): Learn how to convert between Rust types, Alloy primitives, and Solidity types in Stylus smart contracts - [Stylus primitives](/stylus/fundamentals/data-types/primitives.md): Stylus Rust SDK primitives - [Stylus Rust SDK storage](/stylus/fundamentals/data-types/storage.md): Stylus Rust SDK storage types - [Global variables and functions](/stylus/fundamentals/global-variables-and-functions.md): Stylus Rust SDK global variables and functions for blockchain context access - [Prerequisites and setup](/stylus/fundamentals/prerequisites.md): Set up your development environment for Stylus - [Structure of a Stylus Rust project](/stylus/fundamentals/project-structure.md): A quick overview of how contracts are structured with the Stylus Rust SDK - [Testing smart contracts with Stylus](/stylus/fundamentals/testing-contracts.md): A comprehensive guide to writing and running tests for Stylus smart contracts. - [A gentle introduction to Stylus](/stylus/gentle-introduction.md): An introduction to Stylus, which enables writing EVM-compatible smart contracts in programming languages that compile to WASM, such as Rust, C, and C++. - [Caching contracts with Stylus](/stylus/how-tos/caching-contracts.md): A conceptual overview of the Stylus caching strategy and CacheManager contract, and a practical guide for using its functionality. - [Deploying non-Rust WASM contracts](/stylus/how-tos/deploying-non-rust-wasm-contracts.md): Deploy WebAssembly contracts from C, C++, and other languages to Arbitrum Stylus - [Exporting ABIs](/stylus/how-tos/exporting-abi.md): Exporting Solidity ABIs from Stylus contracts - [Import and call external contract interfaces](/stylus/how-tos/importing-interfaces.md): Learn how to import Solidity interfaces and call external contracts from your Stylus smart contracts - [How to optimize Stylus WASM binaries](/stylus/how-tos/optimizing-binaries.md): A guide on optimizing Stylus WASM program sizes - [Composition and trait-based routing model](/stylus/how-tos/trait-based-composition.md): Learn how to implement trait-based composition in your Stylus smart contracts - [Using constructors with Stylus](/stylus/how-tos/using-constructors.md): A comprehensive guide to implementing and deploying smart contracts with constructors in Stylus - [How to verify Stylus contracts on Arbiscan](/stylus/how-tos/verifying-contracts-arbiscan.md): This page provides a step-by-step guide to verifying Stylus contracts on Arbiscan, including contract details, source code submission, and handling previously verified contracts. - [Quickstart: write a smart contract in Rust using Stylus](/stylus/quickstart.md): Leads a developer step by step writing and deploying a smart contract in Rust using Stylus - [Gas and ink costs](/stylus/reference/opcode-hostio-pricing.md): A reference of how much opcodes and host I/Os cost in Stylus, with measurements in ink and gas. - [Stylus Rust SDK overview](/stylus/reference/overview.md): Architecture, crate structure, feature flags, and documentation index for the Stylus Rust SDK v0.10.7 - [Stylus Rust SDK advanced features](/stylus/reference/rust-sdk-guide.md): Advanced features of the Stylus Rust SDK - [Configuration reference](/stylus/reference/stylus-toml-reference.md): A complete reference for the Stylus.toml, Cargo.toml, and rust-toolchain.toml configuration files used in Stylus Rust projects - [Troubleshooting Stylus](/stylus/troubleshooting-building-stylus.md): List of questions and answers frequently asked by developers building with Stylus - [Common issues and solutions](/stylus/troubleshooting/common-issues.md): Troubleshooting guide for frequently encountered Stylus development issues ## stylus-by-example - [ERC-20](/stylus-by-example/applications/erc20.md): An example implementation of the ERC-20 token standard in Rust using Arbitrum Stylus. - [ERC-721](/stylus-by-example/applications/erc721.md): An example implementation of the ERC-721 token standard in Rust using Arbitrum Stylus. - [Multicall](/stylus-by-example/applications/multi_call.md): An example implementation of the Multi Call contract in Rust using Arbitrum Stylus. - [Vending Machine](/stylus-by-example/applications/vending_machine.md): An example implementation of the Vending Machine in Rust using Arbitrum Stylus. - [ABI Decode](/stylus-by-example/basic_examples/abi_decode.md): A simple solidity ABI encode and decode example - [ABI Encode](/stylus-by-example/basic_examples/abi_encode.md): A simple solidity ABI encode and decode example - [Bytes In, Bytes Out](/stylus-by-example/basic_examples/bytes_in_bytes_out.md): A simple bytes in, bytes out Arbitrum Stylus Rust contract that shows a minimal `entrypoint` function. - [Constants](/stylus-by-example/basic_examples/constants.md): How to declare constants in your Rust smart contracts using Arbitrum's Stylus SDK - [Errors](/stylus-by-example/basic_examples/errors.md): How to define and use Errors on Stylus Rust smart contracts - [Events](/stylus-by-example/basic_examples/events.md): How to log events to the chain using the Arbitrum Stylus Rust SDK. - [Functions](/stylus-by-example/basic_examples/function.md): How to define and use internal and external functions in Stylus, and how to return multiple values from functions. - [Function selector](/stylus-by-example/basic_examples/function_selector.md): How to compute the encoded function selector of a contract's function using the Arbitrum Stylus Rust SDK. - [Hasing with keccak256 • Stylus by Example](/stylus-by-example/basic_examples/hashing.md): A simple solidity ABI encode and decode example - [Hello World](/stylus-by-example/basic_examples/hello_world.md): This example shows how to use the `console!` macro from the Arbitrum Stylus Rust SDK to print output to the terminal for debugging. - [Inheritance](/stylus-by-example/basic_examples/inheritance.md): How to leverage inheritance using the Arbitrum Stylus Rust SDK. - [Primitive Data Types](/stylus-by-example/basic_examples/primitive_data_types.md): Defines some of the basic primitives used in Arbitrum Stylus Rust smart contracts and how they map to compatible Solidity constructs. - [Sending Ether](/stylus-by-example/basic_examples/sending_ether.md): How to send Ether in your Rust smart contracts using Arbitrum's Stylus SDK - [Variables](/stylus-by-example/basic_examples/variables.md): An explanation of the types of variables available as part of the Arbitrum Stylus Rust SDK and how it differs from Solidity. - [VM affordances](/stylus-by-example/basic_examples/vm_affordances.md): How to access VM affordances using the Arbitrum Stylus Rust SDK. --- # Full Documentation Content > For a complete page index, fetch # Arbitrum bridge transaction traceability > **INFO** — Audience > > This page is for developers building integrations against the bridge contracts and the [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk). If you're an end user trying to figure out where your funds are, start with [Arbitrum bridge: Troubleshooting](/arbitrum-bridge/troubleshooting.md) and the [cross-chain dashboard](https://retryable-dashboard.arbitrum.io/tx). For protocol-level background, see [Parent-to-child messaging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md) and [Child-to-parent messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md). ## Tracing retryables from parent chain to child chain For the protocol-level model behind retryable tickets, see [Parent-to-child messaging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md). If you only need to redeem a stuck retryable rather than trace it, use the [cross-chain dashboard](https://retryable-dashboard.arbitrum.io/tx). If you want to trace the parent chain transaction to the child chain retryable ticket, the simplest way is to first create a provider using ethers via `providers.JsonRpcProvider(RPC)`. You should have one for both the parent and child chain. Then, using the parent chain provider, get the transaction via `parentProvider.getTransactionReceipt(Hash)`. Wrap the receipt in the `ParentTransactionReceipt` class, which allows you to call the class function `getParentToChildMessages(childProvider)`. This returns a parent-to-child message, in which one of the values is the `retryableCreationId` which is equivalent to the hash. You can then check if it has been submitted to the child chain by searching for that transaction either with the SDK (shown below) or a block explorer. Once the retryable transaction is gathered, you need to check if the transaction has been redeemed or not since it will trigger a separate transaction hash. This can be done by wrapping the `childReceipt` in a `childTransactionReceipt` and then creating a new `ParentToChildMessageReader` (parameters below in the code) object which has the class function `getSuccessfulRedeem`. This returns the status and info of the redeemed message if it has been redeemed. If it has been redeemed, then the `manualRedeemMessage` will have the transaction hash included. The following code demonstrates how to trace a parent-to-child message: ```tsx import { config } from 'dotenv'; import { providers } from 'ethers'; import { ChildTransactionReceipt, ParentToChildMessageStatus, ParentToChildMessageReader, ParentTransactionReceipt } from '@arbitrum/sdk'; const ParentTransactionHash = PARENT_TRANSACTION_HASH; const sepoliaRPC = SEPOLIA_RPC_URL; const arbSepoliaRPC = ARB_SEPOLIA_RPC_URL; var sepoliaProvider = new providers.JsonRpcProvider(sepoliaRPC); var arbSepoliaProvider = new providers.JsonRpcProvider(arbSepoliaRPC); async function main() { if (ParentTransactionHash == null) { throw new Error('Parent transaction hash cannot be null'); } const ParentReceipt = await sepoliaProvider.getTransactionReceipt(ParentTransactionHash); const wrappedParentReceipt = new ParentTransactionReceipt(ParentReceipt); const parentToChildMsg = (await wrappedParentReceipt.getParentToChildMessages(arbSepoliaProvider))[0]; if (parentToChildMsg.retryableCreationId == null) { throw new Error('Child Transaction Hash is null'); } console.log('Child RetryableHash is:', parentToChildMsg.retryableCreationId); //this is the start of searching for the child ticket and its retryable transaction const childReceipt = await arbSepoliaProvider.getTransactionReceipt(parentToChildMsg.retryableCreationId); if (childReceipt == null) { throw new Error('Retryable submission has not been sent to the child chain'); } const retryableReceipt = new ChildTransactionReceipt(childReceipt); const parentToChildMessageReader = new ParentToChildMessageReader(arbSepoliaProvider, arbSepoliaProvider.network.chainId, retryableReceipt.from, parentToChildMsg.messageNumber, parentToChildMsg.parentBaseFee, parentToChildMsg.messageData); const manualRedeemMessage = await parentToChildMessageReader.getSuccessfulRedeem(); if (manualRedeemMessage.status === ParentToChildMessageStatus.REDEEMED) { console.log('Message hash is: ', manualRedeemMessage.childTxReceipt.transactionHash); } else { console.log('Ticket has not been redeemed'); } } main(); ``` It's also possible to use `ParentToChildMessage.calculateSubmitRetryableId()`, however that function takes many parameters, making this process simpler in most cases. ## Tracing `DepositEth` from parent chain to child chain For a simple **ETH** deposit, the process is similar to finding a retryable ticket's hash on the child chain. Simply wrap the transaction receipt in the `ParentEthDepositTransactionReceipt` class, and then use the class function `getEthDeposits(childProvider)`, which returns an **ETH** deposit message containing the `childTxHash`. For the equivalent **ERC-20** flow at the higher level, see [Deposit tokens to Arbitrum](/arbitrum-essentials/bridging/deposit/tokens.md). ```tsx import { providers } from 'ethers'; import { ParentEthDepositTransactionReceipt } from '@arbitrum/sdk'; var depositEthHash = DEPOSIT_ETH_HASH; const sepoliaRPC = SEPOLIA_RPC_URL; const arbSepoliaRPC = ARB_SEPOLIA_RPC_URL; var sepoliaProvider = new providers.JsonRpcProvider(sepoliaRPC); var arbSepoliaProvider = new providers.JsonRpcProvider(arbSepoliaRPC); async function TraceDepositEth() { if (depositEthHash == null) { depositEthHash = '0'; } const depositEthReceipt = await sepoliaProvider.getTransactionReceipt(depositEthHash); const ParentEthDTR = new ParentEthDepositTransactionReceipt(depositEthReceipt); const ParToChildEthDepMsg = (await ParentEthDTR.getEthDeposits(arbSepoliaProvider))[0]; console.log(ParToChildEthDepMsg.childTxHash); } TraceDepositEth(); ``` You can also calculate the transaction hash using `EthDepositMessage.calculateDepositTxId`. The transaction may not be sent to the child chain yet, but the transaction hash will be the one returned via `getEthDeposits()`. ## Tracing retryables from child chain to parent chain Tracing a retryable ticket from child chain to parent chain has multiple steps but is not too complicated. First, you need to create a contract for the [`ArbRetryableTx`](/arbitrum-essentials/precompiles/reference.md#arbretryabletx) precompile and take the hash of the retryable message and use it as a log query for the `RedeemScheduled` event. This is a unique value, so there will only be one log. Once that log is found, get the transaction hash of the log—this is the retryable ticket hash. Grab the transaction info of the retryable ticket and parse its information; it contains a value called `requestId`. This is a unique value given to each retryable ticket and is also an indexed value for the `MessageDelivered` log on the parent chain bridge contract. Query the logs using the `requestId`, and you'll get a single log, which will contain the transaction hash information. If you have the hash of the actual ticket and not the message that is executed from the ticket, you can skip finding the `RedeemScheduled` event and go straight to getting the `requestId`. ```tsx import { providers, Contract } from 'ethers'; import { constants } from '@arbitrum/sdk'; const childMessageHash = CHILD_TRANSACTION_HASH; const sepoliaRPC = SEPOLIA_RPC_URL; const arbSepoliaRPC = ARB_SEPOLIA_RPC_URL; const bridgeContractAddress = BRIDGE_ADDRESS; var sepoliaProvider = new providers.JsonRpcProvider(sepoliaRPC); var arbSepoliaProvider = new providers.JsonRpcProvider(arbSepoliaRPC); async function main() { if (childMessageHash == null) { throw new Error('Transaction hash cannot be empty'); } if (bridgeContractAddress == null) { throw new Error('Bridge address cannot be empty'); } const retryableContract = new Contract(constants.ARB_RETRYABLE_TX_ADDRESS, arbRetryableABI, arbSepoliaProvider); const redeemFilter = retryableContract.filters.RedeemScheduled(null, childMessageHash); const redeemLog = (await retryableContract.queryFilter(redeemFilter))[0]; if (redeemLog == null) { throw new Error('Could not find RedeemScheduled event in given range'); } const retryableTicketID = redeemLog.transactionHash; const retryableTransaction = await arbSepoliaProvider.getTransaction(retryableTicketID); const parsedRetryableTransaction = retryableContract.interface.parseTransaction(retryableTransaction); const retryableID = parsedRetryableTransaction.args.requestId; const bridgeContract = new Contract(bridgeContractAddress, bridgeABI, sepoliaProvider); const bridgeFilter = bridgeContract.filters.MessageDelivered(retryableID); const log = (await bridgeContract.queryFilter(bridgeFilter))[0]; if (log == undefined) { throw new Error('Original Transaction hash not found, logs may not be available anymore'); } console.log('Original Transaction hash found: ', log.transactionHash); } main(); ``` ## Tracing `DepositEth` from child chain to parent chain If you want to trace an `DepositEth` message from child to parent chain, it requires a bit more work and is not as efficient. Even though **ETH** deposit messages are given a `requestId`, the child chain transaction is not given that information, meaning you have no means of using it to query the parent chain bridge logs efficiently. Instead, you must rely on the `SequencerInbox` contract — see [The Sequencer](/how-arbitrum-works/deep-dives/sequencer.md) for how batches and `SequencerBatchDelivered` fit into the protocol. The first thing to do is find which batch your child chain transaction is in. With that info, you can query the `SequencerInbox` contract to find the block number that the batch was posted in, giving you certainty that the original parent chain message was sent before the block with the posted batch. From there, search through the `Bridge` contract for their `MessageDelivered` logs. You can then wrap the transaction that was responsible for emitting those logs with `ParentEthDepositTransactionReceipt`. When wrapped, you can call `getEthDeposits`, and if the hash calculated matches the hash you have, then that is the original parent chain message. ```tsx import { providers, Contract } from 'ethers'; import { ChildTransactionReceipt, ParentEthDepositTransactionReceipt } from '@arbitrum/sdk'; const depositEthHashChild = DEPOSIT_ETH_HASH_CHILD; const sepoliaRPC = SEPOLIA_RPC_URL; const arbSepoliaRPC = ARB_SEPOLIA_RPC_URL; const sequencerInboxAddress = SEQUENCER_INBOX_ADDRESS; const bridgeContractAddress = BRIDGE_ADDRESS; var sepoliaProvider = new providers.JsonRpcProvider(sepoliaRPC); var arbSepoliaProvider = new providers.JsonRpcProvider(arbSepoliaRPC); async function main() { if (depositEthHashChild == null) { throw new Error('Transaction hash cannot be empty'); } if (sequencerInboxAddress == null) { throw new Error('Sequencer Inbox address cannot be empty'); } if (bridgeContractAddress == null) { throw new Error('Bridge address cannot be empty'); } const childEthDepositTransaction = await arbSepoliaProvider.getTransactionReceipt(depositEthHashChild); const ethDepositChildTransactionReceipt = new ChildTransactionReceipt(childEthDepositTransaction); var ethDepositBatchNumber = await ethDepositChildTransactionReceipt.getBatchNumber(arbSepoliaProvider); const sepoliaSequencerInboxContract = new Contract(sequencerInboxAddress, sepoliaSequencerInboxABI, sepoliaProvider); const seqInboxFilter = sepoliaSequencerInboxContract.filters.SequencerBatchDelivered(ethDepositBatchNumber); const batchDeliveredEvent = (await sepoliaSequencerInboxContract.queryFilter(seqInboxFilter))[0]; if (batchDeliveredEvent == null) { throw new Error('Seq batch delivered event not found, log may be unavailable'); } const bridgeContract = new Contract(bridgeContractAddress, bridgeABI, sepoliaProvider); const bridgeFilter = bridgeContract.filters.MessageDelivered(); const bridgeLogs = await bridgeContract.queryFilter(bridgeFilter, batchDeliveredEvent.blockNumber - 100, batchDeliveredEvent.blockNumber); for (let i = 0; i < bridgeLogs.length; i++) { const transactionReceipt = await sepoliaProvider.getTransactionReceipt(bridgeLogs[i].transactionHash); const transactionReceiptWrapped = new ParentEthDepositTransactionReceipt(transactionReceipt); const ethDeposit = await transactionReceiptWrapped.getEthDeposits(arbSepoliaProvider); if (ethDeposit.length == 0) continue; if (depositEthHashChild == ethDeposit[0].childTxHash) { console.log('Original Transaction hash found: ', transactionReceipt.transactionHash); return; } } throw new Error('Original Transaction hash not found, logs may not be available anymore or search window is too small'); } main(); ``` ## Tracing withdrawals from child chain to parent chain For the protocol-level model behind withdrawals (assertions, outbox, dispute window), see [Child-to-parent messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md). The equivalent **ERC-20** withdrawal flow at the higher level is described in [Withdraw tokens to parent chain](/arbitrum-essentials/bridging/withdraw/tokens.md). To trace a withdrawal, you first need to wrap the transaction receipt in a `ChildTransactionReceipt` class. You then need to get the emitted log [`L2ToL1Tx`](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/f49a4889b486fd804a7901203f5f663cfd1581c8/ArbSys.sol#L114) — emitted by the [`ArbSys`](/arbitrum-essentials/precompiles/reference.md#arbsys) precompile — as it contains the parameter `position`, which is a unique value for each withdrawal and can be used for a search on the parent chain. To do this, you need to parse the log via the `arbSysABI` to check for the name of the log being `L2ToL1Tx`. Then call `getChildToParentMessages` on the wrapped receipt to check the status of the withdrawal on the parent chain. If the status is not `EXECUTED`, then the withdrawal transaction has not been executed on the parent chain. Since this is a withdrawal, the assertion containing the withdrawal needs to be confirmed, so there is no point in searching before that assertion has been confirmed. You can estimate this value by parsing the log for `ethBlockNum` and adding it to the `confirmPeriodBlocks` which can be retrieved from the parent chain Rollup contract by calling `confirmPeriodBlocks`. Then start the search on the parent chain for the transaction that executed the withdrawal. To do so, search the outbox contract using two values from `ChildToParentEvent` (retrieved by calling `getChildToParentEvents` on a wrapped receipt) and use its `destination` and `caller` values as two search parameters. Then query the contract starting the search at the previous `ethBlockNum + confirmPeriodBlocks`. If the `transactionIndex` of the emitted log is equal to the position you grabbed before, then this is the transaction that was responsible for executing the withdrawal transaction. ```tsx import { Contract, providers, BigNumber } from 'ethers'; import { ChildTransactionReceipt, ChildToParentMessageStatus } from '@arbitrum/sdk'; import { Interface, LogDescription } from 'ethers/lib/utils'; const childWithdrawHash = WITHDRAW_ETH_HASH; const outboxAddress = OUTBOX_ADDRESS; const rollupAddress = ROLLUP_ADDRESS; const sepoliaRPC = SEPOLIA_RPC_URL; const arbSepoliaRPC = ARB_SEPOLIA_RPC_URL; var sepoliaProvider = new providers.JsonRpcProvider(sepoliaRPC); var arbSepoliaProvider = new providers.JsonRpcProvider(arbSepoliaRPC); async function main() { if (childWithdrawHash == null) { throw new Error('Withdraw hash cannot be null'); } if (outboxAddress == null) { throw new Error('Outbox Address cannot be null'); } if (rollupAddress == null) { throw new Error('Rollup Address cannot be null'); } const withdrawalReceipt = await arbSepoliaProvider.getTransactionReceipt(childWithdrawHash); const withdrawalReceiptWrapped = new ChildTransactionReceipt(withdrawalReceipt); const childToParentMessages = await withdrawalReceiptWrapped.getChildToParentMessages(sepoliaProvider); const status = await childToParentMessages[0].status(arbSepoliaProvider); if (status != ChildToParentMessageStatus.EXECUTED) { throw new Error('Message has not been executed on the parent chain'); } const arbSysInterface = new Interface(arbSysABI); let withdrawalPosition: BigNumber | undefined; let parsedLog!: LogDescription; for (let i = 0; i < withdrawalReceipt.logs.length; i++) { parsedLog = arbSysInterface.parseLog(withdrawalReceipt.logs[i]); if (parsedLog.name == 'L2ToL1Tx') { withdrawalPosition = parsedLog.args.position; break; } } if (withdrawalPosition == undefined) { throw new Error('Correct log not found'); } const rollupContract = new Contract(rollupAddress, rollupABI, sepoliaProvider); const confirmationLength: BigNumber = await rollupContract.confirmPeriodBlocks(); const startingBlock = confirmationLength.add(parsedLog.args.ethBlockNum); const outboxContract = new Contract(outboxAddress, outboxABI, sepoliaProvider); const eventFilter = outboxContract.filters.OutBoxTransactionExecuted(parsedLog.args.destination, parsedLog.args.caller); const events = await outboxContract.queryFilter(eventFilter, startingBlock.toNumber()); console.log(events); for (let i = 0; i < events.length; i++) { const log = outboxContract.interface.parseLog(events[i]); if (withdrawalPosition.eq(log.args.transactionIndex)) { console.log('Parent chain transaction found: ', events[i].transactionHash); return; } } throw new Error('Parent chain transaction not found, log may be unavailable'); } main(); ``` ## Tracing withdrawals from parent chain to child chain Tracing a withdrawal from parent to child chain is not that difficult. It's basically the inverse of the above, but since the parent chain transaction has been executed, you don't need to perform any checks. Since the position is indexed on the child chain, the process is much easier. First, parse the logs. The correct one is emitted by the outbox contract and contains the value `transactionIndex`, which is a unique value given to each withdrawal. Even though this value is indexed, you can easily narrow down the log query even more by parsing the transaction and getting the child chain block. Then you can query the `L2ToL1Tx` log on the `ArbSys` contract, which will give you the transaction. ```tsx import { Contract, providers, BigNumber } from 'ethers'; import { constants } from '@arbitrum/sdk'; import { Interface } from 'ethers/lib/utils'; const ParentWithdrawHash = PARENT_WITHDRAW_HASH; const outboxAddress = OUTBOX_ADDRESS; const sepoliaRPC = SEPOLIA_RPC_URL; const arbSepoliaRPC = ARB_SEPOLIA_RPC_URL; var sepoliaProvider = new providers.JsonRpcProvider(sepoliaRPC); var arbSepoliaProvider = new providers.JsonRpcProvider(arbSepoliaRPC); async function main() { if (ParentWithdrawHash == null) { throw new Error('Parent hash cannot be null'); } const transactionReceipt = await sepoliaProvider.getTransactionReceipt(ParentWithdrawHash); const outboxInterface = new Interface(outboxABI); let withdrawPosition: BigNumber | undefined; for (let i = 0; i < transactionReceipt.logs.length; i++) { if (transactionReceipt.logs[i].address == outboxAddress) { const parsedTransaction = outboxInterface.parseLog(transactionReceipt.logs[i]); withdrawPosition = parsedTransaction.args.transactionIndex; break; } } if (withdrawPosition == undefined) { throw new Error('Could not find correct log'); } const transactionInfo = await sepoliaProvider.getTransaction(ParentWithdrawHash); const parsedTransaction = outboxInterface.parseTransaction(transactionInfo); const l2WithdrawBlock = parsedTransaction.args.l2Block.toHexString(); const arbSysContract = new Contract(constants.ARB_SYS_ADDRESS, arbSysABI, arbSepoliaProvider); const eventFilter = arbSysContract.filters.L2ToL1Tx(null, null, null, withdrawPosition); const event = (await arbSysContract.queryFilter(eventFilter, l2WithdrawBlock, l2WithdrawBlock))[0]; if (event == null) { throw new Error('log not found, may be unavailable'); } console.log('Transaction Hash found: ', event.transactionHash); } main(); ``` ## See also * [Arbitrum bridge: Troubleshooting](/arbitrum-bridge/troubleshooting.md) * [Parent-to-child messaging (retryables) — deep dive](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md) * [Child-to-parent messaging (withdrawals) — deep dive](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md) * [How token bridging works (deep dive)](/how-arbitrum-works/deep-dives/token-bridging.md) * [Deposit tokens to Arbitrum](/arbitrum-essentials/bridging/deposit/tokens.md) * [Withdraw tokens to parent chain](/arbitrum-essentials/bridging/withdraw/tokens.md) * [Precompiles reference](/arbitrum-essentials/precompiles/reference.md) (`ArbSys`, `ArbRetryableTx`) * [Arbitrum SDK on GitHub](https://github.com/OffchainLabs/arbitrum-sdk) * [Cross-chain dashboard](https://retryable-dashboard.arbitrum.io/tx) (redeem stuck retryables and execute withdrawals) --- > For a complete page index, fetch # Arbitrum embedded bridge widget The embedded bridge helps applications bring users and assets into markets built on the Arbitrum Platform. To see what the end-user bridge flow looks like outside the widget, see the [Arbitrum bridge quickstart](/arbitrum-bridge/quickstart.md). You can embed the widget onto your webpage using a simple iframe and configure colors and window sizing by modifying the iframe code. Transactions are routed through [Li.fi's API](https://li.fi), which then requests quotes from various third-party bridging providers, such as Layer Zero, Across, or Relay. The fastest and cheapest paths are highlighted to users based on the quotes. Users can pick their preferred bridging option. > **INFO** — Bridging **USDC** through the widget > > The widget routes **USDC** alongside other tokens. For background on Arbitrum One's two **USDC** forms (native vs `USDC.e`), see [USDC on Arbitrum One](/arbitrum-bridge/usdc-arbitrum-one.md). ## Functionalities ### Core functionalities * Deposits from Base, Ethereum Mainnet, and other Arbitrum chains * Withdrawals to Arbitrum One or other Arbitrum chains * Configuration of supported chains for both deposits and withdrawals * Batch transactions * Long-tail assets on supported bridges and DEXes if liquidity is available * Fiat on-ramps via Moonpay ### Visual features * Normal mode, widget mode * Layout options * Visual customization ### Arbitrum bridge playground You can experiment with Arbitrum bridge configurations in real-time using the [bridge playground](https://playground.arbitrum.io/bridge). This interactive tool allows you to: * **Test different modes**: Switch between normal and widget modes to see how they differ * **Configure feature flags**: Toggle network selection and batch transfers to understand their impact * **Explore layout options**: Try vertical and horizontal layouts for the widget interface * **Generate embed code**: Get ready-to-use iframe code for your integration * **See live preview**: Watch changes reflect immediately in the embedded bridge interface ## Integration details The widget uses an `iframe` under the hood, enabling easy integration for any frontend. Visit the bridge playground to generate `iframe` code with your configured features. ## Support Before integrating, review the current chain offerings to ensure that your desired routes are shown. The embedded bridge uses [Li.fi's](https://docs.li.fi/api-reference/fetch-all-known-tokens) token lists to populate supported routes and quotes. If you plan to use the on-ramp functionalities, notify to facilitate support. Contact to discuss specific chain and token support. For transaction support, users should [create a support ticket ](https://support.arbitrum.io/hc/en-gb/requests/new)with the Arbitrum Foundation and select the "Widget" option in the dropdown. For self-service troubleshooting of common bridging issues, see [Arbitrum bridge: Troubleshooting](/arbitrum-bridge/troubleshooting.md). ### Chain and bridge support The embedded bridge currently supports Arbitrum One, Base, Ethereum Mainnet, Ape Chain, and Superposition. ## Frequently asked questions #### 1. If I'm a dApp on Arbitrum One (or an Arbitrum chain), what do I need to do to adopt? You can adopt the bridge functionality permissionlessly if your chain is already supported. If your chain is already supported, [visit the playground](https://playground.arbitrum.io/bridge) and accept the developer terms of service to generate code for the embedded bridge. If you are interested in on-ramp support, please contact . #### 2. If I'm an Arbitrum chain and I want support, how do I adopt? You'll need an integration with Li.fi and a minimum number of bridges to enable a good experience. Contact to discuss adding support for your chain. To also be listed in the main bridge UI at [bridge.arbitrum.io](https://bridge.arbitrum.io/), see [How to add your Arbitrum chain to Arbitrum's bridge](/launch-arbitrum-chain/integrations/bridge-ui.md). #### 3. Are there any fees associated with the embedded bridge? The embedded bridge routes orders through Li.fi's route aggregator, which charges a 16-basis-point fee on all transactions. ## See also * [Arbitrum bridge quickstart](/arbitrum-bridge/quickstart.md) * [USDC on Arbitrum One](/arbitrum-bridge/usdc-arbitrum-one.md) * [Arbitrum bridge: Troubleshooting](/arbitrum-bridge/troubleshooting.md) * [How to add your Arbitrum chain to Arbitrum's bridge](/launch-arbitrum-chain/integrations/bridge-ui.md) --- > For a complete page index, fetch # Quickstart: Arbitrum bridge This quickstart is for users who want to deposit **ETH** or any **ERC-20** tokens from a parent chain to a child chain or vice versa, using [Arbitrum’s bridge](https://bridge.arbitrum.io/). For example, from Ethereum to Arbitrum One, or from Arbitrum One to a Layer 3 Arbitrum chain. We will walk you through the entire process step by step, providing as much detail as possible. If you feel stuck at any step, see [Arbitrum bridge: Troubleshooting](/arbitrum-bridge/troubleshooting.md), or contact us through our [Discord](https://discord.gg/arbitrum), and we will be happy to help you complete the process. > **INFO** — Bridging **USDC**? > > Arbitrum One supports two distinct forms of **USDC** (native and bridged). Before you deposit, see [USDC on Arbitrum One](/arbitrum-bridge/usdc-arbitrum-one.md) so you receive the form you intend. The only prerequisite for this quickstart is to have a Web3 wallet installed, such as MetaMask or OKX Wallet. If you don’t have one installed, visit the [Arbitrum portal](https://portal.arbitrum.io/?categories=wallet) for a list of available wallets to download. ## Deposit **ETH** or **ERC-20** tokens (from parent chain to child chain) ### Step 1: Get some native currency You’ll need the native currency of the parent chain to bridge your assets to the destination chain. For example, if you want to bridge assets from Ethereum to Arbitrum One, you’ll need **ETH** on Ethereum to initiate the process. There are several ways to obtain the native currency: * Using a [supported centralized exchange](https://portal.arbitrum.io/projects?chains=arbitrum-one_arbitrum-nova\&subcategories=centralized-exchanges), which allows you to purchase **ETH** and withdraw it to your wallet. Most major centralized exchanges support direct withdrawals from your centralized exchange wallet to the Arbitrum network. * Using an [on-ramp service](https://portal.arbitrum.io/projects?chains=arbitrum-one_arbitrum-nova\&subcategories=fiat-on-ramp), which allows you to purchase **ETH** and send it directly to your wallet. * If you are using a testnet, request funds from a Sepolia or Arbitrum Sepolia [faucet](/for-devs/dev-tools-and-resources/chain-info.md#faucet-list). ### Step 2: Add the preferred network to your wallet You'll also need to add the desired chain's RPC endpoint to your wallet. Here, we provide an example of how to do this using MetaMask; however, the process should be relatively similar to any other wallet. 1. First, click the MetaMask extension in your browser. 2. Click the network selector drop-down in the top-right corner. 3. Click the **Add a custom network** and then provide the information corresponding to the chain you want to send your assets to (see below). ![Add the desired destination network to your MetaMask](/img/bridge-quickstart-metamask-set-network.gif) Below are the most common Arbitrum chains. For a more exhaustive list, please visit our [RPC endpoints and providers](/arbitrum-essentials/reference/node-providers.md) page. | Parameter | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia (testnet) | | ------------------ | ------------------------------ | ------------------------------ | ---------------------------------------- | | Network name | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | RPC URL | | | | | Chain ID | 42161 | 42170 | 421614 | | Currency symbol | **ETH** | **ETH** | SepoliaETH | | Block explorer URL | | | | ### Step 3: Initiate the deposit To bridge your **ETH** or **ERC-20** tokens to a different chain, start by visiting [bridge.arbitrum.io](https://bridge.arbitrum.io/). 1. Connect your wallet to the bridge. 2. Ensure the **source** network is selected (from where you want to deposit your assets) at the top of the page. 3. Select the **destination** network (where you want your assets to go), e.g., Arbitrum One. > **CAUTION** > > Note that testnets like Arbitrum Sepolia only appear if you have an established connection to the appropriate parent testnet network (Ethereum Sepolia). > > Also note, that when choosing the **source** or **destination** network, a pop up will appear where you can make your selection (illustrated below). ![Add the desired network to your Web3 wallet](/img/arb-bridge-getting-started-users-2.png) ![Add the desired network to your Web3 wallet](/img/arb-bridge-getting-started-users-3.png) 4. Select the token you want to bridge in the token drop-down menu. ![Select the token to bridge](/img/arb-bridge-getting-started-users-4.png) ![Select the token to bridge](/img/arb-bridge-getting-started-users-5.png) 5. Enter the amount of **ETH** or **ERC-20** tokens you want to bridge over in the **From** box. 6. Press **Move** funds. 7. Follow the additional prompts from your Web3 wallet. > **INFO** — Ensure sufficient **ETH** balance > > Please ensure you have sufficient **ETH** in your wallet to cover the transation costs; otherwise, the Web3 wallet pop-up will not appear. ![Enter the amount of tokens to bridge](/img/arb-bridge-getting-started-users-6.png) After you submit the transaction through your Web3 wallet, you can expect your funds to arrive on the destination chain within roughly 15-30 minutes (depending on chain congestion). Also, ensure your wallet is set to the destination network so you can see when your funds arrive. ## Withdraw **ETH** or **ERC-20** tokens (from child chain to parent chain) > **INFO** — Seven day withdrawal period for Arbitrum One and Nova networks > > Once you withdraw your funds from Arbitrum One or Nova through the Arbitrum bridge, you will have to wait for at least seven days to receive them on the Ethereum mainnet. For more details, see [Arbitrum Bridge: Troubleshooting](/arbitrum-bridge/troubleshooting.md#how-long-does-it-take-before-i-receive-my-funds-when-i-initiate-a-withdrawal-from-arbitrum-chains-one-and-nova). For the protocol-level reason behind the dispute window, see [Child-to-parent messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md) and the FAQ entry on [why one week was chosen](/learn-more/faq.md#why-was-one-week-chosen-for-arbitrum-ones-dispute-window). To bridge your funds back to the parent chain, you must be connected to the [Arbitrum bridge](https://bridge.arbitrum.io/) with your wallet and ensure you establish a connection to the source network (from which you want to withdraw assets) at the top of the page. Then, select the destination network (where you want your assets to go), e.g., Ethereum mainnet. > **CAUTION** > > Testnets like Arbitrum Sepolia only appear if you have an established connection to the appropriate parent testnet network (Ethereum Sepolia). ![Select the token to withdraw](/img/arb-bridge-getting-started-users-7.png) 1. Select the token you want to bridge in the token drop-down menu. 2. Enter the amount of **ETH** or **ERC-20** tokens you want to bridge in the **From** box. 3. Press **Move funds**. 4. Follow the prompts from your Web3 wallet. > **INFO** — Ensure sufficient **ETH** balance > > Please ensure you have sufficient **ETH** in your wallet to cover the transaction costs; otherwise, no Web3 wallet pop-up will appear. ![Enter the amount of token to withdraw](/img/arb-bridge-getting-started-users-8.png) A countdown will appear stating that you'll receive your funds in 7-8 days. You can check the status of your withdrawal by clicking on your profile in the top-right corner and opening the **Transactions** tab, where you can claim it when it's ready. ![See the transaction history](/img/arb-bridge-getting-started-users-9.png) Once the countdown is complete, press the **Claim** button to receive your funds. ## See also * [USDC on Arbitrum One](/arbitrum-bridge/usdc-arbitrum-one.md) * [Arbitrum bridge: Troubleshooting](/arbitrum-bridge/troubleshooting.md) * [Arbitrum embedded bridge widget](/arbitrum-bridge/embedded-bridge-widget.md) * [Bridge transaction traceability](/arbitrum-bridge/bridge-transaction-traceability.md) * [How token bridging works (deep dive)](/how-arbitrum-works/deep-dives/token-bridging.md) * [Bridge tokens programmatically](/arbitrum-essentials/bridging/overview.md) The team working on Arbitrum is always interested and looking forward to engaging with its users. Why not follow us on [X (Twitter)](https://x.com/arbitrum) or join our community on [Discord](https://discord.gg/arbitrum)? --- > For a complete page index, fetch # Troubleshooting: Arbitrum bridge ### How do I move assets between One and Nova? Both Arbitrum One and Arbitrum Nova run as layers on top of Ethereum. Thus, you can always move assets between the two chains in two steps by going "through" Ethereum. In other words: withdraw your assets from Arbitrum One to Ethereum and then deposit them onto Nova, or conversely, withdraw your assets from Nova to Ethereum and then deposit them to Arbitrum One. You can complete these steps using the [Arbitrum Bridge](https://bridge.arbitrum.io/); see the [bridge quickstart](/arbitrum-bridge/quickstart.md) for a step-by-step walkthrough. ### What fees do I have to pay when bridging funds from L1 to L2? When bridging over tokens from L1 to L2, you will have to sign one or two transactions with their corresponding fees: 1. If you are bridging a token for the first time, you'll sign one **approval transaction**. 2. In all cases, you'll sign a **deposit transaction** that will send your tokens to the Bridge. Please note that the approval transaction needs to be executed at least once per token and wallet. This approval means that if you bridge the same token from the same wallet again, you probably won't have to pay for that transaction. However, if you bridge the same token from a different wallet, you will have to pay for that transaction again. In any case, the Bridge and your wallet will guide you through the process, showing the transaction(s) that you need to sign to have your tokens bridged to Arbitrum. ### How long does it take before I receive my funds when I initiate a withdrawal from Arbitrum chains (One and Nova)? Using the official Arbitrum Bridge, the process will take *roughly* one week. However, some users opt to use third-party fast bridges, which often bypass this delay (remember that these bridges are created and maintained by third parties, so please DYOR!). There's some variability in the exact wall-clock time of the dispute window, plus there's some expected additional "padding" time on both ends (no more than about an hour, typically). The variability of the dispute window comes from the slight variance of block times. Arbitrum One's dispute window is 45818 blocks; this converts to about 6.5 days, assuming slightly more than 12 seconds per block, the average block time of Ethereum. The "padding on both ends" involves three events that have to occur between a client receiving their transaction receipt from the Sequencer and their child-to-parent chain message being executable. After getting their receipt, 1. The sequencer posts their transaction in a batch (usually within a few minutes, though the sequencer will wait a bit longer if the parent chain is congested). Then, 2. A validator includes their transaction in an assertion (usually within the hour). Then, after the \~week long dispute window passes, the assertion is confirmable, and 3. Somebody (anybody) confirms the assertion on the parent chain (usually within \~15 minutes). Additionally, in the rare and unlikely event of a dispute, this delay period will be extended for the dispute to resolve. For the protocol-level explanation of these steps, see [Child-to-parent messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md) and [The Sequencer](/how-arbitrum-works/deep-dives/sequencer.md). ### Is there a way to cancel a withdrawal from Arbitrum? Once initiated, it is not possible to cancel a withdrawal. However, you can claim your funds on L1 and deposit them again on L2 once the [withdrawal period](https://docs.arbitrum.io/learn-more/faq#why-was-one-week-chosen-for-arbitrum-ones-dispute-window) is past. ### Can I use a smart contract wallet in the bridge? Support for Smart Contract Wallets is currently limited to token deposits and withdrawals. Keep in mind that when withdrawing funds, you won't be able to claim them on L1 using the [bridge](https://bridge.arbitrum.io/) (unless you also control that address on L1). In that case, you can use the [cross-chain dashboard](https://retryable-dashboard.arbitrum.io/tx) to claim your funds on L1. Developers debugging a stuck transaction in this flow may also find [Bridge transaction traceability](/arbitrum-bridge/bridge-transaction-traceability.md) useful. **ETH** deposits and withdrawals using a Smart Contract Wallet are currently not supported, but will be available soon. ### What's the difference between USDC and USDC.e on Arbitrum One? Arbitrum One supports both Circle-native **USDC** and bridged **USDC.e**. They are different tokens at different contract addresses. See [USDC on Arbitrum One](/arbitrum-bridge/usdc-arbitrum-one.md) for the details and which one you should hold. ### How can I claim withdrawn funds if I don't control on L1 the address that initiated the transaction on L2? Once the [withdrawal period](https://docs.arbitrum.io/learn-more/faq#why-was-one-week-chosen-for-arbitrum-ones-dispute-window) is past, you can use the [cross-chain dashboard](https://retryable-dashboard.arbitrum.io/tx) to execute the message on L1. Paste the transaction hash that initiated the withdrawal on L2, and follow the process described in the dashboard. ## See also * [Arbitrum bridge quickstart](/arbitrum-bridge/quickstart.md) * [USDC on Arbitrum One](/arbitrum-bridge/usdc-arbitrum-one.md) * [Arbitrum embedded bridge widget](/arbitrum-bridge/embedded-bridge-widget.md) * [Bridge transaction traceability](/arbitrum-bridge/bridge-transaction-traceability.md) (for developers diagnosing stuck cross-chain transactions) * [Cross-chain dashboard](https://retryable-dashboard.arbitrum.io/tx) (claim withdrawals and redeem stuck retryable tickets) --- > For a complete page index, fetch # USDC on Arbitrum One Arbitrum One supports two different types of **USDC**: 1. **Native USDC**: **USDC** tokens that are native to the Arbitrum One chain. 2. **Bridged USDC (**USDC.e**)**: Ethereum-native **USDC** tokens that have been bridged to Arbitrum One. ## Differences between USDC and USDC.e | | Native **USDC** | Bridged **USDC** | | ------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | Token Name | **USDC** | Bridged **USDC** | | Token Symbol | **USDC** | **USDC.e** | | Token Address | [0xaf88d065e77c8cC2239327C5EDb3A432268e5831](https://arbiscan.io/token/0xaf88d065e77c8cC2239327C5EDb3A432268e5831) | [0xff970a61a04b1ca14834a43f5de4533ebddb5cc8](https://arbiscan.io/token/0xff970a61a04b1ca14834a43f5de4533ebddb5cc8) | | Benefits | CEX Support, directly redeemable 1:1 for U.S. dollars | | The Arbitrum Bridge will continue to facilitate transfers of all **USDC** tokens. When depositing Ethereum-native **USDC**, the option exists to receive Bridged **USDC** using Arbitrum's bridge (the canonical lock-and-mint flow described in the [token bridging deep dive](/how-arbitrum-works/deep-dives/token-bridging.md)) or Arbitrum-native **USDC** using Circle's [Cross-Chain Transfer Protocol](https://www.circle.com/en/cross-chain-transfer-protocol). To start bridging on Arbitrum refer to the [Quickstart](/arbitrum-bridge/quickstart.md) or the [Embedded bridge widget](/arbitrum-bridge/embedded-bridge-widget.md). If you run into issues bridging **USDC**, see [Arbitrum bridge: Troubleshooting](/arbitrum-bridge/troubleshooting.md). ## For Arbitrum chain operators If you're launching an Arbitrum chain and want to support **USDC** without locking users into the canonical bridged form, see [How to adopt the bridged USDC standard on your Arbitrum chain](/launch-arbitrum-chain/integrations/bridged-usdc.md). It explains the gateway implementation that lets Circle upgrade your chain's **USDC** to native issuance later. ## Historical context The Arbitrum Bridge will continue to facilitate transfers of all **USDC** tokens. When depositing **USDC** from Ethereum, the option exists to receive **USDC** using Circle's Cross-Chain Transfer Protocol or receive Bridged **USDC** using Arbitrum's lock-and-mint bridge. In 2023, Circle launched **USDC** natively on Arbitrum One and added support for the Cross-Chain Transfer Protocol, which enabled direct minting and burning of **USDC** between Ethereum and Arbitrum One. Due to this, the token symbol for Bridged **USDC** has been renamed to **USDC.e** to accommodate an ecosystem-wide liquidity migration to native **USDC**. The expectation is that over time, the liquidity migration of **USDC.e** to **USDC** will continue. ## See also * [Arbitrum bridge quickstart](/arbitrum-bridge/quickstart.md) * [Arbitrum bridge: Troubleshooting](/arbitrum-bridge/troubleshooting.md) * [Arbitrum embedded bridge widget](/arbitrum-bridge/embedded-bridge-widget.md) * [How to adopt the bridged USDC standard on your Arbitrum chain](/launch-arbitrum-chain/integrations/bridged-usdc.md) * [How token bridging works (deep dive)](/how-arbitrum-works/deep-dives/token-bridging.md) --- > For a complete page index, fetch # Monitor withdrawals programmatically This page is intended for chain operators and integration partners (exchanges, bridges, infrastructure providers) who need to track withdrawals programmatically and be alerted when one gets stuck. To trace a single transaction end to end, see [Tracing bridge transactions](/arbitrum-bridge/bridge-transaction-traceability.md). To initiate withdrawals from code, see [Withdraw ETH and messages](/arbitrum-essentials/bridging/withdraw/eth-and-messages.md). To learn the protocol-level background, see [Child-to-parent messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md). A withdrawal from an Arbitrum chain is a child chain-to-parent chain message that passes through several protocol stages before funds are claimable. Most "where is my withdrawal?" questions come down to not knowing which stage a withdrawal is in, or how long each stage is expected to take on a given chain. This guide maps the lifecycle to observable onchain state and to the [`@arbitrum/sdk`](https://github.com/OffchainLabs/arbitrum-sdk) status model, gives expected timelines per chain type, and provides tested code for monitoring one withdrawal or indexing many. ## The withdrawal lifecycle Every withdrawal—whether a token withdrawal through the bridge UI or a raw `ArbSys.sendTxToL1` call—moves through the same stages: | Stage | What happens onchain | SDK status | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | | **1. Initiated** | The child chain transaction calls the [`ArbSys`](/arbitrum-essentials/precompiles/reference.md#arbsys) precompile (directly, or via a token gateway), which emits an `L2ToL1Tx` event containing the withdrawal's `position` | `UNCONFIRMED` | | **2. Batched** | The sequencer posts the batch containing the transaction to the parent chain (typically within minutes) | `UNCONFIRMED` | | **3. Asserted** | A validator posts an assertion covering the withdrawal's child chain block (`AssertionCreated` event on the Rollup contract) | `UNCONFIRMED` | | **4. Confirmed** | After the challenge period passes without a successful challenge, the assertion is confirmed (`AssertionConfirmed` event); the Rollup contract registers the assertion's send root in the outbox (`SendRootUpdated` event) | `CONFIRMED` | | **5. Executed** | Someone calls `Outbox.executeTransaction()` with a Merkle proof (obtained from a child chain node via `NodeInterface.constructOutboxProof()`);; the outbox marks the withdrawal spent (`OutBoxTransactionExecuted` event) and releases the funds | `EXECUTED` | Two properties of this lifecycle cause most confusion: * **Execution is manual.** Confirmation makes a withdrawal *claimable*; it doesn't deliver funds. Someone (the user through the bridge UI's **Claim** button, or your infrastructure) must execute the claim on the parent chain. A withdrawal can sit in `CONFIRMED` indefinitely. To learn why the protocol works this way, see [Why child to parent chain messages require manual execution](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md#why-child-to-parent-chain-messages-require-manual-execution). * **Time-to-claimable is more than the challenge period.** The clock users care about includes batch posting (minutes) *plus* waiting for the next assertion to cover their block (up to the chain's assertion posting interval, commonly up to an hour) *plus* the challenge period after that assertion is created. On a chain with [fast withdrawals](/launch-arbitrum-chain/chain-config/validation/fast-withdrawals.md), the committee's confirmation frequency replaces the challenge period wait, but batch posting and assertion cadence still apply—which is why a "15-minute fast withdrawal chain" can legitimately take an hour or more to reach claimable. ### Mapping bridge UI phases to protocol states The [Arbitrum bridge UI](https://bridge.arbitrum.io/) presents a withdrawal as three user-facing steps, and support conversations often reference them as "phases." They map to the lifecycle above as follows: | Bridge UI step | Lifecycle stages | SDK status | | -------------------------------------------------- | ---------------------------------------------- | ------------------------ | | Phase 1 — withdrawal submitted | Initiated | `UNCONFIRMED` | | Phase 2 — countdown or waiting | Batched → Asserted → challenge period elapsing | `UNCONFIRMED` | | Phase 3 — **Claim** button available, then claimed | Confirmed → Executed | `CONFIRMED` → `EXECUTED` | A withdrawal "stuck between phase 2 and phase 3" is a withdrawal that stayed `UNCONFIRMED` past its expected time-to-claimable: either no assertion covering its block exists yet, or the covering assertion hasn't been confirmed. The [diagnosis table](#diagnose-stuck-withdrawals) below separates those cases. ## Expected timelines by chain type The dominant term in time-to-claimable is the challenge period, set by the chain's `confirmPeriodBlocks` parameter and measured in parent chain blocks. Values for Arbitrum's own chains: | Chain configuration | Challenge period | Typical time-to-claimable | | ------------------------------------ | -------------------------------------------- | --------------------------------------------------------------------------------------- | | Arbitrum One (Rollup) | 45818 blocks ≈ 6.4 days | \~7 days (batch + assertion cadence + challenge period) | | Arbitrum Nova (AnyTrust) | 45818 blocks ≈ 6.4 days | \~7 days—AnyTrust changes data availability, **not** the challenge period | | Arbitrum chain with fast withdrawals | Bypassed by unanimous committee confirmation | As low as \~15 minutes on BoLD-enabled chains, plus batch posting and assertion cadence | | Arbitrum Sepolia (testnet) | 20 blocks ≈ 4.0 minutes | Minutes | Additional timing facts your alerting thresholds should account for: * **A challenged assertion takes longer.** If an assertion acquires a rival, confirmation waits for the challenge to resolve (bounded by the challenge period) plus a grace period (14,400 parent chain blocks ≈ 48 hours on Arbitrum One's configuration) before the winner can be confirmed. Under BoLD, an honest withdrawal is delayed by at most one additional challenge period even during a dispute—see [Assertions](/how-arbitrum-works/deep-dives/assertions.md). * **Time-to-funds = time-to-claimable + execution.** Budget for the claim transaction separately; nothing executes it automatically. * **Custom chains choose their own values.** Read `confirmPeriodBlocks()` from the chain's Rollup contract (shown [below](#query-assertion-and-outbox-state-onchain)) rather than assuming the defaults. To learn how operators configure this, see [Configure the challenge period](/launch-arbitrum-chain/chain-config/validation/challenge-period.md). ## Withdrawals from an L3 A withdrawal from an L3 (an Arbitrum chain that settles on Arbitrum One or another child chain) to Ethereum is **two sequential child-to-parent withdrawals**: L3 → L2, then L2 → L1. Each leg has its own challenge period—by default 45818 parent chain blocks (≈ 6.4 days) per leg on mainnet configurations—and each leg's claim must execute before the next leg can begin. The worst case with default settings is therefore roughly two weeks end to end. Monitor each leg independently with the same code from this guide, pointing the provider pair at the relevant chains (L3 + L2 for the first leg, L2 + L1 for the second). Chain operators have three options for reducing the delay: | Option | Time-to-claimable | Trade-off | | ---------------------------------------------------------------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | [Fast withdrawals](/launch-arbitrum-chain/chain-config/validation/fast-withdrawals.md) | Down to \~15 minutes per leg on BoLD-enabled chains | Adds a committee trust assumption (unanimous validator committee); recommended primarily for AnyTrust chains where the DAC already exists | | [Shorter challenge period](/launch-arbitrum-chain/chain-config/validation/challenge-period.md) | Whatever `confirmPeriodBlocks` is set to | Shrinks the window in which fraud can be challenged; weakens the chain's security model | | Third-party liquidity bridges | Minutes | Users receive funds from a liquidity provider rather than the canonical bridge; adds provider trust and fees; per-asset liquidity limits | ## Monitor withdrawals with the SDK The [`@arbitrum/sdk`](https://github.com/OffchainLabs/arbitrum-sdk) (v4, ethers v5) exposes the lifecycle through `ChildToParentMessage` objects and the `ChildToParentMessageStatus` enum, whose three values match the lifecycle table above: `UNCONFIRMED` (0), `CONFIRMED` (1), and `EXECUTED` (2). The SDK handles both current (BoLD) and legacy assertion models automatically, so the same code works across chains. Arbitrum One, Nova, and Arbitrum Sepolia are pre-registered in the SDK. For any other Arbitrum chain, register it first—all required parameters can be derived from the chain's Rollup contract address: ```ts import { providers } from 'ethers'; import { registerCustomArbitrumNetwork, getArbitrumNetworkInformationFromRollup } from '@arbitrum/sdk'; const parentProvider = new providers.JsonRpcProvider(PARENT_RPC_URL); const rollupData = await getArbitrumNetworkInformationFromRollup(ROLLUP_ADDRESS, parentProvider); registerCustomArbitrumNetwork({ chainId: CHILD_CHAIN_ID, name: 'my-arbitrum-chain', parentChainId: rollupData.parentChainId, ethBridge: rollupData.ethBridge, confirmPeriodBlocks: rollupData.confirmPeriodBlocks, isCustom: true, isTestnet: false, }); ``` ### Check the status of a withdrawal Given the withdrawal's child chain transaction hash: ```ts import { providers } from 'ethers'; import { ChildTransactionReceipt, ChildToParentMessageStatus } from '@arbitrum/sdk'; const withdrawalTxHash = WITHDRAWAL_TX_HASH; const parentProvider = new providers.JsonRpcProvider(PARENT_RPC_URL); const childProvider = new providers.JsonRpcProvider(CHILD_RPC_URL); async function main() { const receipt = await childProvider.getTransactionReceipt(withdrawalTxHash); const childReceipt = new ChildTransactionReceipt(receipt); const messages = await childReceipt.getChildToParentMessages(parentProvider); const message = messages[0]; const status = await message.status(childProvider); console.log('Status:', ChildToParentMessageStatus[status]); if (status === ChildToParentMessageStatus.UNCONFIRMED) { // Estimated parent chain block at which the withdrawal becomes executable const firstExecutableBlock = await message.getFirstExecutableBlock(childProvider); console.log('Estimated first executable parent chain block:', firstExecutableBlock.toString()); } } main(); ``` `getFirstExecutableBlock()` returns an estimate of the parent chain block at which the message becomes claimable (based on the covering assertion's creation block plus the challenge period), or `null` once the message is already `CONFIRMED` or `EXECUTED`. Comparing it against the parent chain's current block number gives you a countdown you can surface to users—the same countdown the bridge UI shows. ### Wait for confirmation and execute the claim `waitUntilReadyToExecute()` polls until the message leaves `UNCONFIRMED`, then returns the terminal status. A `ChildToParentMessageWriter` (obtained by passing a parent chain *signer* instead of a provider) can then execute the claim: ```ts import { providers, Wallet } from 'ethers'; import { ChildTransactionReceipt, ChildToParentMessageStatus } from '@arbitrum/sdk'; const withdrawalTxHash = WITHDRAWAL_TX_HASH; const parentProvider = new providers.JsonRpcProvider(PARENT_RPC_URL); const childProvider = new providers.JsonRpcProvider(CHILD_RPC_URL); const parentSigner = new Wallet(PRIVATE_KEY, parentProvider); async function main() { const receipt = await childProvider.getTransactionReceipt(withdrawalTxHash); const childReceipt = new ChildTransactionReceipt(receipt); // Passing a signer yields ChildToParentMessageWriter instances, which can execute claims const messages = await childReceipt.getChildToParentMessages(parentSigner); const message = messages[0]; // Polls every 30 seconds until the message leaves UNCONFIRMED. // On a chain with a live challenge period this call blocks for days — // use it in a worker, or poll status() on your own schedule instead. const status = await message.waitUntilReadyToExecute(childProvider, 30 * 1000); if (status === ChildToParentMessageStatus.CONFIRMED) { const executeTx = await message.execute(childProvider); const executeReceipt = await executeTx.wait(); console.log('Claim executed on parent chain:', executeReceipt.transactionHash); } else { console.log('Message already executed'); } } main(); ``` Under the hood, `execute()` fetches the Merkle proof from the [`NodeInterface`](/arbitrum-essentials/nodeinterface/reference.md) and calls `Outbox.executeTransaction()` on the parent chain. It throws if the message isn't `CONFIRMED` yet. If you need the proof itself (for example, to execute through your own contracts), call `message.getOutboxProof(childProvider)`. ### Index many withdrawals To monitor every withdrawal on a chain (for example, all withdrawals initiated through your bridge frontend), fetch `L2ToL1Tx` events in bulk instead of tracking individual transaction hashes: ```ts import { providers } from 'ethers'; import { ChildToParentMessage, ChildToParentMessageStatus } from '@arbitrum/sdk'; const parentProvider = new providers.JsonRpcProvider(PARENT_RPC_URL); const childProvider = new providers.JsonRpcProvider(CHILD_RPC_URL); const fromBlock = START_BLOCK; const toBlock = 'latest'; async function main() { const events = await ChildToParentMessage.getChildToParentEvents(childProvider, { fromBlock, toBlock, }); console.log(`Found ${events.length} withdrawal(s)`); for (const event of events) { const message = ChildToParentMessage.fromEvent(parentProvider, event); const status = await message.status(childProvider); console.log(`position ${event.position.toString()} | ` + `child tx ${event.transactionHash} | ` + `destination ${event.destination} | ` + `${ChildToParentMessageStatus[status]}`); } } main(); ``` `getChildToParentEvents` also accepts optional `position`, `destination`, and `hash` filters (all indexed fields of the `L2ToL1Tx` event), so you can restrict the scan to withdrawals bound for addresses you care about. For a production monitor, a practical pattern is: 1. **Ingest**: scan `L2ToL1Tx` events in block-range windows on a schedule; store each withdrawal's `position`, transaction hash, destination, and initiation timestamp. 2. **Track**: poll `status()` for stored withdrawals that aren't `EXECUTED` yet. Withdrawals in `UNCONFIRMED` only change state when an assertion is confirmed, so polling them more often than the chain's assertion cadence adds no information; hourly is plenty on a seven-day chain. 3. **Alert**: flag any withdrawal that is `UNCONFIRMED` past your chain's expected time-to-claimable (challenge period + assertion cadence + margin), and any withdrawal sitting in `CONFIRMED` longer than you expect users to leave claims unexecuted. ## Query assertion and outbox state onchain If you prefer raw contract calls—or want to monitor the assertion pipeline itself rather than individual messages—the same lifecycle is observable from three contracts: the `ArbSys` precompile on the child chain, and the Rollup and Outbox contracts on the parent chain. The following snippet checks every stage for one withdrawal: ```ts import { providers, Contract } from 'ethers'; const withdrawalTxHash = WITHDRAWAL_TX_HASH; const rollupAddress = ROLLUP_ADDRESS; const ARBSYS_ADDRESS = '0x0000000000000000000000000000000000000064'; const NODE_INTERFACE_ADDRESS = '0x00000000000000000000000000000000000000C8'; const arbSysAbi = ['event L2ToL1Tx(address caller, address indexed destination, uint256 indexed hash, uint256 indexed position, uint256 arbBlockNum, uint256 ethBlockNum, uint256 timestamp, uint256 callvalue, bytes data)']; const rollupAbi = ['function outbox() view returns (address)', 'function confirmPeriodBlocks() view returns (uint64)', 'event AssertionConfirmed(bytes32 indexed assertionHash, bytes32 blockHash, bytes32 sendRoot)']; const outboxAbi = ['function roots(bytes32) view returns (bytes32)', 'function isSpent(uint256 index) view returns (bool)']; const nodeInterfaceAbi = ['function constructOutboxProof(uint64 size, uint64 leaf) view returns (bytes32 send, bytes32 root, bytes32[] proof)']; const parentProvider = new providers.JsonRpcProvider(PARENT_RPC_URL); const childProvider = new providers.JsonRpcProvider(CHILD_RPC_URL); async function main() { // 1. The L2ToL1Tx event from the withdrawal transaction identifies the message const receipt = await childProvider.getTransactionReceipt(withdrawalTxHash); const arbSys = new Contract(ARBSYS_ADDRESS, arbSysAbi, childProvider); const l2ToL1TxTopic = arbSys.interface.getEventTopic('L2ToL1Tx'); const event = receipt.logs.filter((log) => log.topics[0] === l2ToL1TxTopic).map((log) => arbSys.interface.parseLog(log))[0]; const { position, arbBlockNum } = event.args; console.log('position (outbox leaf index):', position.toString(), '| child block:', arbBlockNum.toString()); // 2. Latest confirmed assertion: its send root commits to all withdrawals up to // the child chain block it covers const rollup = new Contract(rollupAddress, rollupAbi, parentProvider); const confirmed = await rollup.queryFilter(rollup.filters.AssertionConfirmed(), -100000); const latestConfirmation = confirmed[confirmed.length - 1]; const { blockHash, sendRoot } = latestConfirmation.args; // 3. sendCount of that block = how many withdrawals the confirmed assertion covers const confirmedBlock = await childProvider.send('eth_getBlockByHash', [blockHash, false]); const sendCount = Number(confirmedBlock.sendCount); const isCovered = position.lt(sendCount); console.log('confirmed sendCount:', sendCount, '| withdrawal covered by confirmed assertion:', isCovered); // 4. The send root must be registered in the outbox for claims to succeed const outboxAddress = await rollup.outbox(); const outbox = new Contract(outboxAddress, outboxAbi, parentProvider); const registered = (await outbox.roots(sendRoot)) !== '0x' + '0'.repeat(64); console.log('send root registered in outbox:', registered); // 5. Already claimed? console.log('outbox.isSpent(position):', await outbox.isSpent(position)); // 6. Merkle proof for a manual Outbox.executeTransaction call const nodeInterface = new Contract(NODE_INTERFACE_ADDRESS, nodeInterfaceAbi, childProvider); const proofData = await nodeInterface.constructOutboxProof(sendCount, position.toNumber()); console.log('proof root matches confirmed sendRoot:', proofData.root === sendRoot); } main(); ``` How each check maps to the lifecycle: * **`L2ToL1Tx`** (`ArbSys`, child chain): emitted at initiation. Its `position` is the withdrawal's leaf index in the send Merkle tree—the same `index` that `Outbox.executeTransaction()` and `Outbox.isSpent()` take, and the `leaf` argument to `NodeInterface.constructOutboxProof()`. The `destination`, `hash`, and `position` fields are indexed, so monitors can subscribe to them directly. * **`AssertionCreated`** and **`AssertionConfirmed`** (Rollup contract, parent chain): the assertion pipeline. A withdrawal is covered once an assertion whose child chain block has `sendCount > position` exists; it is claimable once such an assertion is *confirmed*. `AssertionConfirmed` carries the confirmed `blockHash` and `sendRoot`. You can also read `latestConfirmed()` and `getAssertion()` on the Rollup contract for point-in-time checks. For an alerting-oriented view of this pipeline (assertion cadence, missed confirmations, validator health), run the [assertion monitor](/launch-arbitrum-chain/operate/monitoring.md#assertion-monitor). * **`SendRootUpdated`** and **`roots`** (Outbox, parent chain): set by the Rollup contract at assertion confirmation. A claim can only succeed if the proof's root is registered in `roots`—this is the onchain meaning of "ready to execute." * **`OutBoxTransactionExecuted`** and **`isSpent(position)`** (Outbox, parent chain): terminal state. The event's `transactionIndex` field equals the withdrawal's `position`. * **`constructOutboxProof(size, leaf)`** (`NodeInterface`, child chain): builds the Merkle proof for a manual claim, where `size` is the confirmed block's `sendCount` and `leaf` is the withdrawal's `position`. The `NodeInterface` is a node-provided virtual contract—call it with `eth_call` against a child chain node; it doesn't exist onchain. Chains on the legacy (pre-BoLD) protocol On chains that haven't upgraded to BoLD, the Rollup contract identifies assertions by sequential node number instead of hash, and the corresponding events are `NodeCreated` and `NodeConfirmed(nodeNum, blockHash, sendRoot)`. The `ArbSys`, `Outbox`, and `NodeInterface` surfaces are unchanged, and the SDK abstracts the difference entirely. If the parent chain is itself an Arbitrum chain (the L3 case), use the Rollup contract's `getAssertionCreationBlockForLogLookup(assertionHash)` getter to obtain block numbers suitable for event queries on the parent chain. ## Diagnose stuck withdrawals Work through the stages in order—each row's check assumes the previous rows passed. "Phase" refers to the [bridge UI mapping](#mapping-bridge-ui-phases-to-protocol-states) above. | Symptom | Stuck at | What to check | Escalation | | ----------------------------------------------------------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `UNCONFIRMED`, and the transaction isn't in a posted batch yet | Batch posting (phase 2, early) | `NodeInterface.getL1Confirmations(blockHash)` returns `0`, or `findBatchContainingBlock(blockNum)` reverts, for the withdrawal's child chain block | Batch poster is down or backed up—chain operator; see [batch poster troubleshooting](/launch-arbitrum-chain/operate/batch-poster-troubleshooting.md) | | `UNCONFIRMED`, batched, but no assertion covers the block | Assertion creation (phase 2) | Latest `AssertionCreated` on the Rollup contract: does the assertion's child chain block have `sendCount > position`? No recent `AssertionCreated` events at all means the active validator isn't posting | Chain operator (validator or proposer health); the [assertion monitor](/launch-arbitrum-chain/operate/monitoring.md#assertion-monitor) alerts on this automatically | | `UNCONFIRMED`, assertion created, challenge period elapsed, still no `AssertionConfirmed` | Assertion confirmation (phase 2 → 3) | Whether the assertion has a rival (a challenge is in progress—confirmation waits for resolution plus a grace period); whether any validator is calling the confirmation function at all | Chain operator; on a challenged chain, monitor the challenge and expect up to one extra challenge period plus grace period | | `CONFIRMED`, but the claim transaction reverts | Execution (phase 3) | `Outbox.isSpent(position)`—`true` means it was already claimed (find the claim via the `OutBoxTransactionExecuted` event with `transactionIndex == position`); for an L3 withdrawal, confirm you're claiming the correct leg on the correct chain; re-derive the proof with `constructOutboxProof` | If the proof root isn't in `Outbox.roots` despite `CONFIRMED` status, something is inconsistent—collect the position, child chain transaction hash, and chain ID, and contact the chain operator | | "Withdrawal is claimable, but funds never arrived" | Execution (phase 3) | Nobody executed the claim—execution is manual. Check `isSpent(position)`; if `false`, execute via the SDK or the bridge UI's **Claim** button. For token withdrawals, funds are released by the parent chain gateway to the destination address once executed | End-user cases: [bridge troubleshooting](/arbitrum-bridge/troubleshooting.md) | Historical data availability Ethereum consensus nodes are required to serve blob data for only 4,096 epochs (≈18 days). Monitors that reconstruct old withdrawal history need archive access: an [archive beacon endpoint for historical blobs](/run-arbitrum-node/beacon-nodes-historical-blobs.md) to re-derive the chain from batch data, and an archive child chain node for the event logs that `constructOutboxProof` reads. On AnyTrust chains, the equivalent concern is the Data Availability Committee's retention window. Claiming a confirmed withdrawal itself is unaffected—proofs are built from child chain logs, not blobs. ## See also * [Tracing bridge transactions](/arbitrum-bridge/bridge-transaction-traceability.md) — follow a single deposit or withdrawal end to end * [Withdraw ETH and messages](/arbitrum-essentials/bridging/withdraw/eth-and-messages.md) and [Withdraw tokens](/arbitrum-essentials/bridging/withdraw/tokens.md) — initiating withdrawals * [Child-to-parent messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md) and [Assertions](/how-arbitrum-works/deep-dives/assertions.md) — protocol deep dives * [Enable fast withdrawals](/launch-arbitrum-chain/chain-config/validation/fast-withdrawals.md) and [Configure the challenge period](/launch-arbitrum-chain/chain-config/validation/challenge-period.md) — chain owner configuration * [Monitoring tools for your chain](/launch-arbitrum-chain/operate/monitoring.md) — the `arbitrum-monitoring` alerting suite * [Exchange integration checklist](/launch-arbitrum-chain/integrations/exchange-integration-checklist.md) — deposit and withdrawal handling for exchanges * [Precompiles reference](/arbitrum-essentials/precompiles/reference.md) (`ArbSys`) and [NodeInterface reference](/arbitrum-essentials/nodeinterface/reference.md) (`constructOutboxProof`) --- > For a complete page index, fetch ## [📄️Estimate gas](/arbitrum-essentials/how-to-estimate-gas.md) [Learn how to estimate gas costs on Arbitrum using eth\_estimateGas, NodeInterface.gasEstimateComponents(), and the Arbitrum SDK. Covers the two-component fee model with L1 data costs and L2 execution costs.](/arbitrum-essentials/how-to-estimate-gas.md) --- > For a complete page index, fetch # Block gas limit, numbers and time > **INFO** — block number vs `block.number` > > Throughout this and other pages, we note that the block number of a chain does not match the value obtained from `block.number`. When using `block.number` in a smart contract, the value obtained will be the block of the first non-Arbitrum ancestor chain. That is: > > * Ethereum, if the chain is a Layer 2 (L2) chain on top of Ethereum, or a Layer 3 (L3) chain on top of an Arbitrum chain > * The parent chain, if it's not Ethereum or an Arbitrum chain (for example, a chain that settles to Base) As with Ethereum, Arbitrum clients submit transactions, and the system executes them later. In Arbitrum, clients submit transactions by posting messages to the Ethereum chain, either [through the Sequencer](/how-arbitrum-works/deep-dives/sequencer.md) or via the chain's [Delayed Inbox](/how-arbitrum-works/deep-dives/sequencer.md). Once in the chain's core inbox contract, transaction processing occurs in order. Generally, some time will elapse between when a message is put into the inbox (and timestamped) and when the contract processes the message and carries out the transaction requested by the message. Additionally, since the calldata/blobs of Arbitrum transactions (or the DAC certificate on AnyTrustchains) is posted to Ethereum, the gas paid when executing them includes a component for the parent chain to cover the costs of the batch poster. This page explains the implications of this mechanism for the block gas limit, block numbers, and the time assumptions associated with transactions submitted to Arbitrum. ## Block gas limit When submitting a transaction to Arbitrum, users incur fees for both the execution cost on Arbitrum and the cost of posting its calldata to Ethereum. Managing the dual cost structure involves adjusting the transaction's gas limit to reflect these two dimensions, resulting in a higher gas limit value than would be seen for pure execution. The gas limit of an Arbitrum block is set to the sum of all transaction gas limits, including the costs associated with posting parent chain data. To accommodate potential variations in parent chain costs, Arbitrum assigns an artificially large gas limit (`1,125,899,906,842,624`) for each block. However, the effective execution gas limit has a cap of 32 million. This cap means that, although the visible gas limit may appear very high, the actual execution costs are constrained within this limit. Understanding this distinction helps clarify why querying a block might show an inflated gas limit that doesn't match the effective execution costs. For a more detailed breakdown of the gas model, refer to [this article on Arbitrum's 2-dimensional fee structure](https://medium.com/offchainlabs/understanding-arbitrum-2-dimensional-fees-fd1d582596c9). ## Block numbers: Arbitrum vs. Ethereum Arbitrum blocks are assigned their own child chain block numbers, distinct from Ethereum's block numbers. A single Ethereum block can include multiple Arbitrum blocks; however, an Arbitrum block cannot span across multiple Ethereum blocks. Thus, any given Arbitrum transaction is associated with exactly one Ethereum block and one Arbitrum block. ### Ethereum (or parent chain) block numbers within Arbitrum Accessing block numbers within an Arbitrum smart contract (i.e., `block.number` in Solidity) will return a value *close to* (but not necessarily exactly) the block number of the first non-Arbitrum ancestor chain where the sequencer received the transaction. The "first non-Arbitrum ancestor chain" is: * Ethereum, if the chain is an L2 chain on top of Ethereum, or an L3 chain on top of an Arbitrum chain * The parent chain, if it's not Ethereum or an Arbitrum chain (for example, a chain that settles to Base) ```solidity // some Arbitrum contract: block.number // => returns the approximate block number of the first non-Arbitrum ancestor chain ``` As a general rule, any timing assumptions a contract makes about block numbers and timestamps should be considered generally reliable in the longer term (i.e., on the order of at least several hours) but unreliable in the shorter term (minutes). (These are generally the same assumptions one should operate under when using block numbers directly on Ethereum!) For how this affects specific Solidity operations like `block.number` and `block.timestamp`, see [Solidity support](/arbitrum-essentials/arbitrum-vs-ethereum/solidity-support.md). > **INFO** — EIP-2935 difference > > [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) adds another way to retrieve block hashes by making a call to a contract. The contract is at the same address and has the same interface as the original. It was modified to have a larger buffer and different code, but it remains usable in the same way to retrieve past L2 block hashes. ### Arbitrum block numbers Arbitrum blocks have their own block numbers, starting at `0` at the Arbitrum genesis block and updating sequentially. ArbOS and the sequencer are responsible for delineating when one Arbitrum block ends and the next one begins. However, block creation depends entirely on chain usage, meaning that block production only occurs when there are transactions to sequence. In active chains, one can expect to see Arbitrum blocks produced at a relatively steady rate. In less active chains, block production might be sporadic depending on the rate at which transactions are received. A client that queries an Arbitrum node's RPC interface (e.g., transaction receipts) will receive the transaction's Arbitrum block number as the standard block number field. The block number of the first non-Arbitrum ancestor chain will also be included in the added `l1BlockNumber` field. ```typescript const txnReceipt = await arbitrumProvider.getTransactionReceipt('0x...'); /** txnReceipt.l1BlockNumber => Approximate block number of the first non-Arbitrum ancestor chain */ ``` The Arbitrum block number can also be retrieved within an Arbitrum contract via the [ArbSys](/arbitrum-essentials/precompiles/reference.md#arbsys) precompile: ```solidity ArbSys(100).arbBlockNumber() // returns Arbitrum block number ``` ### Example The following example illustrates timings on a chain that settles to Ethereum (similar to Arbitrum One), although it also applies to L3 chains that settle to an Arbitrum chain. | Wall clock time | 12:00 am | 12:00:15 am | 12:00:30 am | 12:00:45 am | 12:01 am | 12:01:15 am | | ------------------------------------- | -------- | ----------- | ----------- | ----------- | -------- | ----------- | | Ethereum `block.number` | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | | Chain's `block.number` \* | 1000 | 1000 | 1000 | 1000 | 1004 | 1004 | | Chain's block number (from RPCs) \*\* | 370000 | 370005 | 370006 | 370008 | 370012 | 370015 | Info txnReceipt.blockNumber => Arbitrum block number \_\* **The chain's `block.number`:** updated to sync with Ethereum's `block.number` every 13 to 15 seconds (occasionally longer). *\*\* **Chain's block number from RPCs:** note that this can be updated multiple times per Ethereum block (this lets the sequencer give sub-Ethereum-block-time transaction receipts.)* ### Case study: the Multicall contract The Multicall contract provides a valuable case study for the differences between various block numbers. The [canonical implementation](https://github.com/makerdao/multicall/) of Multicall returns the value of `block.number`. When used out of the box, some applications may exhibit unintended behavior. You can find a version of the adapted `Multicall2` deployed on Arbitrum One at [0x842eC2c7D803033Edf55E478F461FC547Bc54EB2](https://arbiscan.io/address/0x842eC2c7D803033Edf55E478F461FC547Bc54EB2#code). By default, the `getBlockNumber`, `tryBlockAndAggregate`, and `aggregate` functions return the child chain block number. This function allows you to use this value to compare your state against the tip of the chain. The `getL1BlockNumber` function is queriable if applications need to surface the block number of the first non-Arbitrum ancestor chain. ## Block timestamps: Arbitrum vs. Ethereum Block timestamps on Arbitrum are not linked to the timestamp of the parent chain block. They are updated every child chain block based on the sequencer's clock. These timestamps must follow these two rules: 1. Must always be equal to or greater than the previous child chain block timestamp 2. Must fall within the established boundaries (24 hours earlier than the current time or one hour in the future). More on this below. Furthermore, for transactions that are force-included from the parent chain (bypassing the Sequencer), the block timestamp will be equal to either the parent chain timestamp when the transaction was put in the Delayed Inbox on the parent chain (not when it was force-included), or the child chain timestamp of the previous child chain block, whichever of the two timestamps is greater. ### Timestamp boundaries of the sequencer As mentioned, block timestamps are usually set based on the sequencer's clock. Because there's a possibility that the Sequencer fails to post batches on the parent chain (i.e., Ethereum) for a period of time, it should have the ability to slightly adjust the timestamp of the block to account for those delays and prevent any potential reorganizations of the chain. To limit the degree to which the Sequencer can adjust timestamps, some boundaries are set, currently to 24 hours earlier than the current time, and one hour in the future. --- > For a complete page index, fetch # Differences between Arbitrum and Ethereum: Overview Arbitrum's design is to be as compatible and consistent with Ethereum as possible, from its high-level RPCs to its low-level bytecode and everything in between. Decentralized app developers with experience building on Ethereum will likely find that little to no new specific knowledge is required to build on Arbitrum. This article outlines the key differences, benefits, and potential pitfalls that devs should be aware of when working with Arbitrum. This first page serves as an outline, with links to the relevant pages. ## STF in Ethereum In Ethereum, the STF receives transactions as inputs, processes them via the EVM, and produces the final state as output. The Ethereum state is a vast data structure represented by a modified Merkle Patricia Trie. This structure holds all accounts, linking them via hashes and reducing the entire state to a single root hash stored on the blockchain. The Ethereum Virtual Machine (EVM) operates similarly to a mathematical function: given an input, it produces a deterministic output. Ethereum's STF encapsulates this behavior: $$ Y(S, T) = S' $$ Here, `S` represents the current state, `T` denotes the transaction, and `S'` is the new state resulting from the execution of `T`. The EVM operates as a stack machine with a maximum depth of 1024 items. Each item is a 256-bit word, chosen for compatibility with 256-bit cryptography (e.g., Keccak-256 hashes and secp256k1 signatures). During execution, the EVM uses transient *memory* (a word-addresses byte array) that only persists for the duration of a transaction. In contrast, each contract maintains a persistent Merkle Patricia *storage* trie–a word-addressable word array–that forms part of the global state. smart contract bytecode compiles into a series of EVM opcodes that perform standard stack operations (such as `XOR`, `AND`, `ADD`, `SUB`) and blockchain-specific operations (such as `ADDRESS`, `BALANCE`, `BLOCKHASH`). Geth (go-Ethereum) is one of the primary client implementations of Ethereum, serving as the practical embodiment of both the STF and the EVM execution engine. It processes transactions by executing the smart contract's bytecode and updating the global state, ensuring that every state change is deterministic and secure. In essence, Geth converts transaction inputs into precise computational steps within the EVM, maintaining the intricate data structures that underpin Ethereum's blockchain. Its robust design not only powers the core operations of Ethereum but also provides the foundation for advanced modifications in platforms like the Arbitrum Nitro stack. ## STF on Arbitrum The Arbitrum Nitro stack implements a modified version of Ethereum's STF. While it retains the core principles of Ethereum, several Arbitrum-specific features and processes distinguish it from Ethereum's implementation. Key differences include: ### Block numbers and time Time in Arbitrum chains is tricky. The timing assumptions that apply to Ethereum blocks don't exactly carry over to Arbitrum blocks. See [Block numbers and time](/arbitrum-essentials/arbitrum-vs-ethereum/block-numbers-and-time.md) for details about how block numbers and time work in Arbitrum. ### RPC methods Although the majority of RPC methods follow the same behavior as Ethereum, some methods may produce a different result or add additional information when used on an Arbitrum chain. For more details, see [RPC methods](/arbitrum-essentials/arbitrum-vs-ethereum/rpc-methods.md). ### Solidity support You can deploy Solidity contracts onto Arbitrum just like you do on Ethereum. There are only a few minor functional differences. For more information, refer to [Solidity support](/arbitrum-essentials/arbitrum-vs-ethereum/solidity-support.md). ### Gas accounting The fees for executing an Arbitrum transaction function similarly to gas fees on Ethereum. However, Arbitrum transactions must also pay a fee component to cover the cost of posting their calldata to the parent chain (for example, calldata on Arbitrum One, a child chain, is posted to Ethereum, a parent chain). Find more information about the two components of gas fees in [Gas and fees](/how-arbitrum-works/deep-dives/gas-and-fees.md) and parent chain pricing. ### Cross-chain messaging Arbitrum chains support arbitrary message passing from a parent chain (for example, Ethereum) to a child chain (for example, Arbitrum One or Arbitrum Nova). These are commonly known as "parent chain to child chain messages". Developers using this functionality should familiarize themselves with how they work. For more information, refer to [Parent chain to child chain messaging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md). Similarly, Arbitrum chains can also send messages to the parent chain. Find more information about them in [Child chain to parent chain messaging and the outbox](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md). ### Precompiles Besides supporting all precompiles available in Ethereum, Arbitrum provides child chain-specific precompiles with methods that smart contracts can call in the same way as Solidity functions. For a full reference, see the [Precompiles](/arbitrum-essentials/precompiles/overview.md) page. ### NodeInterface The Arbitrum Nitro software includes a special `NodeInterface` contract, available at address `0xc8`, that is only accessible via RPCs (deployed offchain, making it inaccessible to smart contracts). For more details, see [NodeInterface](/arbitrum-essentials/nodeinterface/overview.md). --- > For a complete page index, fetch # Nonce management Arbitrum does not maintain a long-lived "pending" mempool as an Ethereum node does. Transactions submitted to the [Sequencer](/how-arbitrum-works/deep-dives/sequencer.md) are ordered and executed almost immediately, so out-of-order nonces cannot wait minutes or hours for their predecessor to arrive — they are held only briefly, then rejected. This page explains how Arbitrum's sequencer handles out-of-order nonces, the error you will see when the wait window expires, what `eth_getTransactionCount("pending")` returns, and the submission patterns that work safely under these constraints. ## How Arbitrum handles out-of-order nonces When an Ethereum execution client receives a transaction whose nonce is higher than the sender's current state nonce, it places the transaction in its pending pool and keeps it there for an extended period (typically configured in minutes-to-hours, sometimes longer) while it waits for the missing predecessor transaction(s) to arrive. Concurrent submitters routinely rely on this behavior. Arbitrum's sequencer behaves differently. It maintains an in-memory **nonce-failure retry buffer** rather than a long-lived pending pool: 1. When a transaction arrives whose nonce is higher than the highest nonce the sequencer has already seen for that sender, the sequencer does **not** reject it immediately. Instead, it places the transaction in the retry buffer and starts a short expiry timer. 2. If the predecessor transaction arrives before the timer fires, the held transaction is revived and put back into the execution queue—it goes through normally, as if it had arrived in order. 3. If the timer fires first, the held transaction is dropped, and the sender receives a `nonce too high` error in response to its original `eth_sendRawTransaction` call. The exact wait window and buffer capacity are configured by the operator running the sequencer. On Arbitrum One, the window is on the order of **seconds**, not minutes—short enough that you should treat it as a tolerance for network jitter and tx-ordering races, not as a substitute for the Ethereum pending pool. The practical implication is that fire-and-forget concurrent submission patterns that work on Ethereum will not work reliably on Arbitrum. If you submit transactions `N` and `N+1` from two different processes at the same time, you cannot rely on the sequencer holding `N+1` while it waits for `N` to land — the window is too short. ## The error you will see When the retry window expires without the predecessor arriving, the call that submitted the out-of-order transaction returns the error string: ```text nonce too high ``` This is the same literal error string used by Ethereum's execution layer, but the meaning is different: * **On Ethereum**, `nonce too high` usually means the gap between the sender’s current nonce and the pending pool's buffer is larger than the pending pool can or will buffer (often a multi-thousand-slot gap, configurable per client). * **On Arbitrum**, `nonce too high` simply means the predecessor transaction did not reach the sequencer within the retry window. A typical log line from a real out-of-order submission looks like: ```text reqid=… err="nonce too high" ``` If you see this in production after migrating an Ethereum-based submission pipeline to Arbitrum, the most likely cause is that two or more workers are submitting transactions for the same sender address without coordinating nonce allocation between them—not that any individual nonce is wrong. ## `eth_getTransactionCount("pending")` on Arbitrum On an Ethereum node, `eth_getTransactionCount(address, "pending")` returns a value that includes any transactions currently sitting in the pending pool waiting to be mined. Many concurrent-submission designs rely on this to allocate the next nonce. On Arbitrum, `eth_getTransactionCount(address, "pending")` returns the same value as `eth_getTransactionCount(address, "latest")`. The sequencer's retry buffer is an internal sequencer state and is not exposed through the RPC interface, so there is no way for an external caller to observe whether a higher-nonce transaction is currently being held. Services that previously relied on `"pending"` to coordinate concurrent submitters should track outstanding nonces themselves rather than fetch them from the RPC. ## Further reading * [RPC methods](/arbitrum-essentials/arbitrum-vs-ethereum/rpc-methods.md) — full list of RPC method differences between Arbitrum and Ethereum. * [The Sequencer and Censorship Resistance](/how-arbitrum-works/deep-dives/sequencer.md) — how the sequencer orders and processes transactions in Arbitrum. --- > For a complete page index, fetch # RPC methods Although the majority of RPC methods follow the same behavior as in Ethereum, some methods may produce a different result or add more information when used on an Arbitrum chain. This page covers the differences in response body fields you'll find when calling RPC methods on an Arbitrum chain vs on Ethereum. > **INFO** > > Comprehensive documentation on all generally available JSON-RPC methods for Ethereum can be found at [ethereum.org](https://ethereum.org/en/developers/docs/apis/json-rpc/). As Arbitrum has `go-ethereum` at its core, most of the documented methods there can be used with no modifications. ## Transactions When calling [`eth_getTransactionByHash`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionbyhash) and other methods that return a transaction, Arbitrum includes a few additional fields and leverages some existing fields in different ways than Ethereum. ### Transaction types In addition to the [three transaction types](https://ethereum.org/en/developers/docs/transactions/#types-of-transactions) currently supported on Ethereum, Arbitrum adds additional types listed below and [documented in full detail here](/how-arbitrum-works/reference/geth.md#transaction-types). Many of these types are emitted when [bridging from a parent chain](/arbitrum-essentials/bridging/cross-chain-messaging.md#ethereum-to-arbitrum-messaging). On RPC calls that return transactions, the `type` field will reflect the custom codes where applicable. | Transaction type code | Transaction type name | Description | | --------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `100` | `ArbitrumDepositTxType` | Used to deposit **ETH** from a parent chain to a child chain via the Arbitrum bridge | | `101` | `ArbitrumUnsignedTxType` | Used to call a child chain contract from a parent chain, originated by a user through the Arbitrum bridge | | `102` | `ArbitrumContractTxType` | Used to call a child chain contract from a parent chain, originated by a contract through the Arbitrum bridge | | `104` | `ArbitrumRetryTxType` | Used to [manually redeem a retryable ticket](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md) on a child chain that failed to execute automatically (usually due to low gas) | | `105` | `ArbitrumSubmitRetryableTxType` | Used to [submit a retryable ticket](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#submission) via the Arbitrum bridge on the parent chain | | `106` | `ArbitrumInternalTxType` | Internal transactions created by the ArbOS itself for certain state updates, like the parent chain base fee and the block number | ### Additional fields On RPC calls that return transactions, the following fields are added to the returned object. | Field name | Description | | ----------- | -------------------------------------------------------------------------------------------------- | | `requestId` | On parent to child chain transactions, this field is added to indicate position in the Inbox queue | ## Arbitrum specific `eth` JSON-RPC additions * `eth_sendRawTransactionConditional` submits a raw signed transaction with extra preconditions that the sequencer must verify before accepting it. If any condition fails, the transaction is rejected with JSON-RPC error -32003 (rejected). | `ConditionalOptions` payload | Description | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `knownAccounts` | map of address → (storageRootHash slot: expectedValue). The transaction is rejected unless each account's storage root matches, or each listed slot still holds the expected value. | | `blockNumberMin` / `blockNumberMax` | L1 block-number window the transaction is valid. | | `timestampMin` / `timestampMax` | L2 timestamp window. | * `eth_sendRawTransactionSync` submits a raw signed transaction and blocks until the node sees its receipt (or a timeout fires), returning the full receipt instead of just the tx hash. ### Existing fields with different behavior On RPC calls that return transactions, the following fields will have different content than what's received on Ethereum. | Field name | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `from` | On parent to child chain transactions, this field will contain the [*aliased* version](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#address-aliasing) of the parent chain's `msg.sender` | ## Transaction receipts When calling [`eth_getTransactionReceipt`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionreceipt), Arbitrum includes a few additional fields and leverages some existing fields in different ways than Ethereum. ### Additional fields On RPC calls that return transaction receipts, the following fields are added to the returned object. | Field name | Description | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `l1BlockNumber` | The block number of the first non-Arbitrum ancestor chain that is usable for `block.number` calls. More information in [Block numbers and time](/arbitrum-essentials/arbitrum-vs-ethereum/block-numbers-and-time.md) | | `gasUsedForL1` | The amount of gas spent on parent chain calldata in units of child chain gas. More information in [Gas and fees](/how-arbitrum-works/deep-dives/gas-and-fees.md) | ## Blocks When calling [`eth_getBlockByHash`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getblockbyhash) and other methods that return a block, Arbitrum includes a few additional fields and leverages some existing fields in different ways than Ethereum. ### Additional fields On RPC calls that return a block, the following fields are added to the returned object. | Field name | Description | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `l1BlockNumber` | An approximate block number of the first non-Arbitrum ancestor chain that occurred before this child chain block. More information in [Block numbers and time](/arbitrum-essentials/arbitrum-vs-ethereum/block-numbers-and-time.md) | | `sendCount` | The number of child-to-parent chain messages since Nitro genesis | | `sendRoot` | The Merkle root of the outbox tree state | ### Existing fields with different behavior On RPC calls that return a block, the following fields will have different content than what's received on Ethereum. | Field name | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `extraData` | This field is equivalent to `sendRoot` | | `mixHash` | First 8 bytes are equivalent to `sendCount`, second 8 bytes are equivalent to `l1BlockNumber` | | `difficulty` | Fixed at `0x1` | | `gasLimit` | Value is fixed at `0x4000000000000`, but it's important to note that Arbitrum One currently has a 32M gas limit per block. See [Chain params](/arbitrum-essentials/reference/chain-params.md) for the gas limit of other chains | ## Other methods that are slightly different ### `eth_syncing` Calling `eth_syncing` returns false when the node is fully synced (just like on Ethereum). If the node is still syncing, `eth_syncing` returns an object with data about the synchronization status. Here, we provide more details. #### Understanding messages, batches, and blocks Nitro nodes receive transactions from their parent chain and the sequencer feed in the form of messages. These messages may contain multiple transactions that are executed by the node, which then produces blocks. Each message produces exactly one block. In most Nitro chains, the message number and the block number are the same. However, Arbitrum One has pre-Nitro (classic) blocks, so for that chain, message `0` produced block `22207818` (blocks before that one are 'classic' blocks). Keep in mind that the offset between the message and block number remains constant throughout the chain. On the parent chain, messages appear in batches. The number of messages per batch changes between batches. #### Custom `eth_syncing` fields > **INFO** > > Note that the exact output for the `eth_syncing` RPC call of an out-of-sync Nitro node is not considered a stable API. It is still being actively developed and can be modified without notice between versions. | Field name | Description | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `batchSeen` | Last batch number observed on the parent chain | | `batchProcessed` | Last batch that was processed on the parent chain. Processing means dividing the batch into messages | | `messageOfProcessedBatch` | Last message in the last processed batch | | `msgCount` | Number of messages known/queued by the Nitro node | | `blockNum` | Last block created by the Nitro node (up-to-date child chain block the node is synced to) | | `messageOfLastBlock` | Message that was used to produce the block above | | `broadcasterQueuedMessagesPos` | If different than `0`, this is expected to be greater than `msgCount`. This field notes a message that was read from the feed but not processed because earlier messages are still missing | | `lastL1BlockNum` | Last block number of the first non-Arbitrum ancestor chain that Nitro sees. This is for debugging the connection with the parent chain | | `lastl1BlockHash` | Last block hash from the parent chain that Nitro sees. This is for debugging the connection with the parent chain | > **INFO** — Potential, but not expected error > > If the sync process encounters an error while trying to collect the data above this error will be added to the response. #### Understanding common scenarios * If `batchSeen` > `batchProcessed`, some batches have still not been processed * If `msgCount` > `messageOfLastBlock`, some messages have been processed, but not all relevant blocks have been built (this is usually the longest stage while syncing a new node) * If `broadcasterQueuedMessagesPos` > `msgCount`, the feed is ahead of the last message known to the node ### `debug_traceTransaction` The Nitro node provides a native tracer for debugging Stylus contracts called `stylusTracer`, which returns a JSON array with objects containing the metadata for each executed HostIO. HostIOs are calls the WasmVM makes to read and write data in the EVM. With the result of this tracer and the code for the Stylus contract, you have all the data to understand what happened in a Stylus transaction. > **INFO** > > The `cargo-stylus` command-line tool uses the `stylusTracer` to replay transactions locally inside a debugger. More information can be found on [How to debug Stylus transactions using Cargo Stylus Replay](/stylus/cli-tools/debugging-tx.md). The table below describes each field of the `stylusTracer` return value. | Field Name | Description | | ---------- | ------------------------------------------------------------- | | `name` | Name of the executing HostIO. | | `args` | Arguments of the HostIO encoded as hex. | | `outs` | Outputs of the HostIO encoded as hex. | | `startInk` | Amount of Ink before executing the HostIO. | | `endInk` | Amount of Ink after executing the HostIO. | | `address` | For call HostIOs, the address of the called contract. | | `steps` | For call HostIOs, the steps performed by the called contract. | For example, the command below illustrates how to call this tracer for a transaction: ```shell curl -s \ -X POST \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"debug_traceTransaction","params":["", {"tracer": "stylusTracer"}],"id":1}' \ ``` The result of this call will be something along the lines of: ```shell { "jsonrpc": "2.0", "id": 1, "result": [ { "args": "0x00000024", "endInk": 116090000, "name": "user_entrypoint", "outs": "0x", "startInk": 116090000 }, { "args": "0x", "endInk": 116057558, "name": "msg_reentrant", "outs": "0x00000000", "startInk": 116065958 }, { "args": "0x", "endInk": 115937952, "name": "read_args", "outs": "0x6c5283490000000000000000000000003bdff922e18bc03f1cf7b2a8b65a070cbec944f2", "startInk": 115951512 }, ... ] } ``` --- > For a complete page index, fetch # Solidity support Arbitrum chains are Ethereum-compatible and, therefore, allow you to trustlessly deploy Solidity smart contracts, as well as contracts written in Vyper or any other language that compiles to EVM bytecode. However, when calling certain properties and functions on a Solidity smart contract, there are some differences between the result you'd obtain if that contract were on Ethereum and the result on Arbitrum. This page compiles a list of functions and properties that return a different result when called in Arbitrum. ## Differences from Solidity on Ethereum Although Arbitrum supports Solidity code, there are differences in the effects of a few operations, including language features that don't make sense in the child chain context. | Operation | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `blockhash(x)` | Returns a cryptographically insecure, pseudo-random hash for `x` within the range `block.number - 256 <= x < block.number`. If `x` is outside of this range, `blockhash(x)` will return `0`. This hash includes `blockhash(block.number)`, which always returns `0` just like on Ethereum. The returned hashes do not come from the parent chain. ⚠️ Arbitrum's child chain block hashes should not be relied on as a secure source of randomness. | | `block.coinbase` | Returns the designated internal address `0xA4b000000000000000000073657175656e636572` if a sequencer posted the message. If it's a delayed message, it returns the address of the delayed message's poster (**Note**: the handling of delayed message's `block.coinbase` will likely be changed in a future ArbOS version). | | `block.difficulty` | Returns the constant 1. | | `block.prevrandao` | Returns the constant 1. | | `block.number` | Returns an "estimate" of the block number of the first non-Arbitrum ancestor chain at which the sequencer received the transaction. For more information, see [Block numbers and time](/arbitrum-essentials/arbitrum-vs-ethereum/block-numbers-and-time.md). | | `msg.sender` | Works the same way it does on Ethereum for regular child chain to child chain transactions. For transactions submitted via the delayed inbox, it will return the child chain address alias of the parent chain contract that triggered the message. For more information, see [address aliasing](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#address-aliasing). The aliased value also appears in the `from` field on RPC responses — see [RPC methods](/arbitrum-essentials/arbitrum-vs-ethereum/rpc-methods.md#existing-fields-with-different-behavior). | | OPCODE `PUSH0` | This OPCODE was added as part of ArbOS 11 and is now supported. | --- > For a complete page index, fetch # Configure custom gateway bridging > **CAUTION** — Do you really need a custom gateway? > > Before implementing and deploying a custom gateway, strongly consider the current solutions that Arbitrum's token bridge provides: the [standard gateway](/arbitrum-essentials/bridging/configure-token-gateway/standard.md) and the [generic-custom gateway](/arbitrum-essentials/bridging/configure-token-gateway/generic-custom.md). These solutions cover the majority of bridging needs. If you're unsure about your approach, you can always ask for assistance on our [Discord server](https://discord.gg/arbitrum). In this how-to, you'll learn how to bridge your own token between Ethereum (the parent chain) and Arbitrum (the child chain), using a custom gateway. For alternative ways of bridging tokens, check out the [token bridging overview](/arbitrum-essentials/bridging/overview.md). Familiarity with [Arbitrum's token bridge system](/how-arbitrum-works/deep-dives/token-bridging.md), smart contracts, and decentralized application development is expected. If you're new to developing on Arbitrum, consider reviewing our [Quickstart: Build a dApp with Arbitrum (Solidity, Remix)](/build-decentralized-apps/quickstart-solidity-remix.md) before proceeding. We'll use [Arbitrum's SDK](https://github.com/OffchainLabs/arbitrum-sdk) throughout this how-to, although no prior knowledge is required. We will go through all the steps involved in the process. However, if you want to jump straight to the code, we have created a [custom gateway bridging tutorial script](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/custom-gateway-bridging) that encapsulates the entire process. ## Step 0: Review the prerequisites (a.k.a. do I really need a custom gateway?) Before implementing and deploying a custom gateway, strongly consider the current solutions that Arbitrum's token bridge provides: the [standard gateway](/arbitrum-essentials/bridging/configure-token-gateway/standard.md) and the [generic-custom gateway](/arbitrum-essentials/bridging/configure-token-gateway/generic-custom.md). These solutions cover the majority of bridging needs. If you're unsure about your approach, you can always ask for assistance on our [Discord server](https://discord.gg/arbitrum). There are several prerequisites to consider when deploying your own custom gateway. First of all, the **parent chain counterpart of the gateway** must implement the [`IL1ArbitrumGateway`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/IL1ArbitrumGateway.sol) and [`ITokenGateway`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/libraries/gateway/ITokenGateway.sol) interfaces. This conformity means that it must have, at least: * A method `outboundTransferCustomRefund`, to handle forwarded calls from `L1GatewayRouter.outboundTransferCustomRefund`. It should only allow calls from the router. * A method `outboundTransfer`, to handle forwarded calls from `L1GatewayRouter.outboundTransfer`. It should only allow calls from the router. * A method `finalizeInboundTransfer`, to handle messages coming **only** from the child chain's gateway. * Two methods, `calculateL2TokenAddress` and `getOutboundCalldata`, to handle other bridging operations. * Methods to send cross-chain messages through the [Inbox contract](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/bridge/Inbox.sol). You can view an example implementation in `sendTxToL2` and `sendTxToL2CustomRefund` on [`L1ArbitrumMessenger`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/L1ArbitrumMessenger.sol). Suppose you intend to use permissionless token registration in your gateway. In that case, your parent chain gateway should also have a `registerCustomL2Token` method, similar to the one method in Arbitrum’s [generic-custom gateway](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol#L146). On the other hand, the **child chain counterpart of the gateway** must conform to the [`ITokenGateway`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/libraries/gateway/ITokenGateway.sol) interface, meaning that it must have at least: * A method `outboundTransfer`, to handle external calls, and forwarded calls from `L2GatewayRouter.outboundTransfer`. * A method `finalizeInboundTransfer`, to handle messages coming **only** from the parent chain's gateway. * Two methods, `calculateL2TokenAddress` and `getOutboundCalldata`, to handle other bridging operations. * Methods to send cross-chain messages through the [ArbSys precompile](/arbitrum-essentials/precompiles/reference.md#arbsys). You can view an example implementation of `sendTxToL1` on [L2ArbitrumMessenger](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/L2ArbitrumMessenger.sol). ### What about my custom tokens? If you are deploying custom gateways, you will likely want to support your custom tokens on both the parent and child chains. They must also meet several requirements, detailed in [How to bridge tokens via Arbitrum's generic-custom gateway](/arbitrum-essentials/bridging/configure-token-gateway/generic-custom.md). ## Step 1: Create a gateway and deploy it on the parent chain > **CAUTION** — This code is for testing purposes > > The code in the following sections is intended for testing purposes only and doesn't guarantee any level of security. It hasn't undergone any formal audit or security analysis, so it isn't ready for production use. Exercise caution and due diligence while using this code in any environment. We'll begin by creating our custom gateway and deploying it to the parent chain. A good example of a custom gateway is [Arbitrum’s generic-custom gateway](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol). It implements all required methods and adds additional methods, enabling the gateway to support a wide variety of tokens for bridging. In this case, we’ll use a simpler approach. We’ll create a gateway that supports only one token and can be enabled or disabled by the contract owner. It will also implement all necessary methods. To simplify the deployment process even further, we won’t worry about setting the addresses of the counterpart gateway and the custom tokens at deployment time. Instead, we will use a function, `setTokenBridgeInformation`, that the contract owner will call to initialize the gateway. ```solidity // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; import "./interfaces/ICustomGateway.sol"; import "./CrosschainMessenger.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; /** * @title Example implementation of a custom gateway to be deployed on L1 * @dev Inheritance of Ownable is optional. In this case we use it to call the function setTokenBridgeInformation * and simplify the test */ contract L1CustomGateway is IL1CustomGateway, L1CrosschainMessenger, Ownable { // Token bridge state variables address public l1CustomToken; address public l2CustomToken; address public l2Gateway; address public router; // Custom functionality bool public allowsDeposits; /** * Contract constructor, sets the L1 router to be used in the contract's functions and calls L1CrosschainMessenger's constructor * @param router_ L1GatewayRouter address * @param inbox_ Inbox address */ constructor( address router_, address inbox_ ) L1CrosschainMessenger(inbox_) { router = router_; allowsDeposits = false; } /** * Sets the information needed to use the gateway. To simplify the process of testing, this function can be called once * by the owner of the contract to set these addresses. * @param l1CustomToken_ address of the custom token on L1 * @param l2CustomToken_ address of the custom token on L2 * @param l2Gateway_ address of the counterpart gateway (on L2) */ function setTokenBridgeInformation( address l1CustomToken_, address l2CustomToken_, address l2Gateway_ ) public onlyOwner { require(l1CustomToken == address(0), "Token bridge information already set"); l1CustomToken = l1CustomToken_; l2CustomToken = l2CustomToken_; l2Gateway = l2Gateway_; // Allows deposits after the information has been set allowsDeposits = true; } /// @dev See {ICustomGateway-outboundTransfer} function outboundTransfer( address l1Token, address to, uint256 amount, uint256 maxGas, uint256 gasPriceBid, bytes calldata data ) public payable override returns (bytes memory) { return outboundTransferCustomRefund(l1Token, to, to, amount, maxGas, gasPriceBid, data); } /// @dev See {IL1CustomGateway-outboundTransferCustomRefund} function outboundTransferCustomRefund( address l1Token, address refundTo, address to, uint256 amount, uint256 maxGas, uint256 gasPriceBid, bytes calldata data ) public payable override returns (bytes memory res) { // Only execute if deposits are allowed require(allowsDeposits == true, "Deposits are currently disabled"); // Only allow calls from the router require(msg.sender == router, "Call not received from router"); // Only allow the custom token to be bridged through this gateway require(l1Token == l1CustomToken, "Token is not allowed through this gateway"); address from; uint256 seqNum; { bytes memory extraData; uint256 maxSubmissionCost; (from, maxSubmissionCost, extraData) = _parseOutboundData(data); // The inboundEscrowAndCall functionality has been disabled, so no data is allowed require(extraData.length == 0, "EXTRA_DATA_DISABLED"); // Escrowing the tokens in the gateway IERC20(l1Token).transferFrom(from, address(this), amount); // We override the res field to save on the stack res = getOutboundCalldata(l1Token, from, to, amount, extraData); // Trigger the crosschain message seqNum = _sendTxToL2CustomRefund( l2Gateway, refundTo, from, msg.value, 0, maxSubmissionCost, maxGas, gasPriceBid, res ); } emit DepositInitiated(l1Token, from, to, seqNum, amount); res = abi.encode(seqNum); } /// @dev See {ICustomGateway-finalizeInboundTransfer} function finalizeInboundTransfer( address l1Token, address from, address to, uint256 amount, bytes calldata data ) public payable override onlyCounterpartGateway(l2Gateway) { // Only allow the custom token to be bridged through this gateway require(l1Token == l1CustomToken, "Token is not allowed through this gateway"); // Decoding exitNum (uint256 exitNum, ) = abi.decode(data, (uint256, bytes)); // Releasing the tokens in the gateway IERC20(l1Token).transfer(to, amount); emit WithdrawalFinalized(l1Token, from, to, exitNum, amount); } /// @dev See {ICustomGateway-getOutboundCalldata} function getOutboundCalldata( address l1Token, address from, address to, uint256 amount, bytes memory data ) public pure override returns (bytes memory outboundCalldata) { bytes memory emptyBytes = ""; outboundCalldata = abi.encodeWithSelector( ICustomGateway.finalizeInboundTransfer.selector, l1Token, from, to, amount, abi.encode(emptyBytes, data) ); return outboundCalldata; } /// @dev See {ICustomGateway-calculateL2TokenAddress} function calculateL2TokenAddress(address l1Token) public view override returns (address) { if (l1Token == l1CustomToken) { return l2CustomToken; } return address(0); } /// @dev See {ICustomGateway-counterpartGateway} function counterpartGateway() public view override returns (address) { return l2Gateway; } /** * Parse data received in outboundTransfer * @param data encoded data received * @return from account that initiated the deposit, * maxSubmissionCost max gas deducted from user's L2 balance to cover base submission fee, * extraData decoded data */ function _parseOutboundData(bytes memory data) internal pure returns ( address from, uint256 maxSubmissionCost, bytes memory extraData ) { // Router encoded (from, extraData) = abi.decode(data, (address, bytes)); // User encoded (maxSubmissionCost, extraData) = abi.decode(extraData, (uint256, bytes)); } // -------------------- // Custom methods // -------------------- /** * Disables the ability to deposit funds */ function disableDeposits() external onlyOwner { allowsDeposits = false; } /** * Enables the ability to deposit funds */ function enableDeposits() external onlyOwner { require(l1CustomToken != address(0), "Token bridge information has not been set yet"); allowsDeposits = true; } } ``` `IL1CustomGateway` is an interface very similar to `ICustomGateway`, and `L1CrosschainMessenger` implements a method to send the cross-chain message to the child chain through the Inbox. ```solidity /** * @title Minimum expected implementation of a crosschain messenger contract to be deployed on L1 */ abstract contract L1CrosschainMessenger { IInbox public immutable inbox; /** * Emitted when calling sendTxToL2CustomRefund * @param from account that submitted the retryable ticket * @param to account recipient of the retryable ticket * @param seqNum id for the retryable ticket * @param data data of the retryable ticket */ event TxToL2( address indexed from, address indexed to, uint256 indexed seqNum, bytes data ); constructor(address inbox_) { inbox = IInbox(inbox_); } modifier onlyCounterpartGateway(address l2Counterpart) { // A message coming from the counterpart gateway was executed by the bridge IBridge bridge = inbox.bridge(); require(msg.sender == address(bridge), "NOT_FROM_BRIDGE"); // And the outbox reports that the L2 address of the sender is the counterpart gateway address l2ToL1Sender = IOutbox(bridge.activeOutbox()).l2ToL1Sender(); require(l2ToL1Sender == l2Counterpart, "ONLY_COUNTERPART_GATEWAY"); _; } /** * Creates the retryable ticket to send over to L2 through the Inbox * @param to account to be credited with the tokens in the destination layer * @param refundTo account, or its L2 alias if it have code in L1, to be credited with excess gas refund in L2 * @param user account with rights to cancel the retryable and receive call value refund * @param l1CallValue callvalue sent in the L1 submission transaction * @param l2CallValue callvalue for the L2 message * @param maxSubmissionCost max gas deducted from user's L2 balance to cover base submission fee * @param maxGas max gas deducted from user's L2 balance to cover L2 execution * @param gasPriceBid gas price for L2 execution * @param data encoded data for the retryable * @return seqnum id for the retryable ticket */ function _sendTxToL2CustomRefund( address to, address refundTo, address user, uint256 l1CallValue, uint256 l2CallValue, uint256 maxSubmissionCost, uint256 maxGas, uint256 gasPriceBid, bytes memory data ) internal returns (uint256) { uint256 seqNum = inbox.createRetryableTicket{ value: l1CallValue }( to, l2CallValue, maxSubmissionCost, refundTo, user, maxGas, gasPriceBid, data ); emit TxToL2(user, to, seqNum, data); return seqNum; } } ``` We now deploy that gateway to the parent chain. ```tsx const { ethers } = require('hardhat'); const { providers, Wallet, BigNumber } = require('ethers'); const { getArbitrumNetwork, ParentToChildMessageStatus } = require('@arbitrum/sdk'); const { AdminErc20Bridger, Erc20Bridger } = require('@arbitrum/sdk/dist/lib/assetBridger/erc20Bridger'); require('dotenv').config(); /** * Set up: instantiate L1 / L2 wallets connected to providers */ const walletPrivateKey = process.env.DEVNET_PRIVKEY; const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC); const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC); const l1Wallet = new Wallet(walletPrivateKey, l1Provider); const l2Wallet = new Wallet(walletPrivateKey, l2Provider); const main = async () => { /** * Use l2Network to create an Arbitrum SDK AdminErc20Bridger instance * We'll use AdminErc20Bridger for its convenience methods around registering tokens to a custom gateway */ const l2Network = await getArbitrumNetwork(l2Provider); const erc20Bridger = new Erc20Bridger(l2Network); const adminTokenBridger = new AdminErc20Bridger(l2Network); const l1Router = l2Network.tokenBridge.parentGatewayRouter; const l2Router = l2Network.tokenBridge.childGatewayRouter; const inbox = l2Network.ethBridge.inbox; /** * Deploy our custom gateway to L1 */ const L1CustomGateway = await await ethers.getContractFactory('L1CustomGateway', l1Wallet); console.log('Deploying custom gateway to L1'); const l1CustomGateway = await L1CustomGateway.deploy(l1Router, inbox); await l1CustomGateway.deployed(); console.log(`Custom gateway is deployed to L1 at ${l1CustomGateway.address}`); const l1CustomGatewayAddress = l1CustomGateway.address; }; main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); ``` ## Step 2: Create a gateway and deploy it on the child chain We’ll now create the counterpart of the gateway we created on the parent chain and deploy it on the child chain. A good example of a custom gateway on a child chain is [Arbitrum’s generic-custom gateway on a child chain](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/gateway/L2CustomGateway.sol). As we did with the parent chain gateway, we’ll use a simpler approach with the same characteristics as the parent chain: it supports only one token and can be enabled or disabled by the contract owner. It will also have a `setTokenBridgeInformation` method to be called by the contract owner to initialize the gateway. ```solidity // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; import "./interfaces/ICustomGateway.sol"; import "./CrosschainMessenger.sol"; import "./interfaces/IArbToken.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; /** * @title Example implementation of a custom gateway to be deployed on L2 * @dev Inheritance of Ownable is optional. In this case we use it to call the function setTokenBridgeInformation * and simplify the test */ contract L2CustomGateway is IL2CustomGateway, L2CrosschainMessenger, Ownable { // Exit number (used for tradeable exits) uint256 public exitNum; // Token bridge state variables address public l1CustomToken; address public l2CustomToken; address public l1Gateway; address public router; // Custom functionality bool public allowsWithdrawals; /** * Contract constructor, sets the L2 router to be used in the contract's functions * @param router_ L2GatewayRouter address */ constructor(address router_) { router = router_; allowsWithdrawals = false; } /** * Sets the information needed to use the gateway. To simplify the process of testing, this function can be called once * by the owner of the contract to set these addresses. * @param l1CustomToken_ address of the custom token on L1 * @param l2CustomToken_ address of the custom token on L2 * @param l1Gateway_ address of the counterpart gateway (on L1) */ function setTokenBridgeInformation( address l1CustomToken_, address l2CustomToken_, address l1Gateway_ ) public onlyOwner { require(l1CustomToken == address(0), "Token bridge information already set"); l1CustomToken = l1CustomToken_; l2CustomToken = l2CustomToken_; l1Gateway = l1Gateway_; // Allows withdrawals after the information has been set allowsWithdrawals = true; } /// @dev See {ICustomGateway-outboundTransfer} function outboundTransfer( address l1Token, address to, uint256 amount, bytes calldata data ) public payable returns (bytes memory) { return outboundTransfer(l1Token, to, amount, 0, 0, data); } /// @dev See {ICustomGateway-outboundTransfer} function outboundTransfer( address l1Token, address to, uint256 amount, uint256, /* _maxGas */ uint256, /* _gasPriceBid */ bytes calldata data ) public payable override returns (bytes memory res) { // Only execute if deposits are allowed require(allowsWithdrawals == true, "Withdrawals are currently disabled"); // The function is marked as payable to conform to the inheritance setup // This particular code path shouldn't have a msg.value > 0 require(msg.value == 0, "NO_VALUE"); // Only allow the custom token to be bridged through this gateway require(l1Token == l1CustomToken, "Token is not allowed through this gateway"); (address from, bytes memory extraData) = _parseOutboundData(data); // The inboundEscrowAndCall functionality has been disabled, so no data is allowed require(extraData.length == 0, "EXTRA_DATA_DISABLED"); // Burns L2 tokens in order to release escrowed L1 tokens IArbToken(l2CustomToken).bridgeBurn(from, amount); // Current exit number for this operation uint256 currExitNum = exitNum++; // We override the res field to save on the stack res = getOutboundCalldata(l1Token, from, to, amount, extraData); // Trigger the crosschain message uint256 id = _sendTxToL1( from, l1Gateway, res ); emit WithdrawalInitiated(l1Token, from, to, id, currExitNum, amount); return abi.encode(id); } /// @dev See {ICustomGateway-finalizeInboundTransfer} function finalizeInboundTransfer( address l1Token, address from, address to, uint256 amount, bytes calldata data ) public payable override onlyCounterpartGateway(l1Gateway) { // Only allow the custom token to be bridged through this gateway require(l1Token == l1CustomToken, "Token is not allowed through this gateway"); // Abi decode may revert, but the encoding is done by L1 gateway, so we trust it (, bytes memory callHookData) = abi.decode(data, (bytes, bytes)); if (callHookData.length != 0) { // callHookData should always be 0 since inboundEscrowAndCall is disabled callHookData = bytes(""); } // Mints L2 tokens IArbToken(l2CustomToken).bridgeMint(to, amount); emit DepositFinalized(l1Token, from, to, amount); } /// @dev See {ICustomGateway-getOutboundCalldata} function getOutboundCalldata( address l1Token, address from, address to, uint256 amount, bytes memory data ) public view override returns (bytes memory outboundCalldata) { outboundCalldata = abi.encodeWithSelector( ICustomGateway.finalizeInboundTransfer.selector, l1Token, from, to, amount, abi.encode(exitNum, data) ); return outboundCalldata; } /// @dev See {ICustomGateway-calculateL2TokenAddress} function calculateL2TokenAddress(address l1Token) public view override returns (address) { if (l1Token == l1CustomToken) { return l2CustomToken; } return address(0); } /// @dev See {ICustomGateway-counterpartGateway} function counterpartGateway() public view override returns (address) { return l1Gateway; } /** * Parse data received in outboundTransfer * @param data encoded data received * @return from account that initiated the deposit, * extraData decoded data */ function _parseOutboundData(bytes memory data) internal view returns ( address from, bytes memory extraData ) { if (msg.sender == router) { // Router encoded (from, extraData) = abi.decode(data, (address, bytes)); } else { from = msg.sender; extraData = data; } } // -------------------- // Custom methods // -------------------- /** * Disables the ability to deposit funds */ function disableWithdrawals() external onlyOwner { allowsWithdrawals = false; } /** * Enables the ability to deposit funds */ function enableWithdrawals() external onlyOwner { require(l1CustomToken != address(0), "Token bridge information has not been set yet"); allowsWithdrawals = true; } } ``` `IL2CustomGateway` is also an interface very similar to `ICustomGateway`, and `L2CrosschainMessenger` implements a method to send the cross-chain message to the parent chain through ArbSys. ```solidity /** * @title Minimum expected implementation of a crosschain messenger contract to be deployed on L2 */ abstract contract L2CrosschainMessenger { address internal constant ARB_SYS_ADDRESS = address(100); /** * Emitted when calling sendTxToL1 * @param from account that submits the L2-to-L1 message * @param to account recipient of the L2-to-L1 message * @param id id for the L2-to-L1 message * @param data data of the L2-to-L1 message */ event TxToL1( address indexed from, address indexed to, uint256 indexed id, bytes data ); modifier onlyCounterpartGateway(address l1Counterpart) { require( msg.sender == AddressAliasHelper.applyL1ToL2Alias(l1Counterpart), "ONLY_COUNTERPART_GATEWAY" ); _; } /** * Creates an L2-to-L1 message to send over to L1 through ArbSys * @param from account that is sending funds from L2 * @param to account to be credited with the tokens in the destination layer * @param data encoded data for the L2-to-L1 message * @return id id for the L2-to-L1 message */ function _sendTxToL1( address from, address to, bytes memory data ) internal returns (uint256) { uint256 id = ArbSys(ARB_SYS_ADDRESS).sendTxToL1(to, data); emit TxToL1(from, to, id, data); return id; } } ``` We now deploy that gateway to the child chain. ```tsx const { ethers } = require('hardhat'); const { providers, Wallet, BigNumber } = require('ethers'); const { getArbitrumNetwork, ParentToChildMessageStatus, AdminErc20Bridger, Erc20Bridger } = require('@arbitrum/sdk'); require('dotenv').config(); /** * Set up: instantiate L1 / L2 wallets connected to providers */ const walletPrivateKey = process.env.DEVNET_PRIVKEY; const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC); const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC); const l1Wallet = new Wallet(walletPrivateKey, l1Provider); const l2Wallet = new Wallet(walletPrivateKey, l2Provider); const main = async () => { /** * Use l2Network to create an Arbitrum SDK AdminErc20Bridger instance * We'll use AdminErc20Bridger for its convenience methods around registering tokens to a custom gateway */ const l2Network = await getArbitrumNetwork(l2Provider); const erc20Bridger = new Erc20Bridger(l2Network); const adminTokenBridger = new AdminErc20Bridger(l2Network); const l1Router = l2Network.tokenBridge.l1GatewayRouter; const l2Router = l2Network.tokenBridge.l2GatewayRouter; const inbox = l2Network.ethBridge.inbox; /** * Deploy our custom gateway to L2 */ const L2CustomGateway = await await ethers.getContractFactory('L2CustomGateway', l2Wallet); console.log('Deploying custom gateway to L2'); const l2CustomGateway = await L2CustomGateway.deploy(l2Router); await l2CustomGateway.deployed(); console.log(`Custom gateway is deployed to L2 at ${l2CustomGateway.address}`); const l2CustomGatewayAddress = l2CustomGateway.address; }; main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); ``` ## Step 3: Deploy the custom tokens on the parent and child chains This step will depend on your setup. In this case, since our simplified gateway supports only one token, we'll deploy it on both the parent and child chains to enable calling the `setTokenBridgeInformation` method on both gateways afterwards. We won't go through deploying custom tokens in this how-to, but you can find a detailed explanation on the [generic-custom gateway setup page](/arbitrum-essentials/bridging/configure-token-gateway/generic-custom.md). ## Step 4: Configure your custom tokens on your gateways This step will also depend on your setup. In this case, our simplified gateway requires the method `setTokenBridgeInformation` to be called on both gateways to set the addresses of the counterpart gateway and both custom tokens. ```tsx /** * Set the token bridge information on the custom gateways * (This is an optional step that depends on your configuration. In this example, we've added one-shot * functions on the custom gateways to set the token bridge addresses in a second step. This could be * avoided if you are using proxies or the opcode CREATE2 for example) */ console.log('Setting token bridge information on L1CustomGateway:'); const setTokenBridgeInfoOnL1 = await l1CustomGateway.setTokenBridgeInformation(l1CustomToken.address, l2CustomToken.address, l2CustomGatewayAddress); const setTokenBridgeInfoOnL1Rec = await setTokenBridgeInfoOnL1.wait(); console.log(`Token bridge information set on L1CustomGateway! L1 receipt is: ${setTokenBridgeInfoOnL1Rec.transactionHash}`); console.log('Setting token bridge information on L2CustomGateway:'); const setTokenBridgeInfoOnL2 = await l2CustomGateway.setTokenBridgeInformation(l1CustomToken.address, l2CustomToken.address, l1CustomGatewayAddress); const setTokenBridgeInfoOnL2Rec = await setTokenBridgeInfoOnL2.wait(); console.log(`Token bridge information set on L2CustomGateway! L2 receipt is: ${setTokenBridgeInfoOnL2Rec.transactionHash}`); ``` ## Step 5: Register the custom token with your custom gateway Once all contracts are deployed successfully on their respective chains, and they all have the information of the gateways and tokens, it's time to register the token in your custom gateway. As mentioned in [How to bridge tokens via Arbitrum's generic-custom gateway](/arbitrum-essentials/bridging/configure-token-gateway/generic-custom.md), this action needs to be completed by the parent chain token, and we've implemented the function `registerTokenOnL2` to do it. Now we only need to call that function. In this case, when using this function, a single action occurs: 1. Call the function `setGateway` of `L1GatewayRouter`. This call updates the `l1TokenToGateway` internal mapping and sends a retryable ticket to the counterpart `L2GatewayRouter` contract on the child chain to set its mapping to the new values. To simplify the process, we'll use Arbitrum's SDK and call the method [`registerCustomToken`](https://github.com/OffchainLabs/arbitrum-sdk/blob/main/packages/sdk/src/lib/assetBridger/erc20Bridger.ts) of the [`AdminErc20Bridger`](https://github.com/OffchainLabs/arbitrum-sdk/blob/main/packages/sdk/src/lib/assetBridger/erc20Bridger.ts) class, which will call the `registerTokenOnL2` method of the token passed by parameter. ```tsx /** * Register the custom gateway as the gateway of our custom token */ console.log('Registering custom token on L2:'); const registerTokenTx = await adminTokenBridger.registerCustomToken(l1CustomToken.address, l2CustomToken.address, l1Wallet, l2Provider); const registerTokenRec = await registerTokenTx.wait(); console.log(`Registering token txn confirmed on L1! 🙌 L1 receipt is: ${registerTokenRec.transactionHash}.`); console.log(`Waiting for L2 retryable (takes 10-15 minutes); current time: ${new Date().toTimeString()})`); /** * The L1 side is confirmed; now we listen and wait for the L2 side to be executed; we can do this by computing the expected txn hash of the L2 transaction. * To compute this txn hash, we need our message's "sequence numbers", unique identifiers of each L1 to L2 message. * We'll fetch them from the event logs with a helper method. */ const l1ToL2Msgs = await registerTokenRec.getParentToChildMessages(l2Provider); /** * In this case, the registerTokenOnL2 method creates 1 L1-to-L2 messages to set the L1 token to the Custom Gateway via the Router * Here, We check if that message is redeemed on L2 */ expect(l1ToL2Msgs.length, 'Should be 1 message.').to.eq(1); const setGateways = await l1ToL2Msgs[0].waitForStatus(); expect(setGateways.status, 'Set gateways not redeemed.').to.eq(ParentToChildMessageStatus.REDEEMED); console.log('Your custom token and gateways are now registered on the token bridge 🥳!'); ``` ## Conclusion Upon completion of all the steps, registration of your parent chain and child chain gateways in the token bridge will be complete, and both tokens will have connections through your custom gateway. The full code for this how-to and other more extensive deployment and testing scripts is available in the [custom gateway bridging tutorial package](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/custom-gateway-bridging) of our tutorials repository. ## Next steps Your custom gateway is now configured! Users can: * [Deposit tokens to the child chain](/arbitrum-essentials/bridging/deposit/tokens.md) * [Withdraw tokens back to the parent chain](/arbitrum-essentials/bridging/withdraw/tokens.md) ## Resources 1. [Concept page: Token Bridge](/how-arbitrum-works/deep-dives/token-bridging.md) 2. [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) 3. [Token bridge contract addresses](/arbitrum-essentials/reference/contract-addresses.md) --- > For a complete page index, fetch # Configure generic-custom gateway bridging In this how-to, you'll learn how to bridge your own token between Ethereum (parent chain) and Arbitrum (child chain), using [Arbitrum's generic-custom gateway](/how-arbitrum-works/deep-dives/token-bridging.md#the-arbitrum-generic-custom-gateway). For alternative ways of bridging tokens, check out the [token bridging overview](/how-arbitrum-works/deep-dives/token-bridging.md). Familiarity with [Arbitrum's token bridge system](/how-arbitrum-works/deep-dives/token-bridging.md), smart contracts, and blockchain development is expected. If you're new to blockchain development, consider reviewing our [Quickstart: Build a dApp with Arbitrum (Solidity, Hardhat)](/build-decentralized-apps/quickstart-solidity-remix.md) before proceeding. We'll use [Arbitrum's SDK](https://github.com/OffchainLabs/arbitrum-sdk) throughout this how-to, although no prior knowledge is required. We'll go through all the steps involved in the process. However, if you want to jump straight to the code, we've created a [custom token bridging tutorial script](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/custom-token-bridging) that encapsulates the entire process. ## Step 1: Review the prerequisites As stated in the [token bridge conceptual page](/how-arbitrum-works/deep-dives/token-bridging.md#the-arbitrum-generic-custom-gateway), there are a few prerequisites to keep in mind while using this method to make a token bridgeable. First of all, the **parent chain counterpart of the token** must conform to the [`ICustomToken`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/ICustomToken.sol) interface, meaning that: * It must have an `isArbitrumEnabled` method that returns `0xb1` * It must have a method that makes an external call to `L1CustomGateway.registerCustomL2Token` specifying the address of the child chain contract, and to `L1GatewayRouter.setGateway` specifying the address of the custom gateway. Make these calls only once to configure the gateway. These methods are needed to register the token via the gateway contract. If your parent chain contract does not include these methods and it is not upgradeable, you could register in one of these ways: * As a chain owner, register via an [Arbitrum DAO](https://forum.arbitrum.foundation/) proposal. * By wrapping your parent chain token and registering the wrapped version of your token. Note that registration is a one-time event. Also, the **child chain counterpart of the token** must conform to the [`IArbToken`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/IArbToken.sol) interface, meaning that: * It must have `bridgeMint` and `bridgeBurn` methods callable only by the `L2CustomGateway` contract. * It must have an `l1Address` view method that returns the token's address on the parent chain. > **INFO** — Token compatibility with available tooling > > If you want your token to be compatible out of the box with all the tooling available (e.g., the [Arbitrum bridge](https://bridge.arbitrum.io/)), we recommend that you keep the implementation of the `IArbToken` interface as close as possible to the [L2GatewayToken](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/libraries/L2GatewayToken.sol) implementation example. > > For example, if an allowance check is added to the `bridgeBurn()` function, the token will not be easily withdrawable through the Arbitrum bridge UI, as the UI does not prompt an approval transaction of tokens by default (it expects the tokens to follow the recommended `L2GatewayToken` implementation). ## Step 2: Create a token and deploy it on the parent chain We‘ll begin the process by creating and deploying a sample token on the parent chain that we will later bridge. If you already have a token contract on the parent chain, you don’t need to perform this step. However, you will need to upgrade the contract if it doesn’t include the required methods described in the previous step. We first create a standard **ERC-20** contract using OpenZeppelin’s implementation. We make only one adjustment to that implementation, for simplicity, although it is not required: we specify an `initialSupply` to be pre-minted and sent to the deployer address upon creation. We’ll also add the required methods to make our token bridgeable via the generic-custom gateway. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./interfaces/ICustomToken.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; /** * @title Interface needed to call function registerTokenToL2 of the L1CustomGateway */ interface IL1CustomGateway { function registerTokenToL2( address _l2Address, uint256 _maxGas, uint256 _gasPriceBid, uint256 _maxSubmissionCost, address _creditBackAddress ) external payable returns (uint256); } /** * @title Interface needed to call function setGateway of the L2GatewayRouter */ interface IL2GatewayRouter { function setGateway( address _gateway, uint256 _maxGas, uint256 _gasPriceBid, uint256 _maxSubmissionCost, address _creditBackAddress ) external payable returns (uint256); } contract L1Token is Ownable, ICustomToken, ERC20 { address private customGatewayAddress; address private routerAddress; bool private shouldRegisterGateway; /** * @dev See {ERC20-constructor} and {Ownable-constructor} * * An initial supply amount is passed, which is preminted to the deployer. */ constructor(address _customGatewayAddress, address _routerAddress, uint256 _initialSupply) ERC20("L1CustomToken", "LCT") { customGatewayAddress = _customGatewayAddress; routerAddress = _routerAddress; _mint(msg.sender, _initialSupply * 10 ** decimals()); } /// @dev we only set shouldRegisterGateway to true when in `registerTokenOnL2` function isArbitrumEnabled() external view override returns (uint8) { require(shouldRegisterGateway, "NOT_EXPECTED_CALL"); return uint8(0xb1); } /// @dev See {ICustomToken-registerTokenOnL2} function registerTokenOnL2( address l2CustomTokenAddress, uint256 maxSubmissionCostForCustomGateway, uint256 maxSubmissionCostForRouter, uint256 maxGasForCustomGateway, uint256 maxGasForRouter, uint256 gasPriceBid, uint256 valueForGateway, uint256 valueForRouter, address creditBackAddress ) public override payable onlyOwner { // we temporarily set `shouldRegisterGateway` to true for the callback in registerTokenToL2 to succeed bool prev = shouldRegisterGateway; shouldRegisterGateway = true; IL1CustomGateway(customGatewayAddress).registerTokenToL2{ value: valueForGateway }( l2CustomTokenAddress, maxGasForCustomGateway, gasPriceBid, maxSubmissionCostForCustomGateway, creditBackAddress ); IL2GatewayRouter(routerAddress).setGateway{ value: valueForRouter }( customGatewayAddress, maxGasForRouter, gasPriceBid, maxSubmissionCostForRouter, creditBackAddress ); shouldRegisterGateway = prev; } /// @dev See {ERC20-transferFrom} function transferFrom( address sender, address recipient, uint256 amount ) public override(ICustomToken, ERC20) returns (bool) { return super.transferFrom(sender, recipient, amount); } /// @dev See {ERC20-balanceOf} function balanceOf(address account) public view override(ICustomToken, ERC20) returns (uint256) { return super.balanceOf(account); } } ``` We now deploy that token to the parent chain. ```typescript const { ethers } = require('hardhat'); const { providers, Wallet } = require('ethers'); const { getArbitrumNetwork } = require('@arbitrum/sdk'); require('dotenv').config(); const walletPrivateKey = process.env.DEVNET_PRIVKEY; const l1Provider = new providers.JsonRpcProvider(process.env.L1RPC); const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC); const l1Wallet = new Wallet(walletPrivateKey, l1Provider); /** * For the purpose of our tests, here we deploy an standard ERC20 token (L1Token) to L1 * It sends its deployer (us) the initial supply of 1000 */ const main = async () => { /** * Use l2Network to get the token bridge addresses needed to deploy the token */ const l2Network = await getArbitrumNetwork(l2Provider); const l1Gateway = l2Network.tokenBridge.l1CustomGateway; const l1Router = l2Network.tokenBridge.l1GatewayRouter; /** * Deploy our custom token smart contract to L1 * We give the custom token contract the address of l1CustomGateway and l1GatewayRouter as well as the initial supply (premine) */ console.log('Deploying the test L1Token to L1:'); const L1Token = await (await ethers.getContractFactory('L1Token')).connect(l1Wallet); const l1Token = await L1Token.deploy(l1Gateway, l1Router, 1000); await l1Token.deployed(); console.log(`L1Token is deployed to L1 at ${l1Token.address}`); /** * Get the deployer token balance */ const tokenBalance = await l1Token.balanceOf(l1Wallet.address); console.log(`Initial token balance of deployer: ${tokenBalance}`); }; main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); ``` ## Step 3: Create a token and deploy it on the child chain We’ll now create and deploy the counterpart of the token we created on the parent chain to the child chain. We’ll create a standard **ERC-20** contract using OpenZeppelin’s implementation, and add the required methods from `IArbToken`. ```solidity // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; import "./interfaces/IArbToken.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract L2Token is ERC20, IArbToken { address public l2Gateway; address public override l1Address; modifier onlyL2Gateway() { require(msg.sender == l2Gateway, "NOT_GATEWAY"); _; } constructor(address _l2Gateway, address _l1TokenAddress) ERC20("L2CustomToken", "LCT") { l2Gateway = _l2Gateway; l1Address = _l1TokenAddress; } /** * @notice should increase token supply by amount, and should only be callable by the L2Gateway. */ function bridgeMint(address account, uint256 amount) external override onlyL2Gateway { _mint(account, amount); } /** * @notice should decrease token supply by amount, and should only be callable by the L2Gateway. */ function bridgeBurn(address account, uint256 amount) external override onlyL2Gateway { _burn(account, amount); } // Add any extra functionality you want your token to have. } ``` We now deploy that token to the child chain. ```typescript const { ethers } = require('hardhat'); const { providers, Wallet } = require('ethers'); const { getArbitrumNetwork } = require('@arbitrum/sdk'); require('dotenv').config(); const walletPrivateKey = process.env.DEVNET_PRIVKEY; const l2Provider = new providers.JsonRpcProvider(process.env.L2RPC); const l2Wallet = new Wallet(walletPrivateKey, l2Provider); const l1TokenAddress = '
'; /** * For the purpose of our tests, here we deploy an standard ERC20 token (L2Token) to L2 */ const main = async () => { /** * Use l2Network to get the token bridge addresses needed to deploy the token */ const l2Network = await getArbitrumNetwork(l2Provider); const l2Gateway = l2Network.tokenBridge.childCustomGateway; /** * Deploy our custom token smart contract to L2 * We give the custom token contract the address of childCustomGateway as well as the address of the counterpart L1 token */ console.log('Deploying the test L2Token to L2:'); const L2Token = await (await ethers.getContractFactory('L2Token')).connect(l2Wallet); const l2Token = await L2Token.deploy(l2Gateway, l1TokenAddress); await l2Token.deployed(); console.log(`L2Token is deployed to L2 at ${l2Token.address}`); }; main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); ``` ## Step 4: Register the custom token with the generic-custom gateway Once we deploy both our contracts on their respective chains, it’s time to register the token in the generic-custom gateway. As mentioned earlier, the parent chain token must complete the registration, and we’ve implemented the `registerTokenOnL2` function to accomplish this. So now we only need to call that function. When using this function, you will take two actions: 1. Call the function `registerTokenToL2` of `L1CustomGateway`. This call will change the `l1ToL2Token` internal mapping it holds and send a retryable ticket to the counterpart `L2CustomGateway` contract on the child chain, setting its mapping to the new values as well. 2. Call the function `setGateway` of `L1GatewayRouter`. This call will update the `l1TokenToGateway` internal mapping it holds and send a retryable ticket to the counterpart `L2GatewayRouter` contract on the child chain to set its mapping to the new values. To simplify the process, we'll use Arbitrum's SDK. We'll call the [`registerCustomToken`](https://github.com/OffchainLabs/arbitrum-sdk/blob/main/packages/sdk/src/lib/assetBridger/erc20Bridger.ts) method of the [`AdminErc20Bridger`](https://github.com/OffchainLabs/arbitrum-sdk/blob/main/packages/sdk/src/lib/assetBridger/erc20Bridger.ts) class, which will call the `registerTokenOnL2` method on the token passed as a parameter. ```typescript /** * Register custom token on our custom gateway */ const adminTokenBridger = new AdminErc20Bridger(l2Network); const registerTokenTx = await adminTokenBridger.registerCustomToken(l1CustomToken.address, l2CustomToken.address, l1Wallet, l2Provider); const registerTokenRec = await registerTokenTx.wait(); console.log(`Registering token txn confirmed on L1! 🙌 L1 receipt is: ${registerTokenRec.transactionHash}`); /** * The L1 side is confirmed; now we listen and wait for the L2 side to be executed; we can do this by computing the expected txn hash of the L2 transaction. * To compute this txn hash, we need our message's "sequence numbers", unique identifiers of each L1 to L2 message. * We'll fetch them from the event logs with a helper method. */ const l1ToL2Msgs = await registerTokenRec.getParentToChildMessages(l2Provider); /** * In principle, a single L1 txn can trigger any number of L1-to-L2 messages (each with its own sequencer number). * In this case, the registerTokenOnL2 method created 2 L1-to-L2 messages; * - (1) one to set the L1 token to the Custom Gateway via the Router, and * - (2) another to set the L1 token to its L2 token address via the Generic-Custom Gateway * Here, We check if both messages are redeemed on L2 */ expect(l1ToL2Msgs.length, 'Should be 2 messages.').to.eq(2); const setTokenTx = await l1ToL2Msgs[0].waitForStatus(); expect(setTokenTx.status, 'Set token not redeemed.').to.eq(ParentToChildMessageStatus.REDEEMED); const setGateways = await l1ToL2Msgs[1].waitForStatus(); expect(setGateways.status, 'Set gateways not redeemed.').to.eq(ParentToChildMessageStatus.REDEEMED); console.log('Your custom token is now registered on our custom gateway 🥳 Go ahead and make the deposit!'); ``` ## Conclusion Upon completion, the parent and child chain tokens are connected via the generic-custom gateway. You can bridge tokens between the parent and child chain using the origin parent chain token and the custom token deployed on the child chain, along with the router and gateway contracts from each layer. For an example of bridging a token from the parent to the child chain using Arbitrum's SDK, check out [How to bridge tokens via Arbitrum's standard **ERC-20** gateway](/arbitrum-essentials/bridging/deposit/tokens.md), specifically Steps 2-5. ## Frequently asked questions ### Can I run the same register token process multiple times for the same parent chain token? No, you can only register once a child chain token for the same parent chain token. After that, the call to `registerTokenToL2` will revert if it runs again. ### What can I do if my parent chain token is not upgradable? As mentioned on the concept page, token registration can also be completed as a chain owner registration via an **[Arbitrum DAO](https://forum.arbitrum.foundation/)** proposal. ### Can I set up the generic-custom gateway after a standard **ERC-20** token has been deployed on the child chain? Yes, if your token has a standard **ERC-20** counterpart on the child chain, you can follow the process outlined on this page to register your custom child chain token. At that moment, your parent chain token will have two counterpart tokens on the child chain, but only your new custom child chain token will be minted when depositing tokens from the parent chain (parent-to-child chain bridging). Both child chain tokens will be withdrawable (child-to-parent chain bridging), so users holding the old standard **ERC-20** token will be able to withdraw back to the parent chain (using the `L2CustomGateway` contract instead of the bridge UI) and then deposit to the child chain to get the new custom child chain tokens. ## Next steps Your token is now configured for bridging! Users can: * [Deposit tokens to the child chain](/arbitrum-essentials/bridging/deposit/tokens.md) * [Withdraw tokens back to the parent chain](/arbitrum-essentials/bridging/withdraw/tokens.md) ## Resources 1. [Concept page: Token Bridge](/how-arbitrum-works/deep-dives/token-bridging.md) 2. [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) 3. [Token bridge contract addresses](/arbitrum-essentials/reference/contract-addresses.md) --- > For a complete page index, fetch # Configure standard gateway bridging This guide explains how to configure your **ERC-20** token to work with Arbitrum's standard gateway. The standard gateway is the simplest option—it automatically creates a standard **ERC-20** token on the child chain with no configuration required. ## When to use the standard gateway Use the standard gateway when: * You have a standard **ERC-20** token on the parent chain * You don't need custom functionality on the child chain token * You want automatic setup with no pre-configuration * Your token doesn't have special behaviors (rebasing, fee-on-transfer, and similar) For custom token behavior, see: * [Generic-custom gateway](/arbitrum-essentials/bridging/configure-token-gateway/generic-custom.md)—for custom child chain token logic * [Custom gateway](/arbitrum-essentials/bridging/configure-token-gateway/custom.md)—for advanced use cases ## Prerequisites * A standard **ERC-20** token deployed on the parent chain (or deploy one following this guide) * Familiarity with [Arbitrum's token bridge system](/how-arbitrum-works/deep-dives/token-bridging.md) * Basic understanding of smart contracts and blockchain development ## How the standard gateway works When using the standard gateway: 1. **No pre-configuration needed**: Your token is automatically bridgeable 2. **Automatic child chain deployment**: On the first deposit, a [`StandardArbERC20`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/StandardArbERC20.sol) contract is deployed on the child chain 3. **Escrow model**: Parent chain tokens are escrowed in the gateway; child chain tokens are minted and burned 4. **Router handles routing**: The router automatically directs your token to the standard gateway For architectural details, see [Standard **ERC-20** bridging](/how-arbitrum-works/deep-dives/token-bridging.md#default-standard-bridging). ## Step 1: Deploy your token (or use an existing one) If you already have a token on the parent chain, skip to Step 2. Otherwise, create a standard **ERC-20** token: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract DappToken is ERC20 { constructor(uint256 _initialSupply) ERC20("Dapp Token", "DAPP") { _mint(msg.sender, _initialSupply * 10 ** decimals()); } } ``` Deploy it to the parent chain: ```javascript const { ethers } = require('hardhat'); const { providers, Wallet } = require('ethers'); const parentProvider = new providers.JsonRpcProvider(process.env.PARENT_RPC); const wallet = new Wallet(process.env.PRIVATE_KEY, parentProvider); async function deployToken() { const TokenFactory = await ethers.getContractFactory('DappToken'); const token = await TokenFactory.connect(wallet).deploy(1000000); await token.deployed(); console.log(`Token deployed at: ${token.address}`); return token.address; } ``` ## Step 2: Understand the bridge contracts Two contracts handle token bridging: ### Router contracts * **L1GatewayRouter**: Entry point on parent chain * **L2GatewayRouter**: Entry point on child chain The router maintains a mapping of which gateway handles which token, falling back to the standard gateway for unmapped tokens. ### Gateway contracts * **L1ERC20Gateway**: Escrows parent chain tokens * **L2ERC20Gateway**: Mints/burns child chain tokens You can find contract addresses on the [contract addresses page](/arbitrum-essentials/reference/contract-addresses.md#token-bridge-smart-contracts). ## Step 3: Trigger child chain token deployment The child chain token is created automatically on the first deposit. You can trigger deployment by making a small deposit or by waiting until users make their first deposits. ### Using the Arbitrum SDK ```javascript import { getArbitrumNetwork, Erc20Bridger } from '@arbitrum/sdk'; import { providers, Wallet } from 'ethers'; const parentProvider = new providers.JsonRpcProvider(process.env.PARENT_RPC); const childProvider = new providers.JsonRpcProvider(process.env.CHILD_RPC); const wallet = new Wallet(process.env.PRIVATE_KEY, parentProvider); const childNetwork = await getArbitrumNetwork(childProvider); const erc20Bridge = new Erc20Bridger(childNetwork); // Approve the gateway await erc20Bridge.approveToken({ parentSigner: wallet, erc20ParentAddress: tokenAddress, }); // Make initial deposit to trigger L2 token creation const depositTx = await erc20Bridge.deposit({ amount: ethers.utils.parseUnits('1', 18), erc20ParentAddress: tokenAddress, parentSigner: wallet, childProvider: childProvider, }); const receipt = await depositTx.wait(); console.log(`Deposit complete: ${receipt.transactionHash}`); ``` For complete deposit instructions, see [Deposit tokens](/arbitrum-essentials/bridging/deposit/tokens.md). ## Step 4: Find your child chain token address After the first deposit, find your token's child chain address: ### Using the SDK ```javascript const childTokenAddress = await erc20Bridge.getChildErc20Address(parentTokenAddress, parentProvider); console.log(`L2 token address: ${childTokenAddress}`); ``` ### Manually Call `calculateL2TokenAddress` on the `L1GatewayRouter` contract: ```solidity address l2TokenAddress = l1GatewayRouter.calculateL2TokenAddress(l1TokenAddress); ``` Or look up the token on [Arbiscan](https://arbiscan.io/) by searching for the deployment transaction. ## Step 5: Verify the child chain token The automatically deployed token is an instance of [`StandardArbERC20`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/StandardArbERC20.sol) with: * Same name and symbol as parent chain token * Same decimals as parent chain token * `l1Address()` function returns the parent chain token address * Minting/burning controlled by the `L2ERC20Gateway` You can verify this on [Arbiscan](https://arbiscan.io/) by viewing the token contract. ## Configuration complete Your token is now bridgeable! Users can: * [Deposit tokens to the child chain](/arbitrum-essentials/bridging/deposit/tokens.md) * [Withdraw tokens back to the parent chain](/arbitrum-essentials/bridging/withdraw/tokens.md) ## Important considerations ### Token compatibility The standard gateway works for most **ERC-20** tokens, but **not** for: * Rebasing tokens (supply changes) * Fee-on-transfer tokens * Tokens with transfer hooks * Tokens with unique minting and burning logic For these cases, use a [generic-custom gateway](/arbitrum-essentials/bridging/configure-token-gateway/generic-custom.md) or a [custom gateway](/arbitrum-essentials/bridging/configure-token-gateway/custom.md). ### Gateway assignment Once the first deposit occurs, the token is permanently assigned to the standard gateway. You can't change gateway types after this point. ### Child chain token ownership The gateway controls the automatically deployed child chain token—you can't modify or upgrade it. For control over the child chain token, use the generic-custom gateway. ## Next steps * [Deposit tokens](/arbitrum-essentials/bridging/deposit/tokens.md) * [Withdraw tokens](/arbitrum-essentials/bridging/withdraw/tokens.md) * [Understand token bridge architecture](/how-arbitrum-works/deep-dives/token-bridging.md) * [View example code](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/token-deposit) ## Resources * [Token bridge conceptual overview](/how-arbitrum-works/deep-dives/token-bridging.md) * [Standard **ERC-20** bridging details](/how-arbitrum-works/deep-dives/token-bridging.md#default-standard-bridging) * [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) * [Contract addresses](/arbitrum-essentials/reference/contract-addresses.md) --- > For a complete page index, fetch # Cross-chain messaging The Arbitrum protocol and related tooling make it easy for developers to build cross-chain applications; i.e., applications that involve sending messages from Ethereum to an Arbitrum chain, and/or from an Arbitrum chain to Ethereum. Arbitrum SDK The [`@arbitrum/sdk`](https://www.npmjs.com/package/@arbitrum/sdk) is a TypeScript library for bridging tokens and passing messages between parent and child Arbitrum chains. It provides a typed interface to the underlying bridge and messaging smart contracts. **Links**: [GitHub](https://github.com/OffchainLabs/arbitrum-sdk) | [npm](https://www.npmjs.com/package/@arbitrum/sdk) | [Tutorials](https://github.com/OffchainLabs/arbitrum-tutorials) **Install**: `npm install @arbitrum/sdk` ## Ethereum-to-Arbitrum messaging Creating an arbitrary parent-to-child chain contract call occurs via the Inbox's `createRetryableTicket` method. Upon publishing the parent chain transaction, the child chain side will typically be included within minutes. Commonly, the child chain execution will automatically succeed, but if it reverts, it can be re-executed via a call to the `redeem` method of the [`ArbRetryableTx`](/arbitrum-essentials/precompiles/reference.md#arbretryabletx) precompile. * **How-to guide**: [How to bridge from the parent chain to the child chain](/arbitrum-essentials/bridging/deposit/eth-and-messages.md) * **Protocol details**: [Parent to child chain messaging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md) ## Arbitrum-to-Ethereum messaging Similarly, child chain contracts can send arbitrary messages for execution on the parent chain. These are initiated via calls to the [`ArbSys`](/arbitrum-essentials/precompiles/reference.md#arbsys) precompile contract's `sendTxToL1` method. Upon confirmation (about one week later), you can execute them by retrieving the relevant data via a call to the `NodeInterface` contract's `constructOutboxProof` method, and then calling the `Outbox`'s `executeTransaction` method. * **How-to guide**: [How to bridge to parent chain from child chain](/arbitrum-essentials/bridging/withdraw/eth-and-messages.md) * **Protocol details**: [Child to parent chain messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md) ## Tutorial: cross-chain Greeter This walkthrough demonstrates both messaging directions with a simple Greeter contract that can update its greeting from the other chain. ### The contracts **Base Greeter** — a simple contract deployed on both chains: ```solidity // SPDX-License-Identifier: Apache-2.0 pragma solidity >=0.6.11; contract Greeter { string public greeting; function greet() public view returns (string memory) { return greeting; } function setGreeting(string memory _greeting) public virtual { greeting = _greeting; } } ``` **Parent chain Greeter** — extends the base with a method that sends a greeting to the child chain via a retryable ticket: ```solidity // SPDX-License-Identifier: Apache-2.0 pragma solidity >=0.6.11; import "./Greeter.sol"; import "@arbitrum/nitro-contracts/src/bridge/IInbox.sol"; import "@arbitrum/nitro-contracts/src/bridge/IERC20Inbox.sol"; contract GreeterParent is Greeter { address public childTarget; IInbox public inbox; event RetryableTicketCreated(uint256 indexed ticketId); constructor(string memory _greeting, address _childTarget, address _inbox) { greeting = _greeting; childTarget = _childTarget; inbox = IInbox(_inbox); } function setGreetingInChild( string memory _greeting, uint256 maxSubmissionCost, uint256 maxGas, uint256 gasPriceBid ) public payable returns (uint256) { bytes memory data = abi.encodeWithSelector(Greeter.setGreeting.selector, _greeting); uint256 ticketID = inbox.createRetryableTicket{value: msg.value}( childTarget, 0, maxSubmissionCost, msg.sender, msg.sender, maxGas, gasPriceBid, data ); emit RetryableTicketCreated(ticketID); return ticketID; } } ``` **Child chain Greeter** — extends the base with a method that sends a greeting back to the parent chain via `ArbSys`: ```solidity // SPDX-License-Identifier: Apache-2.0 pragma solidity >=0.6.11; import "./Greeter.sol"; import "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; import "@arbitrum/nitro-contracts/src/libraries/AddressAliasHelper.sol"; contract GreeterChild is Greeter { address public parentTarget; event ChildToParentTxCreated(uint256 indexed withdrawalId); constructor(string memory _greeting, address _parentTarget) { greeting = _greeting; parentTarget = _parentTarget; } function setGreetingInParent(string memory _greeting) public returns (uint256) { bytes memory data = abi.encodeWithSelector(Greeter.setGreeting.selector, _greeting); uint256 withdrawalId = ArbSys(address(100)).sendTxToL1(parentTarget, data); emit ChildToParentTxCreated(withdrawalId); return withdrawalId; } /// Only accept messages from the parent chain Greeter (via address aliasing) function setGreeting(string memory _greeting) public override { require( msg.sender == AddressAliasHelper.applyL1ToL2Alias(parentTarget), "Only callable by parent chain Greeter (aliased)" ); Greeter.setGreeting(_greeting); } } ``` ### Sending a message from parent to child chain The script below deploys both contracts and sends a greeting from the parent chain to the child chain using `ParentToChildMessageGasEstimator` to compute retryable ticket parameters: ```javascript import { providers, Wallet } from 'ethers'; import { ParentToChildMessageGasEstimator, ParentToChildMessageStatus, ParentTransactionReceipt, } from '@arbitrum/sdk'; import { getBaseFee } from '@arbitrum/sdk/dist/lib/utils/lib'; // Set up wallets const parentWallet = new Wallet( process.env.PRIVATE_KEY, new providers.JsonRpcProvider(process.env.PARENT_CHAIN_RPC), ); const childProvider = new providers.JsonRpcProvider(process.env.CHAIN_RPC); // After deploying GreeterParent and GreeterChild (via Hardhat or your preferred tool): const greeterParent = /* deployed GreeterParent contract instance */; const greeterChild = /* deployed GreeterChild contract instance */; // Encode the greeting message const newGreeting = 'Greeting from the parent chain'; const greetingData = greeterChild.interface.encodeFunctionData('setGreeting', [newGreeting]); // Estimate gas parameters for the retryable ticket const gasEstimator = new ParentToChildMessageGasEstimator(childProvider); const baseFee = await getBaseFee(parentWallet.provider); const gasParams = await gasEstimator.estimateAll( { from: greeterParent.address, to: greeterChild.address, l2CallValue: 0, excessFeeRefundAddress: parentWallet.address, callValueRefundAddress: parentWallet.address, data: greetingData, }, baseFee, parentWallet.provider, ); // Send the greeting via retryable ticket const tx = await greeterParent.setGreetingInChild( newGreeting, gasParams.maxSubmissionCost, gasParams.gasLimit, gasParams.maxFeePerGas, { value: gasParams.deposit }, ); const receipt = await tx.wait(); console.log(`Parent chain tx: ${receipt.transactionHash}`); // Wait for the child chain to execute the retryable const parentToChildReceipt = new ParentTransactionReceipt(receipt); const childTxReceipt = await parentToChildReceipt.waitForChildTransactionReceipt(childProvider); if (childTxReceipt.status === ParentToChildMessageStatus.REDEEMED) { const updatedGreeting = await greeterChild.greet(); console.log(`Child chain greeting updated to: ${updatedGreeting}`); } ``` ### Key concepts **Address aliasing**: When a parent chain contract sends a message to the child chain, the `msg.sender` on the child chain is not the original contract address. Instead, it is *aliased* by adding `0x1111000000000000000000000000000000001111` to the address. The `AddressAliasHelper` library handles this — see the `setGreeting` override in `GreeterChild` above. **Gas estimation**: The `ParentToChildMessageGasEstimator` calculates three values: `maxSubmissionCost` (data posting cost), `gasLimit` (child chain execution gas), and `maxFeePerGas` (child chain gas price). Together they determine the `value` (`ETH`) you must attach to the parent chain transaction. **Retryable tickets**: If the child chain execution runs out of gas, the ticket enters a "retry" state. It can be redeemed by anyone within its lifetime (default: 7 days) by calling `ArbRetryableTx.redeem()`. See the [redeem-pending-retryable tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/redeem-pending-retryable) for a worked example. ## Resources * [Greeter tutorial source code](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/greeter) * [Outbox execution source code](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/outbox-execute) * [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) * [Retryable ticket lifecycle](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md) * [Child to parent messaging protocol](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md) --- > For a complete page index, fetch # SDK support for custom gas token Arbitrum chains Arbitrum SDK is a TypeScript library for Client-side interactions with Arbitrum. It provides common helper functionality as well as access to the underlying smart contract interfaces. ## Custom gas token APIs Custom gas token support in the Arbitrum SDK introduces a suite of APIs designed for the specific purpose of facilitating **bridging** operations. These APIs are tailored for use cases where there is a need to transfer a native token or an **ERC-20** token from the parent chain to an Arbitrum chain utilizing a `custom gas token`. The process involves an initial step of authorizing the native token on the parent chain. To streamline this, our APIs provide functionalities for token approval and offer a mechanism to verify the current status of this approval. Detailed below is a guide to how each of these APIs can be effectively utilized for distinct purposes: 1. **`EthBridger` Context:** * **APIs:** `getApproveGasTokenRequest` and `approveGasToken`. * **Purpose:** These APIs are essential for the bridging of native tokens to the Arbitrum chain. They facilitate the necessary approval for native tokens, allowing contracts to manage fund movements. This process includes escrowing a specified amount of the native token on the parent chain and subsequently bridging it to the Arbitrum chain. > **NOTE** > > You should use `EthBridger` when bridging the native token between the parent chain and the Arbitrum chain. 2. **`Erc20Bridger` Context:** * **APIs:** `getApproveGasTokenRequest` and `approveGasToken`. * **Purpose:** In the scenario of bridging **ERC-20** assets to an Arbitrum chain, these APIs play a crucial role. Token Bridging on Arbitrum Nitro stack uses retryable tickets and needs a specific fee to be paid for the creation and redemption of the ticket. For more information about retryable tickets, please take a look at [our chapter about retryable tickets](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#retryable-tickets) part of our docs. The Arbitrum chain operates as a custom gas token network, necessitating the payment of fees in native tokens for the creation of retryable tickets and their redemption on the Arbitrum chain. To cover the submission and execution fees associated with retryable tickets on the Arbitrum chain, an adequate number of native tokens must be approved and allocated to the parent chain to cover the fees. > **NOTE** — Important Notes > > * You should use `Erc20Bridger` when bridging an **ERC-20** token between the parent chain and the Arbitrum chain. > * These APIs are only needed for [`custom gas token`](/launch-arbitrum-chain/chain-config/costs/custom-gas-token-rollup.md) Arbitrum Chains. For **ETH**-powered rollup and AnyTrust Arbitrum chains, you don't need to use them. > * When native tokens are transferred to the custom gas token Arbitrum chain, they function equivalently to **ETH** on EVM chains. This means these tokens will exhibit behavior identical to that of **ETH**, the native currency on EVM chains. This similarity in functionality is a key feature to consider in transactions and operations within the Arbitrum chain. > * Everything else is handled under the hood, and the custom gas token code paths will be executed only if the `ArbitrumNetwork` object config has a `nativeToken` field. ## Registering a custom token in the Token Bridge When [registering a custom token in the Token Bridge](/how-arbitrum-works/deep-dives/token-bridging.md#setting-up-your-token-with-the-generic-custom-gateway) of a custom-gas-token Arbitrum chain, there's an additional step to perform before calling `registerTokenToL2`. Since the Token Bridge [router](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1OrbitGatewayRouter.sol#L142-L144) and the [generic-custom gateway](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1OrbitCustomGateway.sol#L203-L210) expect to have allowance to transfer the native token from the `msg.sender()` to the inbox contract, it's usually the token in the parent chain who handles those approvals. In the [TestCustomTokenL1](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/test/TestCustomTokenL1.sol#L158-L168), we offer as an example of implementation. We see that the contract transfers the native tokens to itself and then approves the router and gateway contracts. If we follow that implementation, we only need to send an approval transaction to the native token to allow the `TestCustomTokenL1` to transfer the native token from the caller of the `registerTokenToL2` function to itself. You can find a [tutorial that deploys two tokens and registers them in the Token Bridge of a custom-gas-token-based chain](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/custom-token-bridging). --- > For a complete page index, fetch # How to bridge from parent chain to child chain This guide explains how to programmatically send messages and bridge assets from a parent chain (like Ethereum) to an Arbitrum child chain. For conceptual information about the messaging protocol, see [Parent-to-child chain messaging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md). ## Prerequisites * A parent chain wallet with funds (**ETH** or the chain's native token) * Access to the parent chain's Inbox contract address * Familiarity with smart contract interactions ## Bridging ETH to a child chain To bridge **ETH** from a parent chain to a child chain, use the `depositEth` method on the Inbox contract: ```javascript function depositEth(address destAddr) external payable override returns (uint256) ``` ### Example: depositing **ETH** ```javascript const inbox = new ethers.Contract(inboxAddress, inboxABI, parentSigner); const tx = await inbox.depositEth(destinationAddress, { value: ethers.utils.parseEther('0.1'), // Amount to deposit }); await tx.wait(); ``` > **WARNING** > > Depositing **ETH** directly via `depositEth` to a contract on a child chain **will not** invoke that contract's fallback function. If you need to trigger a fallback function, use retryable tickets instead. ### How **ETH** deposits work When you deposit **ETH**, the funds are held in the Arbitrum Bridge contract on the parent chain. The bridge then credits the deposited amount to your address on the child chain: ![Depositing Ether](/img/apps-depositing-ether.svg) ### Address aliasing for contract depositors When you deposit **ETH** from the parent chain, the destination address on the child chain depends on the caller type: * **EOA caller**: The deposited **ETH** appears at the same address on the child chain * **Contract caller**: The **ETH** goes to the contract's aliased address on the child chain * **7702-enabled account**: Similar to contracts, uses the aliased address The alias is calculated as: ```solidity Child_Alias = Parent_Contract_Address + 0x1111000000000000000000000000000000001111 ``` To recover the original parent chain address in your child chain contract, use the `AddressAliasHelper` library: ```solidity modifier onlyFromMyL1Contract() { require( AddressAliasHelper.undoL1ToL2Alias(msg.sender) == myL1ContractAddress, "ONLY_COUNTERPART_CONTRACT" ); _; } ``` ## Sending transactions via the Delayed Inbox The Delayed Inbox allows you to send arbitrary messages from the parent chain to the child chain, bypassing the Sequencer if needed. ### Sending signed messages Signed messages prove EOA ownership and execute with the signer's address on the child chain (no aliasing). #### Method 1: sendL2Message More flexible, can be called by EOAs or contracts: ```solidity function sendL2Message( bytes calldata messageData ) external returns (uint256) ``` #### Method 2: sendL2MessageFromOrigin Cheaper gas costs, only callable by EOAs: ```solidity function sendL2MessageFromOrigin( bytes calldata messageData ) external returns (uint256) ``` **Example use case**: [Withdraw Ether tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/blob/master/packages/delayedInbox-l2msg/scripts/withdrawFunds.js#L61-L65) ### Sending unsigned messages Unsigned messages are automatically aliased for security. The Delayed Inbox provides four methods for unsigned messages, divided by sender type (EOA vs contract) and funding source (parent chain vs child chain balance): #### From EOAs (with nonce for replay protection) **`sendL1FundedUnsignedTransaction`** - Transfers value from parent to child chain: ```solidity function sendL1FundedUnsignedTransaction( uint256 gasLimit, uint256 maxFeePerGas, uint256 nonce, address to, bytes calldata data ) external payable returns (uint256) ``` **`sendUnsignedTransaction`** - Uses child chain balance (no L1 funds transferred): ```solidity function sendUnsignedTransaction( uint256 gasLimit, uint256 maxFeePerGas, uint256 nonce, address to, uint256 value, bytes calldata data ) external returns (uint256) ``` #### From contracts (standard Ethereum replay protection) **`sendContractTransaction`** - Uses contract's existing child chain balance: ```solidity function sendContractTransaction( uint256 gasLimit, uint256 maxFeePerGas, address to, uint256 value, bytes calldata data ) external returns (uint256) ``` **`sendL1FundedContractTransaction`** - Transfers additional funds from parent to child: ```solidity function sendL1FundedContractTransaction( uint256 gasLimit, uint256 maxFeePerGas, address to, bytes calldata data ) external payable returns (uint256) ``` ## Creating retryable tickets Retryable tickets are Arbitrum's canonical mechanism for reliable cross-chain message delivery. They automatically retry failed executions. ### Key retryable ticket parameters Understanding these parameters helps ensure successful retryable ticket creation: * **`l1CallValue`** (msg.value): Total **ETH** sent with the transaction from parent chain. This funds the ticket submission, gas, and call value. * **`to`**: The destination child chain address that will receive the retryable ticket execution. * **`l2CallValue`**: The amount of **ETH** to be sent as `callvalue` when executing the retryable on the child chain. This is supplied within the `l1CallValue` deposit. * **`maxSubmissionCost`**: Maximum **ETH** to pay for submitting the ticket. This amount is: * Supplied within the deposit (`l1CallValue`) * Later deducted from the sender's child chain balance * Directly proportional to retryable data size and parent chain basefee * **`excessFeeRefundAddress`**: Where to refund unused gas and submission costs: * Formula: `(gasLimit × maxFeePerGas - execution cost) + (maxSubmissionCost - submission cost)` * **Important**: If auto-redeem fails, excess deposit goes to the alias of the L1 sender, not this address * **`callValueRefundAddress`**: The child chain address to credit the `l2CallValue` if the ticket times out or is canceled. This address is also the "beneficiary" with permission to cancel the ticket. * **`gasLimit`**: Maximum gas for child chain execution of the ticket. Used for the automatic redemption attempt. * **`maxFeePerGas`**: Gas price bid for child chain execution, supplied in the deposit (`l1CallValue`). * **`data`**: Calldata to send to the destination address on the child chain. ### Creating a retryable ticket ```solidity function createRetryableTicket( address to, uint256 l2CallValue, uint256 maxSubmissionCost, address excessFeeRefundAddress, address callValueRefundAddress, uint256 gasLimit, uint256 maxFeePerGas, bytes calldata data ) external payable returns (uint256) ``` ### Example using the Arbitrum SDK ```javascript import { ParentToChildMessageGasEstimator } from '@arbitrum/sdk'; // Estimate gas for the retryable ticket const parentToChildMessageGasEstimator = new ParentToChildMessageGasEstimator(childProvider); const retryableGasParams = await parentToChildMessageGasEstimator.estimateAll( { from: senderAddress, to: destinationAddress, l2CallValue: ethers.utils.parseEther('0.01'), excessFeeRefundAddress: refundAddress, callValueRefundAddress: refundAddress, data: calldata, }, await l1Provider.getBaseFeePerGas(), l1Provider, ); // Create the retryable ticket const inbox = new ethers.Contract(inboxAddress, inboxABI, l1Signer); const tx = await inbox.createRetryableTicket( destinationAddress, ethers.utils.parseEther('0.01'), // l2CallValue retryableGasParams.maxSubmissionCost, refundAddress, refundAddress, retryableGasParams.gasLimit, retryableGasParams.maxFeePerGas, calldata, { value: retryableGasParams.deposit, }, ); await tx.wait(); ``` ### Redeeming retryable tickets Retryable tickets can auto-redeem if sufficient gas is provided. If the initial redemption fails, you can manually redeem using the `ArbRetryableTx` precompile: ```solidity ArbRetryableTx(address(110)).redeem(ticketId); ``` ## Deposit ETH using the Arbitrum SDK The [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) provides an `EthBridger` class that simplifies depositing `ETH` from the parent chain to the child chain: ```ts import { ethers } from 'ethers'; import { getArbitrumNetwork, EthBridger } from '@arbitrum/sdk'; // Get the Arbitrum network and create an EthBridger const childNetwork = await getArbitrumNetwork(childProvider); const ethBridger = new EthBridger(childNetwork); // Deposit ETH from parent chain to child chain const depositTx = await ethBridger.deposit({ amount: ethers.utils.parseEther('0.1'), parentSigner, }); const depositReceipt = await depositTx.wait(); // Wait for the deposit to be processed on the child chain const childResult = await depositReceipt.waitForChildTransactionReceipt(childProvider); console.log('Deposit complete:', childResult.complete); ``` ### Deposit to a different address To deposit `ETH` to a recipient address that differs from the sender, use `depositTo`: ```ts const depositTx = await ethBridger.depositTo({ amount: ethers.utils.parseEther('0.1'), parentSigner, childProvider, destinationAddress: '0x...recipient', }); ``` ### Tutorials * [eth-deposit tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/eth-deposit) — full walkthrough of depositing `ETH` using the SDK * [eth-deposit-to-different-address tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/eth-deposit-to-different-address) — depositing `ETH` to a different recipient ## Next steps * For protocol-level details, see [Parent to child chain messaging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md) * For token bridging, see [Token bridging overview](/how-arbitrum-works/deep-dives/token-bridging.md) * For bridging tokens programmatically, see [Bridge tokens programmatically](/arbitrum-essentials/bridging/overview.md) --- > For a complete page index, fetch # Deposit tokens to Arbitrum This guide shows you how to deposit **ERC-20** tokens from a parent chain (like Ethereum) to an Arbitrum child chain. This assumes your token is already configured for bridging. If you need to set up bridging for a new token, see [Configure token bridging](/arbitrum-essentials/bridging/configure-token-gateway/standard.md). ## Prerequisites * Your token must already be bridgeable (registered with a gateway) * A wallet with the tokens you want to deposit on the parent chain * **ETH** on the parent chain to pay for gas fees * Familiarity with [Arbitrum's token bridge architecture](/how-arbitrum-works/deep-dives/token-bridging.md) ## Depositing tokens using the Arbitrum SDK The simplest way to deposit tokens is using the [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk): ### Step 1: Set up the SDK ```javascript import { getArbitrumNetwork, Erc20Bridger } from '@arbitrum/sdk'; import { providers, Wallet } from 'ethers'; // Set up providers and wallet const parentProvider = new providers.JsonRpcProvider(process.env.PARENT_RPC); const childProvider = new providers.JsonRpcProvider(process.env.CHILD_RPC); const wallet = new Wallet(process.env.PRIVATE_KEY, parentProvider); // Initialize the bridge const childNetwork = await getArbitrumNetwork(childProvider); const erc20Bridge = new Erc20Bridger(childNetwork); ``` ### Step 2: Approve the gateway The gateway contract needs permission to transfer your tokens: ```javascript const approveTx = await erc20Bridge.approveToken({ parentSigner: wallet, erc20ParentAddress: tokenAddress, }); const approveReceipt = await approveTx.wait(); console.log(`Approved gateway: ${approveReceipt.transactionHash}`); ``` ### Step 3: Deposit tokens ```javascript const depositTx = await erc20Bridge.deposit({ amount: ethers.utils.parseUnits('100', 18), // Amount to deposit erc20ParentAddress: tokenAddress, parentSigner: wallet, childProvider: childProvider, }); const depositReceipt = await depositTx.wait(); console.log(`Deposit initiated: ${depositReceipt.transactionHash}`); ``` ### Step 4: Wait for child chain execution ```javascript // Wait for the deposit to be processed on the child chain const childResult = await depositReceipt.waitForChildTransactionReceipt(childProvider); if (childResult.complete) { console.log('Deposit successful!'); console.log(`Child chain transaction: ${childResult.message.childTxReceipt.transactionHash}`); } else { console.log('Deposit failed or pending'); } ``` ## Depositing `ETH` (or native token) using the Arbitrum SDK If you need to deposit the chain's native token (typically `ETH`) rather than an `ERC-20` token, use `EthBridger` instead of `Erc20Bridger`: ```javascript import { utils, providers, Wallet, constants } from 'ethers'; import { getArbitrumNetwork, EthBridger, EthDepositMessageStatus } from '@arbitrum/sdk'; // Set up providers and wallet const parentChainProvider = new providers.JsonRpcProvider(process.env.PARENT_CHAIN_RPC); const childChainProvider = new providers.JsonRpcProvider(process.env.CHAIN_RPC); const parentChainWallet = new Wallet(process.env.PRIVATE_KEY, parentChainProvider); // Initialize the ETH bridger const childChainNetwork = await getArbitrumNetwork(childChainProvider); const ethBridger = new EthBridger(childChainNetwork); // For custom gas token chains, approve the gas token first const isCustomGasTokenChain = ethBridger.nativeToken && ethBridger.nativeToken !== constants.AddressZero; if (isCustomGasTokenChain) { const approvalTx = await ethBridger.approveGasToken({ erc20ParentAddress: ethBridger.nativeToken, parentSigner: parentChainWallet, }); await approvalTx.wait(); } // Deposit 0.0001 ETH (or native token) to the child chain const depositTx = await ethBridger.deposit({ amount: utils.parseEther('0.0001'), parentSigner: parentChainWallet, childProvider: childChainProvider, }); const depositReceipt = await depositTx.wait(); console.log(`Deposit initiated: ${depositReceipt.transactionHash}`); // Wait for the child chain to process (typically a few minutes) const result = await depositReceipt.waitForChildTransactionReceipt(childChainProvider); if (result.complete) { console.log(`Deposit confirmed: ${EthDepositMessageStatus[await result.message.status()]}`); } else { console.error(`Deposit failed: ${EthDepositMessageStatus[await result.message.status()]}`); } ``` `ETH` deposits use Arbitrum's parent-to-child message passing system. The SDK handles computing the retryable ticket's max submission cost and forwards the appropriate fees automatically. For details, see [`ETH` deposits under the hood](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#depositing-native-tokens). ## Depositing tokens manually If you prefer to interact with the contracts directly: ### Step 1: Get contract addresses Find the router and gateway addresses for your network on the [contract addresses page](/arbitrum-essentials/reference/contract-addresses.md#token-bridge-smart-contracts). ### Step 2: Approve the gateway First, find which gateway will handle your token: ```solidity // Call on L1GatewayRouter address gateway = l1GatewayRouter.getGateway(tokenAddress); ``` Then approve it: ```solidity // Call on your ERC-20 token contract token.approve(gateway, amountToDeposit); ``` ### Step 3: Initiate deposit Call the router contract's `outboundTransferCustomRefund` method: ```solidity function outboundTransferCustomRefund( address _token, address _refundTo, address _to, uint256 _amount, uint256 _maxGas, uint256 _gasPriceBid, bytes calldata _data ) external payable returns (bytes memory) ``` **Parameters:** * **`_token`**: Parent chain address of the token to deposit * **`_refundTo`**: Address to receive excess gas refund on the child chain * **`_to`**: Address to receive tokens on the child chain * **`_amount`**: Amount of tokens to deposit * **`_maxGas`**: Max gas for child chain execution * **`_gasPriceBid`**: Gas price for child chain execution * **`_data`**: Encoded data containing `maxSubmissionCost` and extra data **Example:** ```javascript const router = new ethers.Contract(routerAddress, routerABI, wallet); // Encode the data parameter const data = ethers.utils.defaultAbiCoder.encode(['uint256', 'bytes'], [maxSubmissionCost, '0x']); const tx = await router.outboundTransferCustomRefund(tokenAddress, refundAddress, destinationAddress, amount, maxGas, gasPriceBid, data, { value: maxSubmissionCost + maxGas * gasPriceBid }); await tx.wait(); ``` ## How deposits work When you deposit tokens: 1. **Parent chain**: Your tokens are escrowed in the gateway contract (for standard tokens) or burned (for custom tokens) 2. **Message sent**: A retryable ticket is created to mint/release tokens on the child chain 3. **Child chain**: After a few minutes, tokens are minted/released to your address For more details on the underlying protocol, see [Token bridging overview](/how-arbitrum-works/deep-dives/token-bridging.md). ## Troubleshooting ### Deposit not appearing on child chain If your deposit doesn't appear after 10-15 minutes: 1. Check the transaction status on the [Retryables Dashboard](https://retryable-dashboard.arbitrum.io/) 2. If the retryable ticket failed, you may need to redeem it manually 3. Check that you provided sufficient gas fees for child chain execution — see [How to estimate gas](/arbitrum-essentials/how-to-estimate-gas.md) for sizing retryable submission and redemption costs ### "Insufficient allowance" error Make sure you approved the correct gateway contract (not the router). Use `getGateway()` to find the right gateway address. ## Next steps * [Withdraw tokens back to parent chain](/arbitrum-essentials/bridging/withdraw/tokens.md) * [Understand token bridge architecture](/how-arbitrum-works/deep-dives/token-bridging.md) * [Configure bridging for a new token](/arbitrum-essentials/bridging/configure-token-gateway/standard.md) ## Resources * [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) * [ETH deposit source code](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/eth-deposit) * [ERC-20 deposit source code](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/token-deposit) * [Contract addresses](/arbitrum-essentials/reference/contract-addresses.md) --- > For a complete page index, fetch # Bridge tokens from L1 to an Arbitrum L3 Arbitrum chains can be deployed as Layer 3 (L3) on top of an existing Arbitrum Layer 2. When users need to move assets from Ethereum (L1) to an L3, they would normally bridge L1 to L2 first, then L2 to L3—two separate transactions with separate wait times. The **teleport** feature in the Arbitrum SDK solves this by letting users bridge tokens from L1 directly to L3 in a single transaction. Under the hood, the SDK coordinates the intermediate bridging steps automatically. ## How teleportation works A teleport deposit creates a chain of [retryable tickets](/arbitrum-essentials/bridging/cross-chain-messaging.md) that propagate your tokens through each layer: 1. **L1 transaction**: Your tokens are deposited into the L1-to-L2 token bridge 2. **L2 retryable**: The L2 bridge receives the tokens and forwards them to the L2-to-L3 bridge 3. **L3 retryable**: The L3 bridge mints or releases tokens to the destination address For `ETH` bridging, the flow uses a "double retryable"—a retryable ticket on L2 that creates another retryable ticket targeting L3. ### Fee payment Each retryable in the chain requires gas fees on its destination chain: * **L1-to-L2 retryable**: Paid in `ETH` on L1 (or the L2 native token if using a custom gas token chain) * **L2-to-L3 retryable**: Paid in the L3 native token, forwarded through the bridge The SDK calculates and bundles all required fees into a single L1 transaction. ## Prerequisites * An Arbitrum L3 chain accessible via RPC * A funded wallet on L1 (`ETH` for gas and any tokens you want to bridge) * The L3 chain must use Arbitrum's canonical token bridge * Node.js and the Arbitrum SDK installed: ```shell npm install @arbitrum/sdk ethers@^5 ``` ## Bridge `ERC-20` tokens from L1 to L3 ### Step 1: Set up providers and the bridger ```javascript import { providers, Wallet } from 'ethers'; import { Erc20L1L3Bridger, getArbitrumNetwork } from '@arbitrum/sdk'; // Connect to all three chains const l1Provider = new providers.JsonRpcProvider(process.env.L1_RPC); const l2Provider = new providers.JsonRpcProvider(process.env.PARENT_CHAIN_RPC); const l3Provider = new providers.JsonRpcProvider(process.env.CHAIN_RPC); const l1Wallet = new Wallet(process.env.PRIVATE_KEY, l1Provider); // Initialize the L1-to-L3 ERC-20 bridger const l3Network = await getArbitrumNetwork(l3Provider); const bridger = new Erc20L1L3Bridger(l3Network); ``` ### Step 2: Approve token and gas token transfers The bridger needs approval to transfer your `ERC-20` tokens. If the L3 uses a custom gas token, you also need to approve that token for fee payment: ```javascript import { utils } from 'ethers'; const l1TokenAddress = '0x...'; // Your ERC-20 token address on L1 const amount = utils.parseUnits('100', 18); // If L3 uses a custom gas token, approve it first if (l3Network.nativeToken) { const gasTokenApproval = await bridger.approveGasToken({ l1Signer: l1Wallet, l2Provider, }); await gasTokenApproval.wait(); console.log('Gas token approved'); } // Approve the ERC-20 token const tokenApproval = await bridger.approveToken({ erc20L1Address: l1TokenAddress, l1Signer: l1Wallet, }); await tokenApproval.wait(); console.log('Token approved'); ``` ### Step 3: Execute the deposit ```javascript // Deposit tokens — a single L1 transaction handles the full L1 → L2 → L3 flow const depositTx = await bridger.deposit({ erc20L1Address: l1TokenAddress, amount, l1Signer: l1Wallet, l2Provider, l3Provider, }); const depositReceipt = await depositTx.wait(); console.log(`Deposit initiated on L1: ${depositReceipt.transactionHash}`); ``` ### Step 4: Monitor the deposit status The deposit progresses through multiple retryable tickets. You can monitor each one: ```javascript // Using the bridger instance from Step 1 // Check the status of all retryables in the teleport chain const depositStatus = await bridger.getDepositStatus({ txHash: depositReceipt.transactionHash, l1Provider, l2Provider, l3Provider, }); // Each field shows the status of one leg of the journey: // - l1l2GasTokenBridgeRetryable (if custom gas token) // - l1l2TokenBridgeRetryable // - l2ForwarderFactoryRetryable // - l2l3TokenBridgeRetryable console.log('Deposit status:', depositStatus); ``` Status values: `REDEEMED` (success), `CREATION_FAILED`, `EXPIRED`, `FUNDS_DEPOSITED_ON_L2`, `NOT_YET_CREATED`. ## Bridge `ETH` from L1 to L3 `ETH` teleportation uses a "double retryable" pattern—simpler than `ERC-20` because no token approvals are needed. ```javascript import { EthL1L3Bridger, getArbitrumNetwork } from '@arbitrum/sdk'; import { providers, Wallet, utils } from 'ethers'; const l1Provider = new providers.JsonRpcProvider(process.env.L1_RPC); const l2Provider = new providers.JsonRpcProvider(process.env.PARENT_CHAIN_RPC); const l3Provider = new providers.JsonRpcProvider(process.env.CHAIN_RPC); const l1Wallet = new Wallet(process.env.PRIVATE_KEY, l1Provider); // Initialize the ETH bridger const l3Network = await getArbitrumNetwork(l3Provider); const ethBridger = new EthL1L3Bridger(l3Network); // Deposit ETH from L1 to L3 const depositTx = await ethBridger.deposit({ amount: utils.parseEther('0.01'), l1Signer: l1Wallet, l2Provider, l3Provider, }); const depositReceipt = await depositTx.wait(); console.log(`ETH deposit initiated on L1: ${depositReceipt.transactionHash}`); // Monitor status const status = await ethBridger.getDepositStatus({ txHash: depositReceipt.transactionHash, l1Provider, l2Provider, l3Provider, }); console.log('ETH deposit status:', status); ``` ## Troubleshooting ### Retryable stuck at `FUNDS_DEPOSITED_ON_L2` This means the L1-to-L2 leg completed but the L2-to-L3 retryable has not yet been created or executed. Possible causes: * The L2-to-L3 retryable is waiting for the L2 sequencer to process it (wait a few more minutes) * Insufficient gas was provided for the L2-to-L3 leg—the SDK should handle this automatically, but network conditions may have changed ### Retryable shows `EXPIRED` Retryable tickets expire after their lifetime (default: 7 days). If a retryable expired, the tokens are sitting on L2 rather than L3. You can manually bridge them from L2 to L3 using the standard [deposit flow](/arbitrum-essentials/bridging/deposit/tokens.md). ### Custom gas token approval failed If the L3 chain uses a custom gas token (not `ETH`), you must approve the gas token before depositing. The `approveGasToken` step handles this—make sure your wallet holds enough of the gas token on L1. ## Next steps * [Deposit tokens (L1 to L2)](/arbitrum-essentials/bridging/deposit/tokens.md) * [Withdraw tokens (L2 to L1)](/arbitrum-essentials/bridging/withdraw/tokens.md) * [Cross-chain messaging overview](/arbitrum-essentials/bridging/cross-chain-messaging.md) * [Source code: L1-L3 teleport tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/l1-l3-teleport) ## Resources * [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) * [Token bridge architecture](/how-arbitrum-works/deep-dives/token-bridging.md) * [Parent-to-child messaging details](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md) --- > For a complete page index, fetch # Bridging overview Token bridging and cross-chain messaging are fundamental aspects of building on Arbitrum. This page helps you find the right guide for your use case. ## Move **ETH** between chains * **[Deposit ETH to Arbitrum](/arbitrum-essentials/bridging/deposit/eth-and-messages.md#bridging-eth-to-a-child-chain)**: Bridge **ETH** from the parent chain to a child chain using the Inbox contract * **[Withdraw **ETH** to Ethereum](/arbitrum-essentials/bridging/withdraw/eth-and-messages.md#withdrawing-eth)**: Withdraw **ETH** from a child chain back to the parent chain via ArbSys ## Move ERC-20 tokens between chains * **[Deposit tokens to Arbitrum](/arbitrum-essentials/bridging/deposit/tokens.md)**: Move **ERC-20** tokens from the parent chain to the child chain * **[Withdraw tokens to Ethereum](/arbitrum-essentials/bridging/withdraw/tokens.md)**: Move **ERC-20** tokens from the child chain back to the parent chain ## Make my token bridgeable Choose a gateway based on your token's requirements: * **[Standard gateway](/arbitrum-essentials/bridging/configure-token-gateway/standard.md)** (recommended): Automatic deployment of a standard **ERC-20** on Arbitrum, no configuration required * **[Generic-custom gateway](/arbitrum-essentials/bridging/configure-token-gateway/generic-custom.md)**: Custom functionality in your child chain token while using Arbitrum's built-in gateway * **[Custom gateway](/arbitrum-essentials/bridging/configure-token-gateway/custom.md)**: Specialized gateway logic for advanced use cases ## Send arbitrary cross-chain messages * **[Parent → child messaging](/arbitrum-essentials/bridging/deposit/eth-and-messages.md#creating-retryable-tickets)**: Send messages from Ethereum to Arbitrum using retryable tickets * **[Child → parent messaging](/arbitrum-essentials/bridging/withdraw/eth-and-messages.md#sending-a-message-from-the-child-to-the-parent-chain)**: Send messages from Arbitrum to Ethereum via ArbSys and the Outbox ## Build on a custom gas token chain If you're working with an Arbitrum chain that uses a non-**ETH** gas token, see [Custom gas token chain bridging](/arbitrum-essentials/bridging/custom-gas-token-chains.md) for SDK APIs and workflows. ## Example code * [Token deposits (parent → child)](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/token-deposit) * [Token withdrawals (child → parent)](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/token-withdraw) * [Custom token bridging setup](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/custom-token-bridging) ## Learn more * [Cross-chain messaging concepts](/arbitrum-essentials/bridging/cross-chain-messaging.md) * [Token bridge architecture](/how-arbitrum-works/deep-dives/token-bridging.md) * [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) --- > For a complete page index, fetch # How to bridge from child chain to parent chain This guide explains how to programmatically send messages and withdraw assets from an Arbitrum child chain to a parent chain (such as Ethereum). For conceptual information about the messaging protocol, see [Child-to-parent chain messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md). ## Prerequisites * A child chain wallet with funds * Access to parent chain infrastructure (for executing the final step) * Awareness that child-to-parent messages require 6.4 days to finalize ## Overview of the process Child-to-parent chain messaging follows these steps: 1. **Send message on child chain**: Call `ArbSys.sendTxToL1` to initiate the message 2. **Wait for finalization**: The message enters a 6.4-day Challenge Period 3. **Execute on parent chain**: After finalization, call `Outbox.executeTransaction` to complete the transfer ## Sending a message from the child to the parent chain To send a message from the child chain to the parent chain, use the `ArbSys` precompile's `sendTxToL1` method: ```solidity function sendTxToL1( address destination, bytes calldata data ) external payable returns (uint256) ``` ### Parameters * **`destination`**: The parent chain address that will receive the message * **`data`**: Calldata to send to the destination address on the parent chain ### Return value Returns a unique identifier for the message, used to track its status and construct the outbox proof for execution. The `ArbSys` precompile is located at address `0x0000000000000000000000000000000000000064`. **Example**: Sending a simple message ```javascript const arbSys = new ethers.Contract('0x0000000000000000000000000000000000000064', arbSysABI, childChainSigner); const tx = await arbSys.sendTxToL1(parentChainDestination, ethers.utils.toUtf8Bytes('Hello from L2!')); const receipt = await tx.wait(); ``` ## Executing the message on the parent chain After the 6.4-day challenge period, you can execute the message on the parent chain. ### Step 1: Retrieve proof data Use the `NodeInterface` contract to get the Merkle proof: ```solidity function constructOutboxProof( uint64 size, uint64 leaf ) external view returns ( bytes32[] memory proof, uint256 path, address l2Sender, address l1Dest, uint256 l2Block, uint256 l1Block, uint256 timestamp, uint256 amount, bytes memory calldataForL1 ) ``` **Parameters:** * **`size`**: The batch number containing your message * **`leaf`**: The index of your message within the batch (0-indexed) **Example usage:** ```javascript const nodeInterface = new ethers.Contract('0x00000000000000000000000000000000000000C8', nodeInterfaceABI, childChainProvider); const proofData = await nodeInterface.constructOutboxProof(batchNumber, indexInBatch); ``` > **NOTE** > > `NodeInterface` is a "virtual" contract accessible at `0x00000000000000000000000000000000000000C8`. It isn't a true precompile but provides Arbitrum-specific data without requiring a custom RPC. ### Step 2: Execute on the parent chain Call `Outbox.executeTransaction` with the proof data from Step 1: ```solidity function executeTransaction( bytes32[] calldata proof, uint256 path, address l2Sender, address l1Dest, uint256 l2Block, uint256 l1Block, uint256 timestamp, uint256 amount, bytes calldata calldataForL1 ) external ``` All parameters come from the `constructOutboxProof` call. The method executes the message at the `l1Dest` address with the provided calldata and amount. **Example usage:** ```javascript const outbox = new ethers.Contract(outboxAddress, outboxABI, parentChainSigner); const tx = await outbox.executeTransaction(proofData.proof, proofData.path, proofData.l2Sender, proofData.l1Dest, proofData.l2Block, proofData.l1Block, proofData.timestamp, proofData.amount, proofData.calldataForL1); await tx.wait(); ``` ## Withdrawing **ETH** To withdraw **ETH** from the child chain, use the `ArbSys` precompile's `withdrawEth` method: ```solidity function withdrawEth( address destination ) external payable returns (uint256) ``` ### Parameters * **`destination`**: The parent chain address that will receive the **ETH** * **Value** (`msg.value`): The amount of **ETH** to withdraw from the child chain ### Return value Returns a unique identifier for the withdrawal message. ### How **ETH** withdrawal works 1. **On the child chain**: The **ETH** balance is burned, and a message is created 2. **Challenge period**: Wait 6.4 days for the assertion to finalize 3. **On the parent chain**: Execute via `Outbox.executeTransaction` to claim your **ETH** `ArbSys.withdrawEth` is equivalent to calling `ArbSys.sendTxToL1` with an empty calldata argument. Like any child-to-parent message, it requires executing on the parent chain after the dispute period. ### Example: withdrawing **ETH** ```javascript const arbSys = new ethers.Contract('0x0000000000000000000000000000000000000064', arbSysABI, childChainSigner); // Withdraw 0.1 ETH const tx = await arbSys.withdrawEth(parentChainAddress, { value: ethers.utils.parseEther('0.1'), }); const receipt = await tx.wait(); // After 6.4 days, execute on parent chain using the steps above ``` The withdrawal process: ![Process that funds follow during a withdrawal operation](/img/apps-withdrawing-ether.svg) ## Withdrawing **ERC-20** tokens For **ERC-20** token withdrawals, see the dedicated [Withdraw tokens guide](/arbitrum-essentials/bridging/withdraw/tokens.md), which provides detailed instructions for using Arbitrum's canonical token bridge. ## Using the Arbitrum SDK The Arbitrum SDK simplifies child-to-parent messaging: ```javascript import { ChildToParentMessageStatus, ChildTransactionReceipt } from '@arbitrum/sdk'; // Get the L2 transaction receipt const l2Receipt = await childChainProvider.getTransactionReceipt(l2TxHash); const childTxReceipt = new ChildTransactionReceipt(l2Receipt); // Get child-to-parent messages from the transaction const messages = await childTxReceipt.getChildToParentMessages(parentChainSigner); // Wait for the message to be executable const message = messages[0]; await message.waitUntilReadyToExecute(childChainProvider); // Execute the message on the parent chain const executeResult = await message.execute(childChainProvider); await executeResult.wait(); ``` ### Withdraw ETH using the Arbitrum SDK The SDK also provides an `EthBridger` class that simplifies `ETH` withdrawals: ```ts import { getArbitrumNetwork, EthBridger } from '@arbitrum/sdk'; import { ethers } from 'ethers'; const childNetwork = await getArbitrumNetwork(childProvider); const ethBridger = new EthBridger(childNetwork); // Initiate withdrawal from child chain const withdrawTx = await ethBridger.withdraw({ amount: ethers.utils.parseEther('0.1'), childSigner, from: walletAddress, destinationAddress: walletAddress, }); const withdrawReceipt = await withdrawTx.wait(); // Get child-to-parent events for later outbox execution const withdrawEvents = withdrawReceipt.getChildToParentEvents(); ``` After the 6.4-day challenge period, use the outbox execution flow described above (or the SDK's `ChildToParentMessage.execute`) to claim your `ETH` on the parent chain. For a complete example, see the [eth-withdraw tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/eth-withdraw). ## Message lifecycle Child-to-parent messages go through these stages: | Stage | Description | | ---------------------------------------- | -------------------------------------------------------------------------- | | Posted on child chain | The message is sent via `ArbSys.sendTxToL1` | | Waiting for finalization | The assertion containing the message is in the challenge period (6.4 days) | | Confirmed and executable on parent chain | The assertion is confirmed, and the message can be executed in the outbox | ## Next steps * For protocol-level details, see [Child to parent chain messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md) * For token bridging concepts, see [Token bridging overview](/how-arbitrum-works/deep-dives/token-bridging.md) * For the Arbitrum SDK documentation, see [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) --- > For a complete page index, fetch # Withdraw tokens to parent chain This guide shows you how to withdraw **ERC-20** tokens from an Arbitrum child chain back to a parent chain (like Ethereum). This assumes your token is already configured for bridging. If you need to set up bridging for a new token, see [Configure token bridging](/arbitrum-essentials/bridging/configure-token-gateway/standard.md). ## Prerequisites * Your token must already be bridgeable (registered with a gateway) * A wallet with tokens on the child chain * **ETH** on both chains (child chain for initiation, parent chain for final execution) * **Important**: Withdrawals require 6.4 days to finalize due to the challenge period * Familiarity with [child-to-parent messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md) ## Withdraw tokens using the Arbitrum SDK The simplest way to withdraw tokens is using the [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk): ### Step 1: Initiate the withdrawal on the child chain ```javascript import { getArbitrumNetwork, Erc20Bridger } from '@arbitrum/sdk'; import { providers, Wallet } from 'ethers'; // Set up providers and wallet const parentProvider = new providers.JsonRpcProvider(process.env.PARENT_RPC); const childProvider = new providers.JsonRpcProvider(process.env.CHILD_RPC); const childWallet = new Wallet(process.env.PRIVATE_KEY, childProvider); // Initialize the bridge const childNetwork = await getArbitrumNetwork(childProvider); const erc20Bridge = new Erc20Bridger(childNetwork); // Initiate withdrawal const withdrawTx = await erc20Bridge.withdraw({ amount: ethers.utils.parseUnits('100', 18), erc20ParentAddress: parentTokenAddress, childSigner: childWallet, }); const withdrawReceipt = await withdrawTx.wait(); console.log(`Withdrawal initiated: ${withdrawReceipt.transactionHash}`); ``` ### Step 2: Wait for the challenge period (\~6.4 days) ```javascript import { ChildToParentMessageStatus, ChildTransactionReceipt } from '@arbitrum/sdk'; // Get the withdrawal message const childReceipt = new ChildTransactionReceipt(withdrawReceipt); const messages = await childReceipt.getChildToParentMessages(childWallet); const message = messages[0]; // Wait for the message to be confirmed (takes ~6.4 days) await message.waitUntilReadyToExecute(childProvider); console.log('Message confirmed! Ready to execute on parent chain.'); ``` ### Step 3: Execute on the parent chain After the challenge period, use the SDK to execute the withdrawal via the outbox. This requires a parent chain wallet with `ETH` for gas: ```javascript import { providers, Wallet } from 'ethers'; import { ChildTransactionReceipt, ChildToParentMessageStatus } from '@arbitrum/sdk'; const parentChainProvider = new providers.JsonRpcProvider(process.env.PARENT_CHAIN_RPC); const childChainProvider = new providers.JsonRpcProvider(process.env.CHAIN_RPC); const parentChainWallet = new Wallet(process.env.PRIVATE_KEY, parentChainProvider); // Get the withdrawal transaction receipt from the child chain const receipt = await childChainProvider.getTransactionReceipt(withdrawTxHash); const childReceipt = new ChildTransactionReceipt(receipt); // Extract child-to-parent messages (typically one per withdrawal) const messages = await childReceipt.getChildToParentMessages(parentChainWallet); const message = messages[0]; // Check the current status const status = await message.status(childChainProvider); if (status === ChildToParentMessageStatus.EXECUTED) { console.log('Already executed — tokens are on the parent chain'); } else { // Wait until the outbox entry exists (challenge period must pass) await message.waitUntilReadyToExecute(childChainProvider, 60_000); console.log('Ready to execute — submitting parent chain transaction'); const executeTx = await message.execute(childChainProvider); const executeReceipt = await executeTx.wait(); console.log(`Withdrawal complete: ${executeReceipt.transactionHash}`); } ``` ## Withdrawing `ETH` (or native token) using the Arbitrum SDK To withdraw the chain's native token (typically `ETH`) rather than an `ERC-20`, use `EthBridger`: ```javascript import { utils, providers, Wallet } from 'ethers'; import { getArbitrumNetwork, EthBridger } from '@arbitrum/sdk'; const childChainProvider = new providers.JsonRpcProvider(process.env.CHAIN_RPC); const childChainWallet = new Wallet(process.env.PRIVATE_KEY, childChainProvider); const childChainNetwork = await getArbitrumNetwork(childChainProvider); const ethBridger = new EthBridger(childChainNetwork); // Initiate withdrawal of 0.000001 ETH const withdrawTx = await ethBridger.withdraw({ amount: utils.parseEther('0.000001'), childSigner: childChainWallet, destinationAddress: childChainWallet.address, }); const withdrawReceipt = await withdrawTx.wait(); console.log(`Withdrawal initiated: ${withdrawReceipt.transactionHash}`); // Get withdrawal event data const withdrawEvents = withdrawReceipt.getChildToParentEvents(); console.log('Withdrawal data:', withdrawEvents); console.log(`After the challenge period (~6.4 days), execute via the outbox using tx hash: ${withdrawReceipt.transactionHash}`); ``` The withdrawal creates a child-to-parent message. After the challenge period, execute it using the outbox as shown in Step 3 above. The same outbox execution pattern works for both `ETH` and `ERC-20` withdrawals. ## Withdraw tokens manually If you prefer to interact with the contracts directly: ### Step 1: Initiate the withdrawal on the child chain Approve and call the `L2GatewayRouter`: ```javascript // Approve the L2 gateway const childToken = new ethers.Contract(childTokenAddress, erc20ABI, childWallet); const childRouter = new ethers.Contract(childRouterAddress, routerABI, childWallet); // Get the gateway address const gateway = await childRouter.getGateway(parentTokenAddress); // Approve await childToken.approve(gateway, amountToWithdraw); // Initiate withdrawal const withdrawTx = await childRouter.outboundTransfer( parentTokenAddress, // Parent chain token address parentDestination, // Where to send tokens on parent chain amountToWithdraw, '0x', // Extra data (usually empty) ); await withdrawTx.wait(); ``` ### Step 2: Wait for the challenge period After initiating the withdrawal, you must wait approximately 6.4 days for the assertion containing your withdrawal to be confirmed. You can check the status on the [Arbitrum Bridge UI](https://bridge.arbitrum.io/) by connecting your wallet, or programmatically track the message status. ### Step 3: Construct proof and execute on the parent chain After the challenge period, get the proof data using `NodeInterface`: ```javascript const nodeInterface = new ethers.Contract('0x00000000000000000000000000000000000000C8', nodeInterfaceABI, childProvider); // Get proof data (you'll need the batch number and index from the withdrawal receipt) const proofData = await nodeInterface.constructOutboxProof(batchNumber, indexInBatch); ``` Then execute on the parent chain using the Outbox contract: ```javascript const outbox = new ethers.Contract(outboxAddress, outboxABI, parentWallet); const executeTx = await outbox.executeTransaction(proofData.proof, proofData.path, proofData.l2Sender, proofData.l1Dest, proofData.l2Block, proofData.l1Block, proofData.timestamp, proofData.amount, proofData.calldataForL1); await executeTx.wait(); console.log('Withdrawal executed on parent chain!'); ``` ## How withdrawals work The token withdrawal process: ![Withdrawal process using the gateway](/img/apps-bridge_withdrawals.png) 1. **Child chain**: Tokens are burned (or escrowed for custom tokens) 2. **Message created**: A child-to-parent message is encoded and included in an assertion 3. **Challenge period**: Wait 6.4 days for the assertion to be confirmed 4. **Parent chain**: After confirmation, execute the message to release tokens from the parent gateway For more details on the underlying protocol, see: * [Child to parent chain messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md) * [Token bridging overview](/how-arbitrum-works/deep-dives/token-bridging.md) ## Troubleshooting ### "Withdrawal not ready to execute" The challenge period hasn't passed yet. Withdrawals take approximately 6.4 days to finalize. Check the status using: ```javascript const status = await message.status(childProvider); console.log(ChildToParentMessageStatus[status]); // Should show "CONFIRMED" when ready ``` ### "Message already executed" This withdrawal has already been executed. Check the parent chain token balance—the tokens should already be there. ### Find the batch number and index If you need to construct the proof manually, you can find the batch number and index from the withdrawal transaction receipt events. Look for the `L2ToL1Tx` event emitted by `ArbSys`. ## Next steps * [Deposit tokens to child chain](/arbitrum-essentials/bridging/deposit/tokens.md) * [Understand token bridge architecture](/how-arbitrum-works/deep-dives/token-bridging.md) * [General child-to-parent messaging](/arbitrum-essentials/bridging/withdraw/eth-and-messages.md) ## Resources * [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) * [`ETH` withdrawal source code](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/eth-withdraw) * [`ERC-20` withdrawal source code](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/token-withdraw) * [Outbox execution source code](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/outbox-execute) * [Contract addresses](/arbitrum-essentials/reference/contract-addresses.md) * [Arbitrum Bridge UI](https://bridge.arbitrum.io/) --- > For a complete page index, fetch # How to estimate gas in Arbitrum > **INFO** — Looking for Stylus guidance? > > Head over to [the Stylus gas docs](/stylus/reference/opcode-hostio-pricing.md) for Stylus-specific guidance. This how-to covers how gas operates in Arbitrum, how it's calculated, and how to estimate it before submitting transactions. For a deeper understanding of the underlying pricing mechanisms, see the [Gas and Fees](/how-arbitrum-works/deep-dives/gas-and-fees.md) concept page. ## Quick start: use `eth_estimateGas` If you don't need to understand the formula, you can rely on the standard gas estimation process. Call an Arbitrum node's `eth_estimateGas` RPC, which returns a gas limit sufficient to cover the entire transaction fee at the current child chain gas price. Multiplying the value from `eth_estimateGas` by the child chain gas price gives you the total **ETH** required for the transaction to succeed. Note that for a given operation, the `eth_estimateGas` value may vary over time as the parent chain calldata price fluctuates. Alternatively, call `NodeInterface.gasEstimateComponents()` and use the first result (`gasEstimate`) as your gas limit. Multiply by the third result (`baseFee`) to get the total cost. For background on `NodeInterface` itself, see the [NodeInterface overview](/arbitrum-essentials/nodeinterface/overview.md). Note that when working with [parent to child chain messages](/arbitrum-essentials/bridging/cross-chain-messaging.md) (also known as [retryable tickets](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md)), you can use the function [`ParentToChildMessageGasEstimator.estimateAll()`](https://github.com/OffchainLabs/arbitrum-sdk/blob/main/packages/sdk/src/lib/message/ParentToChildMessageGasEstimator.ts) of the Arbitrum SDK or [`NodeInterface.estimateRetryableTicket()`](https://github.com/OffchainLabs/nitro/blob/v3.11.3/nodeInterface/NodeInterface.go#L120) to get all the gas information needed to send a successful transaction. ## The fee formula > **INFO** — L1 fee "baked in" > > The model below explains a two-dimensional fee model that captures both L1 and L2 costs. However, it is important to note that users will see a single fee—the L2 cost with the L1 fee "baked-in." This differs from other Rollups. > > Arbitrum converts L1 calldata costs into equivalent L2 gas, so the user sees a single fee rather than two (as explained in the model below). The formula detailed below is precisely how it is calculated—but just remember it is shown as a single fee to the user. Arbitrum uses a [two-dimensional fee model](https://medium.com/offchainlabs/understanding-arbitrum-2-dimensional-fees-fd1d582596c9) where a transaction's total fee has two components: child chain execution costs and parent chain data posting costs. The total transaction fee is: ```text Transaction fees (TXFEES) = L2 Gas Price (P) * Gas Limit (G) ``` The gas limit includes the gas for child chain computation plus an additional buffer to cover the parent chain gas the Sequencer pays when [posting the batch](/how-arbitrum-works/deep-dives/sequencer.md): ```text Gas Limit (G) = Gas used on L2 (L2G) + Extra Buffer for L1 cost (B) ``` The buffer accounts for the cost of posting the transaction (batched and compressed) on the parent chain. The parent chain estimated posting cost is: ```text L1 Estimated Cost (L1C) = L1 price per byte of data (L1P) * Size of data to be posted in bytes (L1S) ``` Where: * **L1P** estimates the current parent chain price per byte of data, dynamically adjusted by the child chain over time * **L1S** estimates how many bytes the transaction will occupy in the batch by compressing it with Brotli The buffer converts this cost into child chain gas units: ```text Extra Buffer (B) = L1 Estimated Cost (L1C) / L2 Gas Price (P) ``` Combining everything: ```text TXFEES = P * (L2G + ((L1P * L1S) / P)) ``` ## Where to get each variable You can use the [`NodeInterface`](/arbitrum-essentials/nodeinterface/reference.md) to retrieve the fee components: * **P** (L2 Gas Price): Price per gas unit. Starts at a gas floor price and increases with demand. * Call `NodeInterface.gasEstimateComponents()` and get the third element, `baseFee`. * **L2G** (Gas used on L2): Gas consumed by child chain computation, excluding parent chain posting costs. * Call `NodeInterface.gasEstimateComponents()` with the transaction data and subtract the second element (`gasEstimateForL1`) from the first (`gasEstimate`). * **L1P** (L1 estimated price per byte of data): Estimated cost of posting 1 byte of data on the parent chain. * Call `NodeInterface.gasEstimateComponents()`, get the fourth element `l1BaseFeeEstimate` and multiply it by 16. * **L1S** (Size of data to be posted on L1, in bytes): Depends on the transaction data. Arbitrum adds a fixed amount (\~140 bytes) for the static part of the transaction. * Call `NodeInterface.gasEstimateComponents()`, take the second element `gasEstimateForL1` (this is `B` in the formula), multiply by `P` and divide by `L1P`. * For Arbitrum Nova (AnyTrust), the data size is a fixed value since only the Data Availability Certificate (DAC) is posted on the parent chain, [as explained here](/how-arbitrum-works/deep-dives/anytrust-protocol.md#data-availability-certificates-dacert). > **NOTE** > > For L1P and L1S, you can also call `NodeInterface.gasEstimateL1Component()` to get `l1BaseFeeEstimate` and `gasEstimateForL1`. ## Code example Here's how to estimate gas using the [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk) and `NodeInterface`: First, instantiate the `NodeInterface`: ```ts const { NodeInterface__factory } = require("@arbitrum/sdk/dist/lib/abi/factories/NodeInterface__factory"); const { NODE_INTERFACE_ADDRESS } = require("@arbitrum/sdk/dist/lib/dataEntities/constants"); ... // Instantiation of the NodeInterface object const nodeInterface = NodeInterface__factory.connect( NODE_INTERFACE_ADDRESS, baseL2Provider ); ``` Call `gasEstimateComponents()` with your transaction's destination address and data: ```ts // Getting the estimations from NodeInterface.gasEstimateComponents() const gasEstimateComponents = await nodeInterface.callStatic.gasEstimateComponents(destinationAddress, false, txData, { blockTag: 'latest', }); ``` Extract the formula variables: ```ts // Getting useful values for calculating the formula const parentChainGasEstimated = gasEstimateComponents.gasEstimateForL1; const childChainGasUsed = gasEstimateComponents.gasEstimate.sub(gasEstimateComponents.gasEstimateForL1); const childChainEstimatedPrice = gasEstimateComponents.baseFee; const parentChainEstimatedPrice = gasEstimateComponents.l1BaseFeeEstimate.mul(16); // Calculating some extra values to be able to apply all variables of the formula // ------------------------------------------------------------------------------- // NOTE: parentChainGasEstimated (B in the formula) is calculated based on the child chain's gas price const parentChainCost = parentChainGasEstimated.mul(childChainEstimatedPrice); // Guard against zero parent-chain base-fee estimates (some AnyTrust/Orbit configs) const parentChainSize = parentChainEstimatedPrice.eq(0) ? 0 : parentChainCost.div(parentChainEstimatedPrice); // Setting the basic variables of the formula const P = childChainEstimatedPrice; const L2G = childChainGasUsed; const L1P = parentChainEstimatedPrice; const L1S = parentChainSize; ``` Calculate the total fee: ```ts // L1C (L1 Cost) = L1P * L1S const L1C = L1P.mul(L1S); // B (Extra Buffer) = L1C / P const B = L1C.div(P); // G (Gas Limit) = L2G + B const G = L2G.add(B); // TXFEES (Transaction fees) = P * G const TXFEES = P.mul(G); ``` Refer to [our tutorials repository](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/gas-estimation) for a complete working example. ## Checking parent chain confirmations and batch information The `NodeInterface` also exposes methods for querying confirmation status and batch membership of child chain blocks. This is useful when you need to verify that a child chain block has been posted to the parent chain. ```ts import { NodeInterface__factory } from '@arbitrum/sdk/dist/lib/abi/factories/NodeInterface__factory'; import { NODE_INTERFACE_ADDRESS } from '@arbitrum/sdk/dist/lib/dataEntities/constants'; const nodeInterface = NodeInterface__factory.connect(NODE_INTERFACE_ADDRESS, childProvider); // Get parent chain confirmations for a child chain block const { confirmations } = await nodeInterface.functions.getL1Confirmations(blockHash); // Find which batch contains a specific block const { batch } = await nodeInterface.functions.findBatchContainingBlock(blockNumber); ``` For a complete working example, see the [parent-chain-confirmation-checker tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/parent-chain-confirmation-checker). ## Final note Gas estimations from the above techniques are approximate and the actual gas fees may differ. We encourage developers to set this expectation explicitly wherever this information is shared with end-users. --- > For a complete page index, fetch # How to verify child chain state on the parent chain Arbitrum implements a **fraud proof** system that ensures that the state of any given child chain is safely maintained by its parent chain. In this system, a validator is responsible for periodically posting **assertions** about the child chain's state to its parent chain. See [Inside Arbitrum Nitro](/how-arbitrum-works/inside-arbitrum-nitro.md#step-5-validation-and-dispute-resolution) to learn more about the Rollup protocol. Each assertion is a *claim about the impact that a series of child chain blocks (containing transactions) ought to have on the chain's state*. When posted to the parent chain, assertions are recorded by the Rollup contract; once confirmed, the resulting state commitment (block hash + send root) is relayed to the `Outbox` contract. The primary purpose of this commitment is to **secure withdrawals**: it provides the trusted state against which child chain -> parent chain messages (such as withdrawals) are proven before they can be executed on the parent chain. The same committed state can also be reused by off-chain tools — for example, the off-chain state verification described in this guide. Before we begin, we will introduce the key components: the assertion, its global state, and send roots. ## Assertions The Rollup contract stores a chain of **assertions**. Each stored assertion is represented by an `AssertionNode` struct: ```solidity struct AssertionNode { // Block when the first child of this assertion was created uint64 firstChildBlock; // Block when the second child was created (non-zero => assertion was challenged) uint64 secondChildBlock; // The block number when this assertion was created uint64 createdAtBlock; // True if this assertion is the first child of its prev bool isFirstChild; // Status of the assertion: NoAssertion / Pending / Confirmed AssertionStatus status; // Hash of the environment config at creation time (e.g. wasmModuleRoot) bytes32 configHash; } ``` An assertion's validity is bound to its **assertion hash**; the struct above does not store the claimed state directly. An assertion is created together with its **before** and **after** execution states, packaged as `AssertionInputs`: ```solidity struct AssertionInputs { BeforeStateData beforeStateData; AssertionState beforeState; AssertionState afterState; } struct AssertionState { GlobalState globalState; MachineStatus machineStatus; bytes32 endHistoryRoot; } ``` The key field is `afterState.globalState`: it contains the child chain **block hash** and **send root** that this assertion claims. We can extract them with the `GlobalStateLib` helpers `getBlockHash()` and `getSendRoot()`. The assertion hash itself authenticates these values — it is computed as: ```solidity assertionHash = keccak256(abi.encodePacked( parentAssertionHash, afterState.hash(), // keccak256(abi.encode(afterState)) inboxAcc )) ``` ## Send roots The send root mapping is stored in the `Outbox` contract. It maps the Merkle root of each batch of child chain -> parent chain messages (the *send root*) to its corresponding child chain block hash. When an assertion is confirmed, the Rollup contract records the send root to the outbox so that, when a user later triggers a child chain -> parent chain message on the parent chain, the request can be verified. ```solidity mapping(bytes32 => bytes32) public roots; // maps root hashes => child chain block hash ``` Because this mapping stores the block hash, you can also recover the child chain block hash directly from the outbox. ## Verify child chain state on parent chain Assume there is a contract called `foo` on the child chain at address `fooAddress`, and we want to prove its storage value at `slot`. To verify the state, we need a Merkle Trie verifier contract — for example, a `Lib_MerkleTrie`-style library that exposes a `get(key, proof, root)` function. > **NOTE** — Why we can trust an untrusted child chain RPC > > The steps below query a child chain RPC several times (`eth_getBlockByHash`, `eth_getProof`). You do *not* have to trust that RPC. Every value it returns is checked against the trust anchor established in step 1 — the block hash and send root taken from the assertion **confirmed on the parent chain**: > > * The block header is only accepted if its RLP hash reproduces the confirmed block hash. > * The account and storage values are only accepted if their Merkle proofs verify against the state root inside that header. > > Because each link is bound to the confirmed parent chain commitment by a cryptographic hash, a malicious or buggy RPC **cannot forge data that passes verification**. The worst it can do is return wrong or missing data, which makes verification *fail* (a denial of service) — it can never produce a false positive. This one-way guarantee is what makes the whole procedure sound. ### 1. Get a confirmed child chain block hash For security, we use the latest *confirmed* assertion rather than the latest *proposed* one. The `AssertionConfirmed` event emits the block hash and send root directly: * Get the latest confirmed assertion hash: `assertionHash = rollup.latestConfirmed()`, which returns a `bytes32` assertion hash. * Query the confirmation event for that hash: `AssertionConfirmed(bytes32 indexed assertionHash, bytes32 blockHash, bytes32 sendRoot)`. The event gives you `blockHash` and `sendRoot` directly. * (Optional) You can cross-check by reading the global state from the `AssertionCreated` event for the same hash and extracting `blockHash = GlobalStateLib.getBlockHash(assertion.afterState.globalState)` and `sendRoot = GlobalStateLib.getSendRoot(assertion.afterState.globalState)`. * (Optional) You can also look the block hash up in the outbox: `roots[sendRoot]`. ### 2. Prove the state root belongs to the block hash, using the block header With the block hash, fetch the corresponding block from the child chain provider: `l2blockRaw = eth_getBlockByHash(blockHash)`. Then re-derive the block hash by RLP-encoding and hashing the header fields: ```text blockarray = [ l2blockRaw.parentHash, l2blockRaw.sha3Uncles, l2blockRaw.miner, l2blockRaw.stateRoot, l2blockRaw.transactionsRoot, l2blockRaw.receiptsRoot, l2blockRaw.logsBloom, BigNumber.from(l2blockRaw.difficulty).toHexString(), BigNumber.from(l2blockRaw.number).toHexString(), BigNumber.from(l2blockRaw.gasLimit).toHexString(), BigNumber.from(l2blockRaw.gasUsed).toHexString(), BigNumber.from(l2blockRaw.timestamp).toHexString(), l2blockRaw.extraData, l2blockRaw.mixHash, l2blockRaw.nonce, BigNumber.from(l2blockRaw.baseFeePerGas).toHexString(), ]; ``` * Compute `calculated_blockhash = keccak256(RLP.encode(blockarray))`. * Check that it matches the value from step 1: `calculated_blockhash === blockHash`. If they match, the header — and in particular the `stateRoot` — is proven correct. > **CAUTION** — Watch the header field set and zero-value encoding > > The exact list of header fields (and their ordering) must match the block header schema of the chain you are proving against. The 16 fields above match current Arbitrum Nitro headers, but a chain running a different configuration may include additional fields. Additionally, RLP requires minimal integer encoding: a numeric field whose value is `0` must encode to an empty byte string, not `0x00`. Confirm your encoding reproduces the expected hash before relying on it. ### 3. Prove the account in the state root With a trusted state root, verify the account: ```js proof = l2provider.send('eth_getProof', [fooAddress, [slot], { blockHash }]); ``` * Get account proof: `accountProof = RLP.encode(proof.accountProof)` * Get proofKey: `proofKey = ethers.utils.keccak256(fooAddress)` * Call the verifier contract to verify: ```js [acctExists, acctEncoded] = verifier.get(proofKey, accountProof, stateRoot); ``` * Check for equality: `acctExists == true` ### 4. Prove the storage slot is in the account root * Get storage root: `storageRoot = RLP.decode(acctEncoded)[2]` * Get storage slot key: `slotKey = ethers.utils.keccak256(slot)` * Get storageProof: `storageProof = ethers.utils.RLP.encode(proof.storageProof.filter((x) => x.key === slot)[0].proof)` * Call the Merkle verifier contract to verify: ```js const [storageExists, storageEncoded] = await verifier.get(slotKey, storageProof, storageRoot); ``` * Check for equality: `storageExists == true` * Obtain the value of the storage at `slot`: `storageValue = ethers.utils.RLP.decode(storageEncoded)` You have now proven a specific storage value at a specific block height on the child chain, entirely through the parent chain. ### Cross-check the value directly on the child chain * Call the child chain RPC provider to get the value at the corresponding block number: `actualValue = l2provider.getStorageAt(fooAddress, slot, l2blockRaw.number)` * Check for equality: `storageValue === BigNumber.from(actualValue).toHexString()` ## Security considerations and limitations The procedure above is cryptographically sound — its trust is anchored in an assertion confirmed on the parent chain, and every value fetched from a child chain RPC is verified against that anchor. Keep the following caveats in mind: * **You can only prove state at a confirmed block.** The block hash in an assertion's global state is for one specific block (the end of the assertion). To prove state at an arbitrary or more recent block, you must additionally link that block back to a confirmed block through the `parentHash` chain. * **Confirmation depends on the parent chain's finality.** `latestConfirmed()` can change if the parent chain reorganizes. For maximum safety, read it at a finalized parent chain block. * **"Confirmed" reflects the Rollup's trust model.** A confirmed assertion is one that survived its challenge period; its correctness rests on the Rollup's fraud-proof / BoLD security assumptions (at least one honest validator). * **Zero / non-existent values need explicit handling.** The steps above check `acctExists == true` and `storageExists == true`. Proving that a slot's value *is* zero requires handling the corresponding exclusion proof. * **The Merkle Trie verifier must be correct.** The overall guarantee depends on the correctness of the verifier library you use; review and test it before relying on it in production. --- > For a complete page index, fetch # NodeInterface overview The Arbitrum Nitro software includes a special `NodeInterface` contract available at address `0xc8` that is only accessible via [RPCs](/arbitrum-essentials/arbitrum-vs-ethereum/rpc-methods.md) (it's not actually deployed onchain and thus can't be called by smart contracts). The way it works is that the node uses Geth's [`InterceptRPCMessage`](https://github.com/OffchainLabs/go-ethereum/blob/0f618f330b8d78457524839997f0041d86f3cd1a/internal/ethapi/api.go#L1034) hook to detect messages sent to the address `0xc8`, and swaps out the message it's handling before deriving a transaction from it. The [reference page](/arbitrum-essentials/nodeinterface/reference.md) contains information about all methods available in the `NodeInterface`. --- > For a complete page index, fetch # NodeInterface reference The Arbitrum Nitro software includes a special `NodeInterface` contract available at address `0xc8` that is only accessible via RPCs (it's not actually deployed onchain, and thus can't be called by smart contracts). This reference page documents the specific calls available in the `NodeInterface`. For a more conceptual description of what it is and how it works, please refer to the [`NodeInterface` conceptual page](/arbitrum-essentials/nodeinterface/overview.md). ## NodeInterface methods | Method | Solidity interface | Go implementation | Description | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `estimateRetryableTicket(address sender, uint256 deposit, address to, uint256 l2CallValue, address excessFeeRefundAddress, address callValueRefundAddress, bytes calldata data)` | [Interface](https://github.com/OffchainLabs/nitro-contracts/blob/4341b132cfbdcc980ead03765ca5224ff6cb5d97/src/node-interface/NodeInterface.sol#L26) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/execution/nodeinterface/node_interface.go#L128) | Estimates the gas needed for a retryable submission | | `constructOutboxProof(uint64 size, uint64 leaf)` | [Interface](https://github.com/OffchainLabs/nitro-contracts/blob/4341b132cfbdcc980ead03765ca5224ff6cb5d97/src/node-interface/NodeInterface.sol#L45) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/execution/nodeinterface/node_interface.go#L175) | Constructs an outbox proof of an l2->l1 send's existence in the outbox accumulator | | `findBatchContainingBlock(uint64 blockNum)` | [Interface](https://github.com/OffchainLabs/nitro-contracts/blob/4341b132cfbdcc980ead03765ca5224ff6cb5d97/src/node-interface/NodeInterface.sol#L57) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/execution/nodeinterface/node_interface.go#L84) | Finds the L1 batch containing a requested L2 block, reverting if none does | | `getL1Confirmations(bytes32 blockHash)` | [Interface](https://github.com/OffchainLabs/nitro-contracts/blob/4341b132cfbdcc980ead03765ca5224ff6cb5d97/src/node-interface/NodeInterface.sol#L70) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/execution/nodeinterface/node_interface.go#L102) | Gets the number of L1 confirmations of the sequencer batch producing the requested L2 block | | `gasEstimateComponents(address to, bool contractCreation, bytes calldata data)` | [Interface](https://github.com/OffchainLabs/nitro-contracts/blob/4341b132cfbdcc980ead03765ca5224ff6cb5d97/src/node-interface/NodeInterface.sol#L85) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/execution/nodeinterface/node_interface.go#L506) | Same as native gas estimation, but with additional info on the l1 costs | | `gasEstimateL1Component(address to, bool contractCreation, bytes calldata data)` | [Interface](https://github.com/OffchainLabs/nitro-contracts/blob/4341b132cfbdcc980ead03765ca5224ff6cb5d97/src/node-interface/NodeInterface.sol#L113) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/execution/nodeinterface/node_interface.go#L464) | Estimates a transaction's l1 costs | | `legacyLookupMessageBatchProof(uint256 batchNum, uint64 index)` | [Interface](https://github.com/OffchainLabs/nitro-contracts/blob/4341b132cfbdcc980ead03765ca5224ff6cb5d97/src/node-interface/NodeInterface.sol#L136) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/execution/nodeinterface/node_interface.go#L563) | Returns the proof necessary to redeem a message | | `nitroGenesisBlock()` | [Interface](https://github.com/OffchainLabs/nitro-contracts/blob/4341b132cfbdcc980ead03765ca5224ff6cb5d97/src/node-interface/NodeInterface.sol#L157) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/execution/nodeinterface/node_interface.go#L59) | Returns the first block produced using the Nitro codebase | | `blockL1Num(uint64 l2BlockNum)` | [Interface](https://github.com/OffchainLabs/nitro-contracts/blob/4341b132cfbdcc980ead03765ca5224ff6cb5d97/src/node-interface/NodeInterface.sol#L161) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/execution/nodeinterface/node_interface.go#L608) | Returns the L1 block number of the L2 block | | `l2BlockRangeForL1(uint64 blockNum)` | [Interface](https://github.com/OffchainLabs/nitro-contracts/blob/4341b132cfbdcc980ead03765ca5224ff6cb5d97/src/node-interface/NodeInterface.sol#L172) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/execution/nodeinterface/node_interface.go#L636) | Finds the L2 block number range that has the given L1 block number | --- > For a complete page index, fetch # Oracles > **NOTE** > > This is a conceptual overview of oracles. For more detailed information on how to use oracles in your applications, check out our [third-party oracles documentation](/for-devs/oracles/oracles-content-map.md). In this conceptual overview, we'll explore oracles, how they work, and some general applications. This overview will provide a foundational understanding and set expectations for developers who want to integrate oracles into their applications. ## What are oracles? Oracles are third-party services that provide smart contracts with external information. They act as a bridge between blockchains and the outside world, which expands their functionality by enabling smart contracts to access data beyond their native networks. ## Types of oracles Oracles can be classified based on their source, direction of information, trust, and how they provide information to smart contracts. Some common types of oracles include: * **Inbound and Outbound oracles**: Inbound oracles share information from external sources to smart contracts, while outbound oracles send information from smart contracts to the external world. * **Centralized and Decentralized oracles**: A centralized oracle is a single entity and sole data provider for a smart contract. Decentralized oracles increase reliability by relying on multiple sources of truth and distributing trust among participants. * **Push and Pull oracles**: Push oracles proactively provide data to smart contracts without being explicitly requested. They push data to the smart contract when a specified event or condition occurs. On the other hand, pull oracles require smart contracts to request data explicitly. They pull data from external sources in response to a query from the smart contract. * **Software oracles**: These oracles interact with online sources of information, such as databases, servers, or websites, and transmit the data to the blockchain. They often provide real-time information like exchange rates or digital asset prices. * **Hardware oracles**: These oracles obtain information from the physical world using electronic sensors, barcode scanners, or other reading devices. They "translate" real-world events into digital values that smart contracts can understand. ## How do push oracles work? Push oracles proactively provide data to smart contracts without being explicitly requested. When a specified event or condition occurs, the push oracle triggers the smart contract with the relevant data. For example, a push oracle might send weather data to a smart contract once the temperature reaches a certain threshold. ![Push oracle](/img/apps-push-oracle.svg) ## How do pull oracles work? Pull oracles require smart contracts to request data explicitly. A smart contract sends a query to the oracle, retrieving and relaying the requested information to the contract. For example, a smart contract might request the current price of a specific digital asset from a pull oracle. ![Pull oracle](/img/apps-pull-oracle.svg) ## Use cases for oracles Oracles serve a purpose in various applications across industries. Some general use cases include: * **Prediction markets**: Oracles provide real-world data to prediction market platforms, allowing users to bet on future events or outcomes. * **Supply chain management**: Hardware oracles can track the location and status of goods throughout the supply chain, enabling smart contracts to automate various processes and improve efficiency. * **Insurance**: Oracles can supply data about events such as natural disasters, accidents, or price fluctuations, allowing smart contracts to automate claims processing and payouts. * **Decentralized finance (DeFi)**: Oracles provide critical price and market data to DeFi applications, enabling them to operate efficiently and securely. In summary, oracles are a crucial component of the blockchain ecosystem, bridging the gap between onchain and offchain data sources. They enhance the functionality of smart contracts and enable a wide range of applications across various industries. As blockchain technology continues to evolve, developing secure and reliable oracles will remain essential in unlocking the full potential of smart contracts and decentralized applications. ## Resources You can learn more about oracles in our [third-party oracles documentation](/for-devs/oracles/oracles-content-map.md). --- > For a complete page index, fetch # Precompiles overview Precompiles are predefined smart contracts that have special addresses and provide specific functionality which is executed not at the EVM bytecode level, but natively by the Arbitrum client itself. Precompiles are primarily used to introduce specific functions that would be computationally expensive if executed in EVM bytecode, and functions that facilitate the interaction between the parent chain and the child chain. By having them natively in the Arbitrum client, they can be optimized for performance. Besides supporting all precompiles available in Ethereum, Arbitrum provides child chain-specific precompiles with methods smart contracts can call the same way they can solidity functions. For more details on the addresses these precompiles live, and the specific methods available, please refer to the [methods documentation](/arbitrum-essentials/precompiles/reference.md). --- > For a complete page index, fetch # Precompiles reference ArbOS provides child chain-specific precompiles with methods smart contracts can call the same way they can solidity functions. This reference page exhaustively documents the specific calls ArbOS makes available through precompiles. For a more conceptual description of what precompiles are and how they work, please refer to the [precompiles conceptual page](/arbitrum-essentials/precompiles/overview.md). This reference page is divided into two sections. The first one lists all precompiles in a summary table with links to the reference of the specific precompile, along with the address where they live, their purpose and links to the go implementation and solidity interface. The second one details the methods available in each precompile with links to the specific implementation. ## General information of precompiles This section is divided into two tables. We first list precompiles we expect users to most often use, and then the rest of precompiles. However, both tables display the same information: name and purpose of the precompile, address, and links to the solidity interface and the go implementation. ### Common precompiles | Precompile | Address | Solidity interface | Go implementation | Purpose | | --------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------- | | [ArbAggregator](#arbaggregator) | `0x6d` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAggregator.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAggregator.go) | Configuring transaction aggregation | | [ArbGasInfo](#arbgasinfo) | `0x6c` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go) | Info about gas pricing | | [ArbRetryableTx](#arbretryabletx) | `0x6e` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go) | Managing retryables | | [ArbSys](#arbsys) | `0x64` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go) | System-level functionality | | [ArbWasm](#arbwasm) | `0x71` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go) | Manages Stylus contracts | | [ArbWasmCache](#arbwasmcache) | `0x72` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasmCache.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasmCache.go) | Manages Stylus cache | ### Other precompiles | Precompile | Address | Solidity interface | Go implementation | Purpose | | ------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------- | | [ArbAddressTable](#arbaddresstable) | `0x66` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAddressTable.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAddressTable.go) | Supporting compression of addresses | | ArbBLS | - | - | - | **Disabled** (Former registry of BLS public keys) | | [ArbDebug](#arbdebug) | `0xff` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbDebug.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbDebug.go) | Testing tools | | [ArbFunctionTable](#arbfunctiontable) | `0x68` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbFunctionTable.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbFunctionTable.go) | No longer used | | [ArbInfo](#arbinfo) | `0x65` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbInfo.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbInfo.go) | Info about accounts | | [ArbOwner](#arbowner) | `0x70` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go) | Chain administration, callable only by chain owner | | [ArbOwnerPublic](#arbownerpublic) | `0x6b` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go) | Info about chain owners | | [ArbosTest](#arbostest) | `0x69` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbosTest.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbosTest.go) | No longer used | | [ArbStatistics](#arbstatistics) | `0x6f` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbStatistics.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbStatistics.go) | Info about the pre-Nitro state | ## Precompiles reference ### `ArbAddressTable` ArbAddressTable ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAddressTable.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAddressTable.go)) provides the ability to create short-hands for commonly used accounts. For a working example of using `ArbAddressTable` to register addresses and retrieve their indices, see the [address-table tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/address-table). Precompile address: `0x0000000000000000000000000000000000000066` | Method | Solidity interface | Go implementation | Description | | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `addressExists(address addr)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAddressTable.sol#L17) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAddressTable.go#L17) | AddressExists checks if an address exists in the table | | `compress(address addr)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAddressTable.sol#L26) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAddressTable.go#L22) | Compress and returns the bytes that represent the address | | `decompress(bytes calldata buf, uint256 offset)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAddressTable.sol#L36) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAddressTable.go#L27) | Decompress the compressed bytes at the given offset with those of the corresponding account | | `lookup(address addr)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAddressTable.sol#L45) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAddressTable.go#L40) | Lookup the index of an address in the table | | `lookupIndex(uint256 index)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAddressTable.sol#L53) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAddressTable.go#L52) | LookupIndex for an address in the table by index | | `register(address addr)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAddressTable.sol#L62) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAddressTable.go#L67) | Register adds an account to the table, shrinking its compressed representation | | `size()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAddressTable.sol#L69) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAddressTable.go#L73) | Size gets the number of addresses in the table | ### `ArbAggregator` ArbAggregator ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAggregator.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAggregator.go)) provides aggregators and their users methods for configuring how they participate in parent chain aggregation. Arbitrum One's default aggregator is the Sequencer, which a user will prefer unless `SetPreferredAggregator` is invoked to change it. Compression ratios are measured in basis points. Methods that are checkmarked are access-controlled and will revert if not called by the aggregator, its fee collector, or a chain owner. Precompile address: `0x000000000000000000000000000000000000006D` | Method | Solidity interface | Go implementation | Description | | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | | ⚠️`getPreferredAggregator(address addr)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAggregator.sol#L14) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAggregator.go#L25) | GetPreferredAggregator returns the preferred aggregator address. Deprecated: Do not use this method. | | ⚠️`getDefaultAggregator()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAggregator.sol#L20) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAggregator.go#L32) | GetDefaultAggregator returns the default aggregator address. Deprecated: Do not use this method. | | `getBatchPosters()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAggregator.sol#L24) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAggregator.go#L37) | GetBatchPosters gets the addresses of all current batch posters | | `addBatchPoster(address newBatchPoster)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAggregator.sol#L29) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAggregator.go#L42) | Adds additional batch poster address | | `getFeeCollector(address batchPoster)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAggregator.sol#L36) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAggregator.go#L65) | GetFeeCollector gets a batch poster's fee collector | | `setFeeCollector(address batchPoster, address newFeeCollector)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAggregator.sol#L44) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAggregator.go#L74) | SetFeeCollector sets a batch poster's fee collector (caller must be the batch poster, its fee collector, or an owner) | | ⚠️`getTxBaseFee(address aggregator)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAggregator.sol#L49) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAggregator.go#L98) | GetTxBaseFee gets an aggregator's current fixed fee to submit a tx Deprecated: always returns zero | | ⚠️`setTxBaseFee(address aggregator, uint256 feeInL1Gas)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbAggregator.sol#L59) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbAggregator.go#L106) | SetTxBaseFee sets an aggregator's fixed fee (caller must be the aggregator, its fee collector, or an owner) Deprecated: no-op | Note: methods marked with ⚠️ are deprecated and their use is not supported. ### `ArbBLS` > **CAUTION** — Disabled > > This precompile has been disabled. It previously provided a registry of BLS public keys for accounts. ### `ArbDebug` ArbDebug ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbDebug.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbDebug.go)) provides mechanisms useful for testing. The methods of `ArbDebug` are only available for chains with the `AllowDebugPrecompiles` chain parameter set. Otherwise, calls to this precompile will revert. Precompile address: `0x00000000000000000000000000000000000000ff` | Method | Solidity interface | Go implementation | Description | | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------- | | `becomeChainOwner()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbDebug.sol#L13) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbDebug.go#L59) | Caller becomes a chain owner | | `overwriteContractCode(address target, bytes calldata newCode)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbDebug.sol#L16) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbDebug.go#L64) | Overwrite an existing contract's code | | `events(bool flag, bytes32 value)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbDebug.sol#L22) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbDebug.go#L29) | Emits events with values based on the args provided | | `eventsView()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbDebug.sol#L25) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbDebug.go#L48) | Tries (and fails) to emit logs in a view context | | `customRevert(uint64 number)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbDebug.sol#L36) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbDebug.go#L54) | Throws a custom error | | `panic()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbDebug.sol#L41) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbDebug.go#L69) | Halts the chain by panicking in the STF | | `legacyError()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbDebug.sol#L43) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbDebug.go#L74) | Throws a hardcoded error | | Event | Solidity interface | Go implementation | Description | | ------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------ | | `Basic` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbDebug.sol#L28) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbDebug.go#L34) | Emitted in `Events` for testing | | `Mixed` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbDebug.sol#L29) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbDebug.go#L39) | Emitted in `Events` for testing | | `Store` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbDebug.sol#L32) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbDebug.go#L19) | Never emitted (used for testing log sizes) | ### `ArbFunctionTable` ArbFunctionTable ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbFunctionTable.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbFunctionTable.go)) provides aggregators the ability to manage function tables, to enable one form of transaction compression. The Nitro aggregator implementation does not use these, so these methods have been stubbed and their effects disabled. They are kept for backwards compatibility. Precompile address: `0x0000000000000000000000000000000000000068` | Method | Solidity interface | Go implementation | Description | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | `upload(bytes calldata buf)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbFunctionTable.sol#L15) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbFunctionTable.go#L19) | Upload does nothing | | `size(address addr)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbFunctionTable.sol#L20) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbFunctionTable.go#L24) | Size returns the empty table's size, which is 0 | | `get(address addr, uint256 index)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbFunctionTable.sol#L25) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbFunctionTable.go#L29) | Get reverts since the table is empty | ### `ArbGasInfo` ArbGasInfo ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go)) provides insight into the cost of using the chain. These methods have been adjusted to account for Nitro's heavy use of calldata compression. Of note to end-users, we no longer make a distinction between non-zero and zero-valued calldata bytes. For a practical guide on using these methods alongside `NodeInterface` to estimate transaction costs, see [How to estimate gas](/arbitrum-essentials/how-to-estimate-gas.md). Precompile address: `0x000000000000000000000000000000000000006C` | Method | Solidity interface | Go implementation | Description | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `getPricesInWeiWithAggregator(address aggregator)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L24) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L28) | GetPricesInWeiWithAggregator gets prices in wei when using the provided aggregator | | `getPricesInWei()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L38) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L101) | GetPricesInWei gets prices in wei when using the caller's preferred aggregator | | `getPricesInArbGasWithAggregator(address aggregator)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L45) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L106) | GetPricesInArbGasWithAggregator gets prices in ArbGas when using the provided aggregator | | `getPricesInArbGas()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L51) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L156) | GetPricesInArbGas gets prices in ArbGas when using the caller's preferred aggregator | | `getGasAccountingParams()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L57) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L161) | GetGasAccountingParams gets the rollup's speed limit, pool size, and block gas limit | | `getMaxTxGasLimit()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L61) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L169) | GetMaxTxGasLimit gets the max tx gas limit | | `getMinimumGasPrice()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L64) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L176) | GetMinimumGasPrice gets the minimum gas price needed for a transaction to succeed | | `getL1BaseFeeEstimate()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L67) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L181) | GetL1BaseFeeEstimate gets the current estimate of the L1 basefee | | `getL1BaseFeeEstimateInertia()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L70) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L186) | GetL1BaseFeeEstimateInertia gets how slowly ArbOS updates its estimate of the L1 basefee | | `getL1RewardRate()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L74) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L191) | GetL1RewardRate gets the L1 pricer reward rate | | `getL1RewardRecipient()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L78) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L196) | GetL1RewardRecipient gets the L1 pricer reward recipient | | `getL1GasPriceEstimate()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L81) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L201) | GetL1GasPriceEstimate gets the current estimate of the L1 basefee | | `getCurrentTxL1GasFees()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L84) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L206) | GetCurrentTxL1GasFees gets the fee in wei paid to the batch poster for posting this tx | | `getGasBacklog()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L89) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L211) | GetGasBacklog gets the backlogged amount of gas burnt in excess of the speed limit | | `getPricingInertia()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L94) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L216) | GetPricingInertia gets how slowly ArbOS updates the L2 basefee in response to backlogged gas | | `getGasBacklogTolerance()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L99) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L221) | GetGasBacklogTolerance gets the forgivable amount of backlogged gas ArbOS will ignore when raising the basefee | | `getL1PricingSurplus()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L102) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L226) | GetL1PricingSurplus gets the surplus of funds for L1 batch posting payments (may be negative) | | `getPerBatchGasCharge()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L105) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L250) | GetPerBatchGasCharge gets the base charge (in L1 gas) attributed to each data batch in the calldata pricer | | `getAmortizedCostCapBips()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L108) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L255) | GetAmortizedCostCapBips gets the cost amortization cap in basis points | | `getL1FeesAvailable()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L112) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L260) | GetL1FeesAvailable gets the available funds from L1 fees | | `getL1PricingEquilibrationUnits()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L116) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L265) | GetL1PricingEquilibrationUnits gets the equilibration units parameter for L1 price adjustment algorithm (Available since ArbOS 20) | | `getLastL1PricingUpdateTime()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L120) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L270) | GetLastL1PricingUpdateTime gets the last time the L1 calldata pricer was updated (Available since ArbOS 20) | | `getL1PricingFundsDueForRewards()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L124) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L275) | GetL1PricingFundsDueForRewards gets the amount of L1 calldata payments due for rewards (per the L1 reward rate) (Available since ArbOS 20) | | `getL1PricingUnitsSinceUpdate()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L128) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L280) | GetL1PricingUnitsSinceUpdate gets the amount of L1 calldata posted since the last update (Available since ArbOS 20) | | `getLastL1PricingSurplus()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L132) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L285) | GetLastL1PricingSurplus gets the L1 pricing surplus as of the last update (may be negative) (Available since ArbOS 20) | | `getMaxBlockGasLimit()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L136) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L290) | GetMaxBlockGasLimit gets the maximum block gas limit | | `getGasPricingConstraints()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L145) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L295) | GetGasPricingConstraints gets the current gas pricing constraints used by the Multi-Constraint Pricer. | | `getMultiGasPricingConstraints()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L159) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L327) | GetMultiGasPricingConstraints returns the current configuration of multi-gas pricing constraints | | `getMultiGasBaseFee()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbGasInfo.sol#L171) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbGasInfo.go#L381) | GetMultiGasBaseFee gets the current base fee for each resource type used by the Multi-Constraint Pricer | ### `ArbInfo` ArbInfo ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbInfo.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbInfo.go)) provides the ability to lookup basic info about accounts and contracts. Precompile address: `0x0000000000000000000000000000000000000065` | Method | Solidity interface | Go implementation | Description | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------- | | `getBalance(address account)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbInfo.sol#L12) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbInfo.go#L19) | GetBalance retrieves an account's balance | | `getCode(address account)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbInfo.sol#L17) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbInfo.go#L27) | GetCode retrieves a contract's deployed code | ### `ArbNativeTokenManager` ArbNativeTokenManager ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbNativeTokenManager.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbNativeTokenManager.go)) enables minting and burning of the chain's native gas token by callers authorized through the `ArbOwner` precompile. Available since ArbOS 41. Precompile address: `0x0000000000000000000000000000000000000073` | Method | Solidity interface | Go implementation | Description | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `mintNativeToken(uint256 amount)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbNativeTokenManager.sol#L28) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbNativeTokenManager.go#L29) | Mints some amount of the native gas token for this chain to the given address (Available since ArbOS 41) | | `burnNativeToken(uint256 amount)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbNativeTokenManager.sol#L36) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbNativeTokenManager.go#L43) | Burns some amount of the native gas token for this chain from the given address (Available since ArbOS 41) | | Event | Solidity interface | Go implementation | Description | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | | `NativeTokenMinted` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbNativeTokenManager.sol#L17) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbNativeTokenManager.go#L39) | Emitted when native gas token is minted to a NativeTokenOwner | | `NativeTokenBurned` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbNativeTokenManager.sol#L22) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbNativeTokenManager.go#L57) | Emitted when native gas token is burned from a NativeTokenOwner | ### `ArbosTest` ArbosTest ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbosTest.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbosTest.go)) provides a method of burning arbitrary amounts of gas, which exists for historical reasons. In Classic, `ArbosTest` had additional methods only the zero address could call. These have been removed since users don't use them and calls to missing methods revert. Precompile address: `0x0000000000000000000000000000000000000069` | Method | Solidity interface | Go implementation | Description | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | `burnArbGas(uint256 gasAmount)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbosTest.sol#L13) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbosTest.go#L18) | BurnArbGas unproductively burns the amount of L2 ArbGas | ### `ArbOwner` ArbOwner ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go)) provides owners with tools for managing the rollup. Calls by non-owners will always revert. Most of Arbitrum Classic's owner methods have been removed since they no longer make sense in Nitro: * What were once chain parameters are now parts of ArbOS's state, and those that remain are set at genesis. * ArbOS upgrades happen with the rest of the system rather than being independent * Exemptions to address aliasing are no longer offered. Exemptions were intended to support backward compatibility for contracts deployed before aliasing was introduced, but no exemptions were ever requested. Precompile address: `0x0000000000000000000000000000000000000070` | Method | Solidity interface | Go implementation | Description | | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `addChainOwner(address newOwner)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L47) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L65) | AddChainOwner adds account as a chain owner | | `removeChainOwner(address ownerToRemove)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L52) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L76) | RemoveChainOwner removes account from the list of chain owners | | `isChainOwner(address addr)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L57) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L91) | IsChainOwner checks if the account is a chain owner | | `getAllChainOwners()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L62) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L96) | GetAllChainOwners retrieves the list of chain owners | | `setNativeTokenManagementFrom(uint64 timestamp)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L66) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L132) | SetNativeTokenManagementFrom sets native token management enabled-from time. | | `setTransactionFilteringFrom(uint64 timestamp)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L72) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L137) | SetTransactionFilteringFrom sets transaction filtering enabled-from time. | | `addNativeTokenOwner(address newOwner)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L78) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L142) | AddNativeTokenOwner adds account as a native token owner | | `removeNativeTokenOwner(address ownerToRemove)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L84) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L160) | RemoveNativeTokenOwner removes account from the list of native token owners | | `isNativeTokenOwner(address addr)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L90) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L175) | IsNativeTokenOwner checks if the account is a native token owner | | `getAllNativeTokenOwners()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L96) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L180) | GetAllNativeTokenOwners retrieves the list of native token owners | | `addTransactionFilterer(address filterer)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L100) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L185) | AddTransactionFilterer adds account as a transaction filterer (authorized to use ArbFilteredTransactionsManager) | | `removeTransactionFilterer(address filterer)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L106) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L201) | RemoveTransactionFilterer removes account from the list of transaction filterers | | `isTransactionFilterer(address filterer)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L112) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L217) | IsTransactionFilterer checks if the account is a transaction filterer | | `getAllTransactionFilterers()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L118) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L222) | GetAllTransactionFilterers retrieves the list of transaction filterers | | `setFilteredFundsRecipient(address newRecipient)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L123) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L228) | SetFilteredFundsRecipient sets the address that receives funds redirected from filtered transactions. Set to address(0) to use the networkFeeAccount as fallback. | | `getFilteredFundsRecipient()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L130) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L237) | GetFilteredFundsRecipient gets the address that receives funds redirected from filtered transactions. Returns address(0) if not explicitly set (networkFeeAccount is used as fallback at runtime). | | `setL1BaseFeeEstimateInertia(uint64 inertia)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L133) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L242) | SetL1BaseFeeEstimateInertia sets how slowly ArbOS updates its estimate of the L1 basefee | | `setL2BaseFee(uint256 priceInWei)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L138) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L247) | SetL2BaseFee sets the L2 gas price directly, bypassing the pool calculus | | `setMinimumL2BaseFee(uint256 priceInWei)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L143) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L252) | SetMinimumL2BaseFee sets the minimum base fee needed for a transaction to succeed | | `setSpeedLimit(uint64 limit)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L150) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L260) | SetSpeedLimit sets the computational speed limit for the chain | | `setMaxTxGasLimit(uint64 limit)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L155) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L268) | SetMaxTxGasLimit sets the maximum size a tx can be | | `setMaxBlockGasLimit(uint64 limit)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L161) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L276) | SetMaxBlockGasLimit sets the maximum size a block can be | | `setL2GasPricingInertia(uint64 sec)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L168) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L281) | SetL2GasPricingInertia sets the L2 gas pricing inertia | | `setL2GasBacklogTolerance(uint64 sec)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L175) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L289) | SetL2GasBacklogTolerance sets the L2 gas backlog tolerance | | `getNetworkFeeAccount()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L180) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L294) | GetNetworkFeeAccount gets the network fee collector | | `getInfraFeeAccount()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L184) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L299) | GetInfraFeeAccount gets the infrastructure fee collector | | `setNetworkFeeAccount(address newNetworkFeeAccount)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L187) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L304) | SetNetworkFeeAccount sets the network fee collector to the new network fee account | | `setInfraFeeAccount(address newInfraFeeAccount)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L193) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L309) | SetInfraFeeAccount sets the infrastructure fee collector address | | `scheduleArbOSUpgrade(uint64 newVersion, uint64 timestamp)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L198) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L314) | ScheduleArbOSUpgrade to the requested version at the requested timestamp | | `setL1PricingEquilibrationUnits(uint256 equilibrationUnits)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L201) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L319) | Sets equilibration units parameter for L1 price adjustment algorithm | | `setL1PricingInertia(uint64 inertia)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L206) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L324) | Sets inertia parameter for L1 price adjustment algorithm | | `setL1PricingRewardRecipient(address recipient)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L211) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L329) | Sets reward recipient address for L1 price adjustment algorithm | | `setL1PricingRewardRate(uint64 weiPerUnit)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L216) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L334) | Sets reward amount for L1 price adjustment algorithm, in wei per unit | | `setL1PricePerUnit(uint256 pricePerUnit)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L221) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L339) | Set how much ArbOS charges per L1 gas spent on transaction data. | | `setParentGasFloorPerToken(uint64 floorPerToken)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L227) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L344) | Set how much L1 charges per non-zero byte of calldata | | `setPerBatchGasCharge(int64 cost)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L232) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L349) | Sets the base charge (in L1 gas) attributed to each data batch in the calldata pricer | | `setBrotliCompressionLevel(uint64 level)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L238) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L360) | Sets the Brotli compression level used for fast compression (default level is 1) | | `setAmortizedCostCapBips(uint64 cap)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L243) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L354) | Sets the cost amortization cap in basis points | | `releaseL1PricerSurplusFunds(uint256 maxWeiToRelease)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L249) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L365) | Releases surplus funds from L1PricerFundsPoolAddress for use | | `setInkPrice(uint32 price)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L256) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L386) | Sets the amount of ink 1 gas buys | | `setWasmMaxStackDepth(uint32 depth)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L262) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L400) | Sets the maximum depth (in wasm words) a wasm stack may grow | | `setWasmFreePages(uint16 pages)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L268) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L410) | Sets the number of free wasm pages a tx receives | | `setWasmPageGas(uint16 gas)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L274) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L420) | Sets the base cost of each additional wasm page | | `setWasmPageLimit(uint16 limit)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L280) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L430) | Sets the initial number of pages a wasm may allocate | | `setWasmMaxSize(uint32 size)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L286) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L492) | SetMaxWasmSize sets the maximum size the wasm code can be in bytes after decompression. | | `setWasmMinInitGas(uint8 gas, uint16 cached)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L294) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L440) | Sets the minimum costs to invoke a program | | `setWasmInitCostScalar(uint64 percent)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L299) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L451) | Sets the linear adjustment made to program init costs | | `setWasmExpiryDays(uint16 _days)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L305) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L461) | Sets the number of days after which programs deactivate | | `setWasmKeepaliveDays(uint16 _days)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L311) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L471) | Sets the age a program must be to perform a keepalive | | `setWasmBlockCacheSize(uint16 count)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L317) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L481) | Sets the number of extra programs ArbOS caches during a given block | | `addWasmCacheManager(address manager)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L323) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L502) | Adds account as a wasm cache manager | | `removeWasmCacheManager(address manager)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L329) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L507) | Removes account from the list of wasm cache managers | | `setChainConfig(string calldata chainConfig)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L335) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L520) | Sets serialized chain config in ArbOS state | | `setCalldataPriceIncrease(bool enable)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L341) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L570) | SetCalldataPriceIncrease sets the increased calldata price feature on or off (EIP-7623) | | `setGasBacklog(uint64 backlog)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L348) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L575) | SetGasBacklog sets the L2 gas backlog directly (used by single-constraint pricing model only) | | `setGasPricingConstraints(uint64[3][] calldata constraints)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L369) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L580) | SetGasPricingConstraints sets the gas pricing constraints used by the multi-constraint pricing model | | `setMultiGasPricingConstraints(ArbMultiGasConstraintsTypes.ResourceConstraint[] calldata constraints)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L394) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L612) | SetMultiGasPricingConstraints configures the multi-dimensional gas pricing model | | `setCollectTips(bool collectTips)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L401) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L666) | SetCollectTips enables or disables tip collection. When enabled, transaction tips are collected by the network fee account. When disabled (default), tips are dropped. | | `setMaxStylusContractFragments(uint8 maxFragments)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L407) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L670) | | | `setWasmActivationGas(uint64 gas)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L414) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L682) | Sets the constant gas charge applied before each stylus contract activation. Defaults to zero. Can be raised to deter DOS via activations, or set to a value exceeding the block gas limit to block all activations entirely. | | Event | Solidity interface | Go implementation | Description | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `TransactionFiltererAdded` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L21) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L197) | @notice Emitted when an address is added as a transaction filterer. | | `TransactionFiltererRemoved` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L24) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L213) | @notice Emitted when an address is removed as a transaction filterer. | | `FilteredFundsRecipientSet` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L28) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L232) | @notice Emitted when the filtered funds recipient address is changed. @notice Available in ArbOS version 60 and above | | `ChainOwnerAdded` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L32) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L70) | @notice Emitted when an address is added as a chain owner. @notice Available in ArbOS version 60 and above | | `ChainOwnerRemoved` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L36) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L85) | @notice Emitted when an address is removed as a chain owner. @notice Available in ArbOS version 60 and above | | `NativeTokenOwnerAdded` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L40) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L154) | @notice Emitted when an address is added as a native token owner. @notice Available in ArbOS version 60 and above | | `NativeTokenOwnerRemoved` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L44) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L169) | @notice Emitted when an address is removed as a native token owner. @notice Available in ArbOS version 60 and above | | `OwnerActs` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwner.sol#L419) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwner.go#L30) | Emitted when a successful call is made to this precompile | ### `ArbOwnerPublic` ArbOwnerPublic ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go)) provides non-owners with info about the current chain owners. Precompile address: `0x000000000000000000000000000000000000006b` | Method | Solidity interface | Go implementation | Description | | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `isChainOwner(address addr)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L11) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L35) | IsChainOwner checks if the user is a chain owner | | `rectifyChainOwner(address ownerToRectify)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L18) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L26) | RectifyChainOwner checks if the account is a chain owner (Available since ArbOS 11) | | `getAllChainOwners()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L23) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L21) | GetAllChainOwners retrieves the list of chain owners | | `getNativeTokenManagementFrom()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L28) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L51) | GetNativeTokenMangementFrom returns the time in epoch seconds when the native token management becomes enabled | | `isNativeTokenOwner(address addr)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L32) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L40) | IsNativeTokenOwner checks if the account is a native token owner | | `getAllNativeTokenOwners()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L38) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L45) | GetAllNativeTokenOwners retrieves the list of native token owners | | `getTransactionFilteringFrom()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L43) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L57) | TransactionFilteringFrom returns the time in epoch seconds when the transaction filtering feature becomes enabled | | `isTransactionFilterer(address filterer)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L47) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L62) | IsTransactionFilterer checks if the account is a transaction filterer | | `getAllTransactionFilterers()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L53) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L67) | GetAllTransactionFilterers retrieves the list of transaction filterers | | `getFilteredFundsRecipient()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L58) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L73) | GetFilteredFundsRecipient gets the address that receives funds redirected from filtered transactions. Returns address(0) if not explicitly set (networkFeeAccount is used as fallback at runtime). | | `getNetworkFeeAccount()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L61) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L78) | GetNetworkFeeAccount gets the network fee collector | | `getInfraFeeAccount()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L64) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L83) | GetInfraFeeAccount gets the infrastructure fee collector | | `getBrotliCompressionLevel()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L68) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L91) | GetBrotliCompressionLevel gets the current brotli compression level used for fast compression | | `getParentGasFloorPerToken()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L72) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L115) | Get how much L1 charges per non-zero byte of calldata | | `getScheduledUpgrade()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L77) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L97) | GetScheduledUpgrade gets the next scheduled ArbOS version upgrade and its activation timestamp. Returns (0, 0, nil) if no ArbOS upgrade is scheduled. | | `isCalldataPriceIncreaseEnabled()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L84) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L110) | IsCalldataPriceIncreaseEnabled checks if the increased calldata price feature (EIP-7623) is enabled | | `getCollectTips()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L88) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L120) | GetCollectTips returns whether tip collection is enabled. | | `getMaxStylusContractFragments()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L92) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L124) | | | Event | Solidity interface | Go implementation | Description | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------ | | `ChainOwnerRectified` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbOwnerPublic.sol#L94) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbOwnerPublic.go#L31) | Emitted when verifying a chain owner | ### `ArbRetryableTx` ArbRetryableTx ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go)) provides methods for managing retryables. The model has been adjusted for Nitro, most notably in terms of how retry transactions are scheduled. For more information on retryables, please see [the retryable documentation](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#retryable-tickets). For a worked example of creating and redeeming retryables from application code, see [How to bridge from the parent chain](/arbitrum-essentials/bridging/cross-chain-messaging.md#ethereum-to-arbitrum-messaging). Precompile address: `0x000000000000000000000000000000000000006E` | Method | Solidity interface | Go implementation | Description | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `redeem(bytes32 ticketId)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol#L18) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go#L50) | Redeem schedules an attempt to redeem the retryable, donating all of the call's gas to the redeem attempt | | `getLifetime()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol#L26) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go#L162) | GetLifetime gets the default lifetime period a retryable has at creation | | `getTimeout(bytes32 ticketId)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol#L33) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go#L167) | GetTimeout gets the timestamp for when ticket will expire | | `keepalive(bytes32 ticketId)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol#L45) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go#L184) | Keepalive adds one lifetime period to the ticket's expiry | | `getBeneficiary(bytes32 ticketId)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol#L55) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go#L212) | GetBeneficiary gets the beneficiary of the ticket | | `cancel(bytes32 ticketId)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol#L64) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go#L225) | Cancel the ticket and refund its callvalue to its beneficiary | | `getCurrentRedeemer()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol#L73) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go#L256) | Gets the redeemer of the current retryable redeem attempt | | `submitRetryable(bytes32 requestId, uint256 l1BaseFee, uint256 deposit, uint256 callvalue, uint256 gasFeeCap, uint64 gasLimit, uint256 maxSubmissionFee, address feeRefundAddress, address beneficiary, address retryTo, bytes calldata retryData)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol#L79) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go#L264) | Do not call. This method represents a retryable submission to aid explorers. Calling it will always revert. | | Event | Solidity interface | Go implementation | Description | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `TicketCreated` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol#L93) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go#L23) | Emitted when creating a retryable | | `LifetimeExtended` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol#L94) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go#L207) | Emitted when extending a retryable's expiry date | | `RedeemScheduled` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol#L95) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go#L125) | Emitted when scheduling a retryable | | `Canceled` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol#L104) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go#L250) | Emitted when cancelling a retryable | | `Redeemed` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbRetryableTx.sol#L107) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbRetryableTx.go#L33) | DEPRECATED in favour of new `RedeemScheduled` event after the nitro upgrade. | ### `ArbStatistics` ArbStatistics ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbStatistics.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbStatistics.go)) provides statistics about the chain as of just before the Nitro upgrade. In Arbitrum Classic, this was how a user would get info such as the total number of accounts, but there are better ways to get that info in Nitro. Precompile address: `0x000000000000000000000000000000000000006F` | Method | Solidity interface | Go implementation | Description | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | `getStats()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbStatistics.sol#L18) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbStatistics.go#L18) | GetStats returns the current block number and some statistics about the rollup's pre-Nitro state | ### `ArbSys` ArbSys ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go)) provides system-level functionality for interacting with the parent chain and understanding the call stack. Precompile address: `0x0000000000000000000000000000000000000064` | Method | Solidity interface | Go implementation | Description | | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `arbBlockNumber()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L17) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L36) | ArbBlockNumber gets the current L2 block number | | `arbBlockHash(uint256 arbBlockNum)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L23) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L41) | ArbBlockHash gets the L2 block hash, if sufficiently recent | | `arbChainID()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L31) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L62) | ArbChainID gets the rollup's unique chain identifier | | `arbOSVersion()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L38) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L67) | ArbOSVersion gets the current ArbOS version | | `getStorageGasAvailable()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L44) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L73) | GetStorageGasAvailable returns 0 since Nitro has no concept of storage gas | | `isTopLevelCall()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L51) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L78) | IsTopLevelCall checks if the call is top-level (deprecated) | | `mapL1SenderContractAddressToL2Alias(address sender, address unused)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L59) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L83) | MapL1SenderContractAddressToL2Alias gets the contract's L2 alias | | `wasMyCallersAddressAliased()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L68) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L88) | WasMyCallersAddressAliased checks if the caller's caller was aliased | | `myCallersAddressWithoutAliasing()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L74) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L98) | MyCallersAddressWithoutAliasing gets the caller's caller without any potential aliasing | | `withdrawEth(address destination)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L82) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L239) | WithdrawEth send paid eth to the destination on L1 | | `sendTxToL1(address destination, bytes calldata data)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L94) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L113) | SendTxToL1 sends a transaction to L1, adding it to the outbox | | `sendMerkleTreeState()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L105) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L223) | SendMerkleTreeState gets the root, size, and partials of the outbox Merkle tree state (caller must be the 0 address) | | Event | Solidity interface | Go implementation | Description | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `L2ToL1Tx` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L114) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L202) | Logs a send transaction from L2 to L1, including data for outbox proving | | `L2ToL1Transaction` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L127) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L31) | DEPRECATED in favour of the new `L2ToL1Tx` event above after the nitro upgrade | | `SendMerkleUpdate` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbSys.sol#L146) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbSys.go#L186) | Logs a new merkle branch needed for constructing outbox proofs | ### `ArbWasm` ArbWasm ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go)) provides helper methods for managing Stylus contracts Precompile address: `0x0000000000000000000000000000000000000071` | Method | Solidity interface | Go implementation | Description | | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | | `activateProgram(address program)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L17) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L36) | Compile a wasm program with the latest instrumentation | | `stylusVersion()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L23) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L104) | Gets the latest stylus version | | `codehashVersion(bytes32 codehash)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L27) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L186) | Gets the stylus version that program with codehash was most recently compiled with | | `codehashKeepalive(bytes32 codehash)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L33) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L68) | Extends a program's expiration date (reverts if too soon) | | `codehashAsmSize(bytes32 codehash)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L40) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L195) | Gets a program's asm size in bytes | | `programVersion(address program)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L46) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L204) | Gets the stylus version that program at addr was most recently compiled with | | `programInitGas(address program)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L53) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L213) | Gets the cost to invoke the program | | `programMemoryFootprint(address program)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L59) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L222) | Gets the footprint of program at addr | | `programTimeLeft(address program)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L65) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L231) | Gets returns the amount of time remaining until the program expires | | `inkPrice()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L71) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L110) | Gets the amount of ink 1 gas buys | | `maxStackDepth()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L75) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L116) | Gets the wasm stack size limit | | `freePages()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L79) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L122) | Gets the number of free wasm pages a tx gets | | `pageGas()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L83) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L128) | Gets the base cost of each additional wasm page | | `pageRamp()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L87) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L134) | Gets the ramp that drives exponential memory costs | | `pageLimit()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L91) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L140) | Gets the maximum initial number of pages a wasm may allocate | | `minInitGas()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L96) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L146) | Gets the minimum costs to invoke a program | | `initCostScalar()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L100) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L157) | Gets the linear adjustment made to program init costs | | `expiryDays()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L104) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L163) | Gets the number of days after which programs deactivate | | `keepaliveDays()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L108) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L169) | Gets the age a program must be to perform a keepalive | | `blockCacheSize()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L112) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L175) | Gets the number of extra programs ArbOS caches during a given block. | | `activationGas()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L117) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L181) | Gets the constant gas charge applied before each Stylus contract activation. | | Event | Solidity interface | Go implementation | Description | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | `ProgramActivated` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L119) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L64) | Emitted when activating a WASM program | | `ProgramLifetimeExtended` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasm.sol#L126) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasm.go#L80) | Emitted when extending the expiration date of a WASM program | ### `ArbWasmCache` ArbWasmCache ([Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasmCache.sol) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasmCache.go)) provides helper methods for managing Stylus cache Precompile address: `0x0000000000000000000000000000000000000072` | Method | Solidity interface | Go implementation | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `isCacheManager(address manager)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasmCache.sol#L14) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasmCache.go#L16) | See if the user is a cache manager owner. | | `allCacheManagers()` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasmCache.sol#L20) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasmCache.go#L21) | Retrieve all authorized address managers. | | `cacheCodehash(bytes32 codehash)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasmCache.sol#L24) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasmCache.go#L26) | Deprecated: replaced with CacheProgram. | | `cacheProgram(address addr)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasmCache.sol#L33) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasmCache.go#L31) | Caches all programs with a codehash equal to the given address. Caller must be a cache manager or chain owner. | | `evictCodehash(bytes32 codehash)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasmCache.sol#L39) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasmCache.go#L40) | Evicts all programs with the given codehash. Caller must be a cache manager or chain owner. | | `codehashIsCached(bytes32 codehash)` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasmCache.sol#L44) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasmCache.go#L45) | Gets whether a program is cached. Note that the program may be expired. | | Event | Solidity interface | Go implementation | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------- | | `UpdateProgramCache` | [Interface](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/e7e6566ae5b0efa0ad4d779138f64ead11928c66/ArbWasmCache.sol#L48) | [Implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/ArbWasmCache.go#L62) | Emitted when caching a WASM program | --- > For a complete page index, fetch # Arbitrum chains overview Arbitrum chains are Child chain solutions built on top of the Ethereum Blockchain, designed to increase scalability and reduce Transaction costs. In this conceptual overview, we’ll learn about the different Arbitrum Chains and how they relate to each other. We’ll describe the available Arbitrum production and testnet chains, their differences, and the technology stacks that these chains use. ## What Arbitrum production chains are available? ### Arbitrum One **Arbitrum One** is a child chain Optimistic Rollup that implements the Arbitrum Rollup Protocol and settles to Ethereum's parent chain. It lets you build high-performance Ethereum dApps with [low transaction costs](/arbitrum-essentials/how-to-estimate-gas.md) and Ethereum-grade security guarantees, introducing no additional trust assumptions. This is made possible by the [Nitro](/how-arbitrum-works/reference/geth.md) technology stack, a "Geth-at-the-core" architecture that gives Arbitrum One (and Nova) advanced calldata compression, separate contexts for common execution and fault proving, Ethereum parent chain gas compatibility, and more. ### Arbitrum Nova **Arbitrum Nova** is a high-performance alternative to Arbitrum One's chain. While Arbitrum One implements the purely Trustless Rollup protocol, Arbitrum Nova implements the mostly trustless [AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) protocol. The key difference between Rollup and AnyTrust is that the AnyTrust protocol introduces an additional trust assumption in the form of a Data Availability Committee (DAC). This committee (detailed below) is responsible for expediting the process of storing, batching, and posting child chain transaction data to Ethereum's parent chain. This lets you use Arbitrum in scenarios that demand performance and affordability, while Arbitrum One is optimal for scenarios that demand Ethereum's pure trustlessness. ## What Arbitrum testnet chains are available? ### Arbitrum Sepolia Arbitrum Sepolia serves as a testnet chain replicating the capabilities of Arbitrum One's main network. Linked to the Sepolia testnet, it offers developers a secure platform to experiment with and evaluate their smart contracts prior to actual deployment on the mainnet. To deploy your first contract to Arbitrum Sepolia, see the [Solidity quickstart](/build-decentralized-apps/quickstart-solidity-remix.md). ### Arbitrum Goerli Arbitrum Goerli was a testnet chain that mirrored the functionality of the Arbitrum One mainnet and was connected to the Ethereum Goerli testnet. It was deprecated on November 18th 2023, and deactivated on March 18th, 2024. > **CAUTION** > > The old testnet RinkArby was deprecated on December 20th, 2022. ### Stylus testnet (deprecated) Stylus is now available on Arbitrum One and Arbitrum Sepolia. The standalone Stylus testnet was deprecated as of June 17, 2024. For information about developing with Stylus, see the [Stylus documentation](/stylus/gentle-introduction.md). ## What are the differences between the available Arbitrum chains? The main differences between the Arbitrum chains lie in their purpose and the environment they operate in. Arbitrum One and Arbitrum Nova are production chains designed for real-world use. They're connected to the Ethereum mainnet and handle real, valuable transactions. They both use Arbitrum's Nitro technology stack under the hood, but Arbitrum One implements the Rollup protocol, while Nova implements the AnyTrust protocol. Arbitrum One is designed for general use, providing a scalable and cost-effective solution for running Ethereum-compatible smart contracts. On the other hand, Arbitrum Nova is designed for applications that require a higher transaction throughput and don’t require the full decentralization that rollups provide. Finally, Arbitrum Sepolia is a testnet chain. It's designed for testing purposes and is connected to the Sepolia testnet, which uses test Ether with no real-world value. ## What technology stacks use the Arbitrum chains? ### Nitro Nitro is the technology that powers Arbitrum One, Arbitrum Nova (with AnyTrust configuration), and Arbitrum Sepolia. It's designed to offer high throughput and low cost, making it ideal for building blockchain applications. Nitro is a major upgrade to the “Classic” stack, offering several improvements including advanced calldata compression, separate contexts for common execution and fault proving, Ethereum parent chain gas compatibility, and more. You can find more information about Nitro in [How Arbitrum works](/how-arbitrum-works/inside-arbitrum-nitro.md). ### AnyTrust (variant of Nitro) AnyTrust is a variant of the Nitro technology stack that lowers costs by accepting a mild trust assumption. The AnyTrust protocol relies on an external Data Availability Committee (DAC) to store data and provide it on demand. The DAC has `N` members, of which AnyTrust assumes at least two are honest. Keeping the data offchain in the happy/common case means the system can charge the user significantly lower fees. You can find more information about AnyTrust in [Anytrust protocol](/how-arbitrum-works/deep-dives/anytrust-protocol.md). ### Classic (deprecated) The Classic technology stack is the original version of Arbitrum. It has been deprecated and replaced by the Nitro technology stack. ## Conclusion Understanding the different Arbitrum chains and their technology stacks is crucial for developers working on blockchain and Web3 applications. Each chain offers a unique set of features and benefits, making them suitable for different use cases. By choosing the right chain and technology stack, developers can ensure their applications are secure, scalable, and cost-effective. --- > For a complete page index, fetch # Chain parameters ## Chain parameters | Param | Description | Arbitrum One | Arbitrum Nova | Arb Sepolia | | -------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | Dispute window | Time for assertions to get confirmed during which validators can issue a challenge | 45818 blocks (\~ 6.4 days ) | 45818 blocks (\~ 6.4 days) | 20 blocks (\~ 4.0 minutes) | | Minimum bond amount | Amount of funds required for a validator to propose assertion on the parent chain | 3600 ETH | 1 ETH | 1 Sepolia ETH | | Force-include period | Period after which a delayed message can be included into the inbox without any action from the Sequencer | 5760 blocks / 24 hours | 5760 blocks / 24 hours | 5760 blocks / 24 hours | | Gas target | Target gas/sec, over which the congestion mechanism activates | [See child chain gas fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#child-chain-gas-fees) | [See child chain gas fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#child-chain-gas-fees) | [See child chain gas fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#child-chain-gas-fees) | | Gas price floor | Minimum gas price | 0.02 gwei | 0.02 gwei | 0.2 gwei | | Block gas limit | Maximum amount of gas that all the transactions inside a block are allowed to consume | 32,000,000 | 32,000,000 | 32,000,000 | ## Current gas targets | Gas target (Mgas/s) | Adjustment window (seconds) | | ------------------- | --------------------------- | | 60 | 9 | | 41 | 52 | | 29 | 329 | | 20 | 2,105 | | 14 | 13,485 | | 10 | 86,400 | To learn more about the gas target, refer to the [Gas and fees deep-dive](/how-arbitrum-works/deep-dives/gas-and-fees.md#the-gas-target). To determine how to configure the gas target for your chain, refer to the [Dynamic pricing for Arbitrum chains page](/launch-arbitrum-chain/chain-config/costs/dynamic-pricing.md). To calculate the values for your chain, refer to the [How to calculate the values for your chain section](/launch-arbitrum-chain/chain-config/costs/dynamic-pricing.md#how-to-calculate-the-values-for-your-chain) on the same page. --- > For a complete page index, fetch # Smart contract addresses The following information may be useful to those building on Arbitrum. We list the addresses of the smart contracts related to the protocol, the token bridge and precompiles of the different Arbitrum chains. ## Protocol smart contracts ### Core contracts The following contracts are deployed on Ethereum (L1) | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | --------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Rollup | [0x4DCe...Cfc0](https://etherscan.io/address/0x4DCeB440657f21083db8aDd07665f8ddBe1DCfc0) | [0xE7E8...B7Bd](https://etherscan.io/address/0xE7E8cCC7c381809BDC4b213CE44016300707B7Bd) | [0xd808...81C8](https://sepolia.etherscan.io/address/0xd80810638dbDF9081b72C1B33c65375e807281C8) | | Sequencer Inbox | [0x1c47...82B6](https://etherscan.io/address/0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6) | [0x211E...c21b](https://etherscan.io/address/0x211E1c4c7f1bF5351Ac850Ed10FD68CFfCF6c21b) | [0x6c97...be0D](https://sepolia.etherscan.io/address/0x6c97864CE4bEf387dE0b3310A44230f7E3F1be0D) | | CoreProxyAdmin | [0x5547...2dbD](https://etherscan.io/address/0x554723262467F125Ac9e1cDFa9Ce15cc53822dbD) | [0x71D7...7148](https://etherscan.io/address/0x71D78dC7cCC0e037e12de1E50f5470903ce37148) | [0x1ed7...0686](https://sepolia.etherscan.io/address/0x1ed74a4e4F4C42b86A7002e9951e98DBcC890686) | ### Cross-chain messaging contracts The following contracts are deployed on Ethereum (L1) | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Delayed Inbox | [0x4Dbd...AB3f](https://etherscan.io/address/0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f) | [0xc444...3949](https://etherscan.io/address/0xc4448b71118c9071Bcb9734A0EAc55D18A153949) | [0xaAe2...ae21](https://sepolia.etherscan.io/address/0xaAe29B0366299461418F5324a79Afc425BE5ae21) | | Bridge | [0x8315...ed3a](https://etherscan.io/address/0x8315177aB297bA92A06054cE80a67Ed4DBd7ed3a) | [0xC1Eb...76Bd](https://etherscan.io/address/0xC1Ebd02f738644983b6C4B2d440b8e77DdE276Bd) | [0x38f9...33a9](https://sepolia.etherscan.io/address/0x38f918D0E9F1b721EDaA41302E399fa1B79333a9) | | Outbox | [0x0B98...4840](https://etherscan.io/address/0x0B9857ae2D4A3DBe74ffE1d7DF045bb7F96E4840) | [0xD4B8...cc58](https://etherscan.io/address/0xD4B80C3D7240325D18E645B49e6535A3Bf95cc58) | [0x65f0...B78F](https://sepolia.etherscan.io/address/0x65f07C7D521164a4d5DaC6eB8Fac8DA067A3B78F) | | Classic Outbox\*\*\* | [0x7607...1A40](https://etherscan.io/address/0x760723CD2e632826c38Fef8CD438A4CC7E7E1A40)
[0x667e...337a](https://etherscan.io/address/0x667e23ABd27E623c11d4CC00ca3EC4d0bD63337a) | | | \*\*\*Migrated Network Only ### Fraud proof contracts The following contracts are deployed on Ethereum (L1) | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | ------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | ChallengeManager | [0xA556...9fB0](https://etherscan.io/address/0xA5565d266c3c3Ee90B16Be8A5b13d587ef559fB0) | [0xFE66...A688](https://etherscan.io/address/0xFE66b18Ef1B943F8594A2710376Af4B01AcfA688) | [0xC60b...8B4C](https://sepolia.etherscan.io/address/0xC60b56Ff6aAb3FE8B9Bd70040Fe9E95A26258B4C) | | OneStepProver0 | [0x35FB...F731](https://etherscan.io/address/0x35FBC5F03d86E88973B06Fb9C5a913D54AbdF731) | [0x35FB...F731](https://etherscan.io/address/0x35FBC5F03d86E88973B06Fb9C5a913D54AbdF731) | [0x3Fe7...1377](https://sepolia.etherscan.io/address/0x3Fe73F959C44e04d660dBFBbeffd51FD2c091377) | | OneStepProverMemory | [0xe0ba...C48b](https://etherscan.io/address/0xe0ba77e0E24de5369e3B268Ea79fDe716e2EC48b) | [0xe0ba...C48b](https://etherscan.io/address/0xe0ba77e0E24de5369e3B268Ea79fDe716e2EC48b) | [0x6268...ec2d](https://sepolia.etherscan.io/address/0x6268Fc8dB1b5083b405b2C51808Df3619783ec2d) | | OneStepProverMath | [0xaB95...F921](https://etherscan.io/address/0xaB9596a0aaF28bc798c453434EC2DC0F8F0bF921) | [0xaB95...F921](https://etherscan.io/address/0xaB9596a0aaF28bc798c453434EC2DC0F8F0bF921) | [0x42f5...e8Fa](https://sepolia.etherscan.io/address/0x42f58c90583eC3fA0E0b724dEDF755AE1068e8Fa) | | OneStepProverHostIo | [0xa07c...71Cf](https://etherscan.io/address/0xa07cD154340CC74EcF156FFB9fb378Ee29Ca71Cf) | [0xa07c...71Cf](https://etherscan.io/address/0xa07cD154340CC74EcF156FFB9fb378Ee29Ca71Cf) | [0xdB2c...C165](https://sepolia.etherscan.io/address/0xdB2c541e20Bd1830c8a050341Fca0Af51489C165) | | OneStepProofEntry | [0x4397...42d6](https://etherscan.io/address/0x4397fE1E959Ba81B9D5f1A9679Ddd891955A42d6) | [0x4397...42d6](https://etherscan.io/address/0x4397fE1E959Ba81B9D5f1A9679Ddd891955A42d6) | [0xB9cf...AE80](https://sepolia.etherscan.io/address/0xB9cf664A1beD8F74f4B893a18c86eCe876CdAE80) | ## Token bridge smart contracts ### Core contracts The following contracts are deployed on Ethereum (L1) | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | --------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | L1 Gateway Router | [0x72Ce...31ef](https://etherscan.io/address/0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef) | [0xC840...cD48](https://etherscan.io/address/0xC840838Bc438d73C16c2f8b22D2Ce3669963cD48) | [0xcE18...8264](https://sepolia.etherscan.io/address/0xcE18836b233C83325Cc8848CA4487e94C6288264) | | L1 ERC20 Gateway | [0xa3A7...0EeC](https://etherscan.io/address/0xa3A7B6F88361F48403514059F1F16C8E78d60EeC) | [0xB253...21bf](https://etherscan.io/address/0xB2535b988dcE19f9D71dfB22dB6da744aCac21bf) | [0x902b...3aFF](https://sepolia.etherscan.io/address/0x902b3E5f8F19571859F4AB1003B960a5dF693aFF) | | L1 Arb-Custom Gateway | [0xcEe2...180d](https://etherscan.io/address/0xcEe284F754E854890e311e3280b767F80797180d) | [0x2312...232f](https://etherscan.io/address/0x23122da8C581AA7E0d07A36Ff1f16F799650232f) | [0xba2F...40F3](https://sepolia.etherscan.io/address/0xba2F7B6eAe1F9d174199C5E4867b563E0eaC40F3) | | L1 Weth Gateway | [0xd920...e2db](https://etherscan.io/address/0xd92023E9d9911199a6711321D1277285e6d4e2db) | [0xE4E2...0BaE](https://etherscan.io/address/0xE4E2121b479017955Be0b175305B35f312330BaE) | [0xA8aD...0e1E](https://sepolia.etherscan.io/address/0xA8aD8d7e13cbf556eE75CB0324c13535d8100e1E) | | L1 Weth | [0xC02a...6Cc2](https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) | [0xC02a...6Cc2](https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) | [0x7b79...E7f9](https://sepolia.etherscan.io/address/0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9) | | L1 Proxy Admin | [0x9aD4...0aDa](https://etherscan.io/address/0x9aD46fac0Cf7f790E5be05A0F15223935A0c0aDa) | [0xa8f7...e560](https://etherscan.io/address/0xa8f7DdEd54a726eB873E98bFF2C95ABF2d03e560) | [0xDBFC...44b0](https://sepolia.etherscan.io/address/0xDBFC2FfB44A5D841aB42b0882711ed6e5A9244b0) | The following contracts are deployed on the corresponding L2 chain | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | --------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | L2 Gateway Router | [0x5288...F933](https://arbiscan.io/address/0x5288c571Fd7aD117beA99bF60FE0846C4E84F933) | [0x2190...DFa8](https://nova.arbiscan.io/address/0x21903d3F8176b1a0c17E953Cd896610Be9fFDFa8) | [0x9fDD...43C7](https://sepolia.arbiscan.io/address/0x9fDD1C4E4AA24EEc1d913FABea925594a20d43C7) | | L2 ERC20 Gateway | [0x09e9...1EEe](https://arbiscan.io/address/0x09e9222E96E7B4AE2a407B98d48e330053351EEe) | [0xcF9b...9257](https://nova.arbiscan.io/address/0xcF9bAb7e53DDe48A6DC4f286CB14e05298799257) | [0x6e24...b502](https://sepolia.arbiscan.io/address/0x6e244cD02BBB8a6dbd7F626f05B2ef82151Ab502) | | L2 Arb-Custom Gateway | [0x0967...5562](https://arbiscan.io/address/0x096760F208390250649E3e8763348E783AEF5562) | [0xbf54...51F4](https://nova.arbiscan.io/address/0xbf544970E6BD77b21C6492C281AB60d0770451F4) | [0x8Ca1...42C5](https://sepolia.arbiscan.io/address/0x8Ca1e1AC0f260BC4dA7Dd60aCA6CA66208E642C5) | | L2 Weth Gateway | [0x6c41...623B](https://arbiscan.io/address/0x6c411aD3E74De3E7Bd422b94A27770f5B86C623B) | [0x7626...D9eD](https://nova.arbiscan.io/address/0x7626841cB6113412F9c88D3ADC720C9FAC88D9eD) | [0xCFB1...556D](https://sepolia.arbiscan.io/address/0xCFB1f08A4852699a979909e22c30263ca249556D) | | L2 Weth | [0x82aF...Bab1](https://arbiscan.io/address/0x82aF49447D8a07e3bd95BD0d56f35241523fBab1) | [0x722E...5365](https://nova.arbiscan.io/address/0x722E8BdD2ce80A4422E880164f2079488e115365) | [0x980B...7c73](https://sepolia.arbiscan.io/address/0x980B62Da83eFf3D4576C647993b0c1D7faf17c73) | | L2 Proxy Admin | [0xd570...2a86](https://arbiscan.io/address/0xd570aCE65C43af47101fC6250FD6fC63D1c22a86) | [0xada7...d92C](https://nova.arbiscan.io/address/0xada790b026097BfB36a5ed696859b97a96CEd92C) | [0x715D...5FdF](https://sepolia.arbiscan.io/address/0x715D99480b77A8d9D603638e593a539E21345FdF) | ## Precompiles The following precompiles are deployed on every L2 chain and always have the same address | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | ---------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | ArbAddressTable | [0x0000...0066](https://arbiscan.io/address/0x0000000000000000000000000000000000000066) | [0x0000...0066](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000066) | [0x0000...0066](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000066) | | ArbAggregator | [0x0000...006D](https://arbiscan.io/address/0x000000000000000000000000000000000000006D) | [0x0000...006D](https://nova.arbiscan.io/address/0x000000000000000000000000000000000000006D) | [0x0000...006D](https://sepolia.arbiscan.io/address/0x000000000000000000000000000000000000006D) | | ArbFunctionTable | [0x0000...0068](https://arbiscan.io/address/0x0000000000000000000000000000000000000068) | [0x0000...0068](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000068) | [0x0000...0068](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000068) | | ArbGasInfo | [0x0000...006C](https://arbiscan.io/address/0x000000000000000000000000000000000000006C) | [0x0000...006C](https://nova.arbiscan.io/address/0x000000000000000000000000000000000000006C) | [0x0000...006C](https://sepolia.arbiscan.io/address/0x000000000000000000000000000000000000006C) | | ArbInfo | [0x0000...0065](https://arbiscan.io/address/0x0000000000000000000000000000000000000065) | [0x0000...0065](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000065) | [0x0000...0065](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000065) | | ArbOwner | [0x0000...0070](https://arbiscan.io/address/0x0000000000000000000000000000000000000070) | [0x0000...0070](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000070) | [0x0000...0070](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000070) | | ArbOwnerPublic | [0x0000...006b](https://arbiscan.io/address/0x000000000000000000000000000000000000006b) | [0x0000...006b](https://nova.arbiscan.io/address/0x000000000000000000000000000000000000006b) | [0x0000...006b](https://sepolia.arbiscan.io/address/0x000000000000000000000000000000000000006b) | | ArbRetryableTx | [0x0000...006E](https://arbiscan.io/address/0x000000000000000000000000000000000000006E) | [0x0000...006E](https://nova.arbiscan.io/address/0x000000000000000000000000000000000000006E) | [0x0000...006E](https://sepolia.arbiscan.io/address/0x000000000000000000000000000000000000006E) | | ArbStatistics | [0x0000...006F](https://arbiscan.io/address/0x000000000000000000000000000000000000006F) | [0x0000...006F](https://nova.arbiscan.io/address/0x000000000000000000000000000000000000006F) | [0x0000...006F](https://sepolia.arbiscan.io/address/0x000000000000000000000000000000000000006F) | | ArbSys | [0x0000...0064](https://arbiscan.io/address/0x0000000000000000000000000000000000000064) | [0x0000...0064](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000064) | [0x0000...0064](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000064) | | ArbWasm | [0x0000...0071](https://arbiscan.io/address/0x0000000000000000000000000000000000000071) | [0x0000...0071](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000071) | [0x0000...0071](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000071) | | ArbWasmCache | [0x0000...0072](https://arbiscan.io/address/0x0000000000000000000000000000000000000072) | [0x0000...0072](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000072) | [0x0000...0072](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000072) | | NodeInterface | [0x0000...00C8](https://arbiscan.io/address/0x00000000000000000000000000000000000000C8) | [0x0000...00C8](https://nova.arbiscan.io/address/0x00000000000000000000000000000000000000C8) | [0x0000...00C8](https://sepolia.arbiscan.io/address/0x00000000000000000000000000000000000000C8) | ## Misc The following contracts are deployed on the corresponding L2 chain | Function | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | --------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | L2 Multicall | [0x842e...4EB2](https://arbiscan.io/address/0x842eC2c7D803033Edf55E478F461FC547Bc54EB2) | [0x5e1e...cB86](https://nova.arbiscan.io/address/0x5e1eE626420A354BbC9a95FeA1BAd4492e3bcB86) | [0xA115...d092](https://sepolia.arbiscan.io/address/0xA115146782b7143fAdB3065D86eACB54c169d092) | | `ResourceConstraintManager` | [0x8F59...823a](https://arbiscan.io/address/0x8F59C7A53b883563B34cbBb6fF021B03973e823a) | [0x653e...86B7](https://nova.arbiscan.io/address/0x653e31e11769a9c6feE825E4BC822753DE2286B7) | | ## Canonical factory contracts The following factory contracts are deployed on the corresponding chain and are used to deploy new Arbitrum chains (`RollupCreator`) and their token bridges (`TokenBridgeCreator`). For factory contracts on additional chains (Ethereum, Base, and testnets) and deployment instructions, see [Canonical factory contracts](/launch-arbitrum-chain/deploy/canonical-factory-contracts.md). | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | -------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | `RollupCreator` | [0xB90e...eB8b](https://arbiscan.io/address/0xB90e53fd945Cd28Ec4728cBfB566981dD571eB8b) | [0xF916...60F4](https://nova.arbiscan.io/address/0xF916Bfe431B7A7AaE083273F5b862e00a15d60F4) | [0x5F45...16cF](https://sepolia.arbiscan.io/address/0x5F45675AC8DDF7d45713b2c7D191B287475C16cF) | | `TokenBridgeCreator` | [0x2f56...000e](https://arbiscan.io/address/0x2f5624dc8800dfA0A82AC03509Ef8bb8E7Ac000e) | [0x8B9D...8c14](https://nova.arbiscan.io/address/0x8B9D9490a68B1F16ac8A21DdAE5Fd7aB9d708c14) | [0x56C4...bD8E](https://sepolia.arbiscan.io/address/0x56C486D3786fA26cc61473C499A36Eb9CC1FbD8E) | ; --- > For a complete page index, fetch # Debugging tools > **INFO** — KNOW MORE TOOLS? > > See something missing? Let us know on the [Arbitrum Discord](https://discord.gg/arbitrum) or by [opening an issue on GitHub](https://github.com/OffchainLabs/arbitrum-docs/issues/new). The following tools will help you debug your decentralized apps (dApps): ## Tenderly [Tenderly](https://tenderly.co/) is an all-in-one Web3 development platform that empowers developers to build, test, monitor, and operate smart contracts from inception to mass adoption. Tenderly's debugging options focus on providing developers with efficient and user-friendly tools to identify and fix smart contract bugs and production issues. The Debugger enables developers to inspect smart contracts by analyzing precise lines of code in a human-readable format. With Tenderly's Simulator, developers can play out specific historical transactions and current transaction outcomes before sending them onchain, allowing them to change relevant parameters and source code to test and debug contracts. The platform streamlines the debugging process, saving time and resources while improving smart contract reliability. Although Tenderly provides great debugging options, there are certain limitations when debugging [parent-to-child chain messages](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md) (also known as [Retryable Tickets](/arbitrum-essentials/bridging/cross-chain-messaging.md#ethereum-to-arbitrum-messaging)), due to the utilization of custom Geth errors. For further information on this constraint, please refer to the following [resource](/for-devs/troubleshooting-building.md#i-tried-to-create-a-retryable-ticket-but-the-transaction-reverted-on-l1-how-can-i-debug-the-issue). ## Arbiscan [Arbiscan](https://arbiscan.io/) is a prominent blockchain explorer and analytics platform that allows users to access and analyze public data on the Arbitrum network, such as transactions, wallet addresses, and smart contracts. Arbiscan offers VMTrace and Debug tools to aid developers and users in understanding the execution of transactions on the Ethereum network. VMTrace provides a step-by-step visualization of the EVM execution, enabling developers to trace transaction processing and identify potential issues. Debug tools offer additional information such as input data, logs, and events emitted by the smart contract during execution. --- > For a complete page index, fetch # Development frameworks > **INFO** — KNOW MORE TOOLS? > > See something missing? Let us know on the [Arbitrum Discord](https://discord.gg/arbitrum) or by [opening an issue on GitHub](https://github.com/OffchainLabs/arbitrum-docs/issues/new). The following tools will help you develop and test your decentralized apps (dApps): ## Hardhat [Hardhat](https://hardhat.org/) is a comprehensive development environment designed specifically for Ethereum, Arbitrum and, in general, EVM developers. It streamlines the process of creating, compiling, deploying, testing, and debugging smart contracts. By providing a robust and customizable framework, Hardhat makes it easy to manage complex projects and integrate with other tools in the ecosystem. Its features include a built-in console, advanced debugging capabilities, and support for extending functionality through plugins, allowing developers to create efficient and secure decentralized applications. ## Foundry [Foundry](https://github.com/foundry-rs/foundry) is a high-performance, portable, and modular toolkit designed for EVM application development, leveraging the Rust programming language. It offers a comprehensive suite of tools to streamline the process of creating, testing, and deploying smart contracts on the Ethereum, Arbitrum and, in general, any EVM network. Foundry facilitates interactions with EVM smart contracts, transactions, and chain data, while also providing a local node and a user-friendly Solidity REPL environment for efficient development. For a walkthrough that uses Foundry on Arbitrum, see [Create a token using Foundry](/build-decentralized-apps/quickstart-create-a-token.md). ## thirdweb [thirdweb SDK](https://portal.thirdweb.com/sdk) covers all aspects of the Web3 development stack, including connecting to user’s wallets, interacting with the blockchain and smart contracts, decentralized storage, authentication, and more; enabling you to build scalable and performant Web3 applications on any EVM-compatible blockchain. Out of the box, infrastructure is provided for everything required to create decentralized applications, including connection to the blockchain (RPC), decentralized storage (IPFS + pinning services), and tools to create powerful user experiences; such as gasless transactions, wallet connection components, FIAT on-ramps, data APIs, and more. ## Brownie [Brownie](https://github.com/eth-brownie/brownie) is a Python-based framework designed for developing and testing smart contracts on the Ethereum Virtual Machine. It offers full support for Solidity and Vyper programming languages and utilizes pytest for contract testing. Brownie also incorporates trace-based coverage evaluation, property-based and stateful testing with Hypothesis, and powerful debugging tools, including Python-style tracebacks and custom error strings. --- > For a complete page index, fetch # Arbitrum: Understanding the risks Arbitrum One is the first permissionless Ethereum parent chain rollup with full Ethereum smart contract functionality, and is [live on mainnet](https://offchain.medium.com/mainnet-for-everyone-27ce0f67c85e) — as is [Nova](https://medium.com/offchainlabs/its-time-for-a-new-dawn-nova-is-open-to-the-public-a081df1e4ad2), our first [AnyTrust chain](/how-arbitrum-works/deep-dives/anytrust-protocol.md); We're sure you're (almost) as excited as we are! Here are some risks you should know about before using the system: ## State Of progressive decentralization The Arbitrum DAO system is the owner of both the Arbitrum One and Arbitrum AnyTrust chains; see [“State of Progressive Decentralization”](https://docs.arbitrum.foundation/state-of-progressive-decentralization) for more. ## General words of caution: Software bugs Offchain Labs’ [implementation](https://github.com/OffchainLabs/nitro) of the Arbitrum protocol has been carefully constructed, is perpetually being audited by several independent firms, and is continuously reviewed and tested following best engineering practices. That said, there remains a non-zero chance that our codebase contains some undiscovered vulnerabilities that put user funds at risk. Users should carefully factor this risk into their decision to use Arbitrum One and/or Arbitrum Nova, and in deciding how much of their value to entrust into the system. Note that Offchain Labs also sponsors a [multi-million dollar bug bounty program](https://immunefi.com/bounty/arbitrum/) to incentivize any party who finds such a critical bug to disclose it responsibly. ## General words of caution: Scams Arbitrum, like Ethereum, is permissionless; on both platforms, anybody can deploy any smart contract code they want. Users should treat interacting with contracts on Arbitrum exactly as they do with Ethereum, i.e., they should only do so if they have good reason to trust that the application is secure. --- > For a complete page index, fetch # Monitoring tools and block explorers ### Block explorers Look up any transaction, address, or contract on Arbitrum. | Tool | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | ---------- | ----------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------- | | Arbiscan | [arbiscan.io](https://arbiscan.io/) | [nova.arbiscan.io](https://nova.arbiscan.io/) | [sepolia.arbiscan.io](https://sepolia.arbiscan.io) | | Blockscout | [arbitrum.blockscout.com](https://arbitrum.blockscout.com/) | [arbitrum-nova.blockscout.com](https://arbitrum-nova.blockscout.com/) | [arbitrum-sepolia.blockscout.com](https://arbitrum-sepolia.blockscout.com/) | | DexGuru | [arbitrum.dex.guru](https://arbitrum.dex.guru/) | [nova.dex.guru](https://nova.dex.guru/) | — | | OKLINK | [oklink.com/arbitrum](https://www.oklink.com/arbitrum) | — | — | ### Data and analytics Query, index, and visualize onchain data. | Tool | Description | Link | | --------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | Chainbase | Index, transform, and use onchain data at scale | [chainbase.com](https://chainbase.com/) | | Dune | Visualize and analyze Arbitrum network data with SQL queries | [dune.com](https://dune.com/) · [Arbitrum community dashboards](https://dune.com/browse/dashboards?q=arbitrum) | ### Next steps Build on Arbitrum or dive deeper into the network. * [Contract addresses](/arbitrum-essentials/reference/contract-addresses.md) — key Arbitrum contract addresses * [Chain info and RPC endpoints](/for-devs/dev-tools-and-resources/chain-info.md) — chain IDs, RPC URLs, and network parameters * [How to estimate gas](/arbitrum-essentials/how-to-estimate-gas.md) — understand gas costs on Arbitrum * [Quickstart: Build a dApp](/build-decentralized-apps/quickstart-solidity-remix.md) — deploy your first contract on Arbitrum * [Run a full node](/run-arbitrum-node/run-full-node.md) — run your own Arbitrum node > **INFO** — KNOW MORE TOOLS? > > See something missing? Let us know on the [Arbitrum Discord](https://discord.gg/arbitrum) or by [opening an issue on GitHub](https://github.com/OffchainLabs/arbitrum-docs/issues/new). --- > For a complete page index, fetch # RPC endpoints and providers ## Arbitrum public RPC endpoints > **CAUTION** > > * Unlike the RPC Urls, the Sequencer endpoints only support `eth_sendRawTransaction` and `eth_sendRawTransactionConditional` calls. > * Arbitrum public RPCs do not provide Websocket support. > * IPv6 is not supported. This section provides an overview of the available public RPC endpoints for different Arbitrum chains and necessary details to interact with them. | Name | RPC Url(s) | Chain ID | Block explorer | Underlying chain | Tech stack | Sequencer feed URL | Sequencer endpoint⚠️ | | -------------------------- | ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------ | ---------------- | ---------------- | -------------------------------------- | -------------------------------------------------- | | Arbitrum One | | 42161 | [Arbiscan](https://arbiscan.io/), [Blockscout](https://arbitrum.blockscout.com/) | Ethereum | Nitro (Rollup) | wss\://arb1-feed.arbitrum.io/feed | | | Arbitrum Nova | | 42170 | [Blockscout](https://arbitrum-nova.blockscout.com/) | Ethereum | Nitro (AnyTrust) | wss\://nova-feed.arbitrum.io/feed | | | Arbitrum Sepolia (Testnet) | | 421614 | [Arbiscan](https://sepolia.arbiscan.io/), [Blockscout](https://arbitrum-sepolia.blockscout.com/) | Sepolia | Nitro (Rollup) | wss\://sepolia-rollup.arbitrum.io/feed | | > **INFO** — More RPC endpoints > > More Arbitrum chain RPC endpoints can be found in Chain Connect: [Arbitrum One](https://www.alchemy.com/chain-connect/chain/arbitrum-one) and [Arbitrum Nova](https://www.alchemy.com/chain-connect/chain/arbitrum-nova). Alternatively, to interact with public Arbitrum chains, you can rely on many of the same popular node providers that you are already using on Ethereum: ## Third-party RPC providers > **INFO** — WANT TO BE LISTED HERE? > > Complete [this form](https://docs.google.com/forms/d/e/1FAIpQLSc_v8j7sc4ffE6U-lJJyLMdBoIubf7OIhGtCqvK3cGPGoLr7w/viewform) , if you'd like to see your project added to this list (and the [Arbitrum portal](https://portal.arbitrum.one/)). > **INFO** — Need support for Nova 3rd party tooling? > > Complete [this form](https://docs.google.com/forms/d/e/1FAIpQLSfL1SlycJDyeRQJg-izdFZgN0eqLLAuzHkOr4TIEAZlcuBfSg/viewform) to submit a support request. | Provider | Arb One? | Arb Nova? | Arb Sepolia? | Websocket? | Stylus Tracing? | | --------------------------------------------------------------------------------------- | -------- | --------- | ------------ | ---------- | ------------------------------ | | [1RPC](https://docs.1rpc.io/overview/supported-networks#arbitrum) | ✅ | | | | | | [Alchemy](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral) | ✅ | | ✅ | ✅ | Available on paid plans | | [Allnodes](https://arbitrum.publicnode.com) | ✅ | ✅ | | ✅ | | | [All That Node](https://allthatnode.com) | ✅ | | ✅ | ✅ | | | [Ankr](https://www.ankr.com/docs/rpc-service/chains/chains-list/#arbitrum) | ✅ | | | ✅ | Available on paid plans | | [BlockPi](https://docs.blockpi.io/build/api-reference/arbitrum) | ✅ | ✅ | | | | | [Chainbase](https://docs.chainbase.com/introduction/about) | ✅ | | | ✅ | | | [Chainnodes](https://www.chainnodes.org/chains/arbitrum) | ✅ | | | | | | [Chainstack](https://chainstack.com/build-better-with-arbitrum/) | ✅ | | | ✅ | Available on paid plans | | [dRPC](https://drpc.org/public-endpoints/arbitrum) | ✅ | ✅ | ✅ | ✅ | | | [GetBlock](https://getblock.io/nodes/arb/) | ✅ | | | ✅ | | | [Infura](https://www.infura.io/networks/ethereum/arbitrum) | ✅ | | ✅ | ✅ | Enabled on request | | [Lava](https://docs.lavanet.xyz/iprpc#arbitrum) | ✅ | ✅ | | | | | [Moralis](https://docs.moralis.io/reference/introduction) | ✅ | | | | | | [Nirvana Labs](https://nirvanalabs.io) | ✅ | ✅ | ✅ | ✅ | | | [NodeReal](https://nodereal.io/meganode/api-marketplace/arbitrum-nitro-rpc) | ✅ | ✅ | | | | | [NOWNodes](https://nownodes.io/nodes/arbitrum-arb) | ✅ | | | | | | [Pocket Network](https://docs.pocket.network/) | ✅ | | | | | | [PublicNode](https://arbitrum.publicnode.com/) | ✅ | ✅ | ✅ | | | | [Quicknode](https://www.quicknode.com/chains/arb) | ✅ | ✅ | ✅ | ✅ | Testnet supported in free tier | | [Tenderly](https://tenderly.co/) | ✅ | | ✅ | ✅ | Testnet supported in free tier | | [Unifra](https://unifra.io/) | ✅ | | | | | | [Validation Cloud](https://www.validationcloud.io/arbitrum) | ✅ | | ✅ | ✅ | Testnet supported in free tier | #### Compare provider latency For a live latency comparison of the public no-key Arbitrum endpoints listed above, see the [OpenChainBench Arbitrum RPC benchmark tool](https://openchainbench.com/benchmarks/arbitrum-rpc). Measurements are probed from three regions every minute and published as p50, p95 and p99 latency under an open methodology and CC BY 4.0 license. ## Sequencer endpoint behavior Arbitrum One exposes two public endpoints with different roles, shown below: a general-purpose public RPC URL, and a direct sequencer endpoint that accepts only `eth_sendRawTransaction` and `eth_sendRawTransactionConditional`. The table below summarizes how they differ in purpose and operational guarantees. ### The two endpoints at a glance | Endpoint | Purpose | Operational guarantees | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `https://arb1.arbitrum.io/rpc` (public RPC URL) | General-purpose read/write endpoint, useful for development and low-volume reads. | **No uptime, latency, or rate-limit guarantees.** Any application that depends on availability should use a third-party node provider or run its own node. | | `https://arb1-sequencer.arbitrum.io/rpc` (sequencer endpoint) | Direct submission path to the chain's sequencer for write traffic. Accepts only `eth_sendRawTransaction` and `eth_sendRawTransactionConditional`. | Exposes the defined queueing and timeout behavior described below, but it is still a best-effort public endpoint with no formal SLA. | ### Latency and timeout behavior Under nominal load, transactions submitted to the sequencer endpoint are accepted in well under a second. Internally, the sequencer places submissions into a bounded in-memory queue (default capacity 1024). The queue timeout (default 12 seconds) bounds how long a submitted transaction may remain pending in the sequencer's background queue before being picked up or rejected; it does **not** provide a formal end-to-end inclusion latency guarantee. If the transaction is not picked up within that window, `eth_sendRawTransaction` returns `context deadline exceeded`, and the transaction was **not** accepted—it is safe to retry. ### Recommended fallback patterns * Use a third-party node provider as your primary endpoint, or as a fallback when a direct sequencer submission fails. * Retry on transient errors only—`context deadline exceeded` and network/connection errors are safe to retry with backoff. Do not retry terminal errors such as `nonce too low` or contract reverts. ### Monitoring and alerting A successful `eth_sendRawTransaction` response means the sequencer has already sequenced your transaction into an L2 block—the call blocks until the block is created and returns only then, not merely when the transaction is enqueued. Treat this as the sequencer's soft confirmation that your transaction has been ordered and executed. It does **not** by itself mean the transaction has been posted to the parent chain in a batch or finalized there; if your application needs parent-chain finality guarantees, track that separately. For monitoring, alert on submission calls that return errors or exceed your expected latency ceiling rather than assuming a pending-but-unconfirmed state. --- > For a complete page index, fetch # Solidity references Solidity is the dominant high-level, statically typed, object-oriented language for writing smart contracts that run on the Ethereum Virtual Machine (EVM) and EVM-compatible chains (including Layer 2s like Arbitrum). It powers the vast majority of DeFi, NFTs, DAOs, and other onchain applications. ## Official documentation These are the authoritative, always-current references. * [Solidity documentation](https://docs.soliditylang.org/): The single most important resource. Two sections are must-reads: * Solidity by Example * Security Considerations * [Solidity Language Portal](https://www.soliditylang.org/): Overview, translations, links to the compiler/repo. * [Ethereum.org developer docs](https://ethereum.org/developers/docs/): Smart contracts, EVM, testing, compiling, security, and the full stack. Excellent companion to the Solidity docs. * [Official GitHub repository](https://github.com/ethereum/solidity): Compiler source, issues, releases, and vulnerability reporting. ## Learning paths and courses The ecosystem moves fast; high-quality, free project-based courses outperform most books (which are slow to market). * [Cyfrin Updraft (Patrick Collins)](https://updraft.cyfrin.io/): Beginner-to-advanced, heavily project-based, uses Foundry from the start. Includes security/auditing tracks. * [RareSkills Ultimate Solidity Course](https://rareskills.io/learn-solidity): In-depth, trusted by security experts and auditors. Strong on real-world patterns and protocol walkthroughs. * [CryptoZombies](https://cryptozombies.io/): Classic interactive game-based introduction. A little outdated, but still excellent for absolute beginners to learn syntax and basic patterns quickly. * [Alchemy University](https://www.alchemy.com/university/courses/solidity): Online education platform for blockchain and Web3 development courses. * [Risein](https://www.risein.com/#open-programs): Online education for blockchain and Web3 development courses. * [HackQuest](https://www.hackquest.io/learning-track/Ethereum): Ethereum development. * [Stylus course](https://www.hackquest.io/learning-track/Arbitrum) * [LearnWeb3](https://learnweb3.io/degrees/ethereum-developer-degree/): Ethereum-specific developer learning. * [Stylus course](https://learnweb3.io/courses/arbitrum-stylus-course/) * [Ethernaut](https://ethernaut.openzeppelin.com/): Interactive smart contract hacking game. **Paid courses** * [Metana](https://metana.io/web3-solidity-bootcamp-ethereum-blockchain/) ## Development frameworks and tooling * [Foundry (Rust-based)](https://getfoundry.sh/): Excellent fuzzing, mainnet forking, and cheat codes. Preferred by security researchers and DeFi protocols. * [Hardhat](https://hardhat.org/): JavaScript/TypeScript-first. Hardhat 3 brings a Rust-powered runtime for big performance gains. Outstanding stack traces, `console.log` in Solidity, vast plugin ecosystem (verification, gas reporter, etc.). Great for teams with frontend/web devs. * [Remix IDE (browser-based)](https://remix.ethereum.org/): Zero-setup prototyping, debugging, and deployment. Perfect for quickstarts and learning. **Other tools**: VS Code + Solidity extensions, Slither (static analysis, integrates with Foundry), Etherscan/Blockscout for verification. ## Security best practices and auditing Security is non-negotiable—most exploits stem from reentrancy, access control, integer issues, oracle problems, or upgrade logic. ### Core resources * [Solidity Docs](https://docs.soliditylang.org/) → Security Considerations section. * [ConsenSys Diligence Smart Contract Best Practices](https://consensysdiligence.github.io/smart-contract-best-practices/) * [OWASP Smart Contract Top 10](https://owasp.org/www-project-smart-contract-top-10/) * [OpenZeppelin Ethernaut](https://ethernaut.openzeppelin.com/) * [Trail of Bits “Building Secure Contracts” GitHub repo](https://github.com/crytic/building-secure-contracts) * [Cyfrin Updraft Security & Auditing track](https://updraft.cyfrin.io/courses/security) ### Libraries and standards * [OpenZeppelin Contracts](https://docs.openzeppelin.com/contracts/5.x/): The gold standard for ERC-20/ERC-721/ERC-1155, access control (Ownable, Roles), upgradeable proxies (UUPS/Transparent), pausability, etc. Always audit your usage and prefer their implementations over custom code. [OpenZeppelin GitHub repo](https://github.com/OpenZeppelin/openzeppelin-contracts). ## Community, forums, ongoing learning * [Ethereum Stack Exchange](https://ethereum.stackexchange.com/): Best for technical Q\&A. * [awesome-solidity GitHub repo](https://github.com/bkrem/awesome-solidity): Curated list of repos, tools, and examples. ## GitHub repositories worth studying * [Official compiler](https://github.com/argotorg/solidity) and [examples](https://github.com/ethereum/solidity-examples) * [OpenZeppelin](https://github.com/OpenZeppelin)/[openzeppelin-contracts](https://github.com/OpenZeppelin/openzeppelin-contracts) (read every line eventually). * [foundry-rs](https://github.com/foundry-rs/foundry)/[forge-std](https://github.com/foundry-rs/forge-std) * [NomicFoundation/hardhat](https://github.com/NomicFoundation/hardhat) * [ethereum/EIPs](https://github.com/ethereum/EIPs) (track changes affecting the EVM/Solidity). * Protocol repos (Uniswap, Aave, etc.) for real-world patterns. --- > For a complete page index, fetch # Web3 libraries and tools > **INFO** — KNOW MORE TOOLS? > > See something missing? Let us know on the [Arbitrum Discord](https://discord.gg/arbitrum) or by [opening an issue on GitHub](https://github.com/OffchainLabs/arbitrum-docs/issues/new). The following frameworks will help you build your decentralized apps. For practical end-to-end examples that use `ethers.js` against Arbitrum, see [How to bridge from the parent chain](/arbitrum-essentials/bridging/cross-chain-messaging.md#ethereum-to-arbitrum-messaging) and [Deposit tokens](/arbitrum-essentials/bridging/deposit/tokens.md). | Name | Language | Description | Documentation | | ------------ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | Ethers.js | TypeScript | Ethers.js is a lightweight library for Ethereum and EVM-compatible blockchains. It offers secure key management, node compatibility, ENS integration and supports JSON wallets, mnemonic phrases, and HD wallets. The library is TypeScript-ready and well-documented under the MIT License. | [Ethers.js Documentation](https://docs.ethers.org/) | | alloy | Rust | Alloy is a collection of utilities and crates for Ethereum development in Rust. It helps create and manage Rust prototypes that support Ethereum-like smart contract execution. Alloy focuses on interoperability and cross-chain communication.. | [alloy Documentation](https://alloy.rs/) | | thirdweb SDK | TypeScript | thirdweb SDK offers a comprehensive suite for Web3 development on EVM-compatible blockchains. It includes wallet connectivity, blockchain interaction, decentralized storage, and authentication. Gasless transactions, wallet components, FIAT on-ramps, and data APIs are key features. | [thirdweb SDK Portal](https://portal.thirdweb.com/sdk) | | Viem | TypeScript | Viem is a modular tool for Ethereum and EVM-compatible blockchain development. It provides performance-optimized APIs, JSON-RPC API abstractions, and smart contract interaction tools, and it supports environments like Anvil, Hardhat, and Ganache. | [Viem](https://viem.sh/) | | Web3.js | JavaScript | Web3.js is a JavaScript library for Ethereum and EVM-compatible node interaction. It enables transactions via HTTP, IPC, or WebSocket. Compatible with web browsers, Node.js, and Electron, it's commonly used with MetaMask. | [Web3.js GitHub](https://github.com/web3/web3.js/) | | Web3.py | Python | Web3.py is a Python library for interacting with Ethereum and EVM-compatible blockchains. It facilitates transactions, smart contract operations, and blockchain data access. Tailored for Python developers, it's a versatile tool for Ethereum-based applications. | [Web3.py GitHub](https://github.com/ethereum/web3.py/) | --- > For a complete page index, fetch # Security audit reports | Auditor | Audit date (MM/DD/YYY) | Audited code | View report | | ----------------------- | ---------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Trail of Bits** | 08/14/2026 | Tip Collection Toggler | [view](/assets/files/2026_08_14_trail_of_bits_tip_collection_toggler_final_summary_report-328c752d6505abfe1e1187cf49f91810.pdf) | | **Trail of Bits** | 07/31/2026 | Upgrade Action Contract | [view](/assets/files/2026_07_31_trail_of_bits_arbitrum_upgrade_action_contract_summary_report-21c534077514e126151924296c101d7b.pdf) | | **Trail of Bits** | 07/31/2026 | Sequencer Feed Ticketing | [view](/assets/files/2026_07_31_sequencer_feed_ticketing_summary_report-7673b36399a7be50c859bc5da18235d8.pdf) | | **Trail of Bits** | 07/10/2026 | ArbOS 60 & 61 | [view](/assets/files/2026_07_10_trail_of_bits_arbitrum_arbos_60_61_code_review_final_comprehensive_report-b07649fde7599b13706214e36dd2e0ac.pdf) | | **Trail of Bits** | 06/16/2026 | Reward Distributor Fixes | [view](/assets/files/2026_06_16_trail_of_bits_security_audit_reward_distributor_fixes_summary_report-d3150a52aed267a77ee6ff06202bafe6.pdf) | | **Trail of Bits** | 02/18/2026 | Delegated Voting Power (DVP) Quorum & Proposal Cancellation | [view](/assets/files/2026_02_18_trail_of_bits_arbitrum_quorum_changes_with_feb_extension_security_review-b3121ca12b4ea645f83ea8357c242d64.pdf) | | **Trail of Bits** | 01/12/2026 | Nitro External DA & Nitro Contracts v3.2.0 | [view](/assets/files/2026_01_12_trail_of_bits_nitro_external_da_security_review-9f34e469e28e47ebd05209c927ed9e58.pdf) | | **Open Zeppelin** | 12/19/2025 | Arbitrum Stylus SDK Pull Request #370 Audit | [view](/assets/files/2025_12_19_open_zeppelin_stylus_sdk_pull_request_370_audit-157f8eeec7c1dae1a00e9f1f02d5f818.pdf) | | **Open Zeppelin** | 12/10/2025 | Arbitrum Stylus SDK v0.10 Audit | [view](/assets/files/2025_12_10_open_zeppelin_stylus_sdk_v0_10_audit-6260b27df848854e82c63180a7f0841a.pdf) | | **Trail of Bits** | 12/01/2025 | ArbOS 50 & 51 Security Review | [view](/assets/files/2025_12_01_trail_of_bits_arbos_50_and_51_security_review-28b33f865f52b7713fd2ef3d6792cbd4.pdf) | | **Trail of Bits** | 12/01/2025 | Genesis File Generator | [view](/assets/files/2025_12_01_trail_of_bits_genesis_file_generator_security_review-ecc17bd8f262c11ea3c8fd6458ff271e.pdf) | | **Trail of Bits** | 09/15/2025 | Security Council Update | [view](/assets/files/2025_09_15_trail_of_bits_security_council_update_code_review_summary_report_with_fix_review-ca8e6756fa300a17392b4a051d30db05.pdf) | | **Trail of Bits** | 07/30/2025 | Upgrade Executor | [view](/assets/files/2025_07_30_trail_of_bits_upgrade_executor_report-900fbe3a31c6ca81e2949355f6134d33.pdf) | | **Trail of Bits** | 06/02/2025 | Block Hash Pusher | [view](/assets/files/2025_06_02_trail_of_bits_block_hash_pusher_security_review-34ba3892114234e7da66cdfc330d308e.pdf) | | **Trail of Bits** | 06/02/2025 | Mint/Burn Precompile | [view](/assets/files/2025_06_02_trail_of_bits_mint_burn_precompile_security_review-79ff8927ca1cd62424d286b0b30838ae.pdf) | | **Trail of Bits** | 05/06/2025 | ArbOS 40 Nitro | [view](/assets/files/2025_05_06_trail_of_bits_arbos_40_nitro_summary_report-769a942c08fe0f917eef523cba81a459.pdf) | | **Trail of Bits** | 04/18/2025 | Reward Distributor Fixes | [view](/assets/files/2025_04_18_trail_of_bits_reward_distributor_fixes_security_review-95acad5683bf61562ac3cedea313e749.pdf) | | **Trail of Bits** | 03/11/2025 | Sequencer Liveness Review | [view](/assets/files/2025_03_11_trail_of_bits_sequencer_liveness_security_review-298b2cd6810968ed840dff94df1e0c0e.pdf) | | **Trail of Bits** | 02/28/2025 | Security Council Key Rotation Update | [view](/assets/files/2025_02_28_trail_of_bits_security_council_rotation_security_review-6feca69ad7afe171104ecabaefe8971a.pdf) | | **Trail of Bits** | 02/28/2025 | Disable Gateway Action | [view](/assets/files/2025_02_28_trail_of_bits_disable_gateway_action_security_review-11ed2e1370d062c2ade5e5d6b085a8f3.pdf) | | **Trail of Bits** | 02/14/2025 | Custom Fee Token Exchange Rate | [view](/assets/files/2025_02_14_trail_of_bits_custom_fee_token_exchange_rate_security_review-640d7ef454d21c739e50c594fac727d9.pdf) | | **Trail of Bits** | 02/14/2025 | Geth 14.4 Changes for Pectra | [view](/assets/files/2025_02_14_trail_of_bits_geth_14_4_security_review-f24eef2e97e06e030fd5c1cc3a54ce5d.pdf) | | **Trail of Bits** | 02/02/2025 | ERC-20 Bridge Upgrade for Custom Fee Token & EIP-7702 Fixes | [view](/assets/files/2025_02_02_trail_of_bits_custom_fee_erc20_bridge_security_review-ccd6d481c1f7d41436a3ceb474bcd0f3.pdf) | | **Trail of Bits** | 12/26/2024 | BoLD Proposal Payload & Upgrade Actions + Misc Changes for Nitro Contracts v3.0.0 | [view](/assets/files/2024_12_26_trail_of_bits_bold_fixes_security_review-95c9ee3b07ccb11e59e57744ddc017d2.pdf) | | **Trail of Bits** | 10/30/2024 | Changes to BoLD Solidity contracts to support EIP7702 & Fast Withdrawals | [view](/assets/files/2024_10_30_trail_of_bits_security_audit_nitro_contracts_with_bold-90984d87c800f448601b84972e544e1d.pdf) | | **Trail of Bits** | 10/23/2024 | ArbOS 32 Bianca: Emergency Stylus Fixes | [view](/assets/files/2024_10_23_trail_of_bits_security_audit_arbos32_emergency_fixes-d3e018abb506e80f9625508dbaab2358.pdf) | | **Trail of Bits** | 10/07/2024 | Optimizations to BoLD history commitments | [view](/assets/files/2024_10_07_trail_of_bits_security_audit_bold_optimized_history_commitments-025bd74c8af33bb436e606b55a3ef550.pdf) | | **Trail of Bits** | 09/25/2024 | Timeboost Auction Contracts | [view](/assets/files/2024_09_25_trail_of_bits_security_audit_timeboost_auction_contracts-2a8dbdf7b139db4224d30d6d1015aa85.pdf) | | **Open Zeppelin** | 09/05/2024 | Initial Stylus Rust SDK audit | [view](/assets/files/2024_09_05_open_zeppelin_security_audit_stylus_rust_sdk-a78b94ded01f4e5f96dfd55a47158680.pdf) | | **Trail of Bits** | 08/29/2024 | Arbitrum Chains & Governance Upgrade Actions Contracts v2.1 | [view](/assets/files/2024_08_29_trail_of_bits_security_audit_orbit_and_governance_upgrade_actions_v2_1-8d6150a317148e1bfcf428b4e2c8ef2d.pdf) | | **Trail of Bits** | 08/29/2024 | USDC Custom Gateway & ArbOS Timestamp Upgrade Action contract | [view](/assets/files/2024_08_29_trail_of_bits_security_audit_usdc_custom_gateway_and_arbos_upgrade_at_timestamp_action-f490e6aa741551bfbf4b2349fcc82579.pdf) | | **Trail of Bits** | 08/05/2024 | BoLD contract fixes from the May 2024 audit & DAC reward updates | [view](/assets/files/2024_08_05_trail_of_bits_security_audit_bold_and_dac_rewards_updates-d0d6028126d4539be649eb05db5380c4.pdf) | | **Trail of Bits** | 08/01/2024 | Custom fee token | [view](/assets/files/2024_08_01_trail_of_bits_security_audit_custom_fee_token-7ce514634632f4735a710c81b55f2d27.pdf) | | **Trail of Bits** | 07/26/2024 | ArbOS 31 Bianca: Nitro Upgrade | [view](/assets/files/2024_07_26_trail_of_bits_security_audit_arbos_31-4538d946ebcd4187b211a868b6e8ea08.pdf) | | **Trail of Bits** | 07/26/2024 | ArbOS 30 Atlas: Nitro Upgrade | [view](/assets/files/2024_07_26_trail_of_bits_security_audit_arbos30_nitro_upgrade-d3b44d44e482a44a1710c80014a6630a.pdf) | | **Code4rena** | 06/17/2024 | Arbitrum BoLD: Public Audit Competition Report | [view](/assets/files/2024_06_17_code4rena_security_audit_arbos30_nitro_upgrade-3663f40614e5dadebbf4ef0e6a8e5c1e.pdf) | | **Trail of Bits** | 06/10/2024 | Arbitrum Stylus | [view](/assets/files/2024_06_10_trail_of_bits_security_audit_stylus-f2f68cbe59f5ac1c085292f6811c8ac9.pdf) | | **Trail of Bits** | 05/02/2024 | BoLD contract fixes from the Aug 2023 audit & Delay Buffer changes to the sequencer inbox | [view](/assets/files/2024_05_02_trail_of_bits_security_audit_bold_delay_buffer-7329f073827e7e12aede9a9203db1e01.pdf) | | **Chainsecurity** | 03/20/2024 | Nova Fee Router Updates (ArbOS 31) | [view](/assets/files/2024_08_20_chainsecurity_security_audit_nova_fee_router_update_arbos_31-12a4328ecb6f0966acacb54da8f49698.pdf) | | **Trail of Bits** | 03/18/2024 | l1-l3-teleporter | [view](/assets/files/2024_03_18_trail_of_bits_security_audit_l1_l3_teleporter-76736686c28613a9473c149615f94765.pdf) | | **Trail of Bits** | 08/02/2023 | Arbitrum BoLD—initial audit (then called challenge protocol v2) | [view](/assets/files/2023_08_02_trail_of_bits_security_audit_challenge_protocol_v2-b63429218c10faec79c4834f8582f9d3.pdf) | | **Trail of Bits** | 01/06/2023 | Governance & Token Bridge | [view](/assets/files/2023_06_23_trail_of_bits_security_audit_governance_report_governance_token_bridge-ca76b55fb6c017c17ef78d0721f7e714.pdf) | | **Trail of Bits** | 10/10/2022 | Nitro Node & Core Contracts, 2 of 2 | [view](/assets/files/2022_10_22_trail_of_bits_security_audit_nitro_2_of_2-11d8ca6bdf6e154c9b62e401b3220b1e.pdf) | | **ConsenSys Diligence** | 06/24/2022 | Nitro Node & Core Contracts | [view](/assets/files/2024_06_24_consensys_diligence_security_audit_nitro_contracts-b89f0db3702d3eec15a9211233ace9a6.pdf) | | **Trail of Bits** | 03/14/2022 | Nitro Node & Core Contracts, 1 of 2 | [view](/assets/files/2022_03_14_trail_of_bits_security_audit_nitro_1_of_2-d777111730bd602222978f7d98713d40.pdf) | | **ConsenSys Diligence** | 11/05/2021 | Core Contracts, Token Bridge | [view](/assets/files/2021_11_05_consensys_diligence_security_audit_core_contracts_token_bridge-664fbe3e5a14a41acaee4af64ae06100.pdf) | --- > For a complete page index, fetch # Machine Payments Protocol (MPP) This quickstart will guide you through implementing an Arbitrum-specific payment method plugin for the `mppx` library, which implements the Machine Payments Protocol (MPP). MPP defines a generic **challenge → credential → settlement** flow for payments between two parties: * **Server**: the merchant/payee (the one that wants to get paid) * **Client**: the payer (a user or AI agent) `mppx` itself is payment-method-agnostic. This plugin provides methods for settling payments on Arbitrum One or Arbitrum Sepolia with **ERC-20** stablecoins (currently **USDC**) through [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) `authorization`, and with almost any **ERC-20** via `permit2`. ## Core concepts The client (payer) never broadcasts a transaction and never pays gas. 1. The merchant requests payment (issues a challenge). 2. The payer signs an EIP-712 typed-data authorization offchain—no gas, no prior onchain approval is needed. 3. The merchant's server submits the signature onchain, completing the fund transfer. The merchant pays for the gas. This is exactly the "402 Payment Required" flow you'd want for machine/agent commerce: an HTTP request hits a paywalled endpoint, the agent signs a payment authorization, and the server settles it atomically before serving the response. It supports two settlement mechanisms: | Type | Onchain mechanism | Supports splits? | Need prior approval? | | ------------- | --------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------- | | authorization | EIP-3009 transferWithAuthorization (native to USDC) | ❌ | ❌ | | permit2 | Uniswap's Permit2 permitWitnessTransferFrom | ✅ (pay multiple recipients in one transaction) | ✅ payer must pre-approve Permit2 on the token once | > **WARNING** — Other options > > `transaction` and `hash` credential types are stubbed but intentionally not implemented—these have weaker challenge-binding and carry fraud risk. ![MPP Flow](/img/mpp.png) MPP Flow ## What the client does `charge()` returns a `Method.toClient` handler. In `createCredential`: 1. Validates the challenge’s `chainId` is supported and unexpired. 2. Checks the payer’s token balance onchain (`balanceOf`). 3. Branches on `credentialTypes`: * **permit2** (or undefined): builds permitted/`transferDetails` arrays (handling splits, with the primary recipient pushed to the front), derives the nonce from a challenge hash, and signs the Permit2 witness typed-data. permit2 splits The sum of the amounts in the split must be strictly lower than the total amount for the transaction. So if the total transaction is 10,000 and splits have two recipients that will receive 2,000 and 3,000—the main recipient will receive 5,000. * **authorization**: derives nonce = `keccak256(challenge.id, challenge.realm)` for challenge-binding (anti-replay), looks up the token's EIP-712 domain from the local erc3009Tokens registry (not an onchain query), and signs the `TransferWithAuthorization` struct. 4. Returns `Credential.serialize(...)`. No transaction is broadcast. ## What the server does `charge()` returns a `Method.toServer` handler. In `verify(credential, request)`, it independently re-derives and re-checks every value the client claimed (recipient, amount, deadline, nonce/challenge-hash, signature via `verifyTypedData`, `balance`, and, for permit2, the Permit2 allowance and split amounts). Then it: 1. Simulates the transaction with `eth_call` (so a bad credential doesn’t waste gas). 2. Submits `transferWithAuthorization` (authorization) or `permitWitnessTransferFrom` (permit2) from the merchant’s account. 3. `waitForTransactionReceipt`, then verifies the emitted Transfer logs match the expected recipients/amounts. 4. Returns an `mppx` Receipt: `method: "arbitrum", status: "success", timestamp, reference: txHash`. ## How to implement it — server (merchant) side `mppx` has an Express adapter: ```typescript import express from 'express'; import { Mppx } from 'mppx/express'; import { privateKeyToAccount } from 'viem/accounts'; import { charge } from '@arbitrum/mpp/server'; import * as defaults from '@arbitrum/mpp/default'; const account = privateKeyToAccount(process.env.SERVER_PRIVATE_KEY as `0x${string}`); const app = express(); const mppx = Mppx.create({ methods: [ charge({ recipient: account.address, // where funds land currency: defaults.TOKEN_CONTRACTS.USDC_ARBITRUM_SEPOLIA, // which token methodDetails: { chainId: 421614, decimals: 6 }, account, // pays gas to settle }), ], secretKey: process.env.SERVER_PRIVATE_KEY, }); // Gate an endpoint behind a charge: app.get( '/authorization', mppx.charge({ amount: '1000', // raw units: 1000 = 0.001 USDC (6 decimals) description: 'My favorite food', methodDetails: { chainId: 421614, credentialTypes: ['authorization'] }, }), (req, res) => res.json({ data: 'authorization worked!' }), // only runs after payment settles ); app.listen(3000); ``` * Set `credentialTypes` to `['permit2']` to use Permit2 instead, and add a `splits: [...]` array to pay multiple recipients in one transaction. * ⚠️ `amount` uses raw token units — human-readable decimal conversion isn't supported yet. ## How to implement it - client (payer) side ```typescript import { Mppx } from 'mppx/client'; import { privateKeyToAccount } from 'viem/accounts'; import { charge } from '@arbitrum/mpp/client'; const account = privateKeyToAccount(process.env.CLIENT_PRIVATE_KEY as `0x${string}`); const mppx = Mppx.create({ methods: [charge({ account, chainId: 421614 })], }); // mppx intercepts the 402, signs the challenge, retries automatically: const response = await mppx.fetch('http://localhost:3000/authorization'); const data = await response.json(); console.log(`Response: ${data}`); // Payment response ('authorization worked!') const receipt = response.headers.get('payment-receipt'); // base64-encoded mppx Receipt console.log(Buffer.from(receipt!, 'base64').toString('binary')); // Transaction information including hash ``` ## Run the bundled example locally ```shell pnpm install # .env (copy from .env.example) CLIENT_PRIVATE_KEY=0x... # this wallet needs USDC on Arbitrum Sepolia SERVER_PRIVATE_KEY=0x... # this wallet needs ETH (gas) on Arbitrum Sepolia # Terminal 1 pnpm run server # tsx test/server → listens on :3000 # Terminal 2 pnpm run client # tsx test/client → hits /authorization, signs, settles ``` ### Funding requirements * Server needs **ETH** on the chain (it pays gas to submit the settlement transaction). * Client needs **USDC** on the same chain (the funds being pulled). * For Permit2, the client must first approve the Permit2 contract (`0x000000000022D473030F116dDEE9F6B43aC78BA3`) as a spender on the **USDC** token—Permit2 can’t move tokens it hasn’t been allowed to. ### Current limitations * Only **USDC** on Arbitrum One/Sepolia is registered. To add a token, register its address and EIP-712 name/version/chainId. * `amount` is raw units only—no human-readable decimal conversion yet. * For authorization, the `validBefore` expiry is trusted from the server's challenge; a far-future expiry theoretically widens the window in which an unsubmitted authorization could be settled late. (**Note**: the EIP-3009 nonce is challenge-bound—`keccak256(id, realm)`—and single-use onchain, so a literal replay of an already-settled authorization is blocked once the nonce is consumed.) * `transaction` and `hash` credential types are intentionally unimplemented (weak challenge-binding). * Status is v0.1.0 — early/experimental. ### Reference links * [Protocol overview](https://mpp.dev/protocol) * [Custom/first-party SDK](https://mpp.dev/payment-methods/custom#first-party-sdk) * [Method.from](https://mpp.dev/sdk/typescript/Method.from) * [Method.toServer](https://mpp.dev/sdk/typescript/core/Method.toServer) * [Method.toClient](https://mpp.dev/sdk/typescript/core/Method.toClient) * [Unified EVM Spec](https://github.com/tempoxyz/mpp-specs/blob/main/specs/methods/evm/draft-evm-charge-00.md) * [EIP-3009 Transfer with Authorization](https://eips.ethereum.org/EIPS/eip-3009) --- > For a complete page index, fetch # Create a token using Foundry (Quickstart) Deploying an **ERC-20** token on Arbitrum One is fully permissionless and uses the same tooling and workflows as Ethereum. You can deploy directly to Arbitrum One using Foundry, and optionally configure [bridging](/arbitrum-essentials/bridging/overview.md), liquidity, and supporting smart contract infrastructure as part of your token creation. This guide walks you through deploying a standard **ERC-20** token on Arbitrum One, from local setup to testnet and mainnet deployment. ## Prerequisites 1. **Install Foundry**: ````shell curl -L https://foundry.paradigm.xyz | bash foundryup ```bash curl -L https://foundry.paradigm.xyz | bash foundryup ```` 2. \*\*Get test **ETH\***\*: Obtain Arbitrum Sepolia \*\*ETH\*\* from a faucet like Alchemy's Arbitrum Sepolia Faucet, Chainlink's faucet, or QuickNode's faucet. You'll need to connect a wallet (for example, MetaMask) configured for Arbitrum Sepolia and request testnet funds. > **INFO** — Resources > > A list of faucets is available on the [Chain Info page](/for-devs/dev-tools-and-resources/chain-info.md#faucet-list). > > Faucets may require certain actions, for example, bridging, use the official Arbitrum Bridge if the faucet requires it. > > All references to **ETH** on this document refers to test **ETH** on Sepolia. 3. **Set up development environment**: Configure your wallet and tools for Arbitrum testnet deployment. Sign up for an Etherscan account to get an API key for contract verification. ## Project setup 1. **Initialize Foundry Project**: ```bash # Create new project forge init my-token-project cd my-token-project # Remove extra files rm src/Counter.sol script/Counter.s.sol test/Counter.t.sol ``` 2. **Install OpenZeppelin Contracts**: ```bash # Install OpenZeppelin contracts library forge install OpenZeppelin/openzeppelin-contracts ``` ## Smart contract development Create `src/MyToken.sol` (this is a standard **ERC-20** contract and works on any EVM chain like Arbitrum): ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract MyToken is ERC20, Ownable { // Max number of tokens that will exist uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10**18; constructor( string memory name, string memory symbol, uint256 initialSupply, address initialOwner ) ERC20(name, symbol) Ownable(initialOwner) { require(initialSupply <= MAX_SUPPLY, "Initial supply exceeds max supply"); // Mints the initial supply to the contract deployer _mint(initialOwner, initialSupply); } function mint(address to, uint256 amount) public onlyOwner { require(totalSupply() + amount <= MAX_SUPPLY, "Minting would exceed max supply"); _mint(to, amount); } function burn(uint256 amount) public { _burn(msg.sender, amount); } } ``` ## Deployment script Create `script/DeployToken.s.sol`: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {Script, console} from "forge-std/Script.sol"; import {MyToken} from "../src/MyToken.sol"; contract DeployToken is Script { function run() external { // Load contract deployer's private key from environment variables uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address deployerAddress = vm.addr(deployerPrivateKey); // Token configuration parameters string memory name = "My Token"; string memory symbol = "MTK"; uint256 initialSupply = 100_000_000 * 10**18; // Initiates broadcasting transactions vm.startBroadcast(deployerPrivateKey); // Deploys the token contract MyToken token = new MyToken(name, symbol, initialSupply, deployerAddress); // Stops broadcasting transactions vm.stopBroadcast(); // Logs deployment information console.log("Token deployed to:", address(token)); console.log("Token name:", token.name()); console.log("Token symbol:", token.symbol()); console.log("Initial supply:", token.totalSupply()); console.log("Deployer balance:", token.balanceOf(deployerAddress)); } } ``` ## Environment configuration 1. **Create `.env` file**: ```text PRIVATE_KEY=your_private_key_here ARBITRUM_SEPOLIA_RPC_URL=https://sepolia.arbitrum.io/rpc ARBITRUM_ONE_RPC_URL=https://arb1.arbitrum.io/rpc ARBISCAN_API_KEY=your_arbiscan_api_key_here ``` > **INFO** — Resources > > A list of RPCs, and chain IDs are available on the [Chain Info page](https://docs.arbitrum.io/for-devs/dev-tools-and-resources/chain-info). 2. **Update `foundry.toml`** (add chain IDs for verification, as Arbiscan requires them for non-Ethereum chains): ```toml [profile.default] src = "src" out = "out" libs = ["lib"] remappings = ["@openzeppelin/=lib/openzeppelin-contracts/"] [rpc_endpoints] arbitrum_sepolia = "${ARBITRUM_SEPOLIA_RPC_URL}" arbitrum_one = "${ARBITRUM_ONE_RPC_URL}" [etherscan] arbitrum_sepolia = { key = "${ARBISCAN_API_KEY}", url = "https://api-sepolia.arbiscan.io/api", chain = 421614 } arbitrum_one = { key = "${ARBISCAN_API_KEY}", url = "https://api.arbiscan.io/api", chain = 42161 } ``` > **INFO** — Resources > > A list of chain IDs is available on the [Chain Info page](/for-devs/dev-tools-and-resources/chain-info.md#arbitrum-public-rpc-endpoints). ## Testing 1. **Create `test/MyToken.t.sol`** ```text // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {Test, console} from "forge-std/Test.sol"; import {MyToken} from "../src/MyToken.sol"; contract MyTokenTest is Test { MyToken public token; address public owner = address(0x1); address public user = address(0x2); uint256 constant INITIAL_SUPPLY = 100_000_000 * 10**18; function setUp() public { // Deploy token contract before each test vm.prank(owner); token = new MyToken("Test Token", "TEST", INITIAL_SUPPLY, owner); } function testInitialState() public { // Verify the token was deployed with the correct parameters assertEq(token.name(), "Test Token"); assertEq(token.symbol(), "TEST"); assertEq(token.totalSupply(), INITIAL_SUPPLY); assertEq(token.balanceOf(owner), INITIAL_SUPPLY); } function testMinting() public { uint256 mintAmount = 1000 * 10**18; // Only the owner should be able to mint vm.prank(owner); token.mint(user, mintAmount); assertEq(token.balanceOf(user), mintAmount); assertEq(token.totalSupply(), INITIAL_SUPPLY + mintAmount); } function testBurning() public { uint256 burnAmount = 1000 * 10**18; // Owner burns their tokens vm.prank(owner); token.burn(burnAmount); assertEq(token.balanceOf(owner), INITIAL_SUPPLY - burnAmount); assertEq(token.totalSupply(), INITIAL_SUPPLY - burnAmount); } function testFailMintExceedsMaxSupply() public { // This test should fail when attempting to mint more than the max supply uint256 excessiveAmount = token.MAX_SUPPLY() + 1; vm.prank(owner); token.mint(user, excessiveAmount); } function testFailUnauthorizedMinting() public { // This test should fail when a non-owner tries to mint tokens vm.prank(user); token.mint(user, 1000 * 10**18); } } ``` 2. **Run tests**: ````shell # Runs all tests with verbose output forge test -vv ```bash # Runs all tests with verbose output forge test -vv ```` ## Deployment and verification 1. **Deploy to Arbitrum Sepolia** (testnet): ```bash # Load environment variables source .env # Deploy to Arbitrum Sepolia with automatic verification forge script script/DeployToken.s.sol:DeployToken \ --rpc-url arbitrum_sepolia \ --broadcast \ --verify ``` * Uses `https://sepolia.arbitrum.io/rpc` (RPC URL). * Chain ID: 421614. * Verifies on [Sepolia Arbiscan](https://sepolia.arbiscan.io/). 2. **Deploy to Arbitrum One** (mainnet): * Replace `arbitrum_sepolia` with `arbitrum_one` in the command. * Uses `https://arb1.arbitrum.io/rpc` (RPC URL). * Chain ID: 42161. * Verifies on [Arbiscan](https://arbiscan.io/). * Requires sufficient **ETH** on Arbitrum One for gas fees (bridge from Ethereum mainnet if needed). 3. **Example of verifying on Arbiscan**: ```bash forge verify-contract :YourToken \ --verifier etherscan \ --chain-id 42161 \ --num-of-optimizations 200 ``` ## Arbitrum-specific configurations * **RPC URLs**: * Arbitrum Sepolia: `https://sepolia.arbitrum.io/rpc` * Arbitrum One: `https://arb1.arbitrum.io/rpc` * **Chain IDs**: Arbitrum Sepolia: 421614; Arbitrum One: 42161. * **Contract Addresses**: Logged in console output after deployment (e.g., `console.log("Token deployed to:", address(token));`). * **Verification**: Uses Arbiscan API with your API key. The `--verify` flag enables automatic verification. ## Important notes * Always conduct security audits (e.g., via tools like Slither or other professional reviews) before mainnet deployment, as token contracts handle value. * Ensure your wallet has enough **ETH** for gas on the target network. Arbitrum fees are low, but mainnet deployments still cost real **ETH**. See [How to estimate gas](/arbitrum-essentials/how-to-estimate-gas.md) to size your deployment funds. * If you encounter verification issues, double-check your Arbiscan API key and foundry.toml configs. For more advanced deployments, refer to general Foundry deployment docs or Arbitrum developer resources. ## Bridging considerations Two deployment paths are possible: 1. **Native deployment** * Token is deployed directly on Arbitrum One * Use cases: Token Generation Event (TGE), liquidity bootstrapping, airdrops, and L2-native user flows. 2. **Deployment on Ethereum and bridging to Arbitrum One** * Use the [Arbitrum Token Bridge](https://bridge.arbitrum.io) to create an L2 counterpart. For a programmatic setup, see the [token bridging guides](/arbitrum-essentials/bridging/overview.md). ## Post-deployment considerations After deploying a token contract on Arbitrum, you can complete additional setup steps depending on your project's needs. These may include: * Verifying the contract on Arbiscan to improve transparency and readability * Creating liquidity pools on Arbitrum-based DEXs * Publishing token metadata to relevant indexing or aggregation services * Ensuring wallet compatibility by submitting basic token information * Configuring operational security components such as multisigs or timelocks * Connecting to market infrastructure providers where applicable * Setting up monitoring or observability tools for contract activity --- > For a complete page index, fetch # Build a decentralized app with Solidity (Quickstart) > **INFO** — Want to use Rust instead? > > Head over to [the Stylus quickstart](/stylus/quickstart.md) if you'd like to use Rust instead of Solidity. This quickstart is for web developers who want to start building **decentralized applications** using Arbitrum. It makes no assumptions about your prior experience with Ethereum, Arbitrum, or Solidity. Familiarity with Javascript and yarn is expected. If you're new to Ethereum, consider studying the [Ethereum documentation](https://ethereum.org/en/developers/docs/) before proceeding. ## What we'll learn In this tutorial we will learn: 1. The basics of Ethereum vs. client/server architecture 2. What is a Solidity smart contract 3. How to compile and deploy a smart contract 4. How to use an Ethereum wallet We're going to build a digital cupcake vending machine using Solidity smart contracts[1](#user-content-fn-1). This vending machine will follow two rules: 1. The vending machine will distribute a cupcake to anyone who hasn't recently received one. 2. The vending machine's rules can't be changed by anyone. Here's the vending machine implemented with Javascript. To use it, enter a name in the form below and press the **Cupcake please!** button, you should see your cupcake balance go up. #### Free Cupcakes web2NameEnter nameContract addressEnter contract addressCupcake please\![Refresh balance]()🧁 Cupcake balance:0 (no name) We can assume that this vending machine operates as we expect, but it's largely up to the **centralized service provider** that hosts it. In the case of a compromised cloud host: 1. Our centralized service provider can deny access to particular users. 2. A malicious actor can change the rules of the vending machine at any time, for example, to give their friends extra cupcakes. Centralized third-party intermediaries represent a **single point of failure** that malicious actors can exploit. With a blockchain infrastructure such as Ethereum, we decentralize our vending machine's **business logic and data**, making this type of exploits nearly impossible. This is Arbitrum's core value proposition to you, dear developer. Arbitrum makes it easy for you to deploy your vending machines to Ethereum's permissionless, trustless, decentralized network of nodes[2](#user-content-fn-2) **while keeping costs low for you and your users**. Let's implement the "Web3" version of the above vending machine using Arbitrum. ## Prerequisites VS Code VS Code is the IDE we'll use to build our vending machine. See [code.visualstudio.com](https://code.visualstudio.com/) to install. Web3 wallet We will use Metamask as the wallet to interact with our vending machine. See [metamask.io](https://metamask.io/) and click View MetaMask Web or [OKX Wallet](https://www.okx.com/web3) and click Connect Wallet to install. Yarn Yarn is the package manager we'll use to install dependencies. See [yarnpkg.com](https://yarnpkg.com/) to install. Foundry Foundry is the toolchain we'll use to compile and deploy our smart contract. See [getfoundry.sh](https://getfoundry.sh) to install. We'll address any remaining dependencies as we go. ## Ethereum and Arbitrum in a nutshell * **Ethereum** * Ethereum is a decentralized network of [nodes](https://docs.prylabs.network/docs/concepts/nodes-networks) that use Ethereum's client software (like [Offchain's Prysm](https://www.offchainlabs.com/prysm/docs) to maintain a public blockchain data structure. * The data within Ethereum's blockchain data structure changes one transaction at a time. * Smart contracts are small programs that execute transactions according to predefined rules. Ethereum's nodes host and execute smart contracts. * You can use smart contracts to build decentralized apps that use Ethereum's network to process transactions and store data. Think of smart contracts as your app's backend * Apps let users carry their data and identity between applications without trusting centralized service providers. * People who run Ethereum validator nodes[3](#user-content-fn-3) can earn **ETH** for processing and validating transactions on behalf of users and apps. * These transactions can be expensive when the network is under heavy load. * **Arbitrum** * Arbitrum is a finance-native platform providing infrastructure for building applications. * Arbitrum One is a child chain that implements the Arbitrum Rollup protocol. See the [Arbitrum chains overview](/arbitrum-essentials/public-chains.md) for a comparison of Arbitrum One, Nova, and the available testnets. * You can use Arbitrum One to build user-friendly apps with high throughput, low latency, and low transaction costs while inheriting Ethereum's high-security standards[4](#user-content-fn-4). ## Review the Javascript vending machine Here's the vending machine implemented as a Javascript class: VendingMachine.js ```js class VendingMachine { // state variables = internal memory of the vending machine cupcakeBalances = {}; cupcakeDistributionTimes = {}; // Vend a cupcake to the caller giveCupcakeTo(userId) { if (this.cupcakeDistributionTimes[userId] === undefined) { this.cupcakeBalances[userId] = 0; this.cupcakeDistributionTimes[userId] = 0; } // Rule 1: The vending machine will distribute a cupcake to anyone who hasn't recently received one. const fiveSeconds = 5000; const userCanReceiveCupcake = this.cupcakeDistributionTimes[userId] + fiveSeconds <= Date.now(); if (userCanReceiveCupcake) { this.cupcakeBalances[userId]++; this.cupcakeDistributionTimes[userId] = Date.now(); console.log(`Enjoy your cupcake, ${userId}!`); return true; } else { console.error('HTTP 429: Too Many Cupcakes (you must wait at least 5 seconds between cupcakes)'); return false; } } getCupcakeBalanceFor(userId) { return this.cupcakeBalances[userId]; } } ``` The `VendingMachine` class uses *state variables* and *functions* to implement *predefined rules*. This implementation is useful because it automates cupcake distribution, but there's a problem: it's hosted by a centralized server controlled by a third-party service provider. > **INFO** — Working web2 version > > To use the Vending Machine (web2), copy and paste the HTML code below into a text document, then open it in your web browser. > > VendingMachine.html > > ```html > > > > > > > > > > > Cupcake Vending Machine - Arbitrum Quickstart > > > > > > > > > > > >

Cupcake Vending Machine

> >

From the Arbitrum Solidity + Remix Quickstart

> > > >
> > > > > >
> > Web2 > >

Free Cupcakes

> >

Pure JavaScript vending machine. Business logic runs in your browser — no blockchain involved.

> > > > > > > > > >
> > > > > >
> > > >
> > > >
> > Cupcake balance for : > > 0 > >
> > > >
> >
> > > > > > > > > > > > > > > ``` Now, let's decentralize our vending machine's business logic and data by porting the above JavaScript implementation into a Solidity smart contract. ## Review the Solidity vending machine Here is a Solidity implementation of the vending machine. Solidity is a language that compiles to [EVM bytecode](https://blog.chain.link/what-are-abi-and-bytecode-in-solidity/). This means that it is deployable to any Ethereum-compatible blockchain, including Ethereum mainnet, Arbitrum One, and Arbitrum Nova. VendingMachine.sol ```solidity // SPDX-License-Identifier: MIT // Specify the Solidity compiler version - this contract requires version 0.8.9 or higher pragma solidity ^0.8.9; // Define a smart contract named VendingMachine // Unlike regular classes, once deployed, this contract's code cannot be modified // This ensures that the vending machine's rules remain constant and trustworthy contract VendingMachine { // State variables are permanently stored in blockchain storage // These mappings associate Ethereum addresses with unsigned integers // The 'private' keyword means these variables can only be accessed from within this contract mapping(address => uint) private _cupcakeBalances; // Tracks how many cupcakes each address owns mapping(address => uint) private _cupcakeDistributionTimes; // Tracks when each address last received a cupcake // Function to give a cupcake to a specified address // 'public' means this function can be called by anyone // 'returns (bool)' specifies that the function returns a boolean value function giveCupcakeTo(address userAddress) public returns (bool) { // Initialize first-time users // In Solidity, uninitialized values default to 0, so this check isn't strictly necessary // but is included to mirror the JavaScript implementation if (_cupcakeDistributionTimes[userAddress] == 0) { _cupcakeBalances[userAddress] = 0; _cupcakeDistributionTimes[userAddress] = 0; } // Calculate when the user is eligible for their next cupcake // 'seconds' is a built-in time unit in Solidity // 'block.timestamp' gives us the current time in seconds since Unix epoch uint fiveSecondsFromLastDistribution = _cupcakeDistributionTimes[userAddress] + 5 seconds; bool userCanReceiveCupcake = fiveSecondsFromLastDistribution <= block.timestamp; if (userCanReceiveCupcake) { // If enough time has passed, give them a cupcake and update their last distribution time _cupcakeBalances[userAddress]++; _cupcakeDistributionTimes[userAddress] = block.timestamp; return true; } else { // If not enough time has passed, revert the transaction with an error message // 'revert' cancels the transaction and returns the error message to the user revert("HTTP 429: Too Many Cupcakes (you must wait at least 5 seconds between cupcakes)"); } } // Function to check how many cupcakes an address owns // 'public' means anyone can call this function // 'view' means this function only reads data and doesn't modify state // This makes it free to call (no gas cost) when called externally function getCupcakeBalanceFor(address userAddress) public view returns (uint) { return _cupcakeBalances[userAddress]; } } ``` ## Compile your smart contract with Remix Smart contracts need to be compiled to bytecode to be stored and executed onchain by the EVM; we'll use Remix to do that. Remix is a browser-based IDE for EVM development. There are other IDEs to choose from (Foundry, Hardhat), but Remix doesn't require any local environment setup, so we'll use it for this tutorial. Let's first add our smart contract to Remix following these steps: ### 1. Load Remix: ### 2. Create a blank workspace in Remix: File explorer > Workspaces > Create blank ![](/img/apps-remix-create-blank-project-2025-01-07.gif) ### 3. Copy your vending machine contract ### 4. Paste your contract in Remix Select vending machine contract > Click compile menu > Compile ![](/img/apps-remix-paste-vending-machine-contract-2025-01-07.gif) "File explorer > New file" ### 5. Compile your contract in Remix Select vending machine contract > Click compile menu > Compile ![](/img/apps-remix-compile-contract-2025-01-07.gif) Note Ensure that Remix's compiler version matches the one in your contract. You can find your contract's compiler version at the top of your contract's file. It looks like this: ```solidity pragma solidity ^0.8.2; ``` You can easily select the right compiler version in Remix's the "Solidity compiler" menu. ## Deploy the smart contract to a local Ethereum chain Once a smart contract gets compiled, it is deployable to a blockchain. The safest way to do this is to deploy it to a locally hosted chain, where you can test and debug your contract before deploying it to a public chain. To deploy our `VendingMachine` smart contract locally, we will: 1. Run Foundry's local Ethereum node in a terminal window 2. Configure a wallet so we can interact with our smart contract after deployment (1) 3. Deploy our smart contract to (1)'s node using Remix ### Run a local chain Here, we'll use [Foundry's **anvil**](https://book.getfoundry.sh/anvil/) to run a local Ethereum network and node. ```shell curl -L https://foundry.paradigm.xyz | bash && anvil ``` Once you've run the above commands, you should see a prompt showing what test accounts automatically were generated for you and other infos about your local Anvil testnet. ```shell (_) | | __ _ _ __ __ __ _ | | / _` | | '_ \ \ \ / / | | | | | (_| | | | | | \ V / | | | | \__,_| |_| |_| \_/ |_| |_| 0.2.0 (7f0f5b4 2024-08-08T00:19:07.020431000Z) https://github.com/foundry-rs/foundry # Available Accounts (0) 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000.000000000000000000 ETH) (1) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000.000000000000000000 ETH) (2) 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000.000000000000000000 ETH) (3) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000.000000000000000000 ETH) (4) 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 (10000.000000000000000000 ETH) (5) 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc (10000.000000000000000000 ETH) (6) 0x976EA74026E726554dB657fA54763abd0C3a0aa9 (10000.000000000000000000 ETH) (7) 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 (10000.000000000000000000 ETH) (8) 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f (10000.000000000000000000 ETH) (9) 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 (10000.000000000000000000 ETH) # Private Keys (0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 (1) 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d (2) 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a (3) 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6 (4) 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a (5) 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba (6) 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e (7) 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356 (8) 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97 (9) 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 # Wallet Mnemonic: test test test test test test test test test test test junk Derivation path: m/44'/60'/0'/0/ # Chain ID 31337. ``` ### Configure Metamask Next, open Metamask and create or import a wallet by following the displayed instructions. By default, Metamask will connect to Ethereum's mainnet. To connect to our local "testnet," enable test networks for Metamask by clicking **Show/hide test networks**. Next, click Metamask's network selector dropdown and click the **Add Network** button. Click **Add a network manually** and then provide the following information: * Network Name: `localhost` * New RPC URL: `http://127.0.0.1:8545` * Chain ID: `31337` * Currency Symbol: **ETH** Add Localhost 8545 to Metamask ![](/img/apps-metamask-add-localhost-2025-01-13.png) Your wallet won't have a balance on your local testnet's node, but you can import one of the test accounts into Metamask to access to 10,000 testnet **ETH**. Copy the private key of one of the test accounts (it works with or without the `0x` prefix, so e.g., `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` or `ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`) and import it into Metamask. Metamask will ask you if you want to connect this new account to Remix, to which you should answer "yes": ![Connect Metamask to Localhost 8545](/img/apps-quickstart-import-metamask.png) > **CAUTION** — Never share your private keys > > Your Ethereum Mainnet wallet's private key is the password to all of your tokens. Never share it with anyone; avoid copying it to your clipboard. Note that in the context of this quickstart, "account" refers to an EOA (externally owned account), and its associated private key[5](#user-content-fn-5). You should see a balance of 10,000 **ETH**. Keep your private key handy; we'll use it again shortly. As we interact with our cupcake vending machine, we'll use Metamask's network selector dropdown to choose which network our cupcake transactions get sent to. We'll leave the network set to `Localhost 8545` for now. ### Connect Remix to Metamask In the last step, we'll connect Remix to Metamask so we can deploy our smart contract to the local chain using Remix. Connect remix to Metamask ![](/img/apps-remix-connect-metamask-2025-01-13.gif) At this point, we're ready to deploy our smart contract to any chain we want. ### Deploy the smart contract to your local chain * In MetaMask, ensure that the `Localhost` network is selected. * In Remix, deploy the `VendingMachine` contract to the `Localhost` network, then go to the "Deploy & Run Transactions" tab and click "Deploy." Deploy the VendingMachine contract to the Localhost network ![](/img/apps-remix-deploy-to-local-chain-2025-01-14.gif) Then copy and paste your **contract address** below and click **Get cupcake!**. A prompt should ask you to sign a transaction that gives you a cupcake. #### Free Cupcakes web3-localhostMetamask wallet addressEnter metamask wallet addressContract addressEnter contract addressCupcake please\![Refresh balance]()🧁 Cupcake balance:0 (no name) ## What's going on, here? Our first `VendingMachine` is labeled "Web2" because it demonstrates traditional client-server web application architecture: the back-end lives in a centralized network of servers. ![Architecture diagram](/img/apps-quickstart-vending-machine-architecture.png) The "Web3" architecture is similar to the "Web2" architecture, with one key difference: with the "Web3" version, business logic and data are hosted by decentralized network of nodes\*\* Let's take a closer look at the differences between our `VendingMachine` implementations: | | `WEB2`
(the first one) | `WEB3-LOCALHOST`
(the latest one) | `WEB3-ARB-SEPOLIA`
(the next one) | `WEB3-ARB-MAINNET`
(the final one) | | --------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | **Data** (cupcakes) | Stored only in your **browser**. (Usually, stored by centralized infrastructure.) | Stored on your **device** in an **emulated Ethereum network** (via smart contract). | Stored on Ethereum's **decentralized test network** (via smart contract). | Stored on Ethereum's **decentralized mainnet network** (via smart contract). | | **Logic** (vending) | Served from **Offchain's servers**. Executed by your **browser**. | Stored and executed by your **locally emulated Ethereum network** (via smart contract). | Stored and executed by Arbitrum's **decentralized test network** (via smart contract). | Stored and executed by Arbitrum's **decentralized mainnet network** (via smart contract). | | **Presentation** (UI) | Served from **Offchain's servers**. Rendered and executed by your **browser**. | ← same | ← same | ← same | | **Money** | Devs and users pay centralized service providers for server access using fiat currency. | ← same, but only for the presentation-layer concerns (code that supports frontend UI/UX). | ← same, but devs and users pay **testnet ETH** to testnet validators. | ← same, but instead of testnet **ETH**, they use \*\*mainnet \*\*ETH\*\*\*\*. | So far, we've deployed our "Web3" app to an emulated blockchain (Anvil), which is a normal step in EVM development. Next, we'll deploy our smart contract to a network of real nodes: Arbitrum's Sepolia testnet. ## Deploy the smart contract to the Arbitrum Sepolia testnet We were able to deploy to a testnet for free because we were using Remix's built-in network, but now we'll deploy our contract to Arbitrum's Sepolia testnet. Sepolia is powered by a network of nodes ran across the world by various participants, we'll need to compensate them with a small transaction fee in order to deploy our smart contract. To be able to pay the transaction fee, we will: * Use our MetaMask crypto wallet * Obtain some Arbitrum Sepolia testnet's token called **ETH**. Click Metamask's **Network selector** dropdown, and then click the **Add Network** button. Click **Add a network manually** and then provide the following information: * Network Name: `Arbitrum Sepolia` * New RPC URL: `https://sepolia-rollup.arbitrum.io/rpc` * Chain ID: `421614` * Currency Symbol: **ETH** As we interact with the cupcake vending machine, we'll use Metamask's network selector dropdown to determine which network our cupcake transactions are sent to. Next, let's deposit some **ETH** into the wallet corresponding to the private key we added to Remix. At the time of this quickstart's writing, the easiest way to acquire **ETH** is to bridge Sepolia **ETH** from Ethereum's parent chain Sepolia network to Arbitrum's child chain Sepolia network: 1. Use a parent chain Sepolia **ETH** faucet like [sepoliafaucet.com](https://sepoliafaucet.com/) to acquire some testnet **ETH** on parent chain Sepolia. 2. Bridge your parent chain Sepolia **ETH** into Arbitrum child chain using [the Arbitrum bridge](https://bridge.arbitrum.io/). Once you've acquired some **ETH**, you'll be able to deploy your smart contract to Arbitrum's Sepolia testnet. You can proceed exactly as with the local testnet. 1. Connect Remix to the Arbitrum Sepolia testnet 2. Compile your vending machine contract 3. Deploy your vending machine contract to the Arbitrum Sepolia testnet In this last step, your compiled smart contract will be deployed through the RPC endpoint corresponding to "Arbitrum Sepolia" in MetaMask (MetaMask uses [INFURA](https://www.infura.io)'s nodes as endpoints). Congratulations! You've just deployed **business logic and data** to Arbitrum Sepolia. This logic and data will be hashed and submitted within a transaction to Ethereum's parent chian Sepolia network, and then it will be mirrored across all nodes in the Sepolia network[6](#user-content-fn-6). To view your smart contract in a blockchain explorer, visit `https://sepolia.arbiscan.io/address/0x...B3`, but replace the `0x...B3` part of the URL with the full address of your deployed smart contract. Select **Arbitrum Sepolia** from Metamask's dropdown, paste your contract address into the `VendingMachine` below, and click **Get cupcake!**. You should be prompted to sign a transaction that gives you a cupcake. #### Free Cupcakes web3-arb-sepoliaMetamask wallet addressEnter metamask wallet addressContract addressEnter contract addressCupcake please\![Refresh balance]()🧁 Cupcake balance:0 (no name) The final step is deploying our Cupcake machine to a production network, such as Ethereum, Arbitrum One, or Arbitrum Nitro. The good news is: deploying a smart contract in production is exactly the same as for Sepolia Testnet. The harder news: it will cost real money, this time. If you deploy on Ethereum, the fees can be significant and the transaction confirmation time 12 seconds on average. Arbitrum, a child chain, reduces these costs about 10X and a confirmation time in the same order while maintaining a similar level of security and decentralization. For an in-depth look at how fees are calculated and estimated, see [How to estimate gas](/arbitrum-essentials/how-to-estimate-gas.md). ## Summary In this quickstart, we: * Identified **two business rules**: 1) fair and permissionless cupcake distribution 2) immutable business logic and data. * Identified a **challenge**: These rules are difficult to follow in a centralized application. * Identified a **solution**: Using Arbitrum, we can decentralize business logic and data. * Converted a vending machine's Javascript business logic into a **Solidity smart contract**. * **Deployed our smart contract** to a local development network, and then Arbitrum's Sepolia testnet. If you have any questions or feedback, reach out to us on [Discord](https://discord.gg/ZpZuw7p) and/or click the **Request an update** button at the top of this page—we're listening! ## Learning resources For a list of learning resources, repositories, and useful information about Solidity, see the [Solidity references](/arbitrum-essentials/reference/solidity-references.md) page. ## Footnotes 1. The vending machine example was inspired by [Ethereum.org's "Introduction to Smart Contracts"](https://ethereum.org/en/developers/docs/smart-contracts/), which was inspired by [Nick Szabo's "From vending machines to smart contracts"](http://unenumerated.blogspot.com/2006/12/from-vending-machines-to-smart.html). [↩](#user-content-fnref-1) 2. Although application front-ends are usually hosted by centralized services, smart contracts allow the underlying logic and data to be partially or fully decentralized. These smart contracts are hosted and executed by Ethereum's public, decentralized network of nodes. Arbitrum has its own network of nodes that use advanced cryptography techniques to "batch process" Ethereum transactions and then submit them to the Ethereum parent chain, which significantly reduces the cost of using Ethereum. All without requiring developers to compromise on security or decentralization. [↩](#user-content-fnref-2) 3. There are multiple types of Ethereum nodes. The ones that earn **ETH** for processing and validating transactions are called *validators*. See [Nodes and Networks](https://docs.prylabs.network/docs/concepts/nodes-networks) for a beginner-friendly introduction to Ethereum's node types. [↩](#user-content-fnref-3) 4. When our `VendingMachine` contract is deployed to Ethereum, it'll be hosted by Ethereum's decentralized network of nodes. Generally speaking, we won't be able to modify the contract's code after it's deployed. [↩](#user-content-fnref-4) 5. To learn more about how Ethereum wallets work, see [Ethereum.org's introduction to Ethereum wallets](https://ethereum.org/en/wallets/). [↩](#user-content-fnref-5) 6. Visit the [Gentle Introduction to Arbitrum](/get-started/arbitrum-introduction.md) for a beginner-friendly introduction to Arbitrum's Rollup protocol. [↩](#user-content-fnref-6) --- > For a complete page index, fetch # Contribute docs Thank you for considering to contribute to the Arbitrum documentation! We're excited to have you on board. The [`docs.arbitrum.io`](https://docs.arbitrum.io/) docs portal is the **single source of truth** for documentation that supports Offchain Labs' product portfolio. Contributions are welcome from the entire Ethereum community. This document shows you how to craft and publish Arbitrum documentation. Familiarity with [Markdown](https://www.markdownguide.org/basic-syntax/) syntax, Github, and [Docusaurus](https://docusaurus.io/docs) is expected. ### Add a new core document If a document isn't in a `Third-party content` sidebar node, it's a **core document**. To contribute a new core doc: 1. Begin by creating a branch (internal) or fork (external) of the [Arbitrum docs repo](https://github.com/OffchainLabs/arbitrum-docs). 2. Issue a `Draft` pull request into `master`. Pull requests into `master` generate a preview of your changes via a PR-specific Docusaurus deployment; this preview will update as you push commits to your remote. 3. Include answers to the following questions in your PR description: * Audience: Who am I writing for? * Problem: What specific problem are they trying to solve? * Discovery: How are they looking for a solution to this problem? What search terms are they using? * Document type: Which document type is most suitable? * Policy acknowledgment (Third-party docs only): Do you agree to the third-party content policy outlined within "Contribute docs"? 4. As you craft your contribution, refer to the [document types](#document-type-conventions), [Style guidance](#style-conventions), and other conventions below. 5. Mark your PR as `Open` when it's ready for review. ### Add a new third-party document **Third-party docs** are documents that help readers of Arbitrum docs use other products, services, and protocols (like the ones listed in the [Arbitrum portal](https://portal.arbitrum.io/)) with Arbitrum products. See [Contribute third-party docs](/for-devs/third-party-docs/contribute.md) for detailed instructions. ### Request an update If you'd like to request an update or share a suggestion related to an **existing document** without submitting a pull request to implement the improvement yourself, click the `Request an update` button located at the top of each published document. This button will lead you to a prefilled Github issue that you can use to elaborate on your request or suggestion. ### Add a new translation page If you would like to participate in translating the Arbitrum docs, you can: 1. Check whether `i18n` has a corresponding language (currently there are `ja` and `zh`). If not, you can use the following command to add it (we take adding French as an example): ```shell npm run write-translations -- --locale fr ``` It will help generate folder `i18n/fr`. 2. Create the folders `current` and `translated` under the newly generated folder `i18n/fr/docusaurus-plugin-content-docs`: ```shell mkdir i18n/{Your_language}/docusaurus-plugin-content-docs/current && mkdir i18n/{Your_language}/docusaurus-plugin-content-docs/translated ``` 3. Translate one of more docs files located in `docs/`. 4. Place the translated document into the folder `i18n/{Your_language}/docusaurus-plugin-content-docs/translated` according to its relative path in `arbitrum-docs`. For example, if you translated `/arbitrum-docs/how-arbitrum-works/arbos/introduction.md`, then its path in i18n should be `i18n/{Your_language}/docusaurus-plugin-content-docs/translated/how-arbitrum-works/arbos/introduction.md`. Test run: 1. Check that the i18n settings in `docusaurus.config.js` have included your new language: ```js i18n: { defaultLocale: 'en', // locales: ['en', 'ja', 'zh'], locales: ['en'], // You can add your new language to this array }, ``` 2. Check whether the `locale Dropdown` component exists in navbar, if not, add it: ```js navbar: { title: 'Arbitrum Docs', logo: { alt: 'My Site Logo', src: 'img/logo.svg', href: '/get-started/arbitrum-introduction', }, items: [ // note: we can uncomment this when we want to display the locale dropdown in the top navbar // if we enable this now, the dropdown will appear above every document; if `ja` is selected for a document that isn't yet translated, it will 404 // there may be a way to show the dropdown only on pages that have been translated, but that's out of scope for the initial version { type: 'localeDropdown', position: 'right', } ], }, ``` 2. Build translation and docs: ```shell yarn build-translation && yarn build ``` 6. Start docs: ```shell npm run serve ``` ### Document type conventions Every document should be a specific *type* of document. Each type of document has its own purpose: | Document type | Purpose | | ------------------- | ---------------------------------------------------------------------------------- | | Gentle introduction | Onboard a specific reader audience with tailored questions and answers | | Quickstart | Onboard a specific reader audience with step-by-step "learn by doing" instructions | | How-to | Provide task-oriented procedural guidance | | Concept | Explain what things are and how they work | | FAQ | Address frequently asked questions | | Troubleshooting | List common troubleshooting scenarios and solutions | | Reference | Lists and tables of things, such as API endpoints and developer resources | This isn't an exhaustive list, but it includes most of the document types that we use. > **INFO** — About Promotional Content > > While it is acceptable to include conceptual and how-to content that links to products, services, and protocols in the third party section, we do not accept promotional content in our core docs. Feature pieces that are primarily promotional and do not provide actionable guidance to readers are not accepted as third-party docs, either. ### Style conventions The following style guidelines provide a number of loose recommendations that help us deliver **a consistent content experience** across our docs: #### 1. Casing Sentence-case "content labels": document titles, sidebar titles, menu items, section headers, etc. #### 2. Linking Avoid anchoring links to words like "here" or "this". Descriptive anchor text can help set expectations for readers who may hesitate to click on ambiguous links. When linking to docs, try to link to the document's title verbatim. #### 3. Titling Titles should balance brevity with precision—*Node running overview* is preferred to *Overview*. This helps with SEO and reader UX. #### 4. Separate procedural from conceptual (most of the time) Within procedural docs like how-tos and quickstarts, avoid including too much conceptual content. Provide only the conceptual information that the target reader *needs* in order to complete the task at hand. Otherwise, organize conceptual information within conceptual docs, and link to them "just in case" from other docs. #### 5. Voice * Address the reader as "you". * Write like you'd speak to a really smart friend who's in a rush. * Opt for short, clear sentences that use translation-friendly, plain language. * Use contractions wherever it feels natural—this can help convey a friendly and conversational tone. #### 6. Formality * Don't worry too much about formality. The most valuable writing is writing that provides value to readers, and readers generally want to "flow" through guidance. * Aim at "informal professionalism" that prioritizes **audience-tailored problem-solving** and **consistent style and structure**. #### 7. Targeting * Don't try to write for everyone; write for a *specific reader persona* (also referred to as "audience" in this document) who has a *specific need*. * Make assumptions about prior knowledge (or lack thereof) and make these assumptions explicit in the beginning of your document. #### 8. Flow * Set expectations: Begin documents by setting expectations. Who is the document for? What value will it provide to your target audience? What assumptions are you making about their prior knowledge? Are there any prerequisites? * Value up front: Lead with what matters most to the reader persona you're targeting. Then, progressively build a bridge that carries them towards task completion as efficiently as possible. #### 9. Cross-linking We want to maintain both **high discoverability** and **high relevance**. As a general rule of thumb, links to other docs should be "very likely to be useful for most readers". Every link is a subtle call to action; we want to avoid CTA overload. #### 10. Things to avoid * Symbols where words will do: Minimize usage of `&` and `/`—spell out words like "*and*" and "*or*". * Jargon: Using precise technical terminology is ok, as long as your target audience is likely to understand the terminology. When in doubt, opt for clear, unambiguous, *accessible* language. Don't stress too much about checking off all of these boxes; we periodically review and edit our most heavily-trafficked docs, bringing them up to spec with the latest style guidelines. Some important disclaimers: * This isn't an exhaustive list. These are just the min-bar guidelines that will be applied to all new content moving forward. * Many of our docs don't yet follow this guidance. Our team is working on it! If you notice an obvious content bug, feel free to submit an [issue](https://github.com/OffchainLabs/arbitrum-docs/issues) or [PR](https://github.com/OffchainLabs/arbitrum-docs/pulls). ### Banner conventions You can use banners (Docusaurus refers to them as ["admonitions"](https://docusaurus.io/docs/markdown-features/admonitions)) to set expectations for your readers and to emphasize important callouts. Use these conservatively, as they interrupt the flow of the document. #### Under construction banner Example: > **CAUTION** — UNDER CONSTRUCTION > > The following steps are under construction and will be updated with more detailed guidance soon. Stay tuned, and don't hesitate to click the **Request an update** at the top of this document if you have any feedback along the way. Usage: ```markdown :::caution[UNDER CONSTRUCTION] The following steps are under construction and will be updated with more detailed guidance soon. Stay tuned, and don't hesitate to click the **Request an update** at the top of this document if you have any feedback along the way. ::: ``` #### Community member contribution banner Example: > **INFO** — Community member contribution > > The following document was contributed by @todo-twitter-handle. Give them a shoutout if you find it useful! Usage: ```markdown :::info[Community member contribution] The following document was contributed by @todo-twitter-handle. Give them a shoutout if you find it useful! ::: ``` ### Frequently asked questions #### Can I point to my product from core docs? For example—if my product hosts a public RPC endpoint, can I add it to your [RPC endpoints and providers](/arbitrum-essentials/reference/node-providers.md) page? These types of contributions are generally **not merged** unless they're submitted by employees of Offchain Labs. Instead of opening a PR for this type of contribution, click the `Request an update` button at the top of the published document to create an issue. Generally, third-party services are included in core docs only if we can confidently assert that the services are "**trustworthy, highly relevant to the core document at hand, and battle-tested by Arbitrum developers**" under a reasonable scrutiny. #### How long does it take for my third-party content contribution to be reviewed? Our team is continuously balancing competing priorities, so we can't guarantee a specific turnaround time for third-party docs PRs. They're processed in the order in which they're received, generally within a week or two. #### Is there any way to expedite third-party content contribution reviews? The most effective way to expedite processing is to ensure that your PR incorporates the conventions outlined in this document. Please don't ask for status updates—if you've submitted a PR, it's on our radar! --- > For a complete page index, fetch # Arbitrum chain information ## Arbitrum public RPC endpoints > **CAUTION** > > * Unlike the RPC Urls, the Sequencer endpoints only support `eth_sendRawTransaction` and `eth_sendRawTransactionConditional` calls. > * Arbitrum public RPCs do not provide Websocket support. > * IPv6 is not supported. This section provides an overview of the available public RPC endpoints for different Arbitrum chains and necessary details to interact with them. | Name | RPC Url(s) | Chain ID | Block explorer | Underlying chain | Tech stack | Sequencer feed URL | Sequencer endpoint⚠️ | | -------------------------- | ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------ | ---------------- | ---------------- | -------------------------------------- | -------------------------------------------------- | | Arbitrum One | | 42161 | [Arbiscan](https://arbiscan.io/), [Blockscout](https://arbitrum.blockscout.com/) | Ethereum | Nitro (Rollup) | wss\://arb1-feed.arbitrum.io/feed | | | Arbitrum Nova | | 42170 | [Blockscout](https://arbitrum-nova.blockscout.com/) | Ethereum | Nitro (AnyTrust) | wss\://nova-feed.arbitrum.io/feed | | | Arbitrum Sepolia (Testnet) | | 421614 | [Arbiscan](https://sepolia.arbiscan.io/), [Blockscout](https://arbitrum-sepolia.blockscout.com/) | Sepolia | Nitro (Rollup) | wss\://sepolia-rollup.arbitrum.io/feed | | > **INFO** — More RPC endpoints > > More Arbitrum chain RPC endpoints can be found in Chain Connect: [Arbitrum One](https://www.alchemy.com/chain-connect/chain/arbitrum-one) and [Arbitrum Nova](https://www.alchemy.com/chain-connect/chain/arbitrum-nova). Alternatively, to interact with public Arbitrum chains, you can rely on many of the same popular node providers that you are already using on Ethereum: ## Third-party RPC providers > **INFO** — WANT TO BE LISTED HERE? > > Complete [this form](https://docs.google.com/forms/d/e/1FAIpQLSc_v8j7sc4ffE6U-lJJyLMdBoIubf7OIhGtCqvK3cGPGoLr7w/viewform) , if you'd like to see your project added to this list (and the [Arbitrum portal](https://portal.arbitrum.one/)). > **INFO** — Need support for Nova 3rd party tooling? > > Complete [this form](https://docs.google.com/forms/d/e/1FAIpQLSfL1SlycJDyeRQJg-izdFZgN0eqLLAuzHkOr4TIEAZlcuBfSg/viewform) to submit a support request. | Provider | Arb One? | Arb Nova? | Arb Sepolia? | Websocket? | Stylus Tracing? | | --------------------------------------------------------------------------------------- | -------- | --------- | ------------ | ---------- | ------------------------------ | | [1RPC](https://docs.1rpc.io/overview/supported-networks#arbitrum) | ✅ | | | | | | [Alchemy](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral) | ✅ | | ✅ | ✅ | Available on paid plans | | [Allnodes](https://arbitrum.publicnode.com) | ✅ | ✅ | | ✅ | | | [All That Node](https://allthatnode.com) | ✅ | | ✅ | ✅ | | | [Ankr](https://www.ankr.com/docs/rpc-service/chains/chains-list/#arbitrum) | ✅ | | | ✅ | Available on paid plans | | [BlockPi](https://docs.blockpi.io/build/api-reference/arbitrum) | ✅ | ✅ | | | | | [Chainbase](https://docs.chainbase.com/introduction/about) | ✅ | | | ✅ | | | [Chainnodes](https://www.chainnodes.org/chains/arbitrum) | ✅ | | | | | | [Chainstack](https://chainstack.com/build-better-with-arbitrum/) | ✅ | | | ✅ | Available on paid plans | | [dRPC](https://drpc.org/public-endpoints/arbitrum) | ✅ | ✅ | ✅ | ✅ | | | [GetBlock](https://getblock.io/nodes/arb/) | ✅ | | | ✅ | | | [Infura](https://www.infura.io/networks/ethereum/arbitrum) | ✅ | | ✅ | ✅ | Enabled on request | | [Lava](https://docs.lavanet.xyz/iprpc#arbitrum) | ✅ | ✅ | | | | | [Moralis](https://docs.moralis.io/reference/introduction) | ✅ | | | | | | [Nirvana Labs](https://nirvanalabs.io) | ✅ | ✅ | ✅ | ✅ | | | [NodeReal](https://nodereal.io/meganode/api-marketplace/arbitrum-nitro-rpc) | ✅ | ✅ | | | | | [NOWNodes](https://nownodes.io/nodes/arbitrum-arb) | ✅ | | | | | | [Pocket Network](https://docs.pocket.network/) | ✅ | | | | | | [PublicNode](https://arbitrum.publicnode.com/) | ✅ | ✅ | ✅ | | | | [Quicknode](https://www.quicknode.com/chains/arb) | ✅ | ✅ | ✅ | ✅ | Testnet supported in free tier | | [Tenderly](https://tenderly.co/) | ✅ | | ✅ | ✅ | Testnet supported in free tier | | [Unifra](https://unifra.io/) | ✅ | | | | | | [Validation Cloud](https://www.validationcloud.io/arbitrum) | ✅ | | ✅ | ✅ | Testnet supported in free tier | #### Compare provider latency For a live latency comparison of the public no-key Arbitrum endpoints listed above, see the [OpenChainBench Arbitrum RPC benchmark tool](https://openchainbench.com/benchmarks/arbitrum-rpc). Measurements are probed from three regions every minute and published as p50, p95 and p99 latency under an open methodology and CC BY 4.0 license. ## Sequencer endpoint behavior Arbitrum One exposes two public endpoints with different roles, shown below: a general-purpose public RPC URL, and a direct sequencer endpoint that accepts only `eth_sendRawTransaction` and `eth_sendRawTransactionConditional`. The table below summarizes how they differ in purpose and operational guarantees. ### The two endpoints at a glance | Endpoint | Purpose | Operational guarantees | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `https://arb1.arbitrum.io/rpc` (public RPC URL) | General-purpose read/write endpoint, useful for development and low-volume reads. | **No uptime, latency, or rate-limit guarantees.** Any application that depends on availability should use a third-party node provider or run its own node. | | `https://arb1-sequencer.arbitrum.io/rpc` (sequencer endpoint) | Direct submission path to the chain's sequencer for write traffic. Accepts only `eth_sendRawTransaction` and `eth_sendRawTransactionConditional`. | Exposes the defined queueing and timeout behavior described below, but it is still a best-effort public endpoint with no formal SLA. | ### Latency and timeout behavior Under nominal load, transactions submitted to the sequencer endpoint are accepted in well under a second. Internally, the sequencer places submissions into a bounded in-memory queue (default capacity 1024). The queue timeout (default 12 seconds) bounds how long a submitted transaction may remain pending in the sequencer's background queue before being picked up or rejected; it does **not** provide a formal end-to-end inclusion latency guarantee. If the transaction is not picked up within that window, `eth_sendRawTransaction` returns `context deadline exceeded`, and the transaction was **not** accepted—it is safe to retry. ### Recommended fallback patterns * Use a third-party node provider as your primary endpoint, or as a fallback when a direct sequencer submission fails. * Retry on transient errors only—`context deadline exceeded` and network/connection errors are safe to retry with backoff. Do not retry terminal errors such as `nonce too low` or contract reverts. ### Monitoring and alerting A successful `eth_sendRawTransaction` response means the sequencer has already sequenced your transaction into an L2 block—the call blocks until the block is created and returns only then, not merely when the transaction is enqueued. Treat this as the sequencer's soft confirmation that your transaction has been ordered and executed. It does **not** by itself mean the transaction has been posted to the parent chain in a batch or finalized there; if your application needs parent-chain finality guarantees, track that separately. For monitoring, alert on submission calls that return errors or exceed your expected latency ceiling rather than assuming a pending-but-unconfirmed state. ## Chain parameters | Param | Description | Arbitrum One | Arbitrum Nova | Arb Sepolia | | -------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | Dispute window | Time for assertions to get confirmed during which validators can issue a challenge | 45818 blocks (\~ 6.4 days ) | 45818 blocks (\~ 6.4 days) | 20 blocks (\~ 4.0 minutes) | | Minimum bond amount | Amount of funds required for a validator to propose assertion on the parent chain | 3600 ETH | 1 ETH | 1 Sepolia ETH | | Force-include period | Period after which a delayed message can be included into the inbox without any action from the Sequencer | 5760 blocks / 24 hours | 5760 blocks / 24 hours | 5760 blocks / 24 hours | | Gas target | Target gas/sec, over which the congestion mechanism activates | [See child chain gas fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#child-chain-gas-fees) | [See child chain gas fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#child-chain-gas-fees) | [See child chain gas fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#child-chain-gas-fees) | | Gas price floor | Minimum gas price | 0.02 gwei | 0.02 gwei | 0.2 gwei | | Block gas limit | Maximum amount of gas that all the transactions inside a block are allowed to consume | 32,000,000 | 32,000,000 | 32,000,000 | ## Current gas targets | Gas target (Mgas/s) | Adjustment window (seconds) | | ------------------- | --------------------------- | | 60 | 9 | | 41 | 52 | | 29 | 329 | | 20 | 2,105 | | 14 | 13,485 | | 10 | 86,400 | To learn more about the gas target, refer to the [Gas and fees deep-dive](/how-arbitrum-works/deep-dives/gas-and-fees.md#the-gas-target). To determine how to configure the gas target for your chain, refer to the [Dynamic pricing for Arbitrum chains page](/launch-arbitrum-chain/chain-config/costs/dynamic-pricing.md). To calculate the values for your chain, refer to the [How to calculate the values for your chain section](/launch-arbitrum-chain/chain-config/costs/dynamic-pricing.md#how-to-calculate-the-values-for-your-chain) on the same page. ## Faucet list | Name | Network | Tokens | | --------------------------------------------------------------- | ------- | ----------------------------- | | [PK910 PoW Faucet](https://sepolia-faucet.pk910.de/) | Sepolia | Ethereum Sepolia | | [Faucet aggregator](https://arbitrum.faucet.dev/) | Sepolia | Ethereum Sepolia, Arb Sepolia | | [Alchemy’s Sepolia Faucet](https://sepoliafaucet.com/) | Sepolia | Ethereum Sepolia, Arb Sepolia | | [Infura's Sepolia Faucet](https://www.infura.io/faucet/sepolia) | Sepolia | Ethereum Sepolia | | [ethfaucet.com](https://ethfaucet.com/networks/arbitrum) | Sepolia | Arb Sepolia | ## Arbitrum Smart Contract Addresses The following information may be useful to those building on Arbitrum. We list the addresses of the smart contracts related to the protocol, the token bridge and precompiles of the different Arbitrum chains. ## Protocol smart contracts ### Core contracts The following contracts are deployed on Ethereum (L1) | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | --------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Rollup | [0x4DCe...Cfc0](https://etherscan.io/address/0x4DCeB440657f21083db8aDd07665f8ddBe1DCfc0) | [0xE7E8...B7Bd](https://etherscan.io/address/0xE7E8cCC7c381809BDC4b213CE44016300707B7Bd) | [0xd808...81C8](https://sepolia.etherscan.io/address/0xd80810638dbDF9081b72C1B33c65375e807281C8) | | Sequencer Inbox | [0x1c47...82B6](https://etherscan.io/address/0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6) | [0x211E...c21b](https://etherscan.io/address/0x211E1c4c7f1bF5351Ac850Ed10FD68CFfCF6c21b) | [0x6c97...be0D](https://sepolia.etherscan.io/address/0x6c97864CE4bEf387dE0b3310A44230f7E3F1be0D) | | CoreProxyAdmin | [0x5547...2dbD](https://etherscan.io/address/0x554723262467F125Ac9e1cDFa9Ce15cc53822dbD) | [0x71D7...7148](https://etherscan.io/address/0x71D78dC7cCC0e037e12de1E50f5470903ce37148) | [0x1ed7...0686](https://sepolia.etherscan.io/address/0x1ed74a4e4F4C42b86A7002e9951e98DBcC890686) | ### Cross-chain messaging contracts The following contracts are deployed on Ethereum (L1) | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Delayed Inbox | [0x4Dbd...AB3f](https://etherscan.io/address/0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f) | [0xc444...3949](https://etherscan.io/address/0xc4448b71118c9071Bcb9734A0EAc55D18A153949) | [0xaAe2...ae21](https://sepolia.etherscan.io/address/0xaAe29B0366299461418F5324a79Afc425BE5ae21) | | Bridge | [0x8315...ed3a](https://etherscan.io/address/0x8315177aB297bA92A06054cE80a67Ed4DBd7ed3a) | [0xC1Eb...76Bd](https://etherscan.io/address/0xC1Ebd02f738644983b6C4B2d440b8e77DdE276Bd) | [0x38f9...33a9](https://sepolia.etherscan.io/address/0x38f918D0E9F1b721EDaA41302E399fa1B79333a9) | | Outbox | [0x0B98...4840](https://etherscan.io/address/0x0B9857ae2D4A3DBe74ffE1d7DF045bb7F96E4840) | [0xD4B8...cc58](https://etherscan.io/address/0xD4B80C3D7240325D18E645B49e6535A3Bf95cc58) | [0x65f0...B78F](https://sepolia.etherscan.io/address/0x65f07C7D521164a4d5DaC6eB8Fac8DA067A3B78F) | | Classic Outbox\*\*\* | [0x7607...1A40](https://etherscan.io/address/0x760723CD2e632826c38Fef8CD438A4CC7E7E1A40)
[0x667e...337a](https://etherscan.io/address/0x667e23ABd27E623c11d4CC00ca3EC4d0bD63337a) | | | \*\*\*Migrated Network Only ### Fraud proof contracts The following contracts are deployed on Ethereum (L1) | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | ------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | ChallengeManager | [0xA556...9fB0](https://etherscan.io/address/0xA5565d266c3c3Ee90B16Be8A5b13d587ef559fB0) | [0xFE66...A688](https://etherscan.io/address/0xFE66b18Ef1B943F8594A2710376Af4B01AcfA688) | [0xC60b...8B4C](https://sepolia.etherscan.io/address/0xC60b56Ff6aAb3FE8B9Bd70040Fe9E95A26258B4C) | | OneStepProver0 | [0x35FB...F731](https://etherscan.io/address/0x35FBC5F03d86E88973B06Fb9C5a913D54AbdF731) | [0x35FB...F731](https://etherscan.io/address/0x35FBC5F03d86E88973B06Fb9C5a913D54AbdF731) | [0x3Fe7...1377](https://sepolia.etherscan.io/address/0x3Fe73F959C44e04d660dBFBbeffd51FD2c091377) | | OneStepProverMemory | [0xe0ba...C48b](https://etherscan.io/address/0xe0ba77e0E24de5369e3B268Ea79fDe716e2EC48b) | [0xe0ba...C48b](https://etherscan.io/address/0xe0ba77e0E24de5369e3B268Ea79fDe716e2EC48b) | [0x6268...ec2d](https://sepolia.etherscan.io/address/0x6268Fc8dB1b5083b405b2C51808Df3619783ec2d) | | OneStepProverMath | [0xaB95...F921](https://etherscan.io/address/0xaB9596a0aaF28bc798c453434EC2DC0F8F0bF921) | [0xaB95...F921](https://etherscan.io/address/0xaB9596a0aaF28bc798c453434EC2DC0F8F0bF921) | [0x42f5...e8Fa](https://sepolia.etherscan.io/address/0x42f58c90583eC3fA0E0b724dEDF755AE1068e8Fa) | | OneStepProverHostIo | [0xa07c...71Cf](https://etherscan.io/address/0xa07cD154340CC74EcF156FFB9fb378Ee29Ca71Cf) | [0xa07c...71Cf](https://etherscan.io/address/0xa07cD154340CC74EcF156FFB9fb378Ee29Ca71Cf) | [0xdB2c...C165](https://sepolia.etherscan.io/address/0xdB2c541e20Bd1830c8a050341Fca0Af51489C165) | | OneStepProofEntry | [0x4397...42d6](https://etherscan.io/address/0x4397fE1E959Ba81B9D5f1A9679Ddd891955A42d6) | [0x4397...42d6](https://etherscan.io/address/0x4397fE1E959Ba81B9D5f1A9679Ddd891955A42d6) | [0xB9cf...AE80](https://sepolia.etherscan.io/address/0xB9cf664A1beD8F74f4B893a18c86eCe876CdAE80) | ## Token bridge smart contracts ### Core contracts The following contracts are deployed on Ethereum (L1) | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | --------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | L1 Gateway Router | [0x72Ce...31ef](https://etherscan.io/address/0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef) | [0xC840...cD48](https://etherscan.io/address/0xC840838Bc438d73C16c2f8b22D2Ce3669963cD48) | [0xcE18...8264](https://sepolia.etherscan.io/address/0xcE18836b233C83325Cc8848CA4487e94C6288264) | | L1 ERC20 Gateway | [0xa3A7...0EeC](https://etherscan.io/address/0xa3A7B6F88361F48403514059F1F16C8E78d60EeC) | [0xB253...21bf](https://etherscan.io/address/0xB2535b988dcE19f9D71dfB22dB6da744aCac21bf) | [0x902b...3aFF](https://sepolia.etherscan.io/address/0x902b3E5f8F19571859F4AB1003B960a5dF693aFF) | | L1 Arb-Custom Gateway | [0xcEe2...180d](https://etherscan.io/address/0xcEe284F754E854890e311e3280b767F80797180d) | [0x2312...232f](https://etherscan.io/address/0x23122da8C581AA7E0d07A36Ff1f16F799650232f) | [0xba2F...40F3](https://sepolia.etherscan.io/address/0xba2F7B6eAe1F9d174199C5E4867b563E0eaC40F3) | | L1 Weth Gateway | [0xd920...e2db](https://etherscan.io/address/0xd92023E9d9911199a6711321D1277285e6d4e2db) | [0xE4E2...0BaE](https://etherscan.io/address/0xE4E2121b479017955Be0b175305B35f312330BaE) | [0xA8aD...0e1E](https://sepolia.etherscan.io/address/0xA8aD8d7e13cbf556eE75CB0324c13535d8100e1E) | | L1 Weth | [0xC02a...6Cc2](https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) | [0xC02a...6Cc2](https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) | [0x7b79...E7f9](https://sepolia.etherscan.io/address/0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9) | | L1 Proxy Admin | [0x9aD4...0aDa](https://etherscan.io/address/0x9aD46fac0Cf7f790E5be05A0F15223935A0c0aDa) | [0xa8f7...e560](https://etherscan.io/address/0xa8f7DdEd54a726eB873E98bFF2C95ABF2d03e560) | [0xDBFC...44b0](https://sepolia.etherscan.io/address/0xDBFC2FfB44A5D841aB42b0882711ed6e5A9244b0) | The following contracts are deployed on the corresponding L2 chain | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | --------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | L2 Gateway Router | [0x5288...F933](https://arbiscan.io/address/0x5288c571Fd7aD117beA99bF60FE0846C4E84F933) | [0x2190...DFa8](https://nova.arbiscan.io/address/0x21903d3F8176b1a0c17E953Cd896610Be9fFDFa8) | [0x9fDD...43C7](https://sepolia.arbiscan.io/address/0x9fDD1C4E4AA24EEc1d913FABea925594a20d43C7) | | L2 ERC20 Gateway | [0x09e9...1EEe](https://arbiscan.io/address/0x09e9222E96E7B4AE2a407B98d48e330053351EEe) | [0xcF9b...9257](https://nova.arbiscan.io/address/0xcF9bAb7e53DDe48A6DC4f286CB14e05298799257) | [0x6e24...b502](https://sepolia.arbiscan.io/address/0x6e244cD02BBB8a6dbd7F626f05B2ef82151Ab502) | | L2 Arb-Custom Gateway | [0x0967...5562](https://arbiscan.io/address/0x096760F208390250649E3e8763348E783AEF5562) | [0xbf54...51F4](https://nova.arbiscan.io/address/0xbf544970E6BD77b21C6492C281AB60d0770451F4) | [0x8Ca1...42C5](https://sepolia.arbiscan.io/address/0x8Ca1e1AC0f260BC4dA7Dd60aCA6CA66208E642C5) | | L2 Weth Gateway | [0x6c41...623B](https://arbiscan.io/address/0x6c411aD3E74De3E7Bd422b94A27770f5B86C623B) | [0x7626...D9eD](https://nova.arbiscan.io/address/0x7626841cB6113412F9c88D3ADC720C9FAC88D9eD) | [0xCFB1...556D](https://sepolia.arbiscan.io/address/0xCFB1f08A4852699a979909e22c30263ca249556D) | | L2 Weth | [0x82aF...Bab1](https://arbiscan.io/address/0x82aF49447D8a07e3bd95BD0d56f35241523fBab1) | [0x722E...5365](https://nova.arbiscan.io/address/0x722E8BdD2ce80A4422E880164f2079488e115365) | [0x980B...7c73](https://sepolia.arbiscan.io/address/0x980B62Da83eFf3D4576C647993b0c1D7faf17c73) | | L2 Proxy Admin | [0xd570...2a86](https://arbiscan.io/address/0xd570aCE65C43af47101fC6250FD6fC63D1c22a86) | [0xada7...d92C](https://nova.arbiscan.io/address/0xada790b026097BfB36a5ed696859b97a96CEd92C) | [0x715D...5FdF](https://sepolia.arbiscan.io/address/0x715D99480b77A8d9D603638e593a539E21345FdF) | ## Precompiles The following precompiles are deployed on every L2 chain and always have the same address | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | ---------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | ArbAddressTable | [0x0000...0066](https://arbiscan.io/address/0x0000000000000000000000000000000000000066) | [0x0000...0066](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000066) | [0x0000...0066](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000066) | | ArbAggregator | [0x0000...006D](https://arbiscan.io/address/0x000000000000000000000000000000000000006D) | [0x0000...006D](https://nova.arbiscan.io/address/0x000000000000000000000000000000000000006D) | [0x0000...006D](https://sepolia.arbiscan.io/address/0x000000000000000000000000000000000000006D) | | ArbFunctionTable | [0x0000...0068](https://arbiscan.io/address/0x0000000000000000000000000000000000000068) | [0x0000...0068](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000068) | [0x0000...0068](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000068) | | ArbGasInfo | [0x0000...006C](https://arbiscan.io/address/0x000000000000000000000000000000000000006C) | [0x0000...006C](https://nova.arbiscan.io/address/0x000000000000000000000000000000000000006C) | [0x0000...006C](https://sepolia.arbiscan.io/address/0x000000000000000000000000000000000000006C) | | ArbInfo | [0x0000...0065](https://arbiscan.io/address/0x0000000000000000000000000000000000000065) | [0x0000...0065](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000065) | [0x0000...0065](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000065) | | ArbOwner | [0x0000...0070](https://arbiscan.io/address/0x0000000000000000000000000000000000000070) | [0x0000...0070](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000070) | [0x0000...0070](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000070) | | ArbOwnerPublic | [0x0000...006b](https://arbiscan.io/address/0x000000000000000000000000000000000000006b) | [0x0000...006b](https://nova.arbiscan.io/address/0x000000000000000000000000000000000000006b) | [0x0000...006b](https://sepolia.arbiscan.io/address/0x000000000000000000000000000000000000006b) | | ArbRetryableTx | [0x0000...006E](https://arbiscan.io/address/0x000000000000000000000000000000000000006E) | [0x0000...006E](https://nova.arbiscan.io/address/0x000000000000000000000000000000000000006E) | [0x0000...006E](https://sepolia.arbiscan.io/address/0x000000000000000000000000000000000000006E) | | ArbStatistics | [0x0000...006F](https://arbiscan.io/address/0x000000000000000000000000000000000000006F) | [0x0000...006F](https://nova.arbiscan.io/address/0x000000000000000000000000000000000000006F) | [0x0000...006F](https://sepolia.arbiscan.io/address/0x000000000000000000000000000000000000006F) | | ArbSys | [0x0000...0064](https://arbiscan.io/address/0x0000000000000000000000000000000000000064) | [0x0000...0064](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000064) | [0x0000...0064](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000064) | | ArbWasm | [0x0000...0071](https://arbiscan.io/address/0x0000000000000000000000000000000000000071) | [0x0000...0071](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000071) | [0x0000...0071](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000071) | | ArbWasmCache | [0x0000...0072](https://arbiscan.io/address/0x0000000000000000000000000000000000000072) | [0x0000...0072](https://nova.arbiscan.io/address/0x0000000000000000000000000000000000000072) | [0x0000...0072](https://sepolia.arbiscan.io/address/0x0000000000000000000000000000000000000072) | | NodeInterface | [0x0000...00C8](https://arbiscan.io/address/0x00000000000000000000000000000000000000C8) | [0x0000...00C8](https://nova.arbiscan.io/address/0x00000000000000000000000000000000000000C8) | [0x0000...00C8](https://sepolia.arbiscan.io/address/0x00000000000000000000000000000000000000C8) | ## Misc The following contracts are deployed on the corresponding L2 chain | Function | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | --------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | L2 Multicall | [0x842e...4EB2](https://arbiscan.io/address/0x842eC2c7D803033Edf55E478F461FC547Bc54EB2) | [0x5e1e...cB86](https://nova.arbiscan.io/address/0x5e1eE626420A354BbC9a95FeA1BAd4492e3bcB86) | [0xA115...d092](https://sepolia.arbiscan.io/address/0xA115146782b7143fAdB3065D86eACB54c169d092) | | `ResourceConstraintManager` | [0x8F59...823a](https://arbiscan.io/address/0x8F59C7A53b883563B34cbBb6fF021B03973e823a) | [0x653e...86B7](https://nova.arbiscan.io/address/0x653e31e11769a9c6feE825E4BC822753DE2286B7) | | ## Canonical factory contracts The following factory contracts are deployed on the corresponding chain and are used to deploy new Arbitrum chains (`RollupCreator`) and their token bridges (`TokenBridgeCreator`). For factory contracts on additional chains (Ethereum, Base, and testnets) and deployment instructions, see [Canonical factory contracts](/launch-arbitrum-chain/deploy/canonical-factory-contracts.md). | | Arbitrum One | Arbitrum Nova | Arbitrum Sepolia | | -------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | `RollupCreator` | [0xB90e...eB8b](https://arbiscan.io/address/0xB90e53fd945Cd28Ec4728cBfB566981dD571eB8b) | [0xF916...60F4](https://nova.arbiscan.io/address/0xF916Bfe431B7A7AaE083273F5b862e00a15d60F4) | [0x5F45...16cF](https://sepolia.arbiscan.io/address/0x5F45675AC8DDF7d45713b2c7D191B287475C16cF) | | `TokenBridgeCreator` | [0x2f56...000e](https://arbiscan.io/address/0x2f5624dc8800dfA0A82AC03509Ef8bb8E7Ac000e) | [0x8B9D...8c14](https://nova.arbiscan.io/address/0x8B9D9490a68B1F16ac8A21DdAE5Fd7aB9d708c14) | [0x56C4...bD8E](https://sepolia.arbiscan.io/address/0x56C486D3786fA26cc61473C499A36Eb9CC1FbD8E) | ## Nova-specific tooling Effective January 31, 2026 (23:59 UTC), the following third-party tools will no longer be supported for Arbitrum Nova environments: * Alchemy * Nova Arbiscan ([nova.arbiscan.io](https://nova.arbiscan.io)) * Tenderly This change does not impact any other Arbitrum technology, such as Arbitrum One or other Arbitrum chains. A non-exhaustive list of alternatives to the outgoing tools has been compiled. While the capabilities may not be 1-to-1, suitable replacements are provided that cover the core competencies required. | Service | Alternate provider | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | RPC | - [Allnodes](https://arbitrum.publicnode.com/?nova)
- [Quicknode](https://www.quicknode.com/chains/nova)
- More alternatives can be found at [chainlist.org](https://chainlist.org/) | | Block Explorer | - [Blockscout](https://arbitrum-nova.blockscout.com/) | | Webhooks | - [Quicknode](https://www.quicknode.com/chains/nova) | Below, an FAQ can be found to address any further questions or concerns Nova builders and users may have. Additional questions can be submitted [here](https://docs.google.com/forms/d/e/1FAIpQLSfL1SlycJDyeRQJg-izdFZgN0eqLLAuzHkOr4TIEAZlcuBfSg/viewform). ### FAQ #### What exactly is changing on January 31, 2026? The following third-party tools will no longer be supprted for Arbitrum Nova: * Alchemy * Nova Arbiscan [nova.arbiscan.io](https://nova.arbiscan.io) * Tenderly These changes apply only to Nova-specific environments and do not affect Arbitrum One or other Arbitrum chains. #### Does this impact my funds or assets on Arbitrum Nova? User funds and onchain assets on Arbitrum Nova are not affected. All assets on Arbitrum Nova remain accessible and withdrawable regardless of the tooling change. #### Will existing Nova applications continue to function? Yes, continued building is supported, provided that a migration away from deprecated tooling is completed and infrastructure dependencies are updated as required. Reliance on the aforementioned services should be reviewed, and a transition to alternative providers should be completed before January 31, 2026. #### Who can I contact if there are issues or questions I need addressed? Please reach out with additional questions [here](https://docs.google.com/forms/d/e/1FAIpQLSfL1SlycJDyeRQJg-izdFZgN0eqLLAuzHkOr4TIEAZlcuBfSg/viewform). --- > For a complete page index, fetch # API3 [API3](https://api3.org/) is a collaborative project to deliver price feeds to smart contract platforms in a decentralized and trust-minimized way. The Price feeds provided by API3 allow apps to regain lost value with Oracle Extractable Value built in to the price feed allowing the ability to earn additional revenue for your app. ## Querying the price of **ARB** through API3 Here’s an example of how to use an API3 data feed to query the current price of **ARB** onchain. The [API3 market](https://market.api3.org/arbitrum) provides a list of all the dAPIs available across multiple chains including testnets. You can go forward and activate the dAPI you want to use. API3 provides an npm package with the contracts needed to access their feeds. We first install that package in our project: ```bash yarn add @api3/contracts ``` To use a data feed, we retrieve the information through the specific proxy address for that feed. We’ll use the IProxy interface to do so. ```solidity import "@api3/contracts/interfaces/IApi3ReaderProxy.sol"; ``` In this case, we want to obtain the current price of **ARB** in **USD** in Arbitrum One, so we need to know the proxy address that will provide that information. We will search the feed on the API3 Market and connect our wallet. We would then want to see if the feed is active, and if it is, we can check its configuration parameters, deploy the proxy contract and click on `Integrate.` You can find the proxy address of ARB/USD [here](https://market.api3.org/arbitrum?search=ARB%2FUSD). > **INFO** > > If a dAPI is already active, you can use the proxy address directly. If it is not active, you can activate it by clicking on **Activate** and following the instructions to deploy a proxy contract. We can now build the function to get the latest price of **ARB**. We’ll use this example contract: ```solidity contract ARBPriceConsumer { /** * Network: Arbitrum One * Aggregator: ARB/USD * Proxy: 0xE135626568cc83aE32De671adad0FB871B40aF8d */ address constant PROXY = 0xE135626568cc83aE32De671adad0FB871B40aF8d; /** * Returns the latest price. */ function getLatestPrice() external view returns (int224 value, uint256 timestamp) { (value, timestamp) = IProxy(PROXY).read(); // If you have any assumptions about `value` and `timestamp`, make sure // to validate them right after reading from the proxy. } } ``` You can adapt this contract to your needs. Just remember to use the address of the asset you want to request the price for in the appropriate network and to **deploy your contract to the same network**. Remember we have a [Quickstart](/build-decentralized-apps/quickstart-solidity-remix.md) available that goes through the process of compiling and deploying a contract. ## More examples Refer to [API3’s documentation](https://docs.api3.org/) for more examples of querying other data feeds and learn about Oracle Extractable Value. You can also check out some other detailed guides: * [Quickstart](https://docs.api3.org/dapps/quickstart/) * [Get Paid by Using API3 Oracles](https://docs.api3.org/dapps/oev-rewards/) --- > For a complete page index, fetch # Chainlink [Chainlink](https://chain.link/) is a widely-recognized Web3 services platform that specializes in decentralized oracle networks. It lets you build Ethereum and Arbitrum apps that connect to a variety of offchain data feeds and APIs, including those that provide asset prices, weather data, random number generation, and more. ## Querying the price of **ARB** through Chainlink Here’s an example on how to use a price feed from Chainlink to query the current price of **ARB** onchain. We’ll use an interface provided by Chainlink that can be configured with the address of the proxy that holds the information we want to request, and wrap the operation in a contract. Chainlink provides an npm package with the contracts needed to access their feeds. We first install that package in our project: ```tsx yarn add @chainlink/contracts ``` To use a data feed, we retrieve the information through the `AggregatorV3Interface` and the proxy address of the feed we want to query. ```solidity import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; ``` In this case, we want to obtain the current price of **ARB** in **USD** in Arbitrum One, so we need to know the address of the proxy that will provide that information. Chainlink maintains a list of price feed address [here](https://docs.chain.link/data-feeds/price-feeds/addresses?network=arbitrum). For `ARB/USD`, we’ll use the address `0xb2A824043730FE05F3DA2efaFa1CBbe83fa548D6`. We can now build the function to get the latest price of **ARB**. We’ll use this example contract: ```solidity contract ARBPriceConsumer { AggregatorV3Interface internal priceFeed; /** * Network: Arbitrum One * Aggregator: ARB/USD * Address: 0xb2A824043730FE05F3DA2efaFa1CBbe83fa548D6 */ address constant PROXY = 0xb2A824043730FE05F3DA2efaFa1CBbe83fa548D6; constructor() { priceFeed = AggregatorV3Interface(PROXY); } /** * Returns the latest price. */ function getLatestPrice() public view returns (int) { ( /* uint80 roundID */, int price, /*uint startedAt*/, /*uint timeStamp*/, /*uint80 answeredInRound*/ ) = priceFeed.latestRoundData(); return price; } } ``` You can adapt this contract to your needs. Just remember to use the address of the asset you want to request the price for in the appropriate network, and to **deploy your contract to the same network**. Remember we have a [Quickstart](/build-decentralized-apps/quickstart-solidity-remix.md) available that goes through the process of compiling and deploying a contract. ## More examples Refer to [Chainlink’s documentation](https://docs.chain.link/) for more examples of querying price feeds plus other data feeds available. --- > For a complete page index, fetch # Chronicle [Chronicle Protocol](https://chroniclelabs.org/) is a novel Oracle solution that overcomes the current limitations of transferring data onchain by developing scalable, cost-efficient, decentralized, and verifiable Oracles, rewriting the rulebook on data transparency and accessibility. ## Querying the price of **ARB** using Chronicle Chronicle contracts are read-protected by a whitelist, meaning you won't be able to read them onchain without your address being added to the whitelist. On the Testnet, users can add themselves to the whitelist through the `SelfKisser` contract; a process playfully referred to as "kissing" themselves. To access production Oracles on the Mainnet, please open a support ticket in [Discord](https://discord.com/invite/CjgvJ9EspJ) in the 🆘 | support channel. For the deployment addresses, please check out the [Dashboard](https://chroniclelabs.org/dashboard/oracles). ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.16; /** * @title OracleReader * @notice A simple contract to read from Chronicle oracles * @dev To see the full repository, visit https://github.com/chronicleprotocol/OracleReader-Example. * @dev Addresses in this contract are hardcoded for the Arbitrum Sepolia testnet. * For other supported networks, check the https://chroniclelabs.org/dashboard/oracles. */ contract OracleReader { /** * @notice The Chronicle oracle to read from. * Chronicle_ARB_USD_1 - 0xdD7c06561689c73f0A67F2179e273cCF45EFc964 * Network: Arbitrum Sepolia */ IChronicle public chronicle = IChronicle(address(0xdD7c06561689c73f0A67F2179e273cCF45EFc964)); /** * @notice The SelfKisser granting access to Chronicle oracles. * SelfKisser_1:0xc0fe3a070Bc98b4a45d735A52a1AFDd134E0283f * Network: Arbitrum Sepolia */ ISelfKisser public selfKisser = ISelfKisser(address(0xc0fe3a070Bc98b4a45d735A52a1AFDd134E0283f)); constructor() { // Note to add address(this) to chronicle oracle's whitelist. // This allows the contract to read from the chronicle oracle. selfKisser.selfKiss(address(chronicle)); } /** * @notice Function to read the latest data from the Chronicle oracle. * @return val The current value returned by the oracle. * @return age The timestamp of the last update from the oracle. */ function read() external view returns (uint256 val, uint256 age) { (val, age) = chronicle.readWithAge(); } } // Copied from [chronicle-std](https://github.com/chronicleprotocol/chronicle-std/blob/main/src/IChronicle.sol). interface IChronicle { /** * @notice Returns the oracle's current value. * @dev Reverts if no value set. * @return value The oracle's current value. */ function read() external view returns (uint256 value); /** * @notice Returns the oracle's current value and its age. * @dev Reverts if no value set. * @return value The oracle's current value using 18 decimals places. * @return age The value's age as a Unix Timestamp . * */ function readWithAge() external view returns (uint256 value, uint256 age); } // Copied from [self-kisser](https://github.com/chronicleprotocol/self-kisser/blob/main/src/ISelfKisser.sol). interface ISelfKisser { /// @notice Kisses caller on oracle `oracle`. function selfKiss(address oracle) external; } ``` ## More examples For more examples of integrating Chronicle Oracles, please check the [documentation portal](https://docs.chroniclelabs.org/). --- > For a complete page index, fetch # DIA [DIA](https://www.diadata.org/) is a cross-chain, trustless oracle network delivering verifiable price feeds for Arbitrum. DIA sources raw trade data directly from primary markets and computes it onchain, ensuring complete transparency and data integrity. ### Querying the price of **ARB** through DIA Call the getValue(string memory key) function on the oracle contract with the Query Symbol (e.g., "ARB/USD"). The function returns the asset price with 8 decimal precision and the timestamp of the last update. We'll use this oracle contract on the Arbitrum Sepolia testnet: `0x927ccBd9107bD63F8279Af83b96E7DAF924b9a7E`. Solidity Example: ```solidity pragma solidity ^0.8.13; interface IDIAOracleV2 { function getValue(string memory) external view returns (uint128, uint128); } contract DIAOracleSample { /** * @notice The DIA oracle to read from. * Oracle Address: "0x927ccBd9107bD63F8279Af83b96E7DAF924b9a7E" * Network: Arbitrum Sepolia */ address constant diaOracle = "0x927ccBd9107bD63F8279Af83b96E7DAF924b9a7E"; function getPrice(string memory key) external view returns ( uint128 latestPrice, uint128 timestampOfLatestPrice ) { (latestPrice, timestampOfLatestPrice) = IDIAOracleV2(diaOracle).getValue(key); } } ``` ### More examples Please refer to [DIA’s documentation](https://www.diadata.org/docs) for more examples on querying data feeds and learn about DIA's verifiable oracle stack. Additional resources: * [How DIA oracles work on Arbitrum](https://www.diadata.org/docs/guides/chain-specific-guide/arbitrum) * [How to request a custom oracle on Arbitrum](https://www.diadata.org/docs/guides/how-to-guides/request-a-custom-oracle) --- > For a complete page index, fetch # ORA [ORA](https://ora.io) is Ethereum's Trustless AI. As a verifiable oracle protocol, ORA brings AI and complex compute onchain. Its main product, **Onchain AI Oracle (OAO)**, integrates AI capabilities directly onchain. ORA breaks down the limitations of smart contracts by offering verifiable AI inference so that developers can innovate freely. ## OAO quickstart This quickstart is designed to help you build a smart contract on Arbitrum able to interact with OAO. You can find more details in [our docs Quickstart](https://docs.ora.io/doc/oao-onchain-ai-oracle/develop-guide). ### Workflow 1. The user contract sends the AI request to OAO on Arbitrum, by calling `requestCallback` function on the OAO contract. 2. Each AI request will initiate an opML inference. 3. OAO will emit a `requestCallback` event which will be collected by opML node. 4. opML node will run the AI inference, and then upload the result on Arbitrum, waiting for the challenge period. 1. During the challenge period, the opML validators will check the result and challenge it if the submitted result is incorrect. 2. If the submitted result is successfully challenged by one of the validators, the submitted result will be updated on Arbitrum. 3. After the challenge period, the submitted result onchain is finalized. 5. When the result is uploaded or updated on Arbitrum, the provided AI inference in opML will be dispatched to the user's smart contract via its specific callback function. ## Integration ### Overview To integrate with OAO, you will need to write your own contract. To build with AI models of OAO, we provided an example of contract using OAO: [Prompt](https://arbiscan.io/address/0xC20DeDbE8642b77EfDb4372915947c87b7a526bD). ### Smart contract integration 1. Inherit `AIOracleCallbackReceiver` in your contract and bind with a specific OAO address: ```jsx constructor(IAIOracle _aiOracle) AIOracleCallbackReceiver(_aiOracle) {} ``` 2. Write your callback function to handle the AI result from OAO. Note that only OAO can call this function: ```jsx function aiOracleCallback(uint256 requestId, bytes calldata output, bytes calldata callbackData) external override onlyAIOracleCallback() ``` 3. When you want to initiate an AI inference request, call OAO as follows: ```jsx aiOracle.requestCallback(modelId, input, address(this), gas_limit, callbackData); ``` ## Reference **4 models** are available on Arbitrum One: Stable Diffusion (ID: 50), Llama3 8B Instruct (ID: 11), OpenLM Score 7B (ID: 14) and OpenLM Chat 7B (ID: 15). [Prompt](https://docs.ora.io/doc/oao-onchain-ai-oracle/reference) and [SimplePrompt](https://docs.ora.io/doc/oao-onchain-ai-oracle/reference) are both example smart contracts interacted with OAO. For simpler application scenarios (e.g., Prompt Engineering based AI like GPTs), you can directly use Prompt or SimplePrompt. SimplePrompt saves gas by only emitting the event without storing historical data. **Arbitrum One**: * OAO Proxy: [0x0A0f4321214BB6C7811dD8a71cF587bdaF03f0A0](https://arbiscan.io/address/0x0A0f4321214BB6C7811dD8a71cF587bdaF03f0A0) * Prompt: [0xC20DeDbE8642b77EfDb4372915947c87b7a526bD](https://arbiscan.io/address/0xC20DeDbE8642b77EfDb4372915947c87b7a526bD) * SimplePrompt: [0xC3287BDEF03b925A7C7f54791EDADCD88e632CcD](https://arbiscan.io/address/0xC3287BDEF03b925A7C7f54791EDADCD88e632CcD) **Arbitrum Sepolia tesnet**: * OAO Proxy: [0x0A0f4321214BB6C7811dD8a71cF587bdaF03f0A0](https://sepolia.arbiscan.io/address/0x0A0f4321214BB6C7811dD8a71cF587bdaF03f0A0) * Prompt: [0xC3287BDEF03b925A7C7f54791EDADCD88e632CcD](https://sepolia.arbiscan.io/address/0xC3287BDEF03b925A7C7f54791EDADCD88e632CcD) * SimplePrompt: [0xBC24514E541d5CBAAC1DD155187A171a593e5CF6](https://sepolia.arbiscan.io/address/0xBC24514E541d5CBAAC1DD155187A171a593e5CF6) ## Useful links: * Read [ORA documentation](https://docs.ora.io) * [Join our Discord](https://discord.gg/ora-io) where our team can help you * Follow us [on X](https://x.com/OraProtocol) --- > For a complete page index, fetch # Oracles providers Learn how to run an Arbitrum node. ### [API3](/for-devs/oracles/api3/.md) [Learn out to use API3](/for-devs/oracles/api3/.md) ### [Chainlink](/for-devs/oracles/chainlink/.md) [Learn out to use Chainlink](/for-devs/oracles/chainlink/.md) ### [Chronicle](/for-devs/oracles/chronicle/.md) [Learn out to use Chronicle](/for-devs/oracles/chronicle/.md) ### [Ora](/for-devs/oracles/ora/.md) [Learn out to use Ora](/for-devs/oracles/ora/.md) ### [Pyth](/for-devs/oracles/pyth/) [Learn out to use Pyth](/for-devs/oracles/pyth/) ### [Quex](/for-devs/oracles/quex/) [Learn out to use Quex](/for-devs/oracles/quex/) ### [Supra price feed](/for-devs/oracles/supra/use-supras-price-feed-oracle) [Querying price feeds with Supra](/for-devs/oracles/supra/use-supras-price-feed-oracle) ### [Supra VRF](/for-devs/oracles/supra/use-supras-vrf) [Using Supra VRF](/for-devs/oracles/supra/use-supras-vrf) ### [Trellor](/for-devs/oracles/trellor/.md) [Learn out to use Trellor](/for-devs/oracles/trellor/.md) ### [DIA](/for-devs/oracles/dia/) [Learn out to use DIA](/for-devs/oracles/dia/) --- > For a complete page index, fetch # Supra, price feed oracle > **INFO** — Community member contribution > > Shoutout to [@ksdumont](https://github.com/ksdumont) for contributing the following [third-party document](/for-devs/third-party-docs/contribute.md)! Supra is a novel, high-throughput Oracle & IntraLayer: A vertically integrated toolkit of cross-chain solutions (data oracles, asset bridges, automation network, and more) that interlink all blockchains, public (parent and child chains) or private (enterprises). Integrating with Supra's price feeds is quick and easy. Supra currently supports several Solidity/EVM-based networks, like Arbitrum, and non-EVM networks like Sui, Aptos. To see all of the networks Supra is on, please visit [Supras' Networks](https://supraoracles.com/docs/price-feeds/networks)! To get started, you will want to visit [Supras' docs site](https://supraoracles.com/docs/price-feeds/) and review the documentation or continue to follow this guide for a quick start. ## Step 1: Create The S-Value interface Add the following code to the solidity smart contract that you wish to retrieve an S-Value. ```text interface ISupraSValueFeed { function getSvalue(uint64 _pairIndex) external view returns (bytes32, bool); function getSvalues(uint64[] memory _pairIndexes) external view returns (bytes32[] memory, bool[] memory); } ``` This creates the interface that you will later apply in order to fetch a price from SupraOracles. ## Step 2: Configure The S-Value feed address To fetch the S-Value from a SupraOracles smart contract, you must first find the S-Value feed address for the chain of your choice. For Arbitrum, the address is [0x8a358F391d93f7558D5F5E61BDf533e2cc3Cf7a3](https://arbiscan.io/address/0x8a358f391d93f7558d5f5e61bdf533e2cc3cf7a3) When you have the proper address, create an instance of the S-Value feed using the interface we previously defined for Arbitrum: ```text contract ISupraSValueFeedExample { ISupraSValueFeed internal sValueFeed; constructor() { sValueFeed = ISupraSValueFeed(0x8a358F391d93f7558D5F5E61BDf533e2cc3Cf7a3); } } ``` ## Step 3: Add unpack function to decode response for S-Value feed To decode S-value response from SupraOracles smart contract, you need to add the following code in your contract. ```text // Some codefunction unpack(bytes32 data) internal pure returns(uint256[4] memory) { uint256[4] memory info; info[0] = bytesToUint256(abi.encodePacked(data >> 192)); // round info[1] = bytesToUint256(abi.encodePacked(data << 64 >> 248)); // decimal info[2] = bytesToUint256(abi.encodePacked(data << 72 >> 192)); // timestamp info[3] = bytesToUint256(abi.encodePacked(data << 136 >> 160)); // price return info; } function bytesToUint256(bytes memory _bs) internal pure returns (uint256 value) { require(_bs.length == 32, "bytes length is not 32."); assembly { value := mload(add(_bs, 0x20)) } } ``` ## Step 4: Get the S-Value crypto price Now you can simply access the S-Value crypto price of our supported market pairs. In this step, we'll get the price of single or multiple trading pairs in our smart contract. ```text function getPrice(uint64 _priceIndex) external view returns (uint256[4] memory) { (bytes32 val,)= sValueFeed.getSvalue(_priceIndex); uint256[4] memory decoded = unpack(val); return decoded; } function getPriceForMultiplePair(uint64[] memory _pairIndexes) external view returns (uint256[4][] memory) { (bytes32[] memory val, ) = sValueFeed.getSvalues(_pairIndexes); uint256[4][] memory decodedArray = new uint256[4][](val.length); for(uint i=0; i< val.length; i++){ uint256[4] memory decoded = unpack(val[i]); decodedArray[i] = decoded; } return decodedArray; } ``` Tada! You now have a method in your smart contract that you can call at any time to retrieve the price of any crypto pair! ## Going further with Supra If you want to take the next step, consider registering for the [Supra Network Activate Program (SNAP)](https://join.supraoracles.com/network-activate-program). The Supra Network Activate Program (SNAP) offers companies discounted oracle credits, technical documentation, and customer support to embed much-needed oracles and VRF/RNG. SNAP supports Web3 scaling and growth to buffer costs which could typically inhibit a company’s success. The SNAP program is partnered with some of Web3's most prolific names who are helping with project selection and qualification. ## Connect with us! Still looking for answers? We got them! Check out all the ways you can reach us: * Visit us at [supraoracles.com](https://supraoracles.com) * Read our [Docs](https://supraoracles.com/docs/overview) * Chat with us on [Telegram](https://t.me/SupraOracles) * Follow us on [Twitter](https://twitter.com/SupraOracles) * Join our [Discord](https://discord.gg/supraoracles) * Check us out on [Youtube](https://www.youtube.com/SupraOfficial) --- > For a complete page index, fetch # Supra, VRF > **INFO** — Community member contribution > > Shoutout to [@ksdumont](https://github.com/ksdumont) for contributing the following [third-party document](/for-devs/third-party-docs/contribute.md)! Supra’s VRF can provide the exact properties required for a random number generator (RNG) to be fair with tamper-proof, unbiased, and cryptographically verifiable random numbers to be employed by smart contracts. Integrating with Supras' VRF is quick and easy. Supra currently supports several Solidity/EVM-based networks, like Arbitrum, and non-EVM networks like Sui, Aptos. To get started, you will want to visit [Supras' docs site](https://docs.supra.com/oracles/dvrf) and review the documentation or continue to follow this guide for a quick start. Latest version of Supra VRF requires a customer controlled wallet address to act as the main reference for access permissions and call back (response) transaction gas fee payments. Therefore, users planning to consume Supra VRF should get in touch with our team to get your wallet registered with Supra. Once your wallet is registered by the Supra team, you could use it to whitelist any number of VRF requester smart contracts and pre-pay/top up the deposit balance maintained with Supra in order to pay for the gas fees of callback(response) transactions. You will be interacting with two main contracts: * **Supra Deposit Contract:** To whitelist smart contracts under the registered wallet address, pre-pay/top up the callback gas fee deposit maintained with Supra. * **Supra Router Contract:** To request and receive random numbers. ## Step 1: Create the Supra router contract interface Add the following code to the requester contract i.e., the contract which uses VRF as a service. You can also add the code in a separate interface and inherit the interface in the requester contract. ```text interface ISupraRouterContract { function generateRequest(string memory _functionSig, uint8 _rngCount, uint256 _numConfirmations, uint256 _clientSeed, address _clientWalletAddress) external returns(uint256); function generateRequest(string memory _functionSig, uint8 _rngCount, uint256 _numConfirmations, address _clientWalletAddress) external returns(uint256); } ``` This interface will help the requester contract interact with the Supra router contract and through which the requester contract can use the VRF service. ## Step 2: Configure the Supra router contract address Contracts that need random numbers should utilize the Supra router contract. In order to do that, they need to create an interface and bind it to the onchain address of the Supra router contract. ```text contract ExampleContract { ISupraRouter internal supraRouter; constructor(address routerAddress) { supraRouter = ISupraRouter(0x7d86fbfc0701d0bf273fd550eb65be1002ed304e); } } ``` ## Step 3: Use the VRF service and request a random number In this step, we will use the `generateRequest` function of the Supra Router Contract to create a request for random numbers. There are two modes for the `generateRequest` function. The only difference between them is that you can optionally provide a client-side input, which will also be part of the payload being threshold signed to provide randomness. * `functionSig`: a string parameter, here the requester contract will have to pass the function signature which will receive the callback i.e., a random number from the Supra Router Contract. The function signature should be in the form of the function name following the parameters it accepts. We will see an example later in the document. * `rngCount`: an integer parameter, it is for the number of random numbers a particular requester wants to generate. Currently, we can generate a maximum of 255 random numbers per request. * `numConfirmations`: an integer parameter that specifies the number of block confirmations needed before supra VRF can generate the random number. * `clientSeed` (optional): an optional integer parameter that could be provided by the client (defaults to 0). This is for additional unpredictability. The source of the seed can be a UUID of 256 bits. This can also be from a centralized source. * `clientWalletAddress`: an “address” type parameter, which takes the client wallet address which is already registered with the Supra Team, as input. Supra's VRF process requires splitting the contract logic into two functions. * The request function: the signature of this function is up to the developer * The callback function: the signature must be of the form “uint256 nonce, uint256\[] calldata rngList” ```text function exampleRNG() external { //Function validation and logic // requesting 10 random numbers uint8 rngCount = 10; // we want to wait for 1 confirmation before the request is considered complete/final uint256 numConfirmations = 1; address _clientWalletAddress = //Add the whitelisted wallet address here uint256 generated_nonce = supraRouter.generateRequest(“exampleCallback(uint256,uint256[])”, rngCount, numConfirmations, _clientWalletAddress); // store generated_nonce if necessary (eg: in a hashmap) // this can be used to track parameters related to the request, such as user address, nft address etc in a lookup table // these can be accessed inside the callback since the response from supra will include the nonce } ``` ## Step 4: Add the validation in the callback function of requester contract Inside the callback function where the requester contract wants the random number (in this example the callback function is `exampleCallback`), the requester contract will have to add the validation such that only the Supra router contract can call the function. The validation is necessary to protect against malicious contracts/users executing the callback with fake data. For example, if the callback function is pickWinner in the requester contract, the snippet can be as follows. ```text function exampleCallback(uint256 _nonce ,uint256[] _rngList) external { require(msg.sender == address(SupraRouter)); // Following the required logic of the function } ``` ## Step 5: Whitelist your requester contract with Supra deposit contract and deposit funds It is important to note that your wallet address must be registered with Supra before this step. If that is completed, then you need to whitelist your requester smart contract under your wallet address and deposit funds to be paid for your call back transactions gas fees. The simplest way to interact with the deposit contract will be through Remix IDE. Go to Remix IDE & create a file with name `IDepositContract.sol` Paste the following code in the file: ```text interface IDepositUserContract { function depositFundClient() external payable; function addContractToWhitelist(address _contractAddress) external; function removeContractFromWhitelist(address _contractAddress) external; function setMinBalanceClient(uint256 _limit) external; function withdrawFundClient(uint256 _amount) external; function checkClientFund(address _clientAddress) external view returns (uint256); function checkEffectiveBalance(address _clientAddress) external view returns (uint256); function checkMinBalanceSupra() external view returns (uint256); function checkMinBalance(address _clientAddress) external view returns(uint256); function countTotalWhitelistedContractByClient(address _clientAddress) external view returns (uint256); function getSubscriptionInfoByClient(address _clientAddress) external view returns (uint256, uint256, bool); function isMinimumBalanceReached(address _clientAddress) external view returns (bool); function listAllWhitelistedContractByClient(address _clientAddress) external view returns (address[] memory); } ``` Navigate to the “Navigate & run Transactions” tab in remix, and paste the deposit contract address into the text box besides the “At Address” button & press the at address button. You will find the instance for the deposit contract created using which a user can interact and use the features provided by the deposit contract. Following functions will facilitate whitelisting your requester smart contracts and fund deposits. * `addContracttoWhitelist(address)`: The whitelisted users will have to whitelist their contract which they will be using to request for random numbers. The parameter this function takes is the User’s contract address. This function will be called after the user deploys the requester contract post development and makes it ready for interacting with the Supra Contracts. * `depositFundClient()`: is another mandatory function for a user to use once, before the user starts requesting from that contract. This is a function which will deposit funds in the deposit contract from the users for the response/callback transaction. The funds for a specific user should remain higher than the minimum amount set by the Supra( 0.1 **ETH** for Arbitrum testnet) for the new request transactions to be accepted. Basically the gist here is that the user will have to interact with the Deposit contract and add funds for their accounts, which will be utilized for the response transaction gas fee. There will be a script from Supra which will be monitoring the funds and will alert the user if a refill is required. ## Example implementation ```text // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface ISupraRouter { function generateRequest(string memory _functionSig , uint8 _rngCount, uint256 _numConfirmations, uint256 _clientSeed,address _clientWalletAddress) external returns(uint256); function generateRequest(string memory _functionSig , uint8 _rngCount, uint256 _numConfirmations,address _clientWalletAddress) external returns(uint256); } contract Interaction { address supraAddr; constructor(address supraSC) { supraAddr = supraSC; } mapping (uint256 => string ) result; mapping (string => uint256[] ) rngForUser; function getRNGForUser(uint8 rngCount, string memory username) external { uint256 nonce = ISupraRouter(supraAddr).generateRequest("myCallbackUsername(uint256,uint256[])", rngCount, 1, 123, msg.sender); //Can pass "msg.sender" when calling from the whitelisted wallet address result[nonce] = username; } function myCallbackUsername(uint256 nonce, uint256[] calldata rngList) external { require(msg.sender == supraAddr, "only supra router can call this function"); uint8 i = 0; uint256[] memory x = new uint256[](rngList.length); rngForUser[result[nonce]] = x; for(i=0; i For a complete page index, fetch # Trellor [Tellor](https://tellor.io/) is a decentralized oracle network that incentivizes an open, permissionless network of data reporting and validation, ensuring that any verifiable data can be brought onchain. It supports basic spot prices, sophisticated pricing specs (TWAP/VWAP), Snapshot Vote Results, and custom data needs. ## Querying the price of **ETH** through Tellor Here’s an example of how to use a Tellor data feed to query the current price of **ETH** onchain. The way it works is that a query is crafted asking for the price of one currency against another and sent to the oracle contract. If the information for that query is available, it will be returned. Oracle contracts can be found on the [Contracts Reference](https://docs.tellor.io/tellor/the-basics/contracts-reference) page. Tellor provides an npm package with the contracts needed to query the contract. We first install that package in our project: ```bash npm install usingtellor ``` Our function will just wrap the call to the Oracle contract with the query we are interested in. In this case, we want to obtain the “SpotPrice” of **ETH** against **USD**. We will request this information to the Arbitrum oracle contract `0xD9157453E2668B2fc45b7A803D3FEF3642430cC0`. We’ll use this example contract: ```solidity contract ARBPriceConsumer is UsingTellor { /** * Network: Arbitrum One * Aggregator: ARB/USD * Address: 0xD9157453E2668B2fc45b7A803D3FEF3642430cC0 */ constructor(address payable _tellorAddress) UsingTellor(_tellorAddress) {} /** * Returns the latest price. */ function getLatestPrice() public view returns (uint256) { bytes memory _queryData = abi.encode("SpotPrice", abi.encode("eth", "usd")); bytes32 _queryId = keccak256(_queryData); (bytes memory _value, uint256 _timestampRetrieved) = getDataBefore(_queryId, block.timestamp - 20 minutes); if (_timestampRetrieved == 0) return 0; require(block.timestamp - _timestampRetrieved < 24 hours); return abi.decode(_value, (uint256)); } } ``` You can adapt this contract to your needs. Just remember to use the ticker of the assets you want to request the price for and to **deploy your contract to the appropriate network, with the address of the Oracle contract in that network**. Remember, we have a [Quickstart](/build-decentralized-apps/quickstart-solidity-remix.md) available that goes through the process of compiling and deploying a contract. ## See also * [Tellor’s documentation](https://docs.tellor.io/) demonstrates how to query price feeds and other data feeds. --- > For a complete page index, fetch # Circle Paymaster quickstart Gas fees are often a barrier to entry for users interacting with apps that process transactions over blockchain networks. Typically, these fees are paid in the blockchain’s native token, like **ETH**, which adds complexity for users who primarily transact with **USDC**. Circle Paymaster simplifies this process by allowing users to pay for gas fees directly from their **USDC** balance. ## What is Circle Paymaster? [Circle Paymaster](https://hubs.li/Q034mZmH0) is a smart contract within the Account Abstraction (`ERC-4337`) framework that sponsors gas fees on behalf of users. ### How Circle Paymaster handles transactions with a bundler Paymaster is operated by Circle and integrates with bundlers such as Pimlico and Alchemy, which facilitate transaction bundling. A bundler is a service that collects user operations, combines them into a single transaction, and submits it to the blockchain network for execution, optimizing gas usage and efficiency. The following sequential steps outline how Paymaster processes a transaction, with references to relevant contract functions: 1. **USDC Balance Verification:** Paymaster uses the `balanceOf(address)` function of the **USDC** token contract to check whether the user’s balance is sufficient to cover the transaction and associated gas fees. 2. \***\*USDC** to **ETH** Conversion for Gas Calculation:\*\* The `fetchPrice()` function retrieves the real-time **USDC** to **ETH** conversion rate, ensuring accurate calculation of the gas fee equivalent in \*\*USDC\*\* and preventing overcharging. 3. **Transaction Authorization with Bundlers:** The `_validatePaymasterUserOp()` function processes an EIP-2612 permit, which authorizes Paymaster to deduct the required **USDC** amount from the user. The permit is signed offchain by the user and submitted with the transaction. 4. **Gas Payment Processing:** The `_postOp()` function debits the required **USDC** from the user’s balance and handles the exchange of **USDC** for **ETH** using the `swapForNative(uint256 amountIn, uint256 slippageBips, uint24 poolFee)` function. The bundler then processes the transaction on the blockchain. 5. **Transaction Finalization:** The bundler confirms the transaction completion and Paymaster ensures all balances and gas payment records are updated accordingly. Key functions involved in this process include: * `balanceOf(address)`: Retrieves the **USDC** balance of the user. * `getPrice(address token1, address token2)`: Fetches the exchange rate between **USDC** and **ETH** for accurate gas fee calculations. * `processTransaction(bytes calldata userOperation)`: Manages the transaction execution and gas fee deduction. The paymaster contract addresses for Circle Paymaster are as follows: * **Arbitrum Mainnet:** [0x6C973eBe80dCD8660841D4356bf15c32460271C9](https://arbiscan.io/address/0x6C973eBe80dCD8660841D4356bf15c32460271C9) * **Arbitrum Testnet:** [0x31BE08D380A21fc740883c0BC434FcFc88740b58](https://sepolia.arbiscan.io/address/0x31BE08D380A21fc740883c0BC434FcFc88740b58) These addresses are essential for configuring and interacting with Paymaster in your application. The paymaster interacts with the blockchain network to cover gas fees by leveraging offchain signatures that authorize the paymaster to spend a user's **USDC** balance. It calculates the required gas, converts it into an equivalent **USDC** value, and then deducts the amount from the user's balance while ensuring the transaction is processed onchain without needing the native token. ## Why use Circle Paymaster? 1. **Improved User Experience:** Users can interact with your app using only **USDC**, eliminating the need to acquire **ETH** for gas payments. 2. **EIP-2612 Permit Support:** Paymaster supports EIP-2612, allowing users to authorize gas payments through offchain signatures, reducing gas costs. 3. **Reliability from Circle:** Backed by Circle, the issuer of **USDC**, Paymaster offers trust and operational reliability. 4. **Deep Liquidity:** Paymaster is funded with deep native gas token liquidity, ensuring consistent transaction reliability. Here’s a step-by-step guide to building an app that uses Paymaster. You can fork and run the code directly from Circle's Replit [link](https://replit.com/@buildoncircle/Circle-Paymaster-Wallet?v=1)—feel free to check it out! ![image1](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnAAAAD0CAYAAAAbrRtEAAA5kklEQVR4Xu3dd5gU1Z7/8d/fIDkJIqgXAcWIAbOiclUEFTMq14SyZrkoAoIoSDZiQMWEXhHMJMUEIiYQ9V4DkqPEZcksadnz2/dxv7WnDz0wAzTTU/N5PU89VXUqV/dUfeac6u7/50RERESkRPl/cYGIiIiI5DcFOBEREZESRgFOREREpIQpcoD77//+b99t27ZNnTp16tTlSWfXZpE9Rff74usK8/dcqABnL+CWLVvcokWL3OLFi9WpU6dOXR51XJtXrVrltm7dutMLv8iO8P7hffTv//7vuucXY7ds2bId/j3vNMDZC8mK1q5dG08WEZE8snTpUrd582b/T7dIUXHP/6//+i+3ZMmSeJIUA/t7zhbidhjg7IUkCWZbWERE8g8X/U2bNinESZFxz/+P//iPuFiKEbWgvC6xHQY4/vg3bNigi4CISAlDDcqOml9EYrxXqO3hHwDJH2Qw/iGL/5YLDHDMyDNveiFFREoeruHUpGT7z10kG6u02bhxYzxJitn69eu3q0wrMMAxIwv853/+ZzxJRERKAGrh+EdcpDC4769bt04BLg/xGYT4n7EdBjg+0RQnPhERKRmWL1+etelFJBsFuPxVpACnBxlFREo2vgaCVhQFOCkMBbj8pQAnIlKKKMBJUSjA5S8FOBGRUkQBTopCAS5/KcCJiJQiCnBSFApw+UsBTkSkFFGAk6IobICLg0Qo/u5B5rUuxvb4lHS29+eOlgvtjQ9ack7CL8bml6nKlCnju6+//jqaOxN/g8z317/+1ZUrV86tWLHCl5cvX95VrlzZVahQwe2///7RUttTgBMRKUUU4KQoChvgqlatmgyvXr3ahwuCG0Hl+OOPd/vuu6976aWX/PR99tnHHXbYYe6QQw5xZcuWTd6LkyZNcgceeKA76aSTfLCZM2dOss5wuQYNGrgaNWpkTDN8Z90pp5wSF+9RBKyLL77Y3XTTTe7QQw/1Zc2aNfP5qDABrm7duq5Xr15++I8//nDdunWL5nB+PRzLjijAiYiUIgpwUhS7E+B+//1317p166Sc4IVatWolZYS8M888078fCS0hgk7IlscJJ5yQDI8cOdL94x//8MNhgJswYYJ7/PHH3ZQpU/z46NGj3bfffus+/PDDZNlp06a5l19+OalJY5kXXnjBf+etLRNjPwv6LsXCBLg333zT17Bx7AS5r776KpnGuWb/4nORjQKciEgpogAnRbE7AQ5t2rTxYYSQZvkhDHBgOtOOOeaYjPIY81H7Rb9Ro0a+jBo5ssmaNWtckyZNkgB3yy23+N9sx6mnnposz3EMGTLEdenSxb3xxhu+D2oC//Wvf7mZM2f68aOPPtqtXLkyWUfotdde8+s64IAD3GeffZYxrTAB7rvvvvO1ic8884yvhZw+fXoy7d133/XnLA6v2ez1AMeb4YMPPvAnLt7wnsQL8csvv2Qd54VmPBvKeYF5EefNmxdPLhBpnGVJ1HsCb8LXX3+9wJQvIrIrFOCkKAob4Hh2y8ydOzcJcOb7779PapWyBTjmj5/74kunQ2ENXIcOHXwt2fDhw93BBx/s13HssccmAY73+BFHHJEEPlifgHnrrbe6K6+80s2aNStZ5+233+7nsa6gnADuzewf84X3/cIEuOrVq/u/Q/B3yH7HaHL+6aef4uIMezXAWXt42O0J/DoED/+FeDjQ1k/aDrd33XXXFbhtyjlpjRs3dhdccEE82eONTFoPzZ8/3y9Le3Y2VCWTuAvL9lcXWRHZkxTgpCgKG+DCe+oNN9zgNm/e7Nq3b+/uueeepJwwhTDALViwwA0ePNgPh+uw58lCYYDjvs4+2X2SHEANngW4evXqJc+Q1a5d2/dtfRbgevbs6QMgeKaOyiVrSm3btq3/2blsODarXGGd4blhPA5wlF177bXJeM2aNX0zLfgAQ8uWLf1w+DfJMvEzgLG9GuAmT56c8eAhLya1XJx4DoKdJblj6dKlvvrSUMX41ltv+XJwEaIm7ZtvvvHVkRwsnwIxloxB9akFItCn2pKTNW7cOF8Far/vyrQ4wPFJk1GjRiX/Ufz222/bbS8McJTzJuB4ODY88cQTGcuwfxxT+AkW3hQjRoxIPs1i8y5atMiXh+eD5d97772MMt5sX375ZTIuIhJTgJOiKGyA415s99nwPn/QQQf5Mj6U8Omnn/oym49KDWqaDPd0KkeYFtboGVuOeWy5+vXr+zI+/ECzowW4v//978m8559/frI8LMCBpl/K27Vr58fZT8Zt/bZMqE6dOr7SqGLFiu6pp55Kym3/6B544IGM8jDAcU5tO3RkDLA+K2vRokUyf0H2aoCzhxStI6XjnHPO8ePWrkw3aNAg3yf4XH/99T5N86CiLUdVK8Mk1yuuuMIP84mQeFuEIvqENPqkZvo04fLC9unTJ6NGjn4Y4HjDUUa7NCf8qquucldfffV22wsDnB0DgdOWPe200/zwzTff7Pu8OW677TY/zItHn0DbtGlTd/fdd/tx+lS18gZ7/vnnfRnVvbTZU87DmZQtXLjQ92mPJ9UzHL+oIiJQgJOiKGyAK23y4fGmvRrgwEE/+uijviqUoMGnLSzAwYIZ6N9///1+eODAge7000/3ZZdffnnGfDyvZsMhyqi+pU8qJwhRRco4+0G4I6Vb6rVlwgBn2+zevbvfJiErrN0zcYAbO3Zssj72nY9Ph9tg3ayTYT6BQr93797J+mxeLrJ8uoYHNSn74osvfP/jjz/20+3Fo4xjsXVSSygiElOAk6JQgMtfezXAUa3JJ1EMDxhSvRoGOKomw6BDgOP7Vg4//HD/UWDKePCwMAHOqlBtGrVt4Th9whW1WmFZGOD4qDJltJNbV5gAZx+YsG3EAe7OO+9M1mf7TzOpsXkJnVRHT5061ZfRtk7/nXfe8dOplbRaRT4Kbeu0ByRFREIKcFIUCnD5a68GOJ7RImgQ3AhIFnR2FuAIbyeeeKJ75JFHfBmBjgBXpUoVPx/PmlFO02ZoxowZvpzAZxhnXTbMd7zQdh5uMwxwH330kS97//33fXs9gcoermzVqlWy3p0FOL73hWE+5kyf9n3a3BnmuT76Vqtmy1mfBzD/9re/+WFqLPv27etrAmnCDbfJg5GEOIbt2TsRkZACnBSFAlz+2qsBzvDRXD48UBTxd63EuCjxaZai4gMQhUG4Cj8qTBMlb+qi4Fk1w8XTvlxwZ3744Ye4yL8Wn3zyScZFmDJ7SFREJBsFOCkKBbj8VSwBTkREiocCnBSFAlz+UoATESlFFOCkKBTg8pcCnIhIKaIAJ0VBgONbHPiuVskv/BKFfZesUYATEUkpBTgpCt4nfFdptt8EleLDhx+pFVWAExEpJRTgpKj4AB+/XqBvN8gPNJ0S4PgKsfjvWAFORCSlFOCkqKjl4Yvv+XlHftqRZ+IoU7d3O5qyOf+Et2zNp8h5gOP74Ar60ffiwIXMfgtVSqbwN2GzjWfDf5RFxfvWfhMXO9uOXeh2hHXQ8d/tzuYV2V0KcLIreL9Q40OI4NpJFlC3dzvOO8GN16Gge0VOA1yHDh3csGHD/A+xM7wrOnfuHBftFm6c/HqBZGc/Z7a38b1+fJFyYdgPB3fs2NH3C3pv8V8kvyGLBx98MJpaMEIb6+Sn2Hr27OleeeUVX96tW7fMGSMvvvjiTv9mWO+rr77qv4SZn36zL4HOpQkTJsRFUkoowMmu4j1DcCAL0Kyqbu92nHfO/47+dnMW4Lhw8LNRxr7I9u233/Y3FKoGeVhyyJAh/hcSDL8+wA2T7RO2uOFNmzbNT+MH47Pd5KnqZT1ffvmlH//+++/9zZubJAkWPADIOJ+uCQMcJ4r1M40HN/myX27cht85ZdqaNWv8OL/4wC8thPPwA/b8fJbhlxoYZ70cIzjuoUOHZrwYrNO+tJhy+3UGjsV+aovzYB/ptuPjp7UIxvyKhGHf2U875/zsFr8oQXjmGJlmtUm8roQN+yJgjmXUqFH+C5c535MnT07Wa9OZf/r06X6cNxX7yOth4/yGaxgU2D9eS5tuOF72h/MxZswYvxwGDx7sfzfX8P6w33hlXvBacgycb5YlAIF9Zlv2m7SG5QhdvOYEOPYpfN04Zo4jxvrCj9F36tTJ91kf5479IoSB88z7lf0KAxzHyfZiYdjkPNg4rzfL81NvP/74o38P4ddff/VV6Gyb9z4/r8bPu/F62nuJ+cPXnv/YWBe/u4vHHnssOYe8v5nXXhPKGWdfJH0U4ETSK2cBjp/CyoYbFs2qhBer7SHkEE64KdovHoQ3ZxvnIkSVLrUioYceesj3WZ6aHG6ohELmt+WtzzxhgONGHc7Dcb/wwgu++pK+BbB4PQRSAgM/dWUXR36gnl9geO6555J5ubE/+eSTyTz33nuv7xuOi5sp5eH+2s2dIGpNd4MGDfJ9yq2d3NhyBArWYzWXhDGCUbjurl27+j4B7Pfff3f33Xef30/WFwYMEFht35kPNg/LEDAYZ57x48e7L774IhnnxsFxheGgf//+Geec13vSpEk+HMbhnHl4r9j6nn/++aQcYQ0c+05Y5XgM27cauC5duvg+NV6EON579p069l4z8bhhO7Nnz3a9evXy46yTsGTTLMAR+Aib4Tk3jPN+6Nevnz83c+fO9efEXsvu3bv7vr1PbHnrM92C+7PPPuvPG+cc9vrYeeFXPXgfW5B++OGHk18Iidcr6aQAJ5JeOQtwYU2HCW9o1ARwo+SmQ0foInwxnRtgHODo27x2ozJMY35uevPmzfPrsufcmEZIIFyBQBYHOGoobF68/vrrvvaPddh2bRo3XxBsqAWi1snm4QbJzdRqsKiBsea4gvaddXCjjY/XhsMAxw07nsd88803yXZgAY5zyjZgy3F+ucnTUZsU7lO8bnvNmJc+gcVCBnifhM2uhLWwqZFldhTgQK1StgBHUOcfAbb52muvJctYP25C5T0V/mRZGOCsCZXzweu/o9ckPgdWs0U5AY6aTcRBzwJcvG4LiojXDWrd7BzbOi3YWw2lLcd71d7bTzzxRPL68N6nzzj/HLE842zbAly4T3SEu2z7I+mhACeSXjkLcAQPqzECNVWwGwahxG783OC5eTONmxbLxoHGauCY1qNHD1+GqVOnJqGKAMWP3ccBLuxTQxUHOLvJ2zwW4GycbdpwHOCsnH0jcND0ZU2BTCPAEULsAprtmT7mo9nXhhHWwHGMsOCU7aZr54umSEJZQQGOAELtD3hNqJHaUYCzcBuGb+sThidOnJiME8JoJmbcauAYtuZwEDSyBTjOZ9iUDsKx1URxfE8//bQftmV3FuDYjtUExwGO2jMLlvFrwvvr888/98N2DLDzZ/+csJytg/20AMd+WZP7zsJhXGavI+GK9zM1zuE8cYDjtSQA2jz8/dg2ed9Qy2oBjvcn+w+rPcy2P5IeCnAi6ZWzAAduaNwg6AhVCG8YPEvGuN20eCaL8QEDBiTz0RzJfIQDyrhRclEK2TaopSFQ0EwaBzhCAsNMs2e3QAjhRhjOS60HgYNmXcqoveOmyM1x4MCBfh4LcAQm5uGGaMfB+hnmYXvWbzdKOqu9CXGjNgRAO07CG1gXXbyfIZp7KbfmPQslNJnZDdyWY92sj+efePYqDBmcwzB4U3vJcqzPgq4FVwtXBCfG7cMFNGMyTk1f2MTI/ITbOMBRe2ghOf60qIV1ptGcasPgGGz/wH5QmxlimzTtxgGO5ayWimfIYgR1pnFu7O+A8TDAEd5sHTxPx3OPNi/HTrk9t2jC4zb8w2DnuE+fPkl5OK8N8xrY83m8H+z46Ti3nCOCMOMWUu09Bf4JYNjeh9n2R9JDAU4kvXIa4Eojakyogfn2228LdXPkAwmFmU9KF/5psdpekV2lACeSXgpwOUCIo9ajMBdNaoTCZ6REYJ9CFdkdCnAi6aUAJyKSUgpwIumlACciklIKcCLppQAnIpJSCnAi6aUAJyKSUgpwIumlACciklIKcCLppQAnIpJSCnAi6aUAJyKSUgpwIumlACciklIKcCLppQAnIpJSCnAi6aUAJyKSUgpwIumlACciklIKcCLppQAnIpJSCnAi6aUAJyKSUgpwIumlACciklIKcCLppQAnIpJSCnAi6ZXTAMc6NmzYoK6Ed1u2bIlfWhEpARTgRNIrpwHOAsD69evdunXr3Nq1a0ttx/FzHuJwVFI6hTiRkkcBTiS9chbgNm3a5G/8BJc1a9boAvI/LMiW1CAnIiWLApxIeuUswHHDJ6isXr06nlSqbdy40Ye4OByVhI73hIiUHApwIumV0wBH06Ga3ra3cuXKElkLp9dSpGRRgBNJr5wGONW+Zbd8+XIFOBHJOQU4kfTKaYBbtWpVXCz/Y+nSpSWyGVUBTqRkUYATSa9iD3BVq1Z1559/vp+3TJkybsqUKfEsO8VyhcF81atXj4v3KGrWzjzzzLg4w5IlSwoMcIsXL3aTJk1y3bt3dxMnTvTD8TxF7Z5//nl/fuPyonYKcCIliwKcSHoVa4AjrFSqVCkZp2aqT58+7uyzz3Zdu3Z1Rx11lNu6daurVq2aO/LII/3wtm3b3OWXX+7D2FVXXeWXY7hRo0Y+YJx88sl+fO7cucl6zZNPPpkR9ghSlStXdlWqVPHjNPnut99+rnnz5v6Ct3nzZtegQQPXpEkTPxxq3769vziy3UsvvdTts88+vvy4447z2xg/fnzG/KEdBTjrOA+cf4b79evnhgwZ4st++OEH16FDB3fvvfe6FStWuM8//9z9+OOP7p577nH//Oc//fwTJkzw83zxxRd+Owz36NHDD/ft29eP29ebPPfcc65jx46FatJVgBMpWRTgRNKrWANcz5493aOPPhoXu/r167suXbr4gEFAAsMEo8cee8yNGDHClxH0YKGsYcOGbtasWX64QoUKvm8efvhh/9UmhCFqt8ByhBLC2ZgxY/w4QebBBx90zzzzjB8n1E2fPt3VqVMnY31XXnmlD6DMwwWSdROOdrcGzrowwN1///1+/xi/7777fBnb7t+/vxs1apSflzKCWdgfMGCA73PM9Hv37u1mz57thzt37uy/3oV5C1s7pwAnUrIowImkV7EGuFdeecVdcsklGWWEBAKcISCFHWGrdevWvlbOar0swMXzhqhla9asmTvrrLNcxYoVfU0etW2GC5zVxJny5csnw/H6wgBnqAXLVYAjwDI8evRo99BDD/ngxTwEOKt5s+BGrRrDw4YN8+MW4CgLOwJct27dttt2QZ0CnEjJogAnkl7FGuBQtmxZH2hwxhln+FqjMMBZyLKARWCy9do0C1E1a9Z0c+bM8cMEPPPRRx/5ZldzwQUX+D7LEeTw4osv+nGaaYcPH+5rAC0wLly4MNkGoQjU8GULcFwsaXLdkV0JcFZO7SH9cePGuV69evkA9/PPP/syaxq1Wjpq3NiOBThqFmlaZphmUwIcTavxtgvqFOBEShYFOJH0KvYAR0AiiBGEXn31VV8WBjhCg9WoEUZ4To5hwtzQoUN9jRcP+lvYql27th+2ZlbUq1fPzZs3LxlftmyZ69Spk/86D2rxLIQRfghmp5xyir/g0SzKtLp16yZfYss4z+1Rc0UQCwMcwQm1atXy0wtS1ADH84BWTpgjqE2ePNn3aVoNAxz9zz77zA+/9NJLfpymVvocH8GUaTw/xzjN2PG2C+oU4ERKFgU4kfQq9gBXGhUmwOVjpwAnUrIowImklwJcMdD3wInI3qAAJ5JeOQ9w8ddvyJ9NuIX52o586xTgREoWBTiR9MppgOMheZ4zk0ycVwU4Eck1BTiR9MppgKOZkIfluYjIn/jkKsE2DkclodNNQKRkUYATSa+cBThw0yfE0ZTKc198Hcf8+fNLXbdgwQIf3LiYEt5KYu2bmsJFSh4FOJH0ymmAYx3c/AlxfGUFv2pQWjuCG+dB4U1E9hYFOJH0ymmAM3w5Lt+ppq5kdQQ3XfhFSi4FOJH02isBTkRE9j4FOJH0UoATEUkpBTiR9FKAExFJKQU4kfRSgBMRSSkFOJH0UoATEUkpBTiR9FKAExFJKQU4kfRSgBMRSSkFOJH0UoATEUkpBTiR9FKAExFJKQU4kfRSgBMRSSkFOJH0UoATEUkpBTiR9FKAExFJKQU4kfRSgBMRSSkFOJH0ylmA44JRpkyZpPvyyy/jWYqkXr16yfC2bdtc2bJlk3HWv379ej981FFH+YtWrFq1ahn9GMvFuPCx7mzi8q1bt7rRo0dnlImIFCcFOJH0ylmAmzZtWsZFo0KFCr6/YcMGt3LlSrdlyxa3adMmN2fOnGQepv3yyy/JcuvWrXNLly714axmzZp+GWMBbsyYMW7EiBGuRYsWftyCFeuaOnWqPw7EAY7pc+fO9WHQlmN7oQYNGriffvrJLVmyJClbtWqVn8+2w/Jsh/UR4DZv3uw7C5HMT2dYNj7mmTNnJuMiInuKApxIeuUswKFKlSo+6Nx6663+IkIAsyBH+YwZM/x29ttvP9epUyc3YcKEZJr1V69e7Ydr1ar150r/V//+/d3YsWPdvvvu68eZl/B35513uscee8yNHDkyKUcY4ObPn+/ee+89P05I27hx43Y1aqhUqZLvs38YOnSoPw4CWriPYHsEuD59+rhzzjknCXlcOJn/3/7t39ybb77pnn32WT//BRdc4L777jt32223+fEmTZr4vojInqIAJ5JeOQ1w4MLx9NNP+zBDgDv66KN9uQU5EMKoZbvmmmtc5cqVtwtHiAMctXgXX3xxMs8RRxzhOnfu7EMcbrzxRj8tW4AbNGhQMo1u3Lhx2wW4G264wdWtW9c1b97cT1u+fLk7+eSTk+kVK1b0/Xbt2vk+58sCHMES4TbKly/vm1k5bmoPJ0+e7JepXbu2n27LiIjsKQpwIumVswDXtm1bX/NkqI0jwDVu3NiPxwGuRo0aSdOihaly5col88QBDsxHOMSkSZOSkEZtFk2fNg/CADd+/HjfdIkhQ4b4JtI4wIXbfuONN9wVV1zhunTpkpTZ/Icccojv0xxsAe6LL77ImAePPPKIr6VbtmxZ8nwg+zhx4sTt5hUR2RMU4ETSK2cBDhdddJEPJnwAgefACHDHHHOMnxYHuN9++83PW716df+8G8IQRTMmNXShVq1aZYREmi5BbRnratq0qd8ex8F6YUGOmkDm6datmx8fMGBAEqJo6rzyyiv9sGEatYT0aVq1faTZlzLm//DDD13fvn2TpuAVK1b4aXT2/B61eozzjCCo1WN8ypQpf25IRGQPUYATSa+cBjgRESk+CnAi6aUAJyKSUgpwIumlACciklIKcCLppQAnIpJSCnAi6aUAJyKSUgpwIumlACciklIKcCLppQAnIpJSCnAi6aUAJyKSUgpwIumV8wDHF9jyhbaLFi1Sp06dOnWF6Ph1mDVr1sSX0yJTgBNJr5wGOIKb/WSViIgUHqGLILc7FOBE0itnAS78iSsREdk11MjtKgU4kfTKWYBbvHhxXCQiIkVE+Nq0aVNcXCgKcCLplbMAt7tV/yIi8iceR9kVCnAi6aUAJyKS55YvXx4XFYoCnEh6KcCJiOQ5BTgRiSnAiYjkOQU4EYkpwImI5DkFOBGJlZoAt23btrgoqz15oSvsNveGVatW+Qu52Z1929myK1eujIu8bMtlKysOhX3dt27d6o+Pv4/QnvjSVZGCKMCJSCwvAlyZMmVclSpV3IgRI+JJCeYpW7asO+ecc5KymjVruooVK/rOPPbYY65cuXLugAMO8OPz5s1zFSpUcA0aNHCHHnqoL+MmzDzhssy3zz77uMaNG7t77rnHlxF6mjRp4o4//vg/V/6/2BfKd+Syyy5zxx13nJ+Xiy9faMxwvL933XWXH2ea6devX8a44Rji74Rivl9++SWjLNa/f3930003uQ8//NCP16tXzx199NGuTp06GfOxrvXr12eUPfnkk65Ro0bJOPOwH5w/NG/e3J9fjmHZsmXJPDHK2C59OwaGDz74YF8ez7tx48aMstA111zjatWq5Q488EA3cODApJzlKlWq5N5++20/zvHF59swr4VH3nucD46J9wG/HhK+Vu+//36yXPny5d3dd9+93etw0kkn+X79+vWT5ew88F6pVq2aH+/Vq5cv+/rrr/32wv246KKL3F/+8peM88f55TiznVMpPRTgRCRW7AGud+/erkuXLj6E2E3q008/TaZ/9tlnvm/Tunbt6m677TY/TIAL8V1JFiyy2XfffX2fAPfCCy9kTDvssMPclClT/LBtizA4derU7QIcN/zwhrpw4UL33HPPuV9//TUps+k///yzvzET4Owmb7i41q1bN6MM+++/v583rMl68803XefOnTOCw8cff+zOPffcJMBxkSZsvPfee8k8nEv2l/6sWbP8PLZvHTt2TMIOofGDDz5wLVq0SJYlWBAoLMARAGfMmJFMBwEjfq1ZP98DGAYf89133/l1ho499tjk3PHacN55nQ3reuKJJ9y0adOSMmPH8sgjj/jAzTkj6IMAl+3G1aFDB1e7du0kOB1zzDG+P2fOHB8MCXCcsxjnkKBHf+3atb6M12DUqFHbvbY444wzfD8M+7a/BDy+7Jr3F4E0fF2GDx/u7r33Xj9s/4gQDNu2bfvnSqTUUYATkVixBzgLVQQobmDc1L766itfe3Xeeee5d955x0+3m9u6detc1apV/TABjlozOowdO9YvR0CwmzgIUSeccIK7/vrr/TghgZoU1mk3bwJW9erV3ZlnnpkRgBAGuGHDhrk+ffr4mpShQ4f6MgsWL7/8suvZs6cfJuw0a9bMBwHWbTVw7Cu1RHjmmWd8eGnYsKGvoTHUuHCchAkQLKjRI2BagOPCfNZZZ7lWrVolAY5j4nXjgh0GzP322y8ZDoPCnXfemZwTlkW4HDVrsAB3xRVX+H0n/Bx11FG+jABntUsWahh+5ZVXfPCKa78uuOACN2bMmIwy5rcmSWrkrAwPPfSQu//++/0wwc7mu/LKK92RRx6Z1NpyLmw5W5YAV6NGjYxjGj16tF8PIdkC3B133OGaNm3qj42mUKuBY5xu9uzZyfK8P8whhxzi/vWvf/l9CrcBAj3rAQGO9xbzEM7AMNN5r1qgtXXwPiLY81p16tTJ799PP/203Tak9FCAE5FYXgQ4ghehym5qOOWUU3xth7GbFyHBQp8hEHBj/eijj3zzHrhgEbbAjZqmwWy1c9RsEK6uvvpq16NHD7d69eokuJgwwLEfrHvBggXJ/l144YVJk5nVDhKICAPdu3ffruakW7duvk+Asxovwgi1W+zLU0895cvsmNlvthkGOCsLA5yFlzDEIAxwOOigg3y4ISC3a9cuI9RxPnDdddf54AsLcIRIa9rMFiasLJwWDj/77LMZTeCgefef//xnMm7zs3/cfAhJ4TH9+OOPfjpBn9fKAuLZZ5/tgzCvdbZ9430RHmcY4AhX1N6xL9RoFlQDhzDAheE0bGZGuA9WAxfuG317H1uAu/TSS33gZBtW83b77bcn/2RkOy4pHRTgRCRW7AHu5JNPTmqf7AbFxYaaKmrRLNDZtG+++caddtppfy78v3j2iho2asJOPfXUpJzAECKczJ07N6OMmyO1XXHosO0iDnA33nij78KbMah1u+WWWzLKOIfxM14jR470fZrieDYNLVu29M2Lr776arL+ypUr+9eBsBZ2sGHOEX0u0nbTj8UBzlC7NXjwYF/L89e//tVv8+KLL3ZLly71Yci2wbHwLJzVhCFbmAjPRxz0xo8f78NYiOBt4cTwurMfvMbUZvJMYvhaxGz9hGRqvcKyEDVaBL7wmOhzfq0WEvbaFybAhfNYrSp4/1KLauImVM4v/YkTJ/oyao1D/EPTunVrP8x8rI8wbTXNUvoowIlIrNgDHAgh1D4QHkBwAbVtYdMetTfUetjFiJuyPfBvnwKkeZXgZqGFJitqyB5//PHkxj558mT3wAMP+Fo3q21jnJqQt956KwmU1PJcddVVvoaGPsLnt+xmzv6/8cYbfjkLcPahDI6FWjieN7v55pt9aAoDBsfzj3/8IyP8mM8//9wHUxPWwJmwBo6mTZpFX3/99eS8IQ5w1MBR82bnmeXs/FETSu1byGqXeMaQmkKCkjVRE1xokuQDIoRrcAxs48QTT0ye5aKMJljr4jICSps2bfy+w5qBeQ3o06xttZvsAx80IVBajSGhi31g3+15NI7r6aef9q8LNWyhsAaO9VNbS/MtHyqxJlTbN15bEwY43lu8N3gPhk32bM9qCkGAI5RSY2uhj9DIM3Ksz5515HzxoYzwPcD8AwYM2K5WWEoXBTgRieVFgBMRkYIpwIlITAFORCTPKcCJSEwBTkQkzynAiUhMAU5EJM8pwIlITAFORCTPKcCJSEwBTkQkzynAiUhMAU5EJM8RxHaFApxIeuUswMXfVyYiIkXHdzPaT8gVlQKcSHrlLMDZl6SKiMiu251/hhXgRNIrZwEONKPu6n+OIiKl3eLFi+OiIlGAE0mvnAY48JuYBDn+i1SnTp06dTvvCG78xvPuUoATSa+cBzgRESkeCnAi6aUAJyKSUgpwIumlACciklIKcCLppQAnIpJSCnAi6aUAJyKSUgpwIumlACciklIKcCLppQAnIpJSCnAi6aUAJyKSUgpwIumlACciklIKcCLppQAnIpJSCnAi6aUAJyKSUgpwIumlACciklIKcCLppQAnIpJSCnAi6ZXTAPfDDz+4xo0bu/r167vu3bvHk/eYxYsXu9NPPz0u9o455piMcS5oAwcOzCgTEUkjBTiR9MpZgFu/fr27/fbbk/FVq1a5Xr16BXNkt2LFiozxrVu3ZoxnU6ZMGXfttdfGxR7TzLJly9ySJUtc165dkzK7sHGRC1G+bdu2jPGNGzcGc4iI5DcFOJH0ylmA69evX1zkKlSo4Fq1auXmzZvnWrdu7fbdd19fTsjq0aOHq1y5snv33XeT0EW/T58+7sADD3S//fabD3fVq1d3559/frJOAh7rWb58uXvttdd82VtvveXq1q3rLrvssmRdVatWdV999ZUfJ8Cxztq1a7sLL7zQHXfccW7MmDHurLPOcv379/frZ7sM16tXz/fbtm3rBg8enNOaRBGRPUkBTiS9chbgWrRoERe58uXLuzlz5rhrrrnGBykCHQhSBDhCFJhGMNtvv/3c6NGj3YgRI9yhhx66Xe0caKL9+uuv3bRp01y1atV82RFHHOG2bNnih1kXNWkEOsycOTMJcKtXr07mYTt0DN90002uYcOG7tNPP/UXvhdffNHVqVPHjRw50s8vIlISKMCJpFfOAhxNpl26dEnG//jjj6TpkpDUvn17d8stt/jaLi4wBLjvvvsumc72CXBg+pAhQ7IGuLCJtGnTpu7jjz92xx9/fNIkatMrVark++PHj08CnLF5uMg99dRT7uWXX/bb5xiY9vbbb/smYbpweyIi+UwBTiS9chbgQC0WTZBHH310RvChhuvnn3/2FxYrjwMcaN6kho2m1enTp28X4Khle+mll5LxqVOnuho1ari1a9f6dbB9W1ebNm1cy5YtfXNrHOAeeeQRd/bZZ/taOsIbIY7pzZs3dwcddJAbO3asq1Wrlq9VvO+++5LlRETymQKcSHrlNMCB9WzatCkuLjSW35WLT7Zl4g8qhAiD4YcW2K41w4JpmzdvTsZFRPKdApxIeuU8wImISPFQgBNJLwU4EZGUUoATSS8FOBGRlFKAE0kvBTgRkZRSgBNJLwU4EZGUUoATSS8FOBGRlFKAE0kvBTgRkZRSgBNJr70S4PhZLH4IXp06derU7bzjuzP3ROhSgBNJr5wGOJbn56dERKRoCF1LliyJi4tEAU4kvXIW4LhgUPMmIiK7bndCnAKcSHrlLMAtXrw4LhIRkSLiJ/x2NYApwImkV84C3O781ygiIv9nxYoVcVGhKMCJpJcCnIhInlu+fHlcVCgKcCLppQAnIpLnFOBEJKYAJyKS5xTgRCSmACcikucU4EQkpgAnIpLnFOBEJJYXAe6ZZ55xZ599tv+4/M6ce+65yfCll17qx8OyoUOHun322ce1b98+KbN56AYPHpyUx+bMmZOxrqI477zz/Dkzd911lytTpowbMGBAUsaXGl9zzTVu5cqVfpwLa8uWLf189rUrGzZscDVr1nQHHnhgcg5t31l2y5YtvoxtnXbaae6QQw7xyxj24/XXX0/GMWjQIDd79uxknIv55Zdf7tauXevH6Z9++ul+P959911f9sknn7hHHnkkWQZff/11xrkE2z7iiCN8Z66//vpkHm4gYH979+7t98XccsstfpusNzR37tztXod4/Mknn/Svs33XYLhvbD/EsRbGwoUL3dVXXx0XF+iSSy5JzmGI46xSpUrG9yAOHz7c3XDDDcl48+bNk/299tprk/LYiy++6M/RQw89FE+SUkQBTkRixR7g5s2b5w499FD3+++/u+rVq8eTM8yYMcPfzAxBJ8T0/fff3w/feOONSXm4zI6wTN26dePiQiFM3Hnnncn4zz//7Psnnniie+WVV/zP47Du/fbbzy1atMhPmzRpkps1a5Yftn0kiHCxZX4rsz7hzYYPPvhgN2rUKL8uK9u2bZurXLlyxjEw7Y477nA//PBDRlm5cuWScEVos2E7p9kC3AcffJDsu6lQoYIPXIQVAg0IlfENg/n+9re/uXbt2iVln332me8TdsLvDeQYwteMsHb44Ye77777zo9/9dVXrlGjRn64Tp06vv/+++9n/dUPlqlfv35cnNWeCHAff/yxO+qoo/xr0aBBA1/297//3XXs2NG/9objK8wXXbMuEPi+//77aKqUFgpwIhIr9gBngYEAxE2NC02zZs38jXDChAnuzDPP9NO5CHETjwMcQcaC0zvvvOO6du3qh1nPyJEj/TDL9OjRw3eGsrfeesvXVtlNmLLp06e7X3/91Y9zg2W/vvzyS19ThjPOOMPv23PPPeeDJ7755hsfTJg3RHioUaNGsj4ceeSR24UgZAuZYYB74oknXOfOnV2rVq18GeGKGixqL6l1w7Bhw3w5tXdr1qxJ1rNgwYKMAIeqVasmoS3EsiDAsd3PP//clS1b1pcR4Dp16uT69OnjO4T7bcMEOILHddddl3HjYF1hgDOcI3uvjRkzxi8TrpftE84sEJ188sn+hkbNms1HgOP14L1jQY51nnLKKe7hhx9O1nXxxRf7QNulS5ekVo/XjX0jYFmAo0by5ptv9jW606ZN82Xsx8SJE31AXrZsmQ9wlSpVcuPHj0/2g31k/1u0aLHdaxoHOPaL8/jqq6/6ssMOO8y/fldeeaXv8/6zfSfgxuuT0kMBTkRixR7guHlzw6U2ihvpunXrfDk3K26Ohpsf+xTexGbOnOkvTISF7t27+wsV06lRItw99dRTfj7KCDAWYliGJs7Q/PnzfTMuaJoFN9GXXnopnM0vS20K89i+UAtEjQv7aM2j4OZbvnz5jItvtgD37bff+qAQOuGEE5IaKtt/Am3t2rV9GbVv1DZy0yfY2Xzsx9NPP50cOwob4AhPFpIIcG3btvXDRx99tFu6dKkPcB9++KGvCbLaoLPOOsvPx/m380FtKmgatMCJbAGOIETTsKFmENmCofVprmV/OHYrs/cqNZ/U9qFWrVq+WT4McMzPa2evH68ngQ7jxo1LAlwcllgvry/L0XRNkyYBzpp/+/fv7/uEO36InH884nXEAY7XnfM4derUJLTavlltdMWKFd17773nX694fVJ6KMCJSKzYAxw3NUIOwhsUN3ILcIQSphGUrB8iADZu3NgP80zWL7/84m/cv/32my+Lb3zUbPTs2TOj7JxzzvEBkpu/zX/33Xf72rYQ0zgvFhatzJYLn70Dz55RK2TiAEdwjPfviiuucL169UrGw+nVqlXbrizeD86dnVMUJsARIsIaRAIcQRXUVNE8na0JFTQ9Eqji40BYFgc4gk44/fnnn/fHZ69zvXr13D333JNxftG0adOMY45ZGeeBddFEa+8ZpvHaWcf73Gptf/rpJx/geL+F/zyA4Erot+V4fxHgOC8gNHOTpBawoH2LA1zYhMr6qOEL9w28t3kfx+dKShcFOBGJFXuAAzeuNm3aJE1aPAMFmgHDIILwJnbQQQf5pkzKVq1a5cu4uT/66KMZ82W78XFz5wFxbrw0oYbz8NwaQcWCI7UsDRs29NMYp6nWbtQ0w9qzXzYd1ADy7Bvjo0eP9mU8Y0eYoOmPGz/rYTpNjXSgSfekk07KKGMeaokuvPDC5NxQw0No5YF91okwPFrT9K233uoDIU3A/fr182X2AYvWrVv7mroHH3xwu/0gwPG6UAtowY4Ad9lll2XMR7M0Hww54IADkqZoQpgdO6EINPfy+nIe7fnEcJsWto2dR/p286EZkdfMwgxNxjRpg9pTzh01uvbcmAlr4NgPmkf5UEG4DcIlx2k1cDS18joQKq1GkfcMz9+x/9QY87oTLKkZtWZme3aRZlHOlWEZatPCY+eZQI79tttu82U0DVNrSa0v4RU8+9a3b99kX6V0UoATkVheBDjsygWK5lZupKHVq1cnoWFn+AAFtUc7wzNu4SdMuYnvDPPzvFQuL5wca1HPc1HxTODOTJ48Ofl0LKhN4rnBXB47aIIMEbSt5mpHqPmKj2vKlCkZ4+A4eI+EeL/FH1zItmy2ssJg/+fOnZuM8zdIjbKUbrtyfYQCnEh65U2AExGR7BTgRCSmACcikucU4EQkpgAnIpLnFOBEJKYAJyKS5xTgRCSmACcikucU4EQkpgAnIpLn4i/dLiwFOJH0ylmAC3/bUkREdg3hiy8o3xUKcCLplbMAt6sXHBER+T/8ysmuUoATSa+cBTjwO5ciIrJr+NJofnlkVynAiaRXTgMcuHjQnMpPU6lTp06dusJ1e+I5YgU4kfTKeYATEZHioQAnkl4KcCIiKaUAJ5JeCnAiIimlACeSXgpwIiIppQAnkl4KcCIiKaUAJ5JeCnAiIimlACeSXgpwIiIppQAnkl4KcCIiKaUAJ5JeCnAiIimlACeSXjkPcPwmqn6JQZ06deqK1nHd3F0KcCLpldMAxwWI9YiISNHt7s9pKcCJpFfOApyCm4jI7qM2blcpwImkV84C3O5cdERE5E9ci7du3RoXF4oCnEh65SzA7W7Vv4iI/IkgtisU4ETSSwFORCTPLV++PC4qFAU4kfRSgBMRyXMKcCISU4ATEclzCnAiElOAExHJcwpwIhJTgBMRyXMKcCISK/YAd+KJJ2Z0N954YzxLkZ100kmuUaNGcfFeccYZZ8RF3rp169yWLVvi4gyc77vuuisu3utuueUWd/DBB8fF3rfffusGDx7sFi5cGE9yHTp08NNEZM9SgBORWLEHOHPxxRdnjIcXrAULFgRT/hSGoW3btvkO8+fPdzfffHMyLb7wFeZCxrHH861fvz4Z3rhxYzAlc/+qVKmSDM+bNy8ZPuGEE/zPiplwGHPnznVLly51LVu2zChHvL0Y+7pp06aM8TVr1gRz/HmOQvH2w/nLlCkTTMn08ccfu4cfftjNmjUrnuQuu+wy17t377hYRHZTfB0rLAU4kfTKywBHgDj99NN9oKlZs6YbO3asK1u2rL8IMe2+++7zNV2dOnVy9evXd08//bQvY77nn3/eNWvWzIeqcuXKuQkTJriqVau62bNnuzp16rgaNWq4s846K9lW+/bt3SGHHOLXR/B78cUXXdeuXd1tt93mlwPbHjJkiNtnn31cu3bt3Mknn+y3TYipXLmy+/nnn/08sABXqVIlN2XKFN8nLLGfw4cP91/IyX698cYbSVBq2LCh30/WFQe4ChUquEGDBiXz9u/f3/cnTZrkHn/8cXfFFVe4iy66yHXr1s29+uqrrk+fPr4G8sknn3QPPPCA++GHH1y9evXcvffe60499VS/LOt67bXXknUSLj/44AM/vnbtWt/nXB577LE+PBL+DjjgAD9vGOD+8pe/+DJel/Lly/t9GzFihC8TkT1HAU5EYnkZ4AglZtWqVe6TTz5x1apV8+GnVq1avpwauMMPP9w1bdrUHXXUUb5pD9TAEbJAsPv888/dsGHD3CWXXOIDXIyAGF7cwtonG77jjjt8v0mTJr5PsLzgggt8iKHmDAQmEOBWrlzpGjRo4Lc9dOhQd/nllyc1cKeddpobPXq0n0agImjadvjt2DjAMY3jt29ijwNcXFuWbZxt0dk0wuaHH37oNm/enMzzzjvvJDV9Nt/OAhzHPmPGDNe9e3c3fvx4v42ZM2f6+URkz1GAE5FYXga4fv36+T41SH379vXDFuAsSOCwww5zL7/8sh8mVDVv3jwjwL377ru+z0Xso48+yhrg9t13Xx9QON42bdr47RgLMh07dvT9bAFu6tSpvoxlQYCjuZVaL3AOCTYW4Fhu2rRpftqbb77pmy5tOwSiMMARXt977z3//BzzsJ9dunTx0zjuOMDx/CDjdrFu1apVshwGDhzoa9io/aPJlWnsK8+zcfyMc45tnccdd5zfZ24A2QIcCKo2/67+3I+I7JgCnIjE8jrAERZq167tjjnmGFe9enVfGxQHuM6dO/sQ0aJFCx92wgBHsKB5kWbMOXPmZA1w48aN882qPLRPrRTNnoQtap8efPBBP8+OApxtg6ZOWBPqoYce6i699FI/nYsoYYqaQlB29tln+2PDo48+6qdTMxbXwFHGubHjZtlzzz3XHXnkkT7AvfDCC74JmGZhAiBNsfvvv78777zz3MSJE/3rULFiRX8Ozz//fL8OmoLZN8IrmM7za9YMbIGMplv2kXNfUIA78MADfQ0c7rzzTn2IQSQHFOBEJJY3Aa4gO/vkJqj5KWg+apAKc/EKH/Lf0fpChBgCY/gBhxA1Z6FwnVxUQ/G8ofhDDOEHFsC+W3MoON54mXA6wu0zf0HHwPtgRwjP8bpFZM9SgBORWN4HuHxmAa604utGrrrqqrhYRPYwBTgRiSnAiYjkOQU4EYkpwImI5DkFOBGJKcCJiOQ5BTgRiSnAiYjkOQU4EYkpwImI5LkVK1bERYWiACeSXjkLcPyqgIiI7B7C165+VY8CnEh65SzAFeZ71EREZMcWLVoUFxWaApxIeuUswIHfBN3RF9SKiEh2hK7dfRRFAU4kvXIa4GDr4b9IderUqVO3847gVtCvoxSFApxIeuU8wImISPFQgBNJLwU4EZGUUoATSS8FOBGRlFKAE0kvBTgRkZRSgBNJLwU4EZGUUoATSS8FOBGRlFKAE0kvBTgRkZRSgBNJLwU4EZGUUoATSa8dBjh+SWHr1q3xJBERKQGWLVvmNm7cqAAnkkIFBrht27b5n8FasWJFPElEREoAftFh8+bNcbGIpECBAY7/2DZt2rTbv8UnIiJ736pVq3xHa4qIpE+BAQ40n65Zs8Z3IiJSMnDtXrx4sX/+jdYUEUmfHQY4auG2bNnim1H5gWU9RyEikt/Wr1/vli5dqvAmknI7DHCwplQ+0PDHH3/4JtUNGzb45yrUqVOnTl1+dPaPtoU3mk71T7dIeu00wIH/4qiJ4z87vlqETzYR5NSpU6dOXX50fGUIj7vwqVOu2QpvIulWqAAHLgZcFHi2gv/2qJVTp06dOnX50fFPtmrdREqPQgc4EREREckPCnAiIiIiJYwCnIiIiEgJowAnIiIiUsIowImIiIiUMP8fqxw0Blqxt7AAAAAASUVORK5CYII=) ### Step 1: Setup your development environment * **Install Node.js and npm:** Download from [nodejs.org](https://nodejs.org) and [npmjs.com](https://npmjs.com). ### Step 2: Create a new Next.js project ```shell npx create-next-app@latest circle-paymaster-wallet --typescript --tailwind --eslint cd circle-paymaster-wallet npx shadcn@latest init -d ``` ### Step 3: Install required dependencies ```shell npm install @radix-ui/react-label @radix-ui/react-slot @radix-ui/react-tabs @tanstack/query-core @tanstack/react-query class-variance-authority clsx lucide-react next permissionless react react-dom tailwind-merge tailwindcss-animate viem ``` ### Step 4: Configure blockchain interaction * **Setup a Smart Contract Interaction Service:** lib/transfer-service.ts ```javascript import { createPublicClient, http, getContract, encodeFunctionData, encodePacked, parseAbi, parseErc6492Signature, formatUnits, hexToBigInt } from 'viem' import { createBundlerClient } from 'viem/account-abstraction' import { arbitrumSepolia } from 'viem/chains' import { toEcdsaKernelSmartAccount } from 'permissionless/accounts' import { privateKeyToAccount } from 'viem/accounts' import { eip2612Permit, tokenAbi } from './permit-helpers' const ARBITRUM_SEPOLIA_USDC = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d' const ARBITRUM_SEPOLIA_PAYMASTER = '0x31BE08D380A21fc740883c0BC434FcFc88740b58' const ARBITRUM_SEPOLIA_BUNDLER = `https://public.pimlico.io/v2/${arbitrumSepolia.id}/rpc` const MAX_GAS_USDC = 1000000n // 1 USDC export async function transferUSDC( privateKey: `0x${string}`, recipientAddress: string, amount: bigint ) { // Create clients const client = createPublicClient({ chain: arbitrumSepolia, transport: http() }) const bundlerClient = createBundlerClient({ client, transport: http(ARBITRUM_SEPOLIA_BUNDLER) }) // Create accounts const owner = privateKeyToAccount(privateKey) const account = await toEcdsaKernelSmartAccount({ client, owners: [owner], version: '0.3.1' }) // Setup USDC contract const usdc = getContract({ client, address: ARBITRUM_SEPOLIA_USDC, abi: tokenAbi, }) // Verify USDC balance first const balance = await usdc.read.balanceOf([account.address]) if (balance < amount) { throw new Error(`Insufficient USDC balance. Have: ${formatUnits(balance, 6)}, Need: ${formatUnits(amount, 6)}`) } // Construct and sign permit const permitData = await eip2612Permit({ token: usdc, chain: arbitrumSepolia, ownerAddress: account.address, spenderAddress: ARBITRUM_SEPOLIA_PAYMASTER, value: MAX_GAS_USDC }) const signData = { ...permitData, primaryType: 'Permit' as const } const wrappedPermitSignature = await account.signTypedData(signData) const { signature: permitSignature } = parseErc6492Signature(wrappedPermitSignature) // Prepare transfer call const calls = [{ to: usdc.address, abi: usdc.abi, functionName: 'transfer', args: [recipientAddress, amount] }] // Specify the USDC Token Paymaster const paymaster = ARBITRUM_SEPOLIA_PAYMASTER const paymasterData = encodePacked( ['uint8', 'address', 'uint256', 'bytes'], [ 0, // Reserved for future use usdc.address, // Token address MAX_GAS_USDC, // Max spendable gas in USDC permitSignature // EIP-2612 permit signature ] ) // Get additional gas charge from paymaster const additionalGasCharge = hexToBigInt( ( await client.call({ to: paymaster, data: encodeFunctionData({ abi: parseAbi(['function additionalGasCharge() returns (uint256)']), functionName: 'additionalGasCharge' }) }) ?? { data: '0x0' } ).data ) // Get current gas prices const { standard: fees } = await bundlerClient.request({ method: 'pimlico_getUserOperationGasPrice' as any }) as { standard: { maxFeePerGas: `0x${string}`, maxPriorityFeePerGas: `0x${string}` } } const maxFeePerGas = hexToBigInt(fees.maxFeePerGas) const maxPriorityFeePerGas = hexToBigInt(fees.maxPriorityFeePerGas) // Estimate gas limits const { callGasLimit, preVerificationGas, verificationGasLimit, paymasterPostOpGasLimit, paymasterVerificationGasLimit } = await bundlerClient.estimateUserOperationGas({ account, calls, paymaster, paymasterData, paymasterPostOpGasLimit: additionalGasCharge, maxFeePerGas: 1n, maxPriorityFeePerGas: 1n }) // Send user operation const userOpHash = await bundlerClient.sendUserOperation({ account, calls, callGasLimit, preVerificationGas, verificationGasLimit, paymaster, paymasterData, paymasterVerificationGasLimit, paymasterPostOpGasLimit: BigInt(Math.max( Number(paymasterPostOpGasLimit), Number(additionalGasCharge) )), maxFeePerGas, maxPriorityFeePerGas }) // Wait for receipt const userOpReceipt = await bundlerClient.waitForUserOperationReceipt({ hash: userOpHash }) return userOpReceipt } ``` * **Setup Permit Helper for EIP-2612 Integration:** lib/permit-helpers.ts ```javascript import { Address, Chain, TypedDataDomain, getContract } from 'viem' export const eip2612Abi = [ { constant: false, inputs: [ { name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, { name: 'v', type: 'uint8' }, { name: 'r', type: 'bytes32' }, { name: 's', type: 'bytes32' }, ], name: 'permit', outputs: [], payable: false, stateMutability: 'nonpayable', type: 'function', }, ] as const export const tokenAbi = [ ...eip2612Abi, { inputs: [{ name: 'owner', type: 'address' }], name: 'nonces', outputs: [{ name: '', type: 'uint256' }], stateMutability: 'view', type: 'function', }, { inputs: [], name: 'name', outputs: [{ name: '', type: 'string' }], stateMutability: 'view', type: 'function', }, { inputs: [], name: 'version', outputs: [{ name: '', type: 'string' }], stateMutability: 'view', type: 'function', }, { inputs: [ { name: 'recipient', type: 'address' }, { name: 'amount', type: 'uint256' } ], name: 'transfer', outputs: [{ name: '', type: 'bool' }], stateMutability: 'nonpayable', type: 'function', }, { inputs: [{ name: 'account', type: 'address' }], name: 'balanceOf', outputs: [{ name: '', type: 'uint256' }], stateMutability: 'view', type: 'function', } ] as const export async function eip2612Permit({ token, chain, ownerAddress, spenderAddress, value, }: { token: ReturnType chain: Chain ownerAddress: Address spenderAddress: Address value: bigint }) { const [nonce, name, version] = await Promise.all([ token.read.nonces([ownerAddress]), token.read.name(), token.read.version(), ]) const domain: TypedDataDomain = { name, version, chainId: chain.id, verifyingContract: token.address, } const types = { Permit: [ { name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, ], } const message = { owner: ownerAddress, spender: spenderAddress, value, nonce, deadline: BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'), } return { domain, types, message, } } ``` ### Step 5: Build the frontend * **Update Main Component:** app/page.tsx ```javascript 'use client' import { useState, useEffect } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Alert, AlertDescription } from "@/components/ui/alert" import { Loader2 } from 'lucide-react' import { createPublicClient, http, formatUnits } from 'viem' import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import { arbitrumSepolia } from 'viem/chains' import { toEcdsaKernelSmartAccount } from 'permissionless/accounts' import { tokenAbi } from '@/lib/permit-helpers' import { transferUSDC } from '@/lib/transfer-service' const ARBITRUM_SEPOLIA_USDC = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d' export default function SmartWallet() { const [loading, setLoading] = useState(false) const [account, setAccount] = useState(null) const [recipientAddress, setRecipientAddress] = useState('') const [amount, setAmount] = useState('') const [status, setStatus] = useState('') const [usdcBalance, setUsdcBalance] = useState('0.00') useEffect(() => { const fetchBalance = async () => { if (!account?.address) return const client = createPublicClient({ chain: arbitrumSepolia, transport: http() }) const balance = await client.readContract({ address: ARBITRUM_SEPOLIA_USDC, abi: [{ inputs: [{ name: 'account', type: 'address' }], name: 'balanceOf', outputs: [{ name: '', type: 'uint256' }], stateMutability: 'view', type: 'function' }], functionName: 'balanceOf', args: [account.address] }) const formattedBalance = Number(formatUnits(balance as bigint, 6)).toFixed(2) setUsdcBalance(formattedBalance) } fetchBalance() // Set up polling interval const interval = setInterval(fetchBalance, 10000) // Poll every 10 seconds return () => clearInterval(interval) }, [account?.address]) const createAccount = async () => { try { setLoading(true) setStatus('Creating smart account...') // Create RPC client const client = createPublicClient({ chain: arbitrumSepolia, transport: http() }) // Generate private key and create owner account const privateKey = generatePrivateKey() const owner = privateKeyToAccount(privateKey) // Create smart account const smartAccount = await toEcdsaKernelSmartAccount({ client, owners: [owner], version: '0.3.1' }) setAccount({ address: smartAccount.address, owner: owner.address, privateKey: `0x${privateKey.slice(2)}` }) setStatus('Smart account created successfully!') } catch (error) { setStatus('Error creating smart account: ' + (error as Error).message) } finally { setLoading(false) } } const transfer = async () => { try { setLoading(true) setStatus('Checking balance...') // Create client for balance check const client = createPublicClient({ chain: arbitrumSepolia, transport: http() }) // Check balance before transfer const balance = await client.readContract({ address: ARBITRUM_SEPOLIA_USDC, abi: tokenAbi, functionName: 'balanceOf', args: [account.address] }) as bigint // Convert input amount to USDC decimals (6 decimals) const amountInWei = BigInt(Math.floor(parseFloat(amount) \* 1_000_000)) // Required gas buffer (2 USDC to be safe) const gasBuffer = BigInt(2_000_000) // 2 USDC in wei const totalNeeded = amountInWei + gasBuffer // Check if balance is sufficient including gas buffer if (balance < totalNeeded) { const currentBalance = Number(formatUnits(balance, 6)) const requestedAmount = Number(amount) const availableForTransfer = Math.max(0, currentBalance - 2) // Leave 2 USDC for gas throw new Error( `Insufficient balance for this transfer. ` + `\nCurrent balance: ${currentBalance} USDC` + `\nRequested transfer: ${requestedAmount} USDC` + `\nGas buffer needed: 2 USDC` + `\nMaximum you can transfer: ${availableForTransfer.toFixed(2)} USDC` + `\n\nPlease reduce your transfer amount or get more USDC from the faucet.` ) } setStatus('Initiating transfer...') const receipt = await transferUSDC( account.privateKey, recipientAddress, amountInWei ) if (receipt.success) { setStatus('Transfer completed successfully!') setRecipientAddress('') setAmount('') } else { setStatus('Transfer failed. Please try again.') } } catch (error: any) { // Check for specific error signatures if (error.message.includes('0x65c8fd4d')) { setStatus('Error: Insufficient USDC balance for transfer and gas fees (need ~2 USDC for gas)') } else { setStatus(error.message) } } finally { setLoading(false) } } return ( <> {account && (
USDC Balance: ${usdcBalance}
)}
Smart Wallet Interface Create and manage your smart account with Circle's USDC Paymaster Create Account Transfer {!account ? ( ) : (
{account.address}
{account.owner}
)}
setRecipientAddress(e.target.value)} />
setAmount(e.target.value)} />
{status && ( {status} )}
) } ``` * **Update Root Layout for Application:** **File:** `app/layout.tsx` ```javascript 'use client'; import './globals.css'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` * **Setup UI Components:**
Create reusable components in `components/ui/` (e.g., `button.tsx`, `card.tsx`, `input.tsx`). ```shell mkdir -p components/ui ``` components/ui/button.tsx ```javascript import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2", sm: "h-8 rounded-md px-3 text-xs", lg: "h-10 rounded-md px-8", icon: "h-9 w-9", }, }, defaultVariants: { variant: "default", size: "default", }, } ) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button" return ( ) } ) Button.displayName = "Button" export {(Button, buttonVariants)} ``` components/ui/card.tsx ```javascript import * as React from "react" import { cn } from "@/lib/utils" const Card = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) Card.displayName = "Card" const CardHeader = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) CardHeader.displayName = "CardHeader" const CardTitle = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) CardTitle.displayName = "CardTitle" const CardDescription = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) CardDescription.displayName = "CardDescription" const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) CardContent.displayName = "CardContent" const CardFooter = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) CardFooter.displayName = "CardFooter" export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } ``` components/ui/input.tsx ```javascript import * as React from "react" import { cn } from "@/lib/utils" const Input = React.forwardRef>( ({ className, type, ...props }, ref) => { return ( ) } ) Input.displayName = "Input" export {Input} ``` components/ui/label.tsx ```javascript "use client" import * as React from "react" import * as LabelPrimitive from "@radix-ui/react-label" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const labelVariants = cva( "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" ) const Label = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & VariantProps >(({ className, ...props }, ref) => ( )) Label.displayName = LabelPrimitive.Root.displayName export { Label } ``` components/ui/tabs.tsx ```javascript "use client" import * as React from "react" import * as TabsPrimitive from "@radix-ui/react-tabs" import { cn } from "@/lib/utils" const Tabs = TabsPrimitive.Root const TabsList = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef > (({ className, ...props }, ref) => ( )) TabsList.displayName = TabsPrimitive.List.displayName const TabsTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) TabsTrigger.displayName = TabsPrimitive.Trigger.displayName const TabsContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) TabsContent.displayName = TabsPrimitive.Content.displayName export { Tabs, TabsList, TabsTrigger, TabsContent } ``` components/ui/alert.tsx ```javascript import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const alertVariants = cva( "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", { variants: { variant: { default: "bg-background text-foreground", destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", }, }, defaultVariants: { variant: "default", }, } ) const Alert = React.forwardRef< HTMLDivElement, React.HTMLAttributes & VariantProps > (({ className, variant, ...props }, ref) => (
)) Alert.displayName = "Alert" const AlertTitle = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) AlertTitle.displayName = "AlertTitle" const AlertDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) AlertDescription.displayName = "AlertDescription" export { Alert, AlertTitle, AlertDescription } ``` ### Step 6: Start development server ```shell npm run dev ``` Once the development server is running, visit **** in your browser. Follow these steps to test the application: 1. Click on the **Create Smart Account** button to generate a smart wallet address. 2. Deposit testnet **USDC** into the smart wallet address. You can source testnet **USDC** from . 3. Navigate to the **Transfer** tab. 4. Input the recipient address and the amount of **USDC** to transfer. 5. Click on the **Transfer USDC** button to initiate the transfer. This process demonstrates the functionality of the smart wallet and gas fee management using Circle Paymaster. --- > For a complete page index, fetch # USDC quick start guide **USDC** provides the ability to transfer dollars over the Arbitrum network using a smart contract. The smart contract enables users to send, receive, and store dollars onchain with a wallet. This guide will walk you through using the [viem](https://viem.sh/) framework to build a simple app that enables a user to connect their wallet and interact with the Arbitrum network by sending a **USDC** transaction from their address. ## Prerequisites Before you start building the sample app to perform a **USDC** transfer, ensure you meet the following prerequisites: 1. **Node.js and npm**: Ensure that you have Node.js and npm installed on your machine. You can download and install Node.js from [nodejs.org](https://nodejs.org). npm comes with Node.js. 2. [**MetaMask**](https://metamask.io/): Install the MetaMask browser extension and set up your wallet. Ensure that your wallet is funded with: * Some native gas tokens (e.g., **ETH** on the Sepolia network) to cover transaction fees. * **USDC** tokens for the transfer. ([**USDC** Testnet Faucet](https://faucet.circle.com/)) 3. **Project Setup**: Create a new project directory and initialize it with npm: ```shell mkdir usdc-transfer-app cd usdc-transfer-app npm init -y ``` 4. **Dependencies**: Install the required dependencies using the following command: ```shell npm install react@^18.2.0 react-dom@^18.2.0 @types/react@^18.0.27 @types/react-dom@^18.0.10 @vitejs/plugin-react@^3.1.0 typescript@^5.0.3 vite@^4.4.5 ``` This will set up your development environment with the necessary libraries and tools for building a React application with TypeScript and Vite. ## Installation To install viem run the following command. ```shell npm i viem ``` ## Setup public client The public client is used to interact with your desired blockchain network. ```javascript import { http, createPublicClient } from 'viem'; import { arbitrumSepolia } from 'viem/chains'; const publicClient = createPublicClient({ chain: arbitrumSepolia, transport: http(), }); ``` ## Setup wallet client The wallet client is used to interact with Arbitrum accounts to retrieve accounts, execute transactions, and sign messages. ```javascript import { createWalletClient } from 'viem'; import { arbitrumSepolia } from 'viem/chains'; const walletClient = createWalletClient({ chain: arbitrumSepolia, transport: custom(window.ethereum!), }); ``` ## Define **USDC** contract details Define the **USDC** contract address and ABI (Application Binary Interface). The ABI specifies the functions available in the contract. (The **USDC** Token Contract Address referenced in the code is on Ethereum Sepolia) ```javascript const USDC_CONTRACT_ADDRESS = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d'; const USDC_ABI = [ { constant: false, inputs: [ { name: '_to', type: 'address' }, { name: '_value', type: 'uint256' }, ], name: 'transfer', outputs: [{ name: '', type: 'bool' }], type: 'function', }, ]; ``` ## Connect wallet Create a function to connect the user's wallet and retrieve their account address. ```javascript const connect = async () => { const [address] = await walletClient.requestAddresses(); setAccount(address); }; ``` ## Send transaction Create a function to send the **USDC** transfer transaction. This function encodes the transfer function data and sends the transaction using the wallet client. ```javascript const data = encodeFunctionData({ abi: USDC_ABI, functionName: 'transfer', args: [to, valueInWei], }); const hash = await walletClient.sendTransaction({ account, to: USDC_CONTRACT_ADDRESS, data, }); ``` ## Wait for transaction receipt Use the public client to wait for the transaction receipt, which confirms that the transaction has been mined. ```javascript useEffect(() => { (async () => { if (hash) { const receipt = await publicClient.waitForTransactionReceipt({ hash }); setReceipt(receipt); } })(); }, [hash]); ``` ## Final step: build your **USDC** transfer sample app Now that you understand the core components for programmatically performing your first **USDC** transaction, create the following **index.tsx** and **index.html** files to build a sample app. This app will enable you to send **USDC** from one wallet to another. Ensure that your wallet is funded with both the native gas token and **USDC**. index.tsx ```javascript import React, { useEffect, useState } from 'react'; import ReactDOM from 'react-dom/client'; import { http, type Address, type Hash, type TransactionReceipt, createPublicClient, createWalletClient, custom, stringify, encodeFunctionData, } from 'viem'; import { arbitrumSepolia } from 'viem/chains'; import 'viem/window'; const publicClient = createPublicClient({ chain: arbitrumSepolia, transport: http() }); const walletClient = createWalletClient({ chain: arbitrumSepolia, transport: custom(window.ethereum!) }); const USDC_CONTRACT_ADDRESS = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d'; const USDC_ABI = [ { constant: false, inputs: [ { name: '_to', type: 'address' }, { name: '_value', type: 'uint256' }, ], name: 'transfer', outputs: [{ name: '', type: 'bool' }], type: 'function', }, ]; function Example() { const [account, setAccount] = useState
(); const [hash, setHash] = useState(); const [receipt, setReceipt] = useState(); const addressInput = React.createRef(); const valueInput = React.createRef(); const connect = async () => { const [address] = await walletClient.requestAddresses(); setAccount(address); }; const sendTransaction = async () => { if (!account) return; const to = addressInput.current!.value as Address; const value = valueInput.current!.value as `${number}`; const valueInWei = BigInt(value) * BigInt(10 ** 6); // Assuming USDC has 6 decimals const data = encodeFunctionData({ abi: USDC_ABI, functionName: 'transfer', args: [to, valueInWei], }); const hash = await walletClient.sendTransaction({ account, to: USDC_CONTRACT_ADDRESS, data, }); setHash(hash); }; useEffect(() => { (async () => { if (hash) { const receipt = await publicClient.waitForTransactionReceipt({ hash }); setReceipt(receipt); } })(); }, [hash]); if (account) { return ( <>
Connected: {account}
{receipt && (
Receipt:
{stringify(receipt, null, 2)}
)} ); } return ; } ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ); ``` index.html ```html USDC Transfer Sample App

USDC Transfer Sample App

``` By combining these **index.tsx** and **index.html** files, you will have a complete setup that allows you to perform a **USDC** transfer from your wallet. Simply connect your wallet, input the recipient's address and the amount of **USDC** to transfer, and click the “Send” button to execute the transaction. You will receive a transaction receipt once the transaction is confirmed on the blockchain. --- > For a complete page index, fetch # Codex Quickstart - DeFi Data API for Arbitrum [Codex](https://www.codex.io/?utm_source=arbitrum-docs\&utm_medium=partner-docs) is a blockchain data API that provides real-time and historical DeFi data across 100+ networks via GraphQL, including Arbitrum. With access to over 70 million tokens and 700 million wallets, Codex delivers sub-second data for building token explorers, trading bots, portfolio trackers, and DeFi dashboards. ## Quickstart Get started by [creating a free account](https://dashboard.codex.io/signup) and grabbing your API key from the dashboard. All requests are sent as HTTPS POST to `https://graph.codex.io/graphql` with your API key in the `Authorization` header: ```bash curl -X POST https://graph.codex.io/graphql \ -H "Content-Type: application/json" \ -H "Authorization: YOUR_API_KEY" \ -d '{"query": "{ getNetworks { name id } }"}' ``` Use the [GraphQL Explorer](https://docs.codex.io/explore) to interactively build and test queries. ## APIs Codex exposes a single GraphQL endpoint with 73 query operations covering current and historical data. All APIs support Arbitrum alongside 100+ other networks. > **INFO** — Endpoints > > Check out the [Codex API Reference](https://docs.codex.io/api-reference?utm_source=arbitrum-docs\&utm_medium=partner-docs) to see all available query operations. ### Token Data Get real-time and historical token prices, OHLCV candlestick charts, metadata, and scam filtering across all supported networks. ### DEX Trades Fetch swap events across decentralized exchanges with pair-level detail, filtering, and sorting. ### Liquidity Pools Access pool reserves, volume, fees, and newly created pairs for DEX protocols on Arbitrum. ### Wallet Analytics Retrieve token balances, transaction history, and holdings across 100+ networks. Analyze wallet performance and discover high-performing traders. ### Real-Time Subscriptions Codex supports 25 real-time data streams via WebSocket (`wss://graph.codex.io/graphql`), enabling live updates for token price changes, trade events, new DEX pairs, wallet activity, and new token launches. ### Webhooks Push-based notifications for onchain events, delivering data to your server as events occur without requiring persistent WebSocket connections. ## Developer Tools Codex provides two primary ways to interact with the API: 1. **[GraphQL API](https://docs.codex.io/api-reference?utm_source=arbitrum-docs\&utm_medium=partner-docs)**: Send custom GraphQL queries directly to the API endpoint for full flexibility. 2. **[TypeScript/JavaScript SDK](https://docs.codex.io/?utm_source=arbitrum-docs\&utm_medium=partner-docs)**: A thin wrapper around the GraphQL API with predefined queries, mutations, and built-in subscription connection handling. ```text npm install @codex-data/sdk ``` ## Recipes & Guides * [Token Discovery](https://docs.codex.io/recipes/discover-tokens): Build token discovery pages with trending data, advanced filtering, and search * [Price Charts](https://docs.codex.io/recipes/charts): Render token charts with OHLCV data and real-time updates * [Wallet Analytics](https://docs.codex.io/recipes/wallets): Analyze wallet performance and discover high-performing traders * [Token Swap Events](https://docs.codex.io/recipes/events): Fetch and display token swaps with filtering, sorting, and real-time updates * [Launchpad Monitoring](https://docs.codex.io/recipes/launchpads): Build a launchpad dashboard with filtering, sorting, and real-time updates * [Real-Time Price Tracking](https://docs.codex.io/recipes/realtime): Build a Node.js app that listens for token price changes in real time ## Get Started * **[Dashboard](https://dashboard.codex.io/?utm_source=arbitrum-docs\&utm_medium=partner-docs)**: Sign up and get your API key for free * **[Docs](https://docs.codex.io/?utm_source=arbitrum-docs\&utm_medium=partner-docs)**: Full API documentation and guides * **[GraphQL Explorer](https://docs.codex.io/explore)**: Interactively build and test queries * **[GitHub](https://github.com/Codex-Data)**: SDK and examples * **[Discord](https://discord.com/invite/mFpUhT3vAq)**: Community support --- > For a complete page index, fetch # Contribute third-party docs Third-party docs are articles that help readers of Arbitrum docs use other products, services, and protocols (like the ones listed in the [Arbitrum portal](https://portal.arbitrum.io/)). These documents are usually authored by partner teams, but can be authored by anyone. They follow the same general process that [core docs](/for-devs/contribute.md#add-a-new-core-document) follow, in addition to the following guidelines: ## 1.Eligibility * Third-party docs are intended to support products listed in the [Arbitrum portal](https://portal.arbitrum.io/), or infrastructure and services that those products use. * To submit your project to the Arbitrum portal, [apply using this Google form](https://docs.google.com/forms/d/e/1FAIpQLSc_v8j7sc4ffE6U-lJJyLMdBoIubf7OIhGtCqvK3cGPGoLr7w/viewform). ## 2.Purpose * The purpose of our `Third-party docs` sections is to\_meet Arbitrum developer (or user) demand for guidance that helps them use non-Arbitrum products with Arbitrum products. * It's *not* meant to drive traffic to your product (although that may happen); it's meant to solve problems that our readers are actually facing, which are directly related to Arbitrum products. ## 3.Maintenance expectations * Offchain Labs can't commit to maintaining third-party docs, but we make it easy for you to maintain them. * Ensure that your document's YAML frontmatter contains a `third_party_content_owner` property, with the GitHub username of the designated maintainer. This person will be assigned to your document's issues and PRs, and will be expected to resolve them in a timely manner. ## 4.Organization * Third-party docs are organized within the `Third-party content` node located at the bottom of each documentation section's sidebar in path `docs/for-devs/third-party-docs//.mdx` * This node's content is grouped by third-party product. If/when this becomes unwieldy, we'll begin grouping products by [portal](https://portal.arbitrum.io/) category. ## 5.Limited document types * To manage our team's limited capacity, third-party documents must be either\_Quickstarts\_,*How-tos*, or\_Concepts\_. See [document types](/for-devs/contribute.md#document-type-conventions). ## 6.Incremental contributions: One document at a time, procedures first * Third-party document PRs should contain at most one new document. * Any given product's first docs contribution should be a\_Quickstart\_ or\_How-to\_. * Additional documents will be merged only if we can verify that our readers are deriving value from your initial contribution. * The way that we verify this isn't yet formally established, and it isn't publicly disclosed. Our current approach combines a number of objective and subjective measures. ## 7.Policy acknowledgment * Before merging third-party documentation PRs, we ask contributors to acknowledge that they've read, understood, and agree with the following policies: ### 1.Content ownership As the author, you retain ownership of and responsibility for the content you contribute. You're free to use your content in any way you see fit outside of Arbitrum's docs. Remember that when contributing content to our documentation, you must ensure you have the necessary rights to do so, and that the content doesn't infringe on the intellectual property rights of others. ### 2.License for use By contributing your content to our documentation, you grant Offchain Labs a non-exclusive, royalty-free license to use, reproduce, adapt, translate, distribute, and display the content in our documentation. This allows us to integrate your content into our docs and make it available to all users. ### 3.Right to modify or remove Offchain Labs reserves the right to modify or remove third-party content from our documentation at any time. This might be necessary due to a range of reasons, such as content becoming outdated, receiving very low page views over an extended period, or misalignment with our guidelines or goals. --- > For a complete page index, fetch # Quickstart - Covalent Indexing and Querying API [Covalent](https://www.covalenthq.com/?utm_source=arbitrum\&utm_medium=partner-docs) is a hosted blockchain data solution providing access to historical and current onchain data for [200+ supported blockchains](https://www.covalenthq.com/docs/networks/?utm_source=arbitrum\&utm_medium=partner-docs), including Arbitrum One, Nova and Arbitrum chains. Covalent maintains a full replica of every supported blockchain, meaning you have access to: * Current and historical account balances * Full transaction histories * Every contract log event * All NFTs including assets and metadata **Use Covalent if you need:** * Wallet, Transactions, NFT, DEX, Staking or core blockchain data (log events, blocks) * Normalized, aggregated and enhanced multichain data, well beyond what you get from RPC providers * Enterprise-grade performance > **[Sign up to start building on Arbitrum](https://www.covalenthq.com/platform/?utm_source=arbitrum\&utm_medium=partner-docs)** ## APIs The Covalent APIs enables developers to quickly and easily access structured onchain data. This means consistent response schemas regardless of the blockchain. Available APIs and corresponding use cases include: ### Wallet API * **Features:** All token balances (**ERC-20**, `721`, `1155`, native), token transfers and prices (spot & historical) for a wallet. * **Use cases:** [Wallets, portfolio trackers](https://goldrush-wallet-portfolio-ui.vercel.app/?utm_source=arbitrum\&utm_medium=partner-docs), token gating, airdrop snapshots. ### NFT API * **Features:** Media assets, metadata, sales, owners, trait & attribute filters, thumbnails & previews. * **Use cases:** [NFT galleries & marketplaces](https://goldrush-nft-gallery-ui.vercel.app/?utm_source=arbitrum\&utm_medium=partner-docs), real world asset (RWA) tracking, token gating. ### DEX API * **Features:** Positions, rewards, pool and token details for major DEX protocols. * **Use cases:** [Analytics dashboards](https://goldrush-uniswap-dex-dashboard.vercel.app/?utm_source=arbitrum\&utm_medium=partner-docs), leaderboards, reward calculators. ### Cross-Chain Activity API * **Features:** Single API call to fetch a list of active chains and the latest transaction date on each for an address. * **Use cases:** [App onboarding](https://goldrush-wallet-portfolio-ui.vercel.app/activity/0xfc43f5f9dd45258b3aff31bdbe6561d97e8b71de/?utm_source=arbitrum\&utm_medium=partner-docs). ### Transactions API * **Features:** All historical transactions with human-readable log events. Includes gas usage/spend summaries. * **Use cases:** [Accounting and tax tools](https://bit.ly/crypto-tax-tool), branded in-app [transaction receipts](https://goldrush-dfk-tx-receipt-ui.vercel.app/tx/defi-kingdoms-mainnet/0x4e5c0af28b2cea27d06677fae1f573572e0ff863c43ae42d2959ca67b90c4390/?utm_source=arbitrum\&utm_medium=partner-docs). ### Security API * **Features:** NFT and **ERC-20** token allowances, including value-at-risk. * **Use cases:** Revoke features in wallets, security applications. ### Blockchain API * **Features:** Block details, log events by contract address or topic hash, gas prices. * **Use cases:** Custom block explorers. ## Developer Tools There are three primary developer tools for using the APIs: 1. [**Unified API**](https://www.covalenthq.com/docs/api/?utm_source=arbitrum\&utm_medium=partner-docs): enterprise-grade endpoints to use with any programming language. Switch blockchains with one path parameter. ```text curl -X GET https://api.covalenthq.com/v1/arbitrum-mainnet/address/0xf977814e90da44bfa03b6295a0616a897441acec/balances_v2/ \ -H 'Content-Type: application/json' \ -u YOUR_API_KEY: ``` 2. [**Client SDKs**](https://www.covalenthq.com/docs/unified-api/sdk/?utm_source=arbitrum\&utm_medium=partner-docs): official client libraries including TypeScript, Go and Python. TypeScript example: ```text npm install @covalenthq/client-sdk ``` or: ```text yarn add @covalenthq/client-sdk ``` ```text import { CovalentClient } from "@covalenthq/client-sdk"; (async () => { try { const client = new CovalentClient("YOUR_API_KEY"); const transactions = client.TransactionService.getAllTransactionsForAddress("arbitrum-mainnet", "0xf977814e90da44bfa03b6295a0616a897441acec"); for await (const tx of transactions) { console.log("tx", tx); } } catch (error) { console.log(error.message); } })(); ``` 3. [**GoldRush Kit**](https://github.com/covalenthq/goldrush-kit/?utm_source=arbitrum\&utm_medium=partner-docs): beautifully designed React components for your dApp frontend[](https://goldrush-wallet-portfolio-ui.vercel.app/dashboard/balance/0xfc43f5f9dd45258b3aff31bdbe6561d97e8b71de/transfers/eth-mainnet/0xf8c3527cc04340b208c854e985240c02f7b7793f) [![GoldRush Component Example](https://www.datocms-assets.com/86369/1711147954-goldrush_wallet_ui_example.png)](https://goldrush-wallet-portfolio-ui.vercel.app/dashboard/balance/0xfc43f5f9dd45258b3aff31bdbe6561d97e8b71de/transfers/eth-mainnet/0xf8c3527cc04340b208c854e985240c02f7b7793f) ## Get started * **[API Key](https://www.covalenthq.com/platform/auth/register/?utm_source=arbitrum\&utm_medium=partner-docs)**: sign up for free * [**Docs**](https://www.covalenthq.com/docs/unified-api/?utm_source=arbitrum\&utm_medium=partner-docs) - comprehensive knowledge base for all things Covalent * **[Guides](https://www.covalenthq.com/docs/unified-api/guides/?utm_source=arbitrum\&utm_medium=partner-docs)**: learn how to build for various use cases and expand your onchain knowledge --- > For a complete page index, fetch # How to deploy an NFT smart contract and enable credit card and cross-chain payments with no-code > **INFO** — Community member contribution > > Shoutout to [@rohit-710](https://github.com/rohit-710) for contributing the following [third-party document](/for-devs/third-party-docs/contribute.md)! **[Crossmint](http://crossmint.com/?utm_source=backlinks\&utm_medium=docs\&utm_campaign=arbitrum)** is an enterprise-grade Web3 development platform that lets you deploy smart contracts, create email wallets, enable credit-card and cross chain payments, and use APIs to create, distribute, sell, store, and edit NFTs. By abstracting away the core complexities of the blockchains, Crossmint allows you to build NFT applications without requiring any blockchain experience or holding cryptocurrency, and making the blockchain invisible to end users. Crossmint enables you to provide a Web2 experience for for your Web3 apps. Check out **[Crossmint's Docs](https://docs.crossmint.com/?utm_source=backlinks\&utm_medium=docs\&utm_campaign=arbitrum)** to get started. ## Crossmint Console What you can achieve using Crossmint Console: * Create and deploy NFT Collections. * Create and airdrop NFTs. * Generate No-code Storefront and No-code claims page. * Accept credit card and cross-chain payments for your NFT Collections. * Create and configure API Keys for Wallets and Minting. * Create Webhooks to listen to your Crossmint collections's endpoint URL's triggered events. * Whitelist domains and set up Redirect URls for your NFT Collections' checkout. > **INFO** — Crossmint Console > > Please check out the [docs](https://docs.crossmint.com/docs/create-developer-account) to learn more. Click [here](https://www.crossmint.com/console/overview?utm_source=backlinks\&utm_medium=docs\&utm_campaign=arbitrum) to use Crossmint's Production Console and click [here](https://staging.crossmint.com/console/overview?utm_source=backlinks\&utm_medium=docs\&utm_campaign=arbitrum) to use Crossmint's Staging Console. ## How to deploy an NFT smart contract on Arbitrum and enable credit card and cross-chain payments with no-code Please checkout the step-by-step tutorial on the docs [here](https://docs.crossmint.com/docs/create-an-nft-collection?utm_source=backlinks\&utm_medium=docs\&utm_campaign=arbitrum) and [here](https://docs.crossmint.com/docs/storefronts). > **INFO** — Video Tutorial > > You can find a YouTube video for the same [here](https://youtu.be/pq2TVCkfBDI). ## Connect with Crossmint! Need further help? We got you! Check out all the ways you can reach Crossmint for further questions and support: * Visit Crossmint's official website at [crossmint.com](http://crossmint.com/?utm_source=backlinks\&utm_medium=docs\&utm_campaign=arbitrum) * Read developer [Docs](https://docs.crossmint.com/?utm_source=backlinks\&utm_medium=docs\&utm_campaign=arbitrum) * For assistance, contact the Crossmint team via the [Support forum](https://help.crossmint.com/hc/en-us?utm_source=backlinks\&utm_medium=docs\&utm_campaign=arbitrum) * Follow Crossmint on [Twitter](https://twitter.com/crossmint?utm_source=backlinks\&utm_medium=docs\&utm_campaign=arbitrum) * Join the official [Discord server](https://discord.gg/crossmint?utm_source=backlinks\&utm_medium=docs\&utm_campaign=arbitrum) * Check out Crossmint on [Youtube](https://www.youtube.com/@crossmint?utm_source=backlinks\&utm_medium=docs\&utm_campaign=arbitrum) --- > For a complete page index, fetch # Quickstart: Blazing-fast indexing and data analytics using Envio ## Envio HyperIndex [Envio](https://envio.dev/) HyperIndex is a feature-rich indexing solution that provides developers with an efficient way to index and aggregate real-time or historical blockchain data for any EVM. The indexed data is easily accessible through custom GraphQL queries, providing developers with the flexibility and power to retrieve specific information. Envio offers native support for Arbitrum One (testnet and mainnet), Arbitrum Nova (testnet and mainnet), and Arbitrum chains, and has been designed to support high-throughput blockchain applications that rely on real-time data for their business requirements. Designed to optimize the user experience, Envio offers automatic code generation, flexible language support, quickstart templates, and a reliable cost-effective [hosted service](https://docs.envio.dev/docs/hosted-service). Indexers on Envio can be written in JavaScript, TypeScript, or ReScript. ## Envio HyperSync Envio supports [HyperSync](https://docs.envio.dev/docs/hypersync) on Arbitrum. HyperSync is an accelerated data query layer for the Arbitrum networks, providing APIs that bypass JSON-RPC for 20-100x faster syncing of historical data. HyperSync is used by default in Envio HyperIndex, with the use of RPC being optional. Using HyperSync, application developers do not need to worry about RPC URLs, rate-limiting, or managing infrastructure and can easily sync large datasets in a few minutes, something that would usually take hours or days via RPC. HyperSync is also available as a standalone API for data analytic use cases. Data analysts can interact with the HyperSync API using JavaScript, Python, or Rust clients and extract data in JSON, Arrow, or Parquet formats. For more information, visit the HyperSync documentation [here](https://docs.envio.dev/docs/overview-hypersync). ## HyperIndex key features * **Contract Import**: Autogenerate the key boilerplate for an entire Indexer project off a single or multiple smart contracts. Deploy within minutes. * **Multi-chain Support**: Aggregate data across multiple networks into a single database. Query all your data with a unified GraphQL API. * **Asynchronous Mode**: Fetch data from offchain storage such as IPFS, or contract state (e.g., smart contract view functions). * **Quickstart Templates**: Use pre-defined indexing logic for popular OpenZeppelin contracts (e.g., **ERC-20**). ## Getting started Users can choose whether they want to start from a quickstart template, perform a subgraph migration, or use the contract import feature to get started with Envio HyperIndex. The following files are required to run the Envio indexer: * Configuration (defaults to `config.yaml`) * GraphQL Schema (defaults to `schema.graphql`) * Event Handlers (defaults to `src/EventHandlers.*` depending on the language chosen) These files are auto-generated according to the template and language chosen by running the `envio init` command. ### Contract import tutorial This walkthrough explains how to initialize an indexer using a single or multiple contracts that are already deployed on Arbitrum. This process allows a user to quickly and easily start up a basic indexer and a queryable GraphQL API for their application in less than three minutes. ### Initialize your indexer `cd` into the folder of your choice and run ```bash envio init ``` Name your indexer ```bash ? Name your indexer: ``` Choose the directory where you would like to setup your project (default is the current directory) ```bash ? Set the directory: (.) . ``` Select `Contract Import` as the initialization option. ```bash ? Choose an initialization option Template > ContractImport SubgraphMigration [↑↓ to move, enter to select, type to filter] ``` ```bash ? Would you like to import from a block explorer or a local abi? > Block Explorer Local ABI [↑↓ to move, enter to select, type to filter] ``` `Block Explorer` option only requires user to input the contracts address and chain of the contract. If the contract is verified and deployed on one of the supported chains, this is the quickest setup as it will retrieve all needed contract information from a block explorer. `Local ABI` option will allow you to point to a JSON file containing the smart contract ABI. The Contract Import process will then populate the required files from the ABI. #### Select the blockchain that the contract is deployed on ```bash ? Which blockchain would you like to import a contract from? ethereum-mainnet goerli > arbitrum-one arbitrum-nova bsc gnosis v polygon [↑↓ to move, enter to select, type to filter] ``` #### Enter the address of the contract to import ```bash ? What is the address of the contract? [Use the proxy address if your abi is a proxy implementation] ``` Note if you are using a proxy contract with an implementation, the address should be for the proxy contract. #### Choose which events to include in the `config.yaml` file ```bash ? Which events would you like to index? > [x] ClaimRewards(address indexed from, address indexed reward, uint256 amount) [x] Deposit(address indexed from, uint256 indexed tokenId, uint256 amount) [x] NotifyReward(address indexed from, address indexed reward, uint256 indexed epoch, uint256 amount) [x] Withdraw(address indexed from, uint256 indexed tokenId, uint256 amount) [↑↓ to move, space to select one, → to all, ← to none, type to filter] ``` #### Select the continuation option ```bash ? Would you like to add another contract? > I'm finished Add a new address for same contract on same network Add a new network for same contract Add a new contract (with a different ABI) [Current contract: BribeVotingReward, on network: arbitrum-one] ``` The `Contract Import` process will prompt the user whether they would like to finish the import process or continue adding more addresses for same contract on same network, addresses for same contract on different network or a different contract. For more information on contract import feature, visit the documentation [here](https://docs.envio.dev/docs/contract-import). ## Envio examples Click [here](https://docs.envio.dev/docs/example-uniswap-v3) for Envio HyperIndex examples. Click [here](https://docs.envio.dev/docs/hypersync-clients) for Envio HyperSync examples. ## Getting help Indexing can be a rollercoaster, especially for more complex use cases. Our engineers are available to help you with your data availability needs. Join our growing community of elite builders, and find peace of mind with Envio. * [Discord](https://discord.gg/mZHNWgNCAc) * Email: --- > For a complete page index, fetch # Quickstart: Indexing Arbitrum custom data via Flair [Flair](https://flair.dev), Real-time and historical custom data indexing for any EVM chain. Flair offers reusable **indexing primitives** (such as fault-tolerant RPC ingestors, custom processors, re-org aware database integrations) to make it easy to receive, transform, store and access your onchain data. [](https://docs.flair.dev/) [![flair architecture](https://imgur.com/0q5bHZK.png)](https://docs.flair.dev/) ## Why Flair? Compared to other alternatives the main reasons are: * 🚀 Adopting **parallel and distributed processing** paradigm means high scalability and resiliency for your indexing stack. Instead of constrained sequential processing (e.g., Subgraph). * 🧩 Focused on **primitives**, which means on the left you plug-in an RPC and on the right you output the data to any destination database. * 🚄 Native **real-time stream processing** for certain data workload (such as aggregations, rollups) for things like total volume per pool, or total portfolio per user wallet. * ☁️ **Managed** cloud services avoid DevOps and irrelevant engineering costs for dApp developers. * 🧑‍💻 Avoid decentralization **overhead** (consensus, network hops, etc.) since we believe to enable best UX for dApps reading data must be as close to the developers as possible. ### Features * ✅ Listen to **any EVM chain** with just an RPC URL. * Free managed RPC URLs for +8 popular chains already included. * Works with both websocket and https-only RPCs. * ✅ Track and ingest **any contract** for **any event topic.** * Auto-track new contracts deployed from factory contracts. * ✅ **Custom processor scripts** with Javascript runtime (with **Typescript** support) * Make external API or Webhook calls to third-party or your backend. * Get current or historical USD value of any **ERC-20** token amount of any contract address on any chain. * Use any external NPM library. * ✅ **Stream** any stored data to your destination database (Postgres, MongoDB, MySQL, Kafka, Elasticsearch, Timescale, etc.). ## Getting Started 1️⃣ Clone the [starter boilerplate](https://github.com/flair-sdk/starter-boilerplate) template and follow the instructions ```bash git clone https://github.com/flair-sdk/starter-boilerplate.git # ... follow instructions in README.md ``` > **INFO** > > Boilerplate instructions will create a **new cluster**, generate **an API Key**, and set up a manifest.yml to index your **first contract** with **sample custom processor** scripts. > > Learn more about the [structure of manifest.yml](https://docs.flair.dev/reference/manifest.yml). 2️⃣ Configure Arbitrum RPC nodes Set a unique namespace, Arbitrum chainId and RPC endpoint in your config. Remember that you can add up to ten RPC endpoints for resiliency. ```yaml { 'cluster': 'dev', 'namespace': 'my-awesome-arbitrum-indexing-dev', 'indexers': [{ 'chainId': 42161, 'enabled': true, 'ingestionFilterGroup': 'default', 'processingFilterGroup': 'default', 'sources': [ # Highly-recommended to have at least 1 websocket endpoint 'wss://arbitrum-one.publicnode.com', # You can put multiple endpoints for failover 'https://arbitrum.llamarpc.com', ] }] } ``` 3️⃣ Sync some historical data using [backfill command](https://docs.flair.dev/reference/backfilling). Remember that `enabled: true` flag in your `config` enabled your indexer to capture data in real-time already. ```bash # backfill certain contracts or block ranges pnpm flair backfill --chain 42161 --address 0x22dc069183f85a8473553e32b59efc9fec506baf -d backward --max-blocks 10000 # backfill for a specific block number, if you have certain events you wanna test with pnpm flair backfill --chain 42161 -b 132763420 # backfill for the recent data in the last X minute pnpm flair backfill --chain 42161 --min-timestamp="30 mins ago" -d backward ``` 4️⃣ [Query](https://docs.flair.dev/#getting-started) your custom indexed data. 5️⃣ Stream the data to your [own database](https://docs.flair.dev/reference/database#your-own-database). ## Examples Explore real-world usage of Flair indexing primitives for various use-cases. ### DeFi * [Aggregate protocol fees in **USD** across multiple chains](https://github.com/flair-sdk/examples/tree/main/aggregate-protocol-fees-in-usd) * [Calculate "Health Factor" of positions with contract factory tracking](https://github.com/flair-sdk/examples/tree/main/health-factor-with-factory-tracking) * [Index Uniswap v2 swaps with **USD** price for all addresses](https://github.com/flair-sdk/examples/tree/main/uniswap-v2-events-from-all-contracts-with-usd-price) ### NFT * [Index **ERC-721** and **ERC-1155** NFTs on any EVM chain with an RPC URL](https://github.com/flair-sdk/examples/tree/main/erc721-and-erc1155-nft-indexing) ## Need help? [Our engineers](https://docs.flair.dev/talk-to-an-engineer) are available to help you at any stage. --- > For a complete page index, fetch # Getting Started with Gelato VRF ## What is Gelato VRF? Gelato VRF offers real randomness for blockchain applications on Arbitrum by leveraging Drand, a trusted decentralized source for random numbers. With Gelato VRF, developers on Arbitrum get random values that are both genuine and can be checked for authenticity. Explore Gelato VRF's support for all supported networks [here](https://docs.gelato.network/web3-services/vrf/supported-networks). ## Applications of Gelato VRF The potential applications of a reliable and transparent random number generator on the blockchain are vast. Here are just a few use cases: * **Gaming and Gambling**: Determine fair outcomes for online games or decentralized gambling applications. * **Decentralized Finance (DeFi)**: Use in protocols where random selections, like lottery systems, are required. * **NFT Generation**: Randomly generate traits or characteristics for unique digital assets. * **Protocol Decision Making**: In protocols where decisions need to be randomized, such as selecting validators or jurors. ## How does Gelato VRF work? Gelato VRF (Verifiable Random Function) provides trustable randomness on EVM-compatible blockchains. Here's a brief overview: Core Components: * **Drand**: Gelato VRF utilizes Drand, a decentralized randomness beacon ensuring unpredictability and unbiased randomness. Top-level Flow: * **Contract Deployment**: Use `GelatoVRFConsumerBase.sol` as an interface for requesting random numbers. * **Requesting Randomness**: Emit the `RequestedRandomness` event to signal the need for a random number. * **Processing**: Web3 functions fetch the random number from Drand. * **Delivery**: The `fulfillRandomness` function delivers the random number to the requesting contract. ## Quick start guide In order to get your VRF up and running with Gelato, you need to make your contract VRF Compatible. ## Step 1: Setup your development environment Ensure you have either [Foundry](https://book.getfoundry.sh/getting-started/installation) or [Hardhat](https://hardhat.org/) set up in your development environment. ## Step 2: Install the Gelato VRF contracts * For Hardhat users: ```shell npm install --save-dev @gelatodigital/vrf-contracts ``` * For Foundry users: ```shell forge install gelatodigital/vrf-contracts --no-commit ``` ## Step 3: Inherit `GelatoVRFConsumerBase` in your contract ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.18; import {GelatoVRFConsumerBase} from "./GelatoVRFConsumerBase.sol"; contract YourContract is GelatoVRFConsumerBase { // Your contract's code goes here } ``` ## Step 4: Request randomness ```solidity function requestRandomness(bytes memory data) external { require(msg.sender == ...); uint64 requestId = _requestRandomness(data); } ``` Step 5: Implement the `fulfillRandomness` function ```solidity function _fulfillRandomness( bytes32 randomness, uint64 requestId, bytes memory data, ) internal override { } } ``` ## Step 6: Pass dedicated msg.sender When you're ready to deploy your Gelato VRF-compatible contract, an important step is to include the dedicated `msg.sender` as a constructor parameter. This ensures your contract is set up to work with the correct operator to fulfill the randomness requests. It's crucial to ensure that only authorized requests are processed. ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.18; import {GelatoVRFConsumerBase} from "./GelatoVRFConsumerBase.sol"; contract YourContract is GelatoVRFConsumerBase { constructor(address operator) GelatoVRFConsumerBase(operator) { // Additional initializations } // The rest of your contract code } ``` Once your contract is ready & deployed, grab the address and [Deploy your VRF instance](https://docs.gelato.network/web3-services/vrf/quick-start/deploying-your-vrf-instance)! --- > For a complete page index, fetch # LayerZero [LayerZero](https://layerzero.network) is an **omnichain interoperability protocol** that enables smart contracts to communicate between different blockchain networks. With LayerZero V2, applications deployed on Arbitrum can connect and interact with 100+ supported blockchains through secure, configurable messaging channels. ## Key features LayerZero enables cross-chain capabilities for builders on Arbitrum: 1. **Cross-chain messaging**: Send arbitrary messages and data between contracts on different chains 2. **Omnichain tokens**: Deploy tokens (fungible `OFT` and non-fungible `ONFT`) that work across multiple chains 3. **External chain data access** (`lzRead`): Fetch and compute onchain state from other networks 4. **Composed messages**: Chain multiple cross-chain operations together ## How it works 1. **DVNs** independently verify that a message is valid, waiting for a configured number of block confirmations on the source chain. 2. When the message is verified, **Executors** on the destination chain deliver the message to the target contract, paying for the destination gas automatically in the background. The user only pays for gas on the source chain. Because each application can configure its own DVN sets, your security is not locked into a single aggregator or middlechain. For more details, check out the [LayerZero docs](https://docs.layerzero.network/). To run your own DVN as part of your security set, check out the [DVN docs](https://docs.layerzero.network/v2/developers/evm/off-chain/build-dvns). ## Arbitrum Integration When integrating with LayerZero, there are two key aspects to understand: 1. The [LayerZero Endpoint](https://docs.layerzero.network/v2/home/protocol/layerzero-endpoint) * Immutable smart contract that serves as the entry and exit point for messages * Allows applications to configure security and execution parameters * Provides interfaces for sending, receiving and reading cross-chain data 2. [Security Stack](https://docs.layerzero.network/v2/home/modular-security/security-stack-dvns) * Configurable set of Decentralized Verifier Networks (DVNs) that validate messages * Allows applications to customize security and cost tradeoffs * Ensures message integrity across chains ### Contract Addresses | Chain | Chain Id | Endpoint Id | Endpoint Address | | ------------------------ | -------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | Arbitrum Mainnet | 42161 | 30110 | [0x1a44076050125825900e736c501f859c50fE728c](https://layerzeroscan.com/api/explorer/arbitrum/address/0x1a44076050125825900e736c501f859c50fE728c) | | Arbitrum Nova Mainnet | 42170 | 30175 | [0x1a44076050125825900e736c501f859c50fE728c](https://layerzeroscan.com/api/explorer/nova/address/0x1a44076050125825900e736c501f859c50fE728c) | | Arbitrum Sepolia Testnet | 421614 | 40231 | [0x6EDCE65403992e310A62460808c4b910D972f10f](https://layerzeroscan.com/api/explorer/arbitrum-sepolia/address/0x6EDCE65403992e310A62460808c4b910D972f10f) | Once a transaction is submitted, you can trace it on [LayerZero Scan](https://layerzeroscan.com/), which shows cross-chain message flow from source to destination in real time. ## Getting Started Developers should: 1. Deploy contracts on each chain: [Quickstart: Create Your First Omnichain App](https://docs.layerzero.network/v2/developers/evm/create-lz-oapp/start) 2. Configure a Security Stack by selecting DVNs & block confirmations (optional). 3. Optionally configure an Executor or use defaults to deliver messages. 4. Send messages, send tokens (OFT, ONFT), or read state on any chain, using LayerZero. ## Example Use Cases LayerZero powers various cross-chain applications across different categories: 1. **Omnichain Tokens (OFTs)** (e.g., [Ethena's USDe](https://ethena.fi/), Wrapped Bitcoin) * Unified token supply across chains * Native bridging without intermediary tokens * Real-world examples include USDe, sUSDe, ENA tokens, and WBTC 2. **Cross-chain DEXs** (e.g., [Trader Joe](https://traderjoexyz.com/)) * Unified liquidity pools across chains * Cross-chain swaps and trading 3. **Omnichain Lending** (e.g., [Radiant Capital](https://radiant.capital/)) * Supply assets on any chain * Borrow against cross-chain collateral 4. **Cross-chain Governance** (e.g., [Stargate DAO](https://stargate.finance/)) * Vote on one chain, execute on many * Unified governance across deployments 5. **Chain Data Oracles** * Read and verify external chain state * Make decisions based on cross-chain data ## Resources 1. [LayerZero Developer Documentation](https://docs.layerzero.network/v2) 2. [LayerZero Scan](https://layerzeroscan.com/): Message explorer and debugging 3. [Discord Community](https://discord.gg/layerzero) 4. [GitHub](https://github.com/LayerZero-Labs/) --- > For a complete page index, fetch # MetaMask Smart Accounts The [MetaMask Smart Accounts Kit](https://docs.metamask.io/smart-accounts-kit/) enables embedding MetaMask Smart Accounts into dapps. Smart accounts support programmable account behavior and advanced features like delegated permissions, multi-signature approvals, and gas abstraction. Delegation is a core feature of smart accounts, enabling secure, rule-based permission sharing. ## Use MetaMask smart accounts with Arbitrum The MetaMask Smart Accounts Kit [supports multiple networks](https://docs.metamask.io/smart-accounts-kit/get-started/supported-networks/), including Arbitrum One, Arbitrum Nova, and Arbitrum Sepolia. Follow these steps to create your first smart account and send a user operation. > **NOTE** — Prerequisites > > Before starting, ensure you have the following installed: > > * [Node.js](https://nodejs.org/en/download) v18 or later. > * [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), [Yarn](https://yarnpkg.com/), or another package manager. ### 1. Install the Smart Accounts Kit Install the [Smart Accounts Kit](https://www.npmjs.com/package/@metamask/smart-accounts-kit) in your project:
npm ```bash npm install @metamask/smart-accounts-kit ```
Yarn ```bash yarn add @metamask/smart-accounts-kit ```
### 2. Set up a public client Set up a [Viem Public Client](https://viem.sh/docs/clients/public) using Viem's `createPublicClient` function. This client will let the smart account query the signer's account state and interact with the blockchain network. Make sure to import your desired Arbitrum network from `viem/chains`.
Arbitrum One ```typescript import { createPublicClient, http } from 'viem'; import { arbitrum as chain } from 'viem/chains'; const publicClient = createPublicClient({ chain, transport: http(), }); ```
Arbitrum Nova ```typescript import { createPublicClient, http } from 'viem'; import { arbitrumNova as chain } from 'viem/chains'; const publicClient = createPublicClient({ chain, transport: http(), }); ```
Arbitrum Sepolia ```typescript import { createPublicClient, http } from 'viem'; import { arbitrumSepolia as chain } from 'viem/chains'; const publicClient = createPublicClient({ chain, transport: http(), }); ```
### 3. Set up a bundler client Set up a [Viem Bundler Client](https://viem.sh/account-abstraction/clients/bundler) using Viem's `createBundlerClient` function. This lets you use the bundler service to estimate gas for user operations and submit transactions to the network. ```typescript import { createBundlerClient } from 'viem/account-abstraction'; const bundlerClient = createBundlerClient({ client: publicClient, transport: http('https://your-bundler-rpc.com'), }); ``` ### 4. Create a MetaMask smart account Create a MetaMask smart account to send the first user operation. This example configures a Hybrid smart account, which is a flexible smart account implementation that supports both an externally owned account (EOA) owner and any number of passkey (WebAuthn) signers: ```typescript import { Implementation, toMetaMaskSmartAccount } from '@metamask/smart-accounts-kit'; import { privateKeyToAccount } from 'viem/accounts'; const account = privateKeyToAccount('0x...'); const smartAccount = await toMetaMaskSmartAccount({ client: publicClient, implementation: Implementation.Hybrid, deployParams: [account.address, [], [], []], deploySalt: '0x', signer: { account }, }); ``` Learn more about [creating MetaMask Smart Accounts](https://docs.metamask.io/smart-accounts-kit/guides/smart-accounts/create-smart-account/). ### 5. Send a user operation Send a user operation using Viem's [`sendUserOperation`](https://viem.sh/account-abstraction/actions/bundler/sendUserOperation) method. The smart account will remain counterfactual until the first user operation. If the smart account is not deployed, it will be automatically deployed upon the sending first user operation. ```ts import { parseEther } from 'viem'; // Appropriate fee per gas must be determined for the specific bundler being used. const maxFeePerGas = 1n; const maxPriorityFeePerGas = 1n; const userOperationHash = await bundlerClient.sendUserOperation({ account: smartAccount, calls: [ { to: '0x1234567890123456789012345678901234567890', value: parseEther('1'), }, ], maxFeePerGas, maxPriorityFeePerGas, }); ``` Learn more about [sending user operations](https://docs.metamask.io/smart-accounts-kit/guides/smart-accounts/send-user-operation/). ### Next steps Now that you have created and deployed a MetaMask smart account on Arbitrum, you can grant specific permissions to other accounts from your smart account. See MetaMask's documentation on how to [create a delegation](https://docs.metamask.io/smart-accounts-kit/guides/delegation/execute-on-smart-accounts-behalf/). --- > For a complete page index, fetch # Moralis Quickstart - Crypto Data APIs for Arbitrum **[Moralis](https://moralis.io/?utm_source=arbitrum-docs\&utm_medium=partner-docs)** is a blockchain data platform that provides developers with all the data they need to build better blockchain applications. From NFT data, token data and price data, through to raw blockchain data and RPC nodes, Moralis offers a wide range of products that cover all major crypto and blockchain use cases, and it supports Arbitrum together with all other major EVM chains. ## Quickstart Guide Get started with Moralis APIs on Arbitrum by checking out our [Get Started Guide](https://docs.moralis.io/web3-data-api/evm/get-your-api-key/?utm_source=arbitrum-docs\&utm_medium=partner-docs), or watch some of our popular [Youtube tutorials](https://www.youtube.com/@MoralisWeb3/featured). ## Moralis APIs All Moralis APIs have support for Arbitrum and across all other major EVM blockchains. All endpoints have powerful filtering capabilities. > **INFO** — Endpoints > > Please check out the [Moralis API Reference](https://docs.moralis.io/?utm_source=arbitrum-docs\&utm_medium=partner-docs) to see all the available API endpoints. ### Wallet API With Moralis [Wallet API](https://moralis.io/api/wallet/?utm_source=arbitrum-docs\&utm_medium=partner-docs) you can get Wallet balances for tokens, NFTs and native assets, get full wallet history, net worth and a lot more. ### NFT API With Moralis [NFT API](https://moralis.io/api/nft/?utm_source=arbitrum-docs\&utm_medium=partner-docs) you can get NFT data like collections, owners, prices, images and metadata. ### Token API With Moralis [Token API](https://moralis.io/api/token/?utm_source=arbitrum-docs\&utm_medium=partner-docs) you can get **ERC-20** token data like prices, ownership, metadata, transfers, approvals, liquidity, mints and burns. ### RPC Nodes Get access to powerful RPC nodes on all major chains with [Moralis Nodes](https://moralis.io/nodes/?utm_source=arbitrum-docs\&utm_medium=partner-docs). --- > For a complete page index, fetch # OKX - Crypto exchange, app, and wallet [OKX](https://www.okx.com/) is an innovative and reliable cryptocurrency exchange with advanced financial services. OKX relies on blockchain technology to provide everything you need for wise trading and investment. With OKX, you can trade with confidence, knowing that your assets are in safe hands. Users can enjoy hundreds of tokens and trading pairs. With OKX, you can join one of the leading crypto exchanges by trading volume. OKX serves millions of users in over 100 countries, and it’s not just about trading. OKX provides a comprehensive suite of services including spot, margin, expiry, options, perpetual futures trading, DeFi, lending, and mining. ## Wallet API The OKX [wallet API](https://www.okx.com/web3/build/docs/waas/walletapi-introduction) offers a flexible, non-custodial wallet technology solution to build onchain services and applications. * Web3 multi-chain support * DApp embedded wallet * Multi-chain Web3 apps * Exchange wallet ## DEX API The [DEX API](https://www.okx.com/web3/build/docs/waas/dex-introduction) is a trading aggregator for multi-chain and cross-chain transactions. You can use it to create Web3 trading services and applications for various scenarios, including wallets, DApp projects, and DeFi projects. * Multi-chain support * Aggregation over multiple cross-chain bridges and DEXs * Stability and high availability ## NFT marketplace API The [OKX NFT Marketplace](https://www.okx.com/web3/build/docs/waas/marketplace-introduction) is an extensive decentralized platform that supports multi-chain NFT creations and cross-platform transactions. It provides real-time onchain data for both users and developers. * NFT aggregator * Issuance and secondary marketplace ## Defi API OKX Web3 DeFi is the only all-in-one DeFi investment solution available on the market. OKX’s multi-chain DeFi aggregator provides users with a comprehensive platform to discover and access various investment opportunities in DeFi, making it easy to find products that meet their needs. OKX Web3 DeFi connects to over 80 protocols, including Aave, Compound, Curve, Yearn, and Uniswap. OKX supports over 15 networks like Arbitrum, Ethereum, and Polygon. By integrating the [OKX Web3 DeFi Open API](https://www.okx.com/web3/build/docs/waas/defi-introduction) into your application, users can quickly and easily access all DeFi protocols and enjoy the benefits of DeFi investment. --- > For a complete page index, fetch # How to onboard users and make a sponsored transaction > **INFO** — Community member contribution > > The following document was contributed by [@joalavedra](https://github.com/joalavedra). Give them a shoutout if you find it useful! [Openfort](https://openfort.io) is an open-source wallet infrastructure built with authentication and payments. Openfort has developed an SDK that vertically integrates the creation of a wallet and the account abstraction technology all in one. ## [Embedded wallet](https://www.openfort.io/embedded-wallet) Build for React, React Native, Swift, and Unity SDK. It enables app developers to onboard users and generate wallets for them. Features include: * **Plug-and-play UI Components**: Prebuilt, customizable authentication and wallet connection flows that can be deployed in minutes, not weeks, with support for major authentication providers and wallet connector * **Non-custodial Signer**: Secure, self-custodied wallet creation and signing for users, with no need for browser extensions or external apps. * **Onramp Support**: Users can onramp their newly created wallets with traditional methods or depositing crypto. * **Key Export**: Users can always export private keys, allowing them to take the wallet with them. ## [Global wallet](https://www.openfort.io/crossapp-wallet) If you're building your own Arbitrum chain and are thinking about owning the wallet, connect with us. Global wallets are designed for you to have a universal sign-on across all the apps in your ecosystem. * **Ecosystem SDK**: Build your own wallet SDK that can be integrated across your suite of apps, ensuring users have a consistent identity and asset management experience everywhere. * **No App or Extension Required**: Users can create and use wallets instantly via iFrames or embedded flows, compatible with any EVM chain. * **Modern Standards**: Supports the latest Ethereum standards (EIP-1193, 6963, 7702, 4337, and more) for broad compatibility and future-proofing. > **INFO** — Detailed tutorial > > For a detailed tutorial, please refer to the [Quickstart Guide](https://www.openfort.io/docs/). ## Connect with Openfort Need further assistance? Reach out to Openfort for support and stay updated: * Visit Openfort's official website at [openfort.io](https://www.openfort.io) * Read the [Documentation](https://www.openfort.io/docs) * Check out Openfort on [GitHub](https://github.com/openfort-xyz) --- > For a complete page index, fetch # How to make your Arbitrum dApp chain-agnostic with Universal Accounts > **INFO** — Community member contribution > > Shout-out to [@Soos3d](https://github.com/Soos3d) for contributing the following [third-party document](/for-devs/third-party-docs/contribute.md)! [Particle Network](https://particle.network) enables **chain abstraction** through its **Universal Accounts** (UA) infrastructure. This gives users a single unified account and balance across [multiple chains](https://developers.particle.network/universal-accounts/cha/chains#supported-networks). This means that a user can interact with your dApp on **Arbitrum** even if their assets are located on a completely different chain. ![Universal Accounts overview](https://mintcdn.com/particlenetwork-fccf74d2/oXCulWNeMx80sJqa/intro/images/cha-overview.png?fit=max\&auto=format\&n=oXCulWNeMx80sJqa\&q=85\&s=760fd24d1b7d4e7067765f7aae3a4209) **Universal Accounts** unify EVM and non-EVM ecosystems under one identity. This allows: * Cross-chain deposits and swaps without manual bridging. * Unified balance fetching across chains. * Gas abstraction: users can pay fees in any [supported token](https://developers.particle.network/universal-accounts/cha/chains#primary-assets). **Arbitrum** is one of the earliest networks supported by Universal Accounts, with full support for **cross-chain transactions**, **unified balances**, and **universal gas** ## Why this matters for your deposit flow Traditional deposit flows require users to choose a chain, bridge tokens, and manage gas tokens on that chain, all of which introduce friction and drop-offs. With Universal Accounts, your dApp on Arbitrum can accept deposits from **any supported chain** (Ethereum, Polygon, Base, Solana, etc), and the user's balance is unified behind the scenes. That means: * One deposit address per user. * No bridging steps, no chain switching required. * Simpler UX for the user; fewer errors, fewer abandoned flows. * Your contract infrastructure can live on Arbitrum without worrying about which chain the user's assets happen to be on. ## Getting Started This tutorial shows how to build a **Next.js** app using the [Universal Accounts SDK](https://developers.particle.network/universal-accounts/cha/web-quickstart) to: * Fetch the unified balance * Accept deposits from *any chain* * Send a cross-chain transaction ### Prerequisites * Node.js 18+ * Yarn or npm * Basic familiarity with React / Next.js ### Start from the starter app Start from the starter app with the authentication logic ([Particle Connect](https://developers.particle.network/social-logins/connect/introduction)) already implemented on [GitHub](https://github.com/Particle-Network/connectkit-starter). > Note that we use Particle Connect for authentication, but you can use any other provider or browser wallet. ### 1. Install Dependencies After you initialize the starter app, you'll need the Universal Accounts SDK for cross-chain logic. ```bash yarn add @particle-network/universal-account-sdk ethers ``` > Note that `ethers.js` is a dependency of the Universal Accounts SDK, but you can use any other library to interact with the blockchain. ### 2. Configure the Particle Dashboard Go to the [Particle Dashboard](https://dashboard.particle.network/) and create a new project to get your credentials. You'll need: * `projectId` * `clientKey` * `appId` Store these in your `.env.local`: ```bash NEXT_PUBLIC_PROJECT_ID='YOUR_PROJECT_ID' NEXT_PUBLIC_CLIENT_KEY='YOUR_CLIENT_KEY' NEXT_PUBLIC_APP_ID='YOUR_APP_ID' ``` ### 3. Initialize a Universal Account The Universal Account is the core object that enables cross-chain logic, and it's a smart account owned by the user's EOA. After login, create a Universal Account instance tied to that user's EOA. ```typescript import { UniversalAccount } from '@particle-network/universal-account-sdk'; const { address } = useAccount(); const universalAccountInstance = new UniversalAccount({ projectId: process.env.NEXT_PUBLIC_PROJECT_ID!, clientKey: process.env.NEXT_PUBLIC_CLIENT_KEY!, appId: process.env.NEXT_PUBLIC_APP_ID!, ownerAddress: address!, // The EOA from login from Particle Connect useAccount hook }); ``` This object enables: * Fetching the Universal Account addresses. * Querying the unified balance. * Sending cross-chain transactions. ### 4. Fetch Addresses and Balances The Universal Account SDK provides a few methods to fetch the user's balance and addresses. ```typescript // Universal Account addresses. One for EVM assets and one for Solana assets const universalAccountData = await universalAccountInstance.getSmartAccountOptions(); console.log('EVM Universal Account:', universalAccountData.evmSmartAccount); console.log('Solana Universal Account:', universalAccountData.solanaSmartAccount); // Primary assets aggregated across chains const primaryAssets = await universalAccountInstance.getPrimaryAssets(); // Full breakdown of assets on all chains console.log(primaryAssets); // Total amount in USD console.log(primaryAssets.totalAmountInUSD); ``` Here you retrieve the Universal Account addresses, and the user's **primary assets** aggregated across chains (e.g., **USDC**, **ETH**, **USDT**). ### 5. Interacting with your dApp Your dApp runs on Arbitrum, but the user holds assets on another chain like **Base**, **Polygon**, or **Solana**. Thanks to Universal Accounts, you can still interact with your dApp. In this example, we mint an NFT on Arbitrum, but the user can pay with any supported token from any chain: ```typescript const CONTRACT_ADDRESS = '0x702E0755450aFb6A72DbE3cAD1fb47BaF3AC525C'; // NFT contract on Arbitrum const contractInterface = new Interface(['function mint() external']); const transaction = await universalAccountInstance.createUniversalTransaction({ chainId: CHAIN_ID.ARBITRUM_MAINNET_ONE, expectTokens: [], transactions: [ { to: CONTRACT_ADDRESS, data: contractInterface.encodeFunctionData('mint'), // value: "0x0", }, ], }); ``` The `transaction` object contains all the transaction details, you only need to sign the `rootHash` returned and send it via the SDK: ```typescript const signature = await walletClient?.signMessage({ account: address as `0x${string}`, message: { raw: transaction.rootHash }, }); const result = await universalAccountInstance.sendTransaction(transaction, signature); ``` > This example shows how to use Particle Connect, but you can use any wallet client to sign the `rootHash`. And you can use UniversalX as a block explorer to track the transaction: `https://universalx.app/activity/details?id=${result.transactionId}` The SDK handles fund routing so the user can pay with any supported token from any chain. > Find a complete implementation of this example in the [GitHub repo](https://github.com/Particle-Network/universal-accounts-workshop-arbitrum/blob/949abdc011d66f97d7949dca9c26f2778fe503d1/workshop-completed/app/page.tsx#L87). ## Deposit Flow The previous example demonstrates how to use **Universal Accounts** specifically to interact with a smart contract on **Arbitrum**, but a big use case is also to accept deposits directly from any chain and various assets: Here's a simplified sequence when your Arbitrum dApp accepts assets from any chain: 1. Get the user's UA deposit address (via `getSmartAccountOptions()`). 2. Display it in your UI: "Send **USDC**/**USDT** from any supported chain." 3. User sends on any chain (Ethereum, Polygon, Base, Solana, etc). 4. UA detects the deposit and credits it to the unified balance. 5. User interacts with your dApp on Arbitrum—UA automatically uses liquidity routing & abstracts gas. 6. Your contract logic on Arbitrum receives/uses funds as if they were local. No bridging step. No chain switching prompt. One unified experience. ## Putting It All Together **Universal Accounts** turn multi-chain interaction into a single, unified experience. By integrating them into your Arbitrum dApp, you're no longer limited by where a user's funds live. Your app can accept deposits, perform swaps, and interact with smart contracts using liquidity from any supported chain. ## Resources * Universal Accounts [Quickstart](https://developers.particle.network/universal-accounts/cha/web-quickstart) * [Particle Dashboard](https://dashboard.particle.network/) --- > For a complete page index, fetch # QuickNode Backfill Templates ## What are Backfill Templates? Backfill Templates are pre-built solutions within [Streams](https://www.quicknode.com/streams) (our ETL/streaming tool) designed to simplify the process of acquiring historical blockchain data. With just one click, users can backfill extensive datasets across various chains, including blocks, transactions, receipts, traces, and more. ## Key Benefits * **Speed**: Start backfilling in less than 10 minutes. * **Transparency**: Immediate cost and time estimates for your selected datasets. * **Reliability**: Guaranteed data delivery to platforms like Snowflake, Amazon S3, Webhooks, etc. ## Available Backfill Templates Below is a table of available Backfill Templates for the Arbitrum network: | Template Name | Description | Link | | ----------------------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Backfill Blocks and Transactions | Backfill historical Arbitrum blocks and transactions data. | [Use template](https://dashboard.quicknode.com/streams/new?dataset=block\&network=arbitrum_mainnet\&start=1\&utm_source=arbitrum_backfill_template_click) | | Backfill Blocks, Transactions, and Receipts | Backfill historical Arbitrum blocks, transactions, and receipts data. | [Use template](https://dashboard.quicknode.com/streams/new?dataset=block_with_receipts\&network=arbitrum_mainnet\&start=1\&utm_source=arbitrum_backfill_template_click) | | Backfill Receipts | Backfill historical Arbitrum receipts data. | [Use template](https://dashboard.quicknode.com/streams/new?dataset=receipts\&network=arbitrum_mainnet\&start=1\&utm_source=arbitrum_backfill_template_click) | | Backfill Traces (debug\_trace) | Backfill historical Arbitrum traces (debug\_trace) data. | [Use template](https://dashboard.quicknode.com/streams/new?dataset=debug_trace\&network=arbitrum_mainnet\&start=1\&utm_source=arbitrum_backfill_template_click) | | Backfill Blocks, Transactions, Receipts, Traces | Backfill historical Arbitrum blocks, transactions, receipts, and traces (debug\_trace). | [Use template](https://dashboard.quicknode.com/streams/new?dataset=block_with_receipts_debug_trace\&network=arbitrum_mainnet\&start=1\&utm_source=arbitrum_backfill_template_click) | | Backfill all `ERC-20/721/1155` Transfers | Backfill historical Arbitrum `ERC-20/721/1155` transfers data. | [Use template](https://dashboard.quicknode.com/streams/new?network=arbitrum_mainnet\&dataset=block_with_receipts\&start=1\&filter=ZnVuY3Rpb24gc3RyaXBQYWRkaW5nKGxvZ1RvcGljKSB7CiAgICByZXR1cm4gbG9nVG9waWMgPyAnMHgnICsgbG9nVG9waWMuc2xpY2UoLTQwKS50b0xvd2VyQ2FzZSgpIDogJyc7Cn0KCmZ1bmN0aW9uIHBhcnNlU2luZ2xlRGF0YShkYXRhKSB7CiAgICBpZiAoIWRhdGEgfHwgZGF0YSA9PT0gJzB4JykgcmV0dXJuIHsgdG9rZW5JZDogMCwgdmFsdWU6IDAgfTsKICAgIGNvbnN0IGlkSGV4ID0gZGF0YS5zbGljZSgyLCA2NikucmVwbGFjZSgvXjArLywgJycpIHx8ICcwJzsKICAgIGNvbnN0IHZhbHVlSGV4ID0gZGF0YS5zbGljZSg2NikucmVwbGFjZSgvXjArLywgJycpIHx8ICcwJzsKICAgIGNvbnN0IGlkID0gaWRIZXggPT09ICcwJyA_IDAgOiBCaWdJbnQoJzB4JyArIGlkSGV4KTsKICAgIGNvbnN0IHZhbHVlID0gdmFsdWVIZXggPT09ICcwJyA_IDAgOiBCaWdJbnQoJzB4JyArIHZhbHVlSGV4KTsKICAgIHJldHVybiB7IHRva2VuSWQ6IGlkLCB2YWx1ZTogdmFsdWUgfTsKfQoKZnVuY3Rpb24gcGFyc2VCYXRjaERhdGEoZGF0YSkgewogICAgaWYgKCFkYXRhIHx8IGRhdGEubGVuZ3RoIDwgMTMwKSByZXR1cm4geyBpZHM6IFtdLCB2YWx1ZXM6IFtdIH07CiAgICBjb25zdCBpZHNBcnJheU9mZnNldCA9IHBhcnNlSW50KGRhdGEuc2xpY2UoMiwgNjYpLCAxNikgKiAyICsgMjsKICAgIGNvbnN0IHZhbHVlc0FycmF5T2Zmc2V0ID0gcGFyc2VJbnQoZGF0YS5zbGljZSg2NiwgMTMwKSwgMTYpICogMiArIDI7CiAgICBjb25zdCB0b2tlbkNvdW50ID0gKHZhbHVlc0FycmF5T2Zmc2V0IC0gaWRzQXJyYXlPZmZzZXQpIC8gNjQ7CgogICAgY29uc3QgaWRzID0gQXJyYXkuZnJvbSh7IGxlbmd0aDogdG9rZW5Db3VudCB9LCAoXywgaSkgPT4gewogICAgICAgIGNvbnN0IGlkSGV4ID0gZGF0YS5zbGljZShpZHNBcnJheU9mZnNldCArIGkgKiA2NCwgaWRzQXJyYXlPZmZzZXQgKyAoaSArIDEpICogNjQpLnJlcGxhY2UoL14wKy8sICcnKSB8fCAnMCc7CiAgICAgICAgcmV0dXJuIGlkSGV4ID09PSAnMCcgPyAwIDogQmlnSW50KCcweCcgKyBpZEhleCk7CiAgICB9KTsKCiAgICBjb25zdCB2YWx1ZXMgPSBBcnJheS5mcm9tKHsgbGVuZ3RoOiB0b2tlbkNvdW50IH0sIChfLCBpKSA9PiB7CiAgICAgICAgY29uc3QgdmFsdWVIZXggPSBkYXRhLnNsaWNlKHZhbHVlc0FycmF5T2Zmc2V0ICsgaSAqIDY0LCB2YWx1ZXNBcnJheU9mZnNldCArIChpICsgMSkgKiA2NCkucmVwbGFjZSgvXjArLywgJycpIHx8ICcwJzsKICAgICAgICByZXR1cm4gdmFsdWVIZXggPT09ICcwJyA_IDAgOiBCaWdJbnQoJzB4JyArIHZhbHVlSGV4KTsKICAgIH0pOwoKICAgIHJldHVybiB7IGlkcywgdmFsdWVzIH07Cn0KCmZ1bmN0aW9uIG1haW4oZGF0YSkgewogICAgdHJ5IHsKICAgICAgICBpZiAoIWRhdGEgfHwgIWRhdGEuc3RyZWFtRGF0YSkgewogICAgICAgICAgICByZXR1cm4gbnVsbDsKICAgICAgICB9CgogICAgICAgIGNvbnN0IHN0cmVhbURhdGEgPSBBcnJheS5pc0FycmF5KGRhdGEuc3RyZWFtRGF0YSkgPyBkYXRhLnN0cmVhbURhdGEgOiBbZGF0YS5zdHJlYW1EYXRhXTsKICAgICAgICBjb25zdCBlcmMyMFRyYW5zZmVycyA9IFtdOwogICAgICAgIGNvbnN0IGVyYzcyMVRyYW5zZmVycyA9IFtdOwogICAgICAgIGNvbnN0IGVyYzExNTVUcmFuc2ZlcnMgPSBbXTsKCiAgICAgICAgc3RyZWFtRGF0YS5mb3JFYWNoKHN0cmVhbSA9PiB7CiAgICAgICAgICAgIGlmICghc3RyZWFtIHx8ICFzdHJlYW0uYmxvY2sgfHwgIXN0cmVhbS5yZWNlaXB0cykgewogICAgICAgICAgICAgICAgcmV0dXJuOwogICAgICAgICAgICB9CgogICAgICAgICAgICBjb25zdCBibG9ja1RpbWVzdGFtcCA9IHN0cmVhbS5ibG9jay50aW1lc3RhbXAgPyBwYXJzZUludChzdHJlYW0uYmxvY2sudGltZXN0YW1wLCAxNikgKiAxMDAwIDogRGF0ZS5ub3coKTsKCiAgICAgICAgICAgIHN0cmVhbS5yZWNlaXB0cy5mb3JFYWNoKHJlY2VpcHQgPT4gewogICAgICAgICAgICAgICAgaWYgKCFyZWNlaXB0IHx8ICFyZWNlaXB0LmxvZ3MpIHJldHVybjsKCiAgICAgICAgICAgICAgICByZWNlaXB0LmxvZ3MuZm9yRWFjaChsb2cgPT4gewogICAgICAgICAgICAgICAgICAgIGlmICghbG9nIHx8ICFsb2cudG9waWNzIHx8IGxvZy50b3BpY3MubGVuZ3RoID09PSAwKSByZXR1cm47CgogICAgICAgICAgICAgICAgICAgIGlmIChsb2cudG9waWNzWzBdID09PSAnMHhkZGYyNTJhZDFiZTJjODliNjljMmIwNjhmYzM3OGRhYTk1MmJhN2YxNjNjNGExMTYyOGY1NWE0ZGY1MjNiM2VmJykgewogICAgICAgICAgICAgICAgICAgICAgICBpZiAobG9nLnRvcGljcy5sZW5ndGggPT09IDMgJiYgbG9nLmRhdGEgJiYgbG9nLmRhdGEgIT09ICcweCcpIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbnN0IHZhbHVlSGV4ID0gbG9nLmRhdGEuc2xpY2UoMikucmVwbGFjZSgvXjArLywgJycpOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgY29uc3QgdmFsdWUgPSB2YWx1ZUhleCA_IEJpZ0ludCgnMHgnICsgdmFsdWVIZXgpLnRvU3RyaW5nKCkgOiAnMCc7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICBlcmMyMFRyYW5zZmVycy5wdXNoKHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0eXBlOiAnRVJDMjAnLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlbmRlcjogc3RyaXBQYWRkaW5nKGxvZy50b3BpY3NbMV0pLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlY2VpdmVyOiBzdHJpcFBhZGRpbmcobG9nLnRvcGljc1syXSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWU6IHZhbHVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbnRyYWN0OiBsb2cuYWRkcmVzcywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0eEhhc2g6IGxvZy50cmFuc2FjdGlvbkhhc2gsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHhJbmRleDogbG9nLnRyYW5zYWN0aW9uSW5kZXgsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYmxvY2tUaW1lc3RhbXA6IGJsb2NrVGltZXN0YW1wCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB9KTsKICAgICAgICAgICAgICAgICAgICAgICAgfSBlbHNlIGlmIChsb2cudG9waWNzLmxlbmd0aCA9PT0gNCAmJiAoIWxvZy5kYXRhIHx8IGxvZy5kYXRhID09PSAnMHgnKSkgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgY29uc3QgdG9rZW5JZCA9IEJpZ0ludChsb2cudG9waWNzWzNdKS50b1N0cmluZygpOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgZXJjNzIxVHJhbnNmZXJzLnB1c2goewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHR5cGU6ICdFUkM3MjEnLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlbmRlcjogc3RyaXBQYWRkaW5nKGxvZy50b3BpY3NbMV0pLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlY2VpdmVyOiBzdHJpcFBhZGRpbmcobG9nLnRvcGljc1syXSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdG9rZW5JZDogdG9rZW5JZCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjb250cmFjdDogbG9nLmFkZHJlc3MsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHhIYXNoOiBsb2cudHJhbnNhY3Rpb25IYXNoLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHR4SW5kZXg6IGxvZy50cmFuc2FjdGlvbkluZGV4LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJsb2NrVGltZXN0YW1wOiBibG9ja1RpbWVzdGFtcAogICAgICAgICAgICAgICAgICAgICAgICAgICAgfSk7CiAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICB9IGVsc2UgaWYgKGxvZy50b3BpY3NbMF0gPT09ICcweGMzZDU4MTY4YzVhZTczOTc3MzFkMDYzZDViYmYzZDY1Nzg1NDQyNzM0M2Y0YzA4MzI0MGY3YWFjYWEyZDBmNjInKSB7CiAgICAgICAgICAgICAgICAgICAgICAgIGNvbnN0IHsgdG9rZW5JZCwgdmFsdWUgfSA9IHBhcnNlU2luZ2xlRGF0YShsb2cuZGF0YSk7CiAgICAgICAgICAgICAgICAgICAgICAgIGVyYzExNTVUcmFuc2ZlcnMucHVzaCh7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0eXBlOiAnRVJDMTE1NV9TaW5nbGUnLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgb3BlcmF0b3I6IHN0cmlwUGFkZGluZyhsb2cudG9waWNzWzFdKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlbmRlcjogc3RyaXBQYWRkaW5nKGxvZy50b3BpY3NbMl0pLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVjZWl2ZXI6IHN0cmlwUGFkZGluZyhsb2cudG9waWNzWzNdKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRva2VuSWQ6IHRva2VuSWQudG9TdHJpbmcoKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlOiB2YWx1ZS50b1N0cmluZygpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgY29udHJhY3Q6IGxvZy5hZGRyZXNzLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdHhIYXNoOiBsb2cudHJhbnNhY3Rpb25IYXNoLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdHhJbmRleDogbG9nLnRyYW5zYWN0aW9uSW5kZXgsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBibG9ja1RpbWVzdGFtcDogYmxvY2tUaW1lc3RhbXAKICAgICAgICAgICAgICAgICAgICAgICAgfSk7CiAgICAgICAgICAgICAgICAgICAgfSBlbHNlIGlmIChsb2cudG9waWNzWzBdID09PSAnMHg0YTM5ZGMwNmQ0YzBkYmM2NGI3MGFmOTBmZDY5OGEyMzNhNTE4YWE1ZDA3ZTU5NWQ5ODNiOGMwNTI2YzhmN2ZiJykgewogICAgICAgICAgICAgICAgICAgICAgICBjb25zdCB7IGlkcywgdmFsdWVzIH0gPSBwYXJzZUJhdGNoRGF0YShsb2cuZGF0YSk7CiAgICAgICAgICAgICAgICAgICAgICAgIGlkcy5mb3JFYWNoKChpZCwgaW5kZXgpID0-IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGVyYzExNTVUcmFuc2ZlcnMucHVzaCh7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHlwZTogJ0VSQzExNTVfQmF0Y2gnLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9wZXJhdG9yOiBzdHJpcFBhZGRpbmcobG9nLnRvcGljc1sxXSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZnJvbTogc3RyaXBQYWRkaW5nKGxvZy50b3BpY3NbMl0pLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRvOiBzdHJpcFBhZGRpbmcobG9nLnRvcGljc1szXSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdG9rZW5JZDogaWQudG9TdHJpbmcoKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZTogdmFsdWVzW2luZGV4XS50b1N0cmluZygpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbnRyYWN0OiBsb2cuYWRkcmVzcywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0eEhhc2g6IGxvZy50cmFuc2FjdGlvbkhhc2gsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHhJbmRleDogbG9nLnRyYW5zYWN0aW9uSW5kZXgsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYmxvY2tUaW1lc3RhbXA6IGJsb2NrVGltZXN0YW1wCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB9KTsKICAgICAgICAgICAgICAgICAgICAgICAgfSk7CiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfSk7CiAgICAgICAgICAgIH0pOwogICAgICAgIH0pOwoKICAgICAgICBpZiAoIWVyYzIwVHJhbnNmZXJzLmxlbmd0aCAmJiAhZXJjNzIxVHJhbnNmZXJzLmxlbmd0aCAmJiAhZXJjMTE1NVRyYW5zZmVycy5sZW5ndGgpIHsKICAgICAgICAgICAgcmV0dXJuIG51bGw7CiAgICAgICAgfQoKICAgICAgICByZXR1cm4geyAKICAgICAgICAgICAgZXJjMjA6IGVyYzIwVHJhbnNmZXJzLCAKICAgICAgICAgICAgZXJjNzIxOiBlcmM3MjFUcmFuc2ZlcnMsIAogICAgICAgICAgICBlcmMxMTU1OiBlcmMxMTU1VHJhbnNmZXJzCiAgICAgICAgfTsKICAgIH0gY2F0Y2ggKGUpIHsKICAgICAgICBjb25zb2xlLmVycm9yKCdFcnJvciBpbiBtYWluIGZ1bmN0aW9uOicsIGUpOwogICAgICAgIHJldHVybiB7IGVycm9yOiBlLm1lc3NhZ2UgfTsKICAgIH0KfQo) | | Backfill all Uniswap V2/V3 Swaps | Backfill historical Arbitrum Uniswap V2/V3 swaps data. | [Use template](https://dashboard.quicknode.com/streams/new?dataset=receipts\&network=arbitrum_mainnet\&start=169\&filter=Ly8gVXNlIHJlY2VpcHRzIGRhdGFzZXQKLy8gc3RhcnQgYXQgYmxvY2sgMTY5CgpmdW5jdGlvbiBtYWluKGRhdGEpIHsKICAgIHRyeSB7CiAgICAgICAgdmFyIHN0cmVhbURhdGEgPSBkYXRhLnN0cmVhbURhdGE7CiAgICAgICAgdmFyIGZpbHRlcmVkUmVjZWlwdHMgPSBbXTsKICAgICAgICAKICAgICAgICAvLyBTd2FwIHRvcGljcyBmb3IgVW5pc3dhcCBWMiBhbmQgVjMKICAgICAgICBjb25zdCB1bmlzd2FwVjJTd2FwVG9waWMgPSAiMHhkNzhhZDk1ZmE0NmM5OTRiNjU1MWQwZGE4NWZjMjc1ZmU2MTNjZTM3NjU3ZmI4ZDVlM2QxMzA4NDAxNTlkODIyIjsKICAgICAgICBjb25zdCB1bmlzd2FwVjNTd2FwVG9waWMgPSAiMHhjNDIwNzlmOTRhNjM1MGQ3ZTYyMzVmMjkxNzQ5MjRmOTI4Y2MyYWM4MThlYjY0ZmVkODAwNGUxMTVmYmNjYTY3IjsKICAgICAgICAKICAgICAgICAvLyBLbm93biBVbmlzd2FwIFJvdXRlciBhZGRyZXNzZXMKICAgICAgICBjb25zdCB1bmlzd2FwVjJSb3V0ZXJzID0gWwogICAgICAgICAgICAiMHg0NzUyYmE1ZGJjMjNmNDRkODc4MjYyNzZiZjZmZDZiMWMzNzJhZDI0Ii50b0xvd2VyQ2FzZSgpIC8vIFVuaXN3YXAgVjIgUm91dGVyCiAgICAgICAgXTsKICAgICAgICAKICAgICAgICBjb25zdCB1bmlzd2FwVjNSb3V0ZXJzID0gWwogICAgICAgICAgICAiMHhFNTkyNDI3QTBBRWNlOTJEZTNFZGVlMUYxOEUwMTU3QzA1ODYxNTY0Ii50b0xvd2VyQ2FzZSgpIC8vIFVuaXN3YXAgVjMgUm91dGVyCiAgICAgICAgXTsKCiAgICAgICAgY29uc3QgdW5pdmVyc2FsUm91dGVycyA9IFsKICAgICAgICAgICAgIjB4NjhiMzQ2NTgzM2ZiNzJBNzBlY0RGNDg1RTBlNEM3YkQ4NjY1RmM0NSIudG9Mb3dlckNhc2UoKSwKICAgICAgICAgICAgIjB4NUUzMjVlREE4MDY0YjQ1NmY0NzgxMDcwQzA3MzhkODQ5YzgyNDI1OCIudG9Mb3dlckNhc2UoKSAvLyBVbml2ZXJzYWwgJiBvdGhlciBSb3V0ZXJzCiAgICAgICAgXTsKCiAgICAgICAgY29uc3QgYWxsUm91dGVycyA9IG5ldyBTZXQoWy4uLnVuaXN3YXBWMlJvdXRlcnMsIC4uLnVuaXN3YXBWM1JvdXRlcnMsIC4uLnVuaXZlcnNhbFJvdXRlcnNdKTsKICAgICAgICBjb25zdCBzd2FwVG9waWNzID0gbmV3IFNldChbdW5pc3dhcFYyU3dhcFRvcGljLCB1bmlzd2FwVjNTd2FwVG9waWNdKTsKCiAgICAgICAgc3RyZWFtRGF0YS5mb3JFYWNoKHJlY2VpcHQgPT4gewogICAgICAgICAgICBjb25zdCB0b0FkZHJlc3MgPSByZWNlaXB0LnRvLnRvTG93ZXJDYXNlKCk7CiAgICAgICAgICAgIGlmIChhbGxSb3V0ZXJzLmhhcyh0b0FkZHJlc3MpKSB7CiAgICAgICAgICAgICAgICBsZXQgaGFzUmVsZXZhbnRMb2cgPSByZWNlaXB0LmxvZ3Muc29tZShsb2cgPT4gCiAgICAgICAgICAgICAgICAgICAgc3dhcFRvcGljcy5oYXMobG9nLnRvcGljc1swXSkKICAgICAgICAgICAgICAgICk7CiAgICAgICAgICAgICAgICBpZiAoaGFzUmVsZXZhbnRMb2cpIHsKICAgICAgICAgICAgICAgICAgICBmaWx0ZXJlZFJlY2VpcHRzLnB1c2gocmVjZWlwdCk7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICB9KTsKCiAgICAgICAgcmV0dXJuIGZpbHRlcmVkUmVjZWlwdHMubGVuZ3RoID4gMCA_IHsgZmlsdGVyZWRSZWNlaXB0cyB9IDogbnVsbDsKICAgIH0gY2F0Y2ggKGUpIHsKICAgICAgICByZXR1cm4geyBlcnJvcjogZS5tZXNzYWdlIH07CiAgICB9Cn0) | For detailed pricing and timing information, visit our [Streams Backfills: Arbitrum](https://www.quicknode.com/streams/backfills/arbitrum) page. ## Additional Resources * [QuickNode: Arbitrum Chain Page](https://www.quicknode.com/chains/arb) * [QuickNode: Arbitrum Documentation](https://www.quicknode.com/docs/arbitrum) * [QuickNode Builders Guide: Arbitrum Chains](https://www.quicknode.com/builders-guide/tools/arbitrum-orbits-by-arbitrum-foundation) * [QuickNode: Arbitrum Faucet](https://faucet.quicknode.com/arbitrum) --- > For a complete page index, fetch # Reactive Network Reactive Network is an EVM-compatible execution layer designed for decentralized automation across blockchains. It introduces reactive contracts, a new type of smart contract that operates using inversion of control, executing based on event logs from other chains rather than direct user interaction. Unlike traditional contracts that rely on EOAs to initiate function calls, reactive contracts respond to event logs emitted by contracts on other chains. These contracts can independently evaluate conditions and initiate outbound messages or state changes without requiring user transactions. Reactive Network supports fast and cost-efficient execution through a parallelized EVM implementation, optimized for processing large volumes of event-driven computation. ## Reactive Contracts Reactive contracts are standard Solidity contracts deployed on the Reactive Network that follow a specific execution pattern. Instead of exposing functions for users to call directly, these contracts are configurable to: * Monitor specified chains, contracts, and event signatures * Receive and process event logs emitted on those chains * Execute logic conditionally based on the contents of the received logs * Initiate transactions or cross-chain messages in response to valid triggers Each Reactive contract defines a subscription-like configuration for the event it listens to. When a matching log is detected, the contract is executed with structured input data derived from the event, allowing it to perform actions such as: * Writing to the internal state * Sending data to another chain via Reactive's cross-chain infrastructure * Triggering domain-specific workflows (e.g., settlements, governance updates, etc.) Reactive contracts are compatible with the standard EVM toolchain and can be written in Solidity using custom ABIs defined for event-based execution. Execution is trust-minimized: the logs are verifiable, emitted on origin chains, and the Reactive Network enforces that only matching, authentic events trigger contract logic. ## Resources and Contacts * [Reactive Docs](https://dev.reactive.network/) * [Telegram](https://t.me/reactivedevs) * [GitHub](https://github.com/Reactive-Network) * [X (Twitter)](https://x.com/0xReactive) * [Discord](https://discord.com/invite/SaZAfkgZhj) * [Blog](https://blog.reactive.network/) --- > For a complete page index, fetch # Reown (prev. known as WalletConnect > **INFO** — Community member contribution > > Shoutout to [@rohit-710](https://github.com/rohit-710) for contributing the following [third-party document](/for-devs/third-party-docs/contribute.md)! **[Reown](https://reown.com/?utm_source=arbitrum\&utm_medium=docs\&utm_campaign=backlinks)** gives developers the tools to build user experiences that make digital ownership effortless, intuitive, and secure. ## AppKit AppKit is a powerful, free, and fully open-source SDK for developers looking to integrate wallet connections and other Web3 functionalities into their apps on any EVM and non-EVM chain. In just a few simple steps, you can provide your users with wallet access, one-click authentication, social logins, and notifications—streamlining their experience while enabling advanced features like on-ramp functionality, in-app token swaps and smart accounts. ## How to get started with AppKit on Arbitrum Learn how to use Reown AppKit to enable wallet connections and interact with the Arbitrum network. With AppKit, you can provide wallet connections, including email and social logins, on-ramp functionality, smart accounts, one-click authentication, and wallet notifications, all designed to deliver an exceptional user experience. In this tutorial, you will learn how to: 1. Set up Reown AppKit. 2. Configure a wallet connection modal and enable interactions with the Arbitrum network. This guide takes approximately ten minutes to complete. Let’s get started! ### Setup In this section, you'll learn how to set up the development environment to use AppKit with Arbitrum. For this tutorial, we'll be using Next.js, though you can use any other framework compatible with AppKit. > AppKit is available on eight frameworks, including React, Next.js, Vue, JavaScript, React Native, Flutter, Android, iOS, and Unity. Now, let’s create a Next app. In order to do so, please run the command given below: ```bash npx create-next-app@latest appkit-example ``` The above command creates a Next app and sets the name of the Next app as `appkit-example`. #### Install AppKit Now, we need to install AppKit and other dependencies that we need for our app to function as expected. For this tutorial, we will be using “wagmi” as our preferred Ethereum library. However, you can also use [Ethers](https://docs.reown.com/appkit/next/core/installation?platform=ethers). ```bash npm install @reown/appkit @reown/appkit-wagmi-adapter wagmi @tanstack/react-query ``` > You can also use other package managers such as `yarn`, `bun`, `pnpm`, etc. #### Create a new project on Reown Cloud Now, we need to get a project Id from Reown Cloud that we will use to set up AppKit with Wagmi config. Navigate to [cloud.reown.com](https://cloud.reown.com/?utm_source=arbitrum\&utm_medium=docs\&utm_campaign=backlinks) and sign in. If you have not created an account yet, please do so before we proceed. After you have logged in, please navigate to the “**Projects**” section of the Cloud and click on **Create Project**. ![](/img/third-party-reown1.png) Now, enter the name for your project and click on **Continue**. ![](/img/third-party-reown.png) Select the product as “**AppKit**” and click on **Continue**. ![](/img/third-party-reown3.png) Select the framework as “**Next.js**” and click on **Create**. Reown Cloud will now create a new project for you which will also generate a project Id. ![](/img/third-party-reown4.png) You will notice that your project was successfully created. On the top left corner, you will be able to find your Project Id. Please copy that as you will need that later. ![](/img/third-party-reown6.png) ### Build the App using AppKit Before we build the app, let’s first configure our `.env` file. On the root level of your code directory, create a new file named `.env`. Open that file and create a new variable `NEXT_PUBLIC_PROJECT_ID`. You will assign the project Id that you copied in the previous step to this environment variable that you just created. This is what it will look like: ```jsx NEXT_PUBLIC_PROJECT_ID = ``` > **INFO** > > Please make sure you follow the best practices when you are working with secret keys and other sensitive information. Environment variables that start with `NEXT_PUBLIC` will be exposed by your app which can be misused by bad actors. #### Configure AppKit On the root level of your code directory, create a new folder named `config` and within that folder, create a new code file named `config/index.tsx`. Now, paste the code snippet shared below inside the code file, i.e., `config/index.tsx`. ```tsx import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'; import { cookieStorage, createStorage } from 'wagmi'; import { arbitrum, arbitrumSepoliaarbitrum, arbitrumSepolia } from '@reown/appkit/networks'; // Get projectId from https://cloud.reown.com export const projectId = process.env.NEXT_PUBLIC_PROJECT_ID; export const networks = [arbitrum, arbitrumSepolia]; if (!projectId) throw new Error('Project ID is not defined'); // Set up the Wagmi Adapter (config) export const wagmiAdapter = new WagmiAdapter({ storage: createStorage({ storage: cookieStorage, }), ssr: true, networks, projectId, }); export const config = wagmiAdapter.wagmiConfig; ``` So what's happening in the above code? Let's understand it step-by-step: 1. First, we need to import the necessary functions from their respective packages. 1. `WagmiAdapter`: this is used to create a WAGMI configuration which is then initialized to the `wagmiAdapter` 2. `cookieStorage`**,** `createStorage` **:** this provides a storage mechanism using cookies and a function to create custom storage solutions (in this case, using cookies). #### Create the Modal for your app Now, we need to create a context provider to wrap our application in and initialize AppKit. On the root level of your code directory, create a new folder named `context` and within that folder, create a new code file named `context/index.tsx`. Now, paste the code snippet shared below inside the code file, i.e., `context/index.tsx`. ```tsx 'use client'; import { wagmiAdapter, projectId } from '@/config'; import { createAppKit } from '@reown/appkit/react'; import { arbitrum, arbitrumSepolia } from '@reown/appkit/networks'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import React, { type ReactNode } from 'react'; import { cookieToInitialState, WagmiProvider, type Config } from 'wagmi'; // Set up queryClient const queryClient = new QueryClient(); if (!projectId) { throw new Error('Project ID is not defined'); } // Set up metadata const metadata = { //this is optional name: 'appkit-example-arbitrum', description: 'AppKit Example - Arbitrum', url: 'https://arbitrum-app.com', // origin must match your domain & subdomain icons: ['https://avatars.githubusercontent.com/u/179229932'], }; // Create the modal const modal = createAppKit({ adapters: [wagmiAdapter], projectId, networks: [arbitrum, arbitrumSepolia], metadata: metadata, features: { analytics: true, // Optional - defaults to your Cloud configuration }, themeMode: 'light', }); function ContextProvider({ children, cookies }: { children: ReactNode; cookies: string | null }) { const initialState = cookieToInitialState(wagmiAdapter.wagmiConfig as Config, cookies); return ( {children} ); } export default ContextProvider; ``` Let’s understand what is happening in the above code: 1. First, we import the necessary functions from their respective packages. After this, we need to create the modal component for our app. 2. `metadata`: (*optional*)This object contains information about our application that will be used by AppKit. This includes the name of the app, the description, the url and the icons representing our app. 3. `createAppKit`: this is called to initialize the AppKit component, which handles the user interface for connecting to blockchain wallets. The function is configured with various options, such as the app's metadata, theming, and enabling features like analytics and onramp services. 1. `networks`: these are the networks that we want our app to support. So import the chains you want your app to support from `@reown/appkit/network` and assign it to this network parameter. Since we want to enable wallet interactions on the Arbitrum network, we import the Arbitrum mainnet and testnet. You can view the complete list of supported chains [here](https://wagmi.sh/core/api/chains). 4. `WagmiProvider`: Provides blockchain and wallet connection context to the app. 1. `QueryClientProvider`: Provides the React Query context for managing server-state data. Now, let’s create the layout for our app. In `app/layout.tsx`, remove the existing code and paste the code snippet given below. ```tsx import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import './globals.css'; const inter = Inter({ subsets: ['latin'] }); import { headers } from 'next/headers'; // added import ContextProvider from '@/context'; export const metadata: Metadata = { title: 'AppKit Example App', description: 'Powered by Reown', }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { const cookies = headers().get('cookie'); return ( {children} ); } ``` #### Create the UI for your app For our app to have the UI with which your users can interact, you need to set a simple UI and configure the modal. Since, we have already set up AppKit, you can use `` which will serve as a “Connect Wallet” button or you can build your own custom button using the [hooks](https://docs.walletconnect.com/appkit/next/core/hooks) that AppKit provides. Open the `app/page.tsx` file and remove the existing boilerplate code, and then replace it with the code snippet given below. ```tsx 'use client'; import { useAccount } from 'wagmi'; export default function Home() { const { isConnected } = useAccount(); return (
logo
Reown - AppKit + Arbitrum

Examples

Connect your wallet



{isConnected && (

Network selection button

)}
); } ``` The code above uses the AppKit configuration to provide two buttons: one for users to connect their wallet to the app, and another to allow users to switch networks. You can now run the app and test it out. In order to do so, run the command given below. ```bash npm run dev ``` > If you are using alternative package managers, you can try either of these commands: `yarn dev`, or `pnpm dev`, or `bun dev`. ### Conclusion And that’s it! You have now learned how to create a simple app using AppKit that allows users to connect their wallet and interact with the Arbitrum network. **Reown AppKit** is a powerful solution for developers looking to integrate wallet connections and other Web3 functionalities into their apps on any EVM chain. In just a few simple steps, you can provide your users with wallet access, one-click authentication, social logins, and notifications—streamlining their experience while enabling advanced features like on-ramp functionality and smart accounts. By following this guide, you'll quickly get up and running with Reown's AppKit, enhancing your app's user experience and onchain interactions. You can view the [complete code repository](https://github.com/rohit-710/reown-appkit-evm). ### What's Next? If you're wondering how to use Reown for various use cases and build apps with great UX, feel free to check out our other blogs [here](https://reown.com/blog). ### Need help? For support, please join the official [Reown Discord Server](https://discord.com/invite/kdTQHQ6AFQ). You will be able to use Reown AppKit to power end-to-end wallet interactions on your Web3 app deployed on Arbitrum. ## Learn more about Reown: * [Website](https://reown.com/?utm_source=arbitrum\&utm_medium=docs\&utm_campaign=backlinks) * [Blog](https://reown.com/blog?utm_source=arbitrum\&utm_medium=docs\&utm_campaign=backlinks) * [Docs](https://docs.reown.com/?utm_source=arbitrum\&utm_medium=docs\&utm_campaign=backlinks) --- > For a complete page index, fetch # The Graph: index and query Arbitrum data ## The Graph Getting historical data on a smart contract can be frustrating when building a dApp. [The Graph](https://thegraph.com/) provides an easy way to query smart contract data through APIs known as subgraphs, which utilize `GraphQL`. The Graph's infrastructure relies on a decentralized network of indexers, enabling your dApp to become truly decentralized. ## Quick start These subgraphs only take a few minutes to set up and get running. To get started, follow these three steps: 1. [Initialize your subgraph project](#1-initialize-your-subgraph-project) 2. [Deploy & publish](#2-deploy--publish) 3. [Query from your dApp](#sample-query) Pricing: **All developers receive 100K free monthly queries on the decentralized network**. After these free queries, you only pay based on usage at $4 for every 100K queries. Here's a step by step walkthrough: ## 1. Initialize your subgraph project ### Create a subgraph on Subgraph Studio Go to the [Subgraph Studio](https://thegraph.com/studio/) and connect your wallet. Once your wallet is connected, you can begin by clicking "Create a Subgraph". Please choose a good name for the subgraph: this name can't be edited later. It is recommended to use Title Case: "Subgraph Name Chain Name." ![Create a Subgraph](https://lh7-us.googleusercontent.com/docsz/AD_4nXf8OTdwMxlKQGKzIF_kYR7NPKeh9TmWnZBYxb7ft_YbdOdx_VVtbp6PslN7N1KGUzNpIDCmaXppdrllM1cw_J4L8Na03BXOWzJTK1POCve0nkRjQYgWJ60QHAdtQ4Niy83SMM8m0F0f-N-AJj4PDqDPlA5M?key=fnI6SyFgXU9SZRNX5C5vPQ) You will then land on your subgraph's page. All the CLI commands you need will be visible on the right side of the page: ![CLI commands](https://lh7-us.googleusercontent.com/docsz/AD_4nXe3YvCxiOH_LupSWe8zh9AmP-VrV4PlOq3f7Ix6hNlBUYcANUFuLuVIWR74OGiBs0nrugTyT0v3o6RPmTsgHONdv_ZJNWtcDWEkRntXPHlQGFcqmEBa-D6j4aoIPzUKYdOJMVUPu8O3fwjdZ4IaXXZoTzY?key=fnI6SyFgXU9SZRNX5C5vPQ) ### Install the Graph CLI On your local machine, run the following: ```shell npm install -g @graphprotocol/graph-cli ``` ### Initialize your Subgraph You can copy this directly from your subgraph page to include your specific subgraph slug: ```shell graph init --studio ``` You'll be prompted to provide some info on your subgraph like this: ![cli sample](https://lh7-us.googleusercontent.com/docsz/AD_4nXdTAUsUb5vbs3GtCrhKhuXM1xYoqqooYTxw6lfJfYtLJNP8GKVOhTPmjxlM1b6Qpx-pXNVOzRuc8BL12wZXqy4MIj8ja0tp15znfuJD_Mg84SSNj3JpQ4d31lNTxPYnpba4UOzZx8pmgOIsbI7vCz70v9gC?key=fnI6SyFgXU9SZRNX5C5vPQ) Simply have your contract verified on the block explorer, and the CLI will automatically obtain the ABI and set up your subgraph. The default settings will generate an entity for each event. ## 2. Deploy & publish ### Deploy to Subgraph Studio First, run these commands in your terminal ```shell graph codegen graph build ``` Then, invoke these commands to authenticate and deploy your subgraph. You can copy these commands directly from your subgraph's page in Studio to include your specific deploy key and subgraph slug: ```shell graph auth --studio graph deploy --studio ``` You will be asked for a version label. You can enter something like `V0.0.1`, but you're free to choose the format. ### Test your subgraph You can test your subgraph by making a sample query in the playground section. The Details tab will show you an API endpoint. You can use that endpoint to test from your dApp. ![Playground](https://lh7-us.googleusercontent.com/docsz/AD_4nXf3afwSins8_eO7BceGPN79VvwolDxmFNUnkPk0zAJCaUA-3-UAAjVvrMzwr7q9vNYWdrEUNgm2De2VfQpWauiT87RkFc-cVfoPSsQbYSgsmwhyY1-tpPdv2J1H4JAMq70nfWBhb8PszZBFjsbDAaJ5eto?key=fnI6SyFgXU9SZRNX5C5vPQ) ### Publish your subgraph to The Graph's decentralized network Once your subgraph is ready for production, you can publish it to the decentralized network. On your subgraph's page in Subgraph Studio, click on the Publish button: ![publish button](https://edgeandnode.notion.site/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fa7d6afae-8784-4b15-a90e-ee8f6ee007ba%2F2f9c4526-123d-4164-8ea8-39959c8babbf%2FUntitled.png?table=block\&id=37005371-76b4-4780-b044-040a570e3af6\&spaceId=a7d6afae-8784-4b15-a90e-ee8f6ee007ba\&width=1420\&userId=\&cache=v2) Before you can query your subgraph, Indexers need to begin serving queries on it. In order to streamline this process, you can curate your own subgraph using `$GRT`. When publishing, you'll see the option to curate your subgraph. As of May 2024, it is recommended that you curate your own subgraph with at least 3,000 `GRT` to ensure that it is indexed and available for querying as soon as possible. ![Publish screen](https://lh7-us.googleusercontent.com/docsz/AD_4nXerUr-IgWjwBZvp9Idvz5hTq8AFB0n_VlXCzyDtUxKaCTANT4gkk-2O77oW-a0ZWOh3hnqQsY7zcSaLeCQin9XU1NTX1RVYOLFX9MuVxBEqcMryqgnGQKx-MbDnOWKuMoLBhgyVWQereg3cdWtCPcTQKFU?key=fnI6SyFgXU9SZRNX5C5vPQ) ## 3. Query your Subgraph Congratulations! You can now query your subgraph on the decentralized network! You can start querying any subgraph on the decentralized network by passing a `GraphQL` query into the subgraph's query URL, which can be found at the top of its Explorer page. Here's an example from the [CryptoPunks Ethereum subgraph](https://thegraph.com/explorer/subgraphs/HdVdERFUe8h61vm2fDyycHgxjsde5PbB832NHgJfZNqK) by Messari: ![Query URL](https://lh7-us.googleusercontent.com/docsz/AD_4nXebivsPOUjPHAa3UVtvxoYTFXaGBao9pQOAJvFK0S7Uv0scfL6TcTVjmNCzT4DgsIloAQyrPTCqHjFPtmjyrzoKkfSeV28FjS32F9-aJJm0ILAHey2gqMr7Seu4IqPz2d__QotsWG3OKv2dEghiD74eypzs?key=fnI6SyFgXU9SZRNX5C5vPQ) The query URL for this subgraph is: ```shell https://gateway-arbitrum.network.thegraph.com/api/**[api-key]**/subgraphs/id/HdVdERFUe8h61vm2fDyycHgxjsde5PbB832NHgJfZNqK ``` Now, you simply need to fill in your own API Key to start sending `GraphQL` queries to this endpoint. ### Getting your own API key ![API keys](https://lh7-us.googleusercontent.com/docsz/AD_4nXdz7H8hSRf2XqrU0jN3p3KbmuptHvQJbhRHOJh67nBfwh8RVnhTsCFDGA_JQUFizyMn7psQO0Vgk6Vy7cKYH47OyTq5PqycB0xxLyF4kSPsT7hYdMv2MEzAo433sJT6VlQbUAzgPnSxKI9a5Tn3ShSzaxI?key=fnI6SyFgXU9SZRNX5C5vPQ) In Subgraph Studio, you'll see the "API Keys" menu at the top of the page. Here, you can create API Keys. ## Appendix ### Sample query This query shows the most expensive CryptoPunks sold. ```graphql { trades(orderBy: priceETH, orderDirection: desc) { priceETH tokenId } } ``` Passing this into the query URL returns this result: ```graphql { "data": { "trades": [ { "priceETH": "124457.067524886018255505", "tokenId": "9998" }, { "priceETH": "8000", "tokenId": "5822" }, // ... ``` 💡 Trivia: Looking at the top sales on [CryptoPunks website](https://cryptopunks.app/cryptopunks/topsales) it looks like the top sale is Punk #5822, not #9998. Why? Because they censored the flash-loan sale that happened. ### Sample code ```js const axios = require('axios'); const graphqlQuery = `{ trades(orderBy: priceETH, orderDirection: desc) { priceETH tokenId } }`; const queryUrl = 'https://gateway-arbitrum.network.thegraph.com/api/[api-key]/subgraphs/id/HdVdERFUe8h61vm2fDyycHgxjsde5PbB832NHgJfZNqK'; const graphQLRequest = { method: 'post', url: queryUrl, data: { query: graphqlQuery, }, }; // Send the `GraphQL` query axios(graphQLRequest) .then((response) => { // Handle the response here const data = response.data.data; console.log(data); }) .catch((error) => { // Handle any errors console.error(error); }); ``` ### Additional resources: * To explore all the ways you can optimize & customize your subgraph for better performance, read more about [creating a subgraph here](https://thegraph.com/docs/en/developing/creating-a-subgraph/). * You can find more information in our article about [querying data from your subgraph](https://thegraph.com/docs/en/querying/querying-the-graph/). --- > For a complete page index, fetch # Venly Tools <> Arbitrum ## [Venly](https://venly.io/) Venly is a developer platform, designed to streamline digital asset management. Known for its performance and strong security features, Venly’s non-custodian model ensures you retain complete ownership and control of your assets, distinguishing it from other platforms. At Venly, the core principles guide their commitment to you: * **Security First**: Venly prioritizes the highest level of security for your assets and operations. * **Developer-Centric**: Venly's intuitive tools and resources are designed to help developers succeed. * **Optimal Performance**: Venly guarantees consistent high performance with a focus on efficiency and reliability. * **Innovation**: They are dedicated to providing solutions, staying at the forefront of technology. The [Venly](https://venly.io/) platform is anchored by three main pillars: **Digital Wallets, Digital Assets, and Payments**, each is integrated to enhance your blockchain experience. ```text Venly Tools │ ├── Digital Wallets: Secure & scalable SSS-based wallets. │ ├── Digital Assets: API solutions for digital assets. │ ├── Payments: Customizable payment forms for fiat. │ ├── Gaming SDK: │ │ │ ├── Unity: C# SDK │ │ │ └── Unreal Engine: C++ SDK │ └── Integrations: │ ├── Zapier: no-code NFT minting with zaps. │ ├── Shopify: Selling NFTs made easy. │ └── SiteManager: Create mint pages in minutes, all no-code. ``` ### [Digital Wallets](https://docs.venly.io/docs/wallet-api-overview) Secure and scalable SSS-based wallets with key management custody digital assets. The Venly security protocol redefines private key security, never gathering a private key as one whole, eliminating risk. Venly customers use their wallets for a range of operations, such as treasury, trading, cold storage, royalties, NFTs, smart contracts, user wallets, and other digital assets. ### [Digital Assets](https://docs.venly.io/docs/nft-api-overview) Tokenization is based on industry standards and is secured by several code and security audits. The Venly platform facilitates no-code and API solutions to manage, transfer, and gather information on different token asset classes, such as **ERC-20**, **ERC-721**, and **ERC-1155**, which customers use in industries such as Finance, E-commerce, and Gaming. ### [Payments](https://docs.venly.io/docs/pay-api-overview) With PAY, the Venly platform offers a low-code payment integration that creates a customizable form for collecting payments. You can embed Pay directly on your website or redirect customers to a hosted payment page. It offers a range of payment methods, from credit cards to PayPal, Apple Pay, Google Pay, instant bank transfers, and more, enabling customers to choose their preferred option. ## Product Specific Documentation | Category | Product | Documentation | | --------------- | ------------------------------- | ------------------------------------------------------------------------------ | | Digital Wallets | Widget | [API Reference](https://venly.readme.io/docs/product-overview) | | Digital Wallets | Wallet API | [API Reference](https://venly.readme.io/reference/viewwallet) | | Digital Assets | NFT API | [API Reference](https://docs.venly.io/reference/getcontracts-1) | | Digital Assets | Shopify NFT Minting Application | [App Store](https://apps.shopify.com/partners/arkane-network1) | | Digital Assets | Zapier Integration | [Documentation](https://docs.venly.io/docs/zapier-integration) | | Digital Assets | SiteManager | [Documentation](https://docs.venly.io/docs/sitemanager) | | Payments | Venly PAY | [API Reference](https://docs.venly.io/docs/pay-api-overview) | | Gaming SDK | Unity | [Documentation](https://docs.venly.io/docs/getting-started-with-unity) | | Gaming SDK | Unreal Engine | [Documentation](https://docs.venly.io/docs/getting-started-with-unreal-engine) | # [Venly](https://venly.io/): Arbitrum Venly supports the Arbitrum chain on its Wallet API which allows you to create wallets on the Arbitrum chain. You can send and receive funds to and from Arbitrum wallets directly through the Wallet API, enabling integration with applications using Arbitrum. ## Wallet API The Wallet API allows developers to interact with blockchain networks and offer wallet functionality to their users without having to build everything from scratch. This can include features like account creation, transaction management, balance inquiries, and more. * Welcome your users with custom wallet branding. You can customize the user interface to your requirements. * You are completely in charge of the wallet user experience to optimize user conversion. Get total freedom with regard to UX and asset management with the Venly Wallet API. * You and your users have complete control over digital assets without any third-party interference. Securely manage wallets with complete autonomy and privacy. * In the event of loss of login credentials, you and your users can recover access to wallets with a security code or biometric verification. ## Key features | Features | Description | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Wallet management | Developers can use the API to create, manage, and secure wallets for their users. | | Transaction services | The API can enable the initiation and monitoring of blockchain transactions. | | Token support | It may allow the handling of various tokens and assets on supported blockchain networks. | | Blockchain interactions | Developers can integrate functionalities like reading data from the blockchain or writing data to it, along with creating and interacting with smart contracts. | | Security features | The API might offer features to enhance the security of user funds and transactions. | | User experience enhancement | It can contribute to a smoother and more user-friendly interaction with blockchain applications. | | Multi-blockchain support | Venly supports multiple blockchain networks, allowing developers to offer wallets for different cryptocurrencies. | ## Creating an Arbitrum wallet ### Prerequisites 1. You need a Venly business account. If you don't have one, register on the [Developer Portal](https://portal.venly.io), or follow the [Getting Started with Venly](https://venly.readme.io/docs/getting-started) guide. 2. You need your client ID and client secret which can be obtained from the [Portal](https://portal.venly.io/). 3. You need a bearer token to authenticate API calls. Click [here](https://docs.venly.io/docs/authentication) to read how to authenticate. ### Request Endpoint: [reference](https://docs.venly.io/reference/createwallet) ```https POST /api/wallets ``` #### Header params | Parameter | Param type | Value | Description | | ---------------- | ---------- | ---------- | --------------------------------------------------------------------------------------------- | | `Signing-Method` | Header | `id:value` | `id`: This is the ID of the signing method. `value`: This is the value of the signing method. | #### Body params | Parameter | Param type | Description | Data type | Mandatory | | ------------ | ---------- | ------------------------------------------------------ | --------- | --------- | | `secretType` | Body | The blockchain on which to create the wallet | String | ✅ | | `userId` | Body | The ID of the user who you want to link this wallet to | String | ❌ | ### Request body ```json { "secretType": "ARBITRUM", "userId": "9cf9228e-1f2b-4940-9508-4335064cbc76" } ``` ### Response body > Wallet created! The wallet has been created and linked to the specified user (`userId`). ```json { "success": true, "result": { "id": "590f7276-2886-475c-a2d6-a28421f8f367", "address": "0xADc25e8A385213Fd820bc17Aa799076688f9fBd5", "walletType": "API_WALLET", "secretType": "ARBITRUM", "createdAt": "2024-06-05T11:19:12.038340492", "archived": false, "description": "Elegant Moose", "primary": false, "hasCustomPin": false, "userId": "9cf9228e-1f2b-4940-9508-4335064cbc76", "custodial": false, "balance": { "available": true, "secretType": "ARBITRUM", "balance": 0, "gasBalance": 0, "symbol": "ETH", "gasSymbol": "ETH", "rawBalance": "0", "rawGasBalance": "0", "decimals": 18 } } } ``` ## Transferring Arbitrum Tokens ### Request Endpoint: [reference](https://docs.venly.io/reference/executetransaction_1) ```http POST /api/transactions/execute ``` #### Header params | Parameter | Param type | Value | Description | | ---------------- | ---------- | ---------- | --------------------------------------------------------------------------------------------- | | `Signing-Method` | Header | `id:value` | `id`: This is the ID of the signing method. `value`: This is the value of the signing method. | #### Body params | Parameter | Param Type | Description | Data Type | Mandatory | | ------------------------------- | ---------- | ------------------------------------------------------------------ | --------- | --------- | | `transactionRequest` | Body | This object includes the transaction information | Object | ✅ | | transactionRequest.`type` | Body | This will be **TRANSFER** | String | ✅ | | transactionRequest.`walletId` | Body | The `id` of the wallet that will initiate the transaction | String | ✅ | | transactionRequest.`to` | Body | Destination Address (can be a blockchain address or email address) | String | ✅ | | transactionRequest.`secretType` | Body | On which blockchain the transaction will be executed | String | ✅ | | transactionRequest.`value` | Body | The amount you want to transfer | Integer | ✅ | ### Request Body: ```json { "transactionRequest": { "type": "TRANSFER", "walletId": "590f7276-2886-475c-a2d6-a28421f8f367", "to": "0x1588aCD59c9baF27C1b777eAa71A67d6b6024077", "value": "0.0005", "secretType": "ARBITRUM" } } ``` ### Response Body: > The coins were successfully transferred! ```json { "success": true, "result": { "id": "34d51bb3-c963-486d-856e-1e3f12638e3d", "transactionHash": "0x804d14bcda10628e61e7ae9085ecad63eafea09d3fdb3cb4ec8cb8dc312dc5b7" } } ``` ### Next Steps > Ready to try it out? Click to read the [getting started guide for Wallet API.](https://docs.venly.io/docs/wallet-api-getting-started) --- > For a complete page index, fetch # What is the Webacy Risk Data Network? [Webacy](https://webacy.com/) is a risk data network that helps wallets and applications protect their users against scams, hacks, and mistakes across the blockchain. Wallets, protocols, and applications use Webacy throughout their user experience: ## Address trust and safety * Assess the safety of interacting with a given address (any address: EOA, smart contract, token, etc.). Screen for denylists, sanctioned addresses, malicious behavior, and other potential flags * Analyze smart contract code in real-time * Filter spam and sybil addresses ## Connected wallets * Block sanctioned addresses and wallets involved in malicious behavior * Educate users with a Wallet Safety Score * Delight users by enabling additional features or providing additional value * Display open approvals and the risks associated ## Before a transaction * Block harmful apps and links * Review address trust and safety prior to signature * Protect users from interacting with malicious smart contracts ## Monitoring and Notifications * Monitor all onchain activity associated with your protocol or smart contracts * Enable wallet monitoring and flag for risky transactions * Proactively notify users (or be notified) of any potentially risk activity involved with a given address ## Get started with Webacy Start building in minutes: * Reach out to for an API key * Check out the[ Quick Start Integration Guide](https://docs.webacy.com/api-embedded-safety/quick-start-integration-guide) ## APIs [Webacy’s APIs](https://www.webacy.com/safetyscore) are REST-based APIs that expose your platform to Webacy's Risk Engine and Wallet Watch Notifications Platform. With over 15+ data providers, along with their data analytics and algorithms, Webacy has the broadest risk coverage across the blockchain ecosystem. From compliance and regulatory data to social engineering scams and crowdsourced reports, they process millions of monthly signals, updating their models with the latest and most up-to-date information. For detailed technical documentation and to begin testing the APIs directly, visit their [technical documentation](https://webacy.readme.io/reference/webacy-api-overview-pre-release). Available APIs and corresponding use cases include: ### Threat risks API This API indicates if a given address is a risk or a threat to others. It returns risk data associated with the supplied address. It flags if the address appears in any sanctioned databases, has been historically flagged as malicious, is associated with a scam smart contract, and so on. It also includes filtering for spam/sybil signals. Some common use cases for this endpoint include: * Filtering addresses for spam * Blocking high-risk addresses from utilizing your service * Presenting high-risk addresses to others as potentially risky to interact with * Protecting your platform by restricting high-risk addresses ### Approval risks API This API returns a list of approvals for a given address and the associated risk of the spender for that approval. Approvals are commonplace in crypto—now you know which ones put you at risk. Check out your open approvals [here](https://dapp.webacy.com/?mode=approvals). If you're a wallet interested in native revoke and approval risk scoring, [reach out to Webacy](https://docs.webacy.com/other/contact-us). ### Transaction risks API This API returns risk data for a given transaction. Pass in any transaction hash, and the API will return a risk score result that incorporates counterparty EOA risk profiles, address risk, involved asset smart contract risk, and more. Some common use cases for this endpoint include: * Understanding the historical behavior of an address * Providing data to give recommendations about onchain activity * Gaining insight into a particular transaction or action * Flagging previously unknown activity that was potentially at risk ### Exposure risk API The original Webacy Safety Score, this API returns a 'risk profile' or 'exposure risk' of a given address. This indicates the exposure the address has to risky activity through historical transactions, behavior, and owned assets. This endpoint **does not** assess whether the supplied address is a risk to others (Threat Risk). Instead, it assesses whether the supplied address is **at risk** from others. Some common use cases for this endpoint include: * Gaining a holistic understanding of a client or personal wallet * Enabling recommendations and analysis on past behavior * Assessing common traits of a user base * Determining types of users to better serve them * Triggering warnings to internal teams or external users based on changes in risk profile based on ongoing activity * Understanding the behavioral activity of a user base Check out your risk exposure [here](https://dapp.webacy.com/risk-score). ### Contract risk API This API returns a contract risk analysis for a given contract address. The on-demand analysis leverages multiple techniques, such as fuzzing, static analysis, and dynamic analysis, for real-time smart contract scanning. Some common use cases for this endpoint include: * Scanning contracts before listing them on your site * Verifying that you are not promoting malicious contracts * Checking a contract before interacting with it * Reviewing code as you build * Assessing your contracts before submitting them for a formal audit process ### URL risk Given a URL, this endpoint analyzes its safety. It helps you determine if a given link is a phishing scam, sending you to a dangerous place, or is otherwise malicious. Some common use cases for this endpoint include: * Assessing the safety of a dapp/website * Warning your end-users from interacting with a potentially malicious website * Blocking websites ### Wallet watch API These APIs enable you to register users to Webacy's real-time notification infrastructure. If you're interested in setting up your own private instance with custom messaging and triggers, [contact us](https://docs.webacy.com/other/contact-us). --- > For a complete page index, fetch # Quickstart - Zerion API for Arbitrum **[Zerion API](https://zerion.io/api?utm_source=arbitrum-docs\&utm_medium=partner-docs)** is an enterprise-grade wallet data API that provides token balances, DeFi positions, NFTs, and transaction history on Arbitrum, other major EVM chains, and Solana. It powers the [Zerion app](https://zerion.io) used by millions of users, delivering sub-second responses through a unified schema across all supported chains—no per-chain parsing or custom logic needed. **Use Zerion API if you need:** * Wallet portfolios, token balances, and price data across Arbitrum and 50+ other chains * Decoded DeFi positions across 8,000+ protocols (lending, staking, LPs) * Enriched transaction history formatted for UIs or data exports * NFT holdings with metadata, floor prices, and media * Real-time transaction notifications via webhooks > **[Get a free API key to start building on Arbitrum](https://dashboard.zerion.io/?utm_source=arbitrum-docs\&utm_medium=partner-docs)** ## APIs The Zerion API enables developers to quickly access structured onchain data with consistent response schemas across all supported chains. All endpoints support Arbitrum alongside every other supported blockchain. > **INFO** — Endpoints > > Check out the [Zerion API Reference](https://developers.zerion.io?utm_source=arbitrum-docs\&utm_medium=partner-docs) to see all available endpoints. ### Wallet portfolio API * **Features:** Total wallet value, individual token positions, and portfolio breakdown across all supported chains. Prices in USD and other major currencies. * **Use cases:** Wallets, portfolio trackers, accounting dashboards, airdrop snapshots. ### DeFi positions API * **Features:** Decoded and normalized positions across 8,000+ DeFi protocols—lending, staking, LPs, and more. Unified schema eliminates per-protocol parsing. * **Use cases:** Portfolio trackers, DeFi analytics, risk monitoring, reward calculators. ### Transaction history API * **Features:** Enriched transaction feeds decoded into human-readable actions (trades, transfers, approvals). Filterable by type, chain, or date. * **Use cases:** In-app transaction receipts, accounting and tax tools, activity feeds. ### NFT API * **Features:** Complete NFT portfolio data, including metadata, floor prices, and media across chains. * **Use cases:** NFT galleries, marketplaces, and token gating. ### Fungible tokens API * **Features:** Token search, real-time and historical prices, market cap charts, and token metadata. * **Use cases:** Price feeds, market data dashboards, token explorers. ### Webhooks * **Features:** Real-time transaction notifications without polling. Subscribe to wallet activity and get instant alerts. * **Use cases:** Notifications, monitoring, automated workflows. ## Quickstart ### 1. Get your API key Sign up at the [Zerion Dashboard](https://dashboard.zerion.io/?utm_source=arbitrum-docs\&utm_medium=partner-docs) to get a free API key. ### 2. Make your first request The Zerion API uses HTTP Basic Authentication with your API key as the username and an empty password. **cURL:** ```bash curl --request GET \ --url https://api.zerion.io/v1/wallets/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045/portfolio \ --header 'accept: application/json' \ --header 'authorization: Basic YOUR_BASE64_ENCODED_API_KEY' ``` **JavaScript:** ```javascript const apiKey = 'YOUR_API_KEY'; const address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; const response = await fetch(`https://api.zerion.io/v1/wallets/${address}/portfolio`, { headers: { accept: 'application/json', authorization: `Basic ${btoa(apiKey + ':')}`, }, }); const data = await response.json(); console.log(data); ``` **Python:** ```python import requests from base64 import b64encode api_key = "YOUR_API_KEY" address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" headers = { "accept": "application/json", "authorization": f"Basic {b64encode(f'{api_key}:'.encode()).decode()}" } response = requests.get( f"https://api.zerion.io/v1/wallets/{address}/portfolio", headers=headers ) print(response.json()) ``` ### 3. Explore more endpoints * [Wallet fungible positions](https://developers.zerion.io/api-reference/wallets/get-wallet-fungible-positions?utm_source=arbitrum-docs\&utm_medium=partner-docs) — every token a wallet holds with quantities and USD values * [Wallet transactions](https://developers.zerion.io/api-reference/wallets/get-wallet-transactions?utm_source=arbitrum-docs\&utm_medium=partner-docs) — decoded transaction history * [Wallet DeFi positions](https://developers.zerion.io/recipes/defi-positions#get-a-wallets-defi-positions) — active DeFi positions across protocols ## Get started * **[API Key](https://dashboard.zerion.io/?utm_source=arbitrum-docs\&utm_medium=partner-docs)**: sign up for free * **[Docs](https://developers.zerion.io?utm_source=arbitrum-docs\&utm_medium=partner-docs)**: full API reference and guides * **[Quickstart](https://developers.zerion.io/quickstart?utm_source=arbitrum-docs\&utm_medium=partner-docs)**: step-by-step getting started guide * **[Supported Blockchains](https://developers.zerion.io/reference/supported-blockchains?utm_source=arbitrum-docs\&utm_medium=partner-docs)**: full list of supported chains, including Arbitrum --- > For a complete page index, fetch # ZeroDev Smart Account Integration Guide for Arbitrum ## ZeroDev smart account Gas fees are one of the biggest barriers to mainstream adoption. ZeroDev, built by Offchain Labs (the creators of Arbitrum), solves this problem by enabling gasless transactions on Arbitrum through sponsored User Operations (UserOps). These are powered by our `ERC-4337` compliant Kernel Smart Account. With ZeroDev, you can: * Eliminate gas fees for your users by sponsoring their transactions. * Use plugins for advanced features like Passkey support, fine-grained permissions, and Session Keys. * Improve user experience with onboarding using Social Login (email, social media, passkey). * Support `EIP-7702` for enhanced smart account functionality, allowing EOAs to gain smart account features while keeping their existing address. ## Objectives After you complete this guide, you'll be able to: * Configure a ZeroDev project with the latest V3 RPC endpoint and gas sponsorship policies. * Set up the necessary clients using the Core SDK (@zerodev/sdk) and viem. * Implement smart account creation and send sponsored User Operations on Arbitrum using KERNEL\_V3\_3. ## Prerequisites: Setting up ZeroDev for sponsorship To enable gasless transactions, you need a ZeroDev Project ID and a configured gas sponsorship policy. Quick Reference Examples: For complete, runnable code examples refer to these scripts: * [Batch Transactions Example](https://github.com/zerodevapp/zerodev-examples/tree/main/batch-transactions) * [EIP-7702 Example](https://github.com/zerodevapp/zerodev-examples/blob/main/7702/7702.ts) ### Step 1. Create your ZeroDev account and project configuration 1. Sign up or log in to the [ZeroDev Dashboard](https://dashboard.zerodev.app/). 2. If you have an existing project, you can simply enable Arbitrum One or Arbitrum Sepolia for it—you don't need to create a new project for every chain. ### Step 2. Configure a gas sponsorship policy To manage your gas costs, you must set up a gas sponsorship policy: 1. Navigate to the **Gas Policies** section within your project dashboard. 2. Create a new policy. Without a policy, ZeroDev won't sponsor any transactions. 3. Choose between post-pay (credit card) or pre-pay (gas credits). ### Step 3. Obtain the ZeroDev RPC URL The official V3 endpoint is the preferred method for connecting to ZeroDev infrastructure. `https://rpc.zerodev.app/api/v3{YOUR_PROJECT_ID}/chain/{ARBITRUM_CHAIN_ID}` * Replace `{YOUR_PROJECT_ID}` with the ID from your dashboard. * Replace `{ARBITRUM_CHAIN_ID}` with 42161 for Mainnet or 421614 for Sepolia. ## Send gasless transactions for your users This section uses the Core SDK (@zerodev/sdk) and viem to send a sponsored transaction. 1. Install dependencies ```shell # Using npm npm install @zerodev/sdk @zerodev/ecdsa-validator viem ``` 2. Set up clients and the Kernel smart account ```typescript import { createKernelAccount, createKernelAccountClient, createZeroDevPaymasterClient } from '@zerodev/sdk'; import { getEntryPoint, KERNEL_V3_3 } from '@zerodev/sdk/constants'; import { signerToEcdsaValidator } from '@zerodev/ecdsa-validator'; import { http, createPublicClient, parseAbi } from 'viem'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { arbitrumSepolia } from 'viem/chains'; // --- Configuration --- const ZERODEV_PROJECT_ID = 'YOUR_ZERODEV_PROJECT_ID'; const ARBITRUM_CHAIN_ID = 421614; // Arbitrum Sepolia const ZERODEV_RPC = `https://rpc.zerodev.app/api/v3/${ZERODEV_PROJECT_ID}/chain/${ARBITRUM_CHAIN_ID}`; // 1. Setup Public Client const chain = arbitrumSepolia; const publicClient = createPublicClient({ transport: http(), // Viem natively supports Arbitrum chain, }); // 2. Construct a Signer EOA const privateKey = generatePrivateKey(); const signer = privateKeyToAccount(privateKey); const entryPoint = getEntryPoint('0.7'); // 3. Construct the Paymaster Client const zerodevPaymaster = createZeroDevPaymasterClient({ chain, transport: http(ZERODEV_RPC), }); // 4. Construct the Kernel Smart Account const ecdsaValidator = await signerToEcdsaValidator(publicClient, { signer, entryPoint, kernelVersion: KERNEL_V3_3, }); const account = await createKernelAccount(publicClient, { entryPoint, plugins: { sudo: ecdsaValidator, }, kernelVersion: KERNEL_V3_3, }); // 5. Construct the Kernel Account Client const kernelClient = createKernelAccountClient({ account, chain, bundlerTransport: http(ZERODEV_RPC), client: publicClient, paymaster: { getPaymasterData: (userOperation) => { return zerodevPaymaster.sponsorUserOperation({ userOperation }); }, }, }); ``` 3. Send a gasless transaction (User Operation) ```typescript const targetContractAddress = '0x...'; // Your contract const contractABI = parseAbi(['function setValue(uint256 newValue) public']); const callData = await kernelClient.account.encodeCalls([ { to: targetContractAddress, value: 0n, data: kernelClient.account.encodeFunctionData({ abi: contractABI, functionName: 'setValue', args: [42n], }), }, ]); const userOpHash = await kernelClient.sendUserOperation({ callData: callData, }); const receipt = await kernelClient.waitForUserOperationReceipt({ hash: userOpHash, }); console.log('Transaction Hash:', receipt.receipt.transactionHash); ``` ## Batching transactions Kernel allows you to batch multiple transactions into a single sponsored User Operation. ```typescript const userOpHashBatch = await kernelClient.sendUserOperation({ callData: await kernelClient.account.encodeCalls([ { to: targetContractAddress, value: 0n, data: '...' }, { to: targetContractAddress, value: 0n, data: '...' }, ]), }); ``` ## Conclusion You've successfully integrated ZeroDev and implemented gasless, batched transactions on Arbitrum. This configuration, using the V3 RPC and Kernel V3.3, ensures a high-performance, gas-free experience for your users. ## FAQ #### What is the recommended way to implement account abstraction (AA) on Arbitrum? ZeroDev is the preferred smart account infrastructure for the Arbitrum ecosystem. Now part of Offchain Labs (the creators of Arbitrum), ZeroDev provides a vertically integrated stack that allows developers to build invisible wallet experiences. By using ZeroDev's `ERC-4337` and `EIP-7702` compliant Kernel accounts, developers can: * Eliminate gas friction by sponsoring 100% of user gas fees via the ZeroDev Paymaster. * Simplify onboarding by allowing users to log in with familiar social accounts (Google, Facebook) or Passkeys (FaceID/TouchID), removing the need for seed phrases. * Enable one-click actions using Session Keys and Batching to combine complex multi-step transactions (like Approve + Swap) into a single signature. * Benefit from infrastructure that is built and maintained by the same team that scales the Arbitrum network, ensuring maximum reliability and performance. For technical integration, visit the [ZeroDev Documentation](https://docs.zerodev.app/). ## Connect with ZeroDev Need further help? Check out all the ways you can reach ZeroDev for further questions and support: * Visit ZeroDev's official website at [zerodev.app](https://zerodev.app) * Read [Developer Docs](http://docs.zerodev.app/) * For assistance, contact the ZeroDev team at * Follow [ZeroDev on X](https://x.com/zerodev_app) * Connect with [ZeroDev on LinkedIn](https://www.linkedin.com/company) --- > For a complete page index, fetch # Troubleshooting: Building Arbitrum dApps ### How does gas work on Arbitrum? Fees on Arbitrum chains are collected on L2 in the chains' native currency (**ETH** on both Arbitrum One and Nova). A transaction fee is comprised of both an L1 and an L2 component: The L1 component is meant to compensate the Sequencer for the cost of posting transactions on L1 (but no more). (See [L1 Pricing](https://developer.arbitrum.io/arbos/l1-pricing).) The L2 component covers the cost of operating the L2 chain; it uses Geth for gas calculation and thus behaves nearly identically to L1 Ethereum. One difference is that, unlike on Ethereum, Arbitrum chains enforce a gas price floor. See the [chain info page](https://docs.arbitrum.io/for-devs/dev-tools-and-resources/chain-info#chain-parameters) for current values. L2 Gas price adjusts responsively to chain congestion, ala EIP-1559. ### I tried to create a retryable ticket but the transaction reverted on L1. How can I debug the issue? Creation of retryable tickets can revert with one of these custom errors: 1. **`InsufficientValue`**: not enough gas included in your L1 transaction's callvalue to cover the total cost of your retryable ticket; i.e., `msg.value < (maxSubmissionCost + l2CallValue + gasLimit * maxFeePerGas)`. Note that your L1 transaction's callvalue must cover this full cost. See [Retryable Tickets Lifecycle](https://docs.arbitrum.io/arbos/l1-to-l2-messaging#submission) for more information. 2. **`InsufficientSubmissionCost`\*\***:\*\* The provided submission cost isn't high enough to create your retryable ticket. 3. **`GasLimitTooLarge`\*\***:\*\* provided gas limit is greater than 2^64 4. **`DataTooLarge`**: provided data is greater than 117.964 KB (90% of Geth's 128 KB transaction size limit). To figure out which error caused your transaction to revert, we recommend using Etherscan's Parity VM trace support (Tenderly is generally a useful debugging tool; however, it can be buggy when it comes to custom Geth errors). Use the following link to view the Parity VM trace of your failed transaction (replacing the `tx-hash` with your own, and using the appropriate etherscan root url): To find out the reversion error signature, go to the **Raw Traces** tab, and scroll down to find the last **Subtrace** where your transaction was reverted. Then find **Output** field of that subtrace. (In the above example, the desirable **Output** is: `0xfadf238a0000000000000000000000000000000000000000000000000000c4df7e2903b00000000000000000000000000000000000000000000000000000a39a1d002808`) The first four bytes of the output are the custom error signature; in our example, it is `0xfadf238a`. To let's find out which custom error this signature represents, we can use this handy tool by Samzcsun:   Checking `0xfadf238a` gives us `InsufficientSubmissionCost(uint256,uint256)`. ### How is the L1 portion of an Arbitrum transaction's gas fee computed? The L1 fee that a transaction is required to pay is determined by compressing its data with Brotli and multiplying the size of the result (in bytes) by ArbOS's current calldata price. The calldata price value can be queried via the `getPricesInWei` method of the `ArbGasInfo` precompile. You can find more information about gas calculations in [Understanding Arbitrum: 2-Dimensional Fees](https://medium.com/offchainlabs/understanding-arbitrum-2-dimensional-fees-fd1d582596c9) and [How to Estimate Gas in Arbitrum](https://developer.arbitrum.io/devs-how-tos/how-to-estimate-gas). ### What is a retryable ticket's "submission fee"? How can I calculate it? What happens if I the fee I provide is insufficient? A [retryable's](https://developer.arbitrum.io/arbos/l1-to-l2-messaging) submission fee is a special fee a user must pay to create a retryable ticket. The fee is directly proportional to the size of the L1 calldata the retryable ticket uses. The fee can be queried using the `Inbox.calculateRetryableSubmissionFee`method. If insufficient fee is provided, the transaction will revert on L1, and the ticket won't get created. ### Which method in the Inbox contract should I use to submit a retryable ticket (aka L1 to L2 message)? The method you should (almost certainly) use is `Inbox.createRetryableTicket`. There is an alternative method, `Inbox.unsafeCreateRetryableTicket`, which, as the name suggests, should only be used by those who fully understand its implications. There are two differences between `createRetryableTicket` and `unsafeCreateRetryableTicket`: 1. Method `createRetryableTicket` will check that provided L1 callvalue is sufficient to cover the costs of creating and executing the retryable ticket (at the specified parameters) and otherwise [revert directly at L1](https://docs.arbitrum.io/for-devs/troubleshooting-building#i-tried-to-create-a-retryable-ticket-but-the-transaction-reverted-on-l1--how-can-i-debug-the-issue). `unsafeCreateRetryableTicket`, in contrast, will allow a retryable ticket to be created that is guaranteed to revert on L2. 2. Method `createRetryableTicket` will check if either the provided `excessFeeRefundAddress` or the `callValueRefundAddress` are contracts on L1; if they are, to prevent the situation where refunds are *guaranteed* to be irrecoverable on L2, it will convert them to their [address alias](https://developer.arbitrum.io/arbos/l1-to-l2-messaging#address-aliasing), providing a *potential* path for fund recovery. `unsafeCreateRetryableTicket` will allow the creation of a retryable ticket with refund addresses that are L1 contracts; since no L1 contract can alias to an address that is also itself an L1 contract, refunds to these addresses on L2 will be irrecoverable. (Astute observers may note a third ticket creation method, `createRetryableTicketNoRefundAliasRewrite`; this is included only for backwards compatibility, but should be considered deprecated in favor of `unsafeCreateRetryableTicket`) ### Why do I get "custom tx type" errors when I use hardhat? In Arbitrum, we use a number of non-standard [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) typed transactions. Feel free to consult the [full list of transaction types](https://developer.arbitrum.io/arbos/geth#transaction-types) and the rationale. Note that if you're using Hardhat, [v2.12.2](https://github.com/NomicFoundation/hardhat/releases/tag/hardhat%402.12.2) added support for forking networks like Arbitrum with custom transaction types (find more information [in this HardHat GitHub issue](https://github.com/NomicFoundation/hardhat/issues/2995)). ### Why does it look like two identical transactions consume a different amount of gas? Calling an Arbitrum node's `eth_estimateGas` RPC returns a value sufficient to cover both the L1 and L2 components of the fee for the current gas price; this is the value that, e.g., will appear in users' wallets in the **Gas Limit** field. Thus, if the L1 calldata price changes over time, it will appear (in e.g., a wallet) that a transaction's gas limit is changing. In fact, the L2 gas limit isn't changing, merely the total gas required to cover the transaction's L1 + L2 fees. See [2-D fees](https://medium.com/offchainlabs/understanding-arbitrum-2-dimensional-fees-fd1d582596c9) and [How to estimate gas in Arbitrum](https://developer.arbitrum.io/devs-how-tos/how-to-estimate-gas) for more. ### Why am I getting error "429 Too Many Requests" when using one of Offchain Labs' Public RPCs? Offchain Labs offers public RPCs for free, but limits requests to prevent denial of service (DoS) attacks. Hitting the rate limit can result from either the frequency of your requests or the resources required to process them. If you are hitting our rate limit, we recommend [running your own node](https://developer.arbitrum.io/node-running/how-tos/running-a-full-node) or [using a third-party node provider](https://developer.arbitrum.io/node-running/node-providers). ### How do block.number and block.timestamp work on Arbitrum? Solidity calls to `block.number` on Arbitrum will return the block number/ timestamp of the underlying L1 with a slight delay; i.e., updated every few minutes. Note that L2 block numbers (i.e., as seen in block explorers / returned by RPCs) are different, and are typically updated roughly every second. Solidity calls to `block.timestamp` on Arbitrum are not linked to the timestamp of the L1 block. It is updated every L2 block based on the Sequencer's clock. Furthermore, for transactions that are force-included from the L1 (bypassing the Sequencer) `block.timestamp` will be equal to the L1 timestamp when the transaction was put in the delayed inbox on L1 (not force-included), or the L2 timestamp of the previous L2 block (whichever is greater of the two timestamps). For more information, see [the block numbers and time](https://docs.arbitrum.io/for-devs/concepts/differences-between-arbitrum-ethereum/block-numbers-and-time). ### Do I need to download any special npm libraries in order to use web3.js, ethers.js or viem on Arbitrum? Nope, web3.js, ethers.js and viem will work out of the box just like they do on L1 Ethereum. Once upon a time, Arbitrum developers were required to download supplemental packages with names like "arb-provider-ethers" and "arb-ethers-web3-bridge", but these packages are deprecated and no longer required! Any guide that directs devs to use them should be considered outdated. ### How many block numbers must we wait for in Arbitrum before we can confidently state that the transaction has reached finality? Arbitrum's block intervals fluctuate with throughput, so relying on block numbers for finality isn't recommended. However, Arbitrum nodes support Ethereum's JSON RPC, enabling the use of [`eth_getBlockByNumber()`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getblockbynumber) to determine block finality. Here, we provide additional details on how to achieve this. You can use [`eth_getBlockByNumber()`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getblockbynumber) with the string `"latest"`, `"safe"`, or `"finalized"`, each offering varying degrees of finality: * `latest`: Provides you with the most recent Arbitrum block number, also known as the tip of the chain. This block is typically the last sequenced block and may not yet be posted on L1. As long as you trust the Sequencer to eventually post this information on L1, relying on the `latest` block should be fine. * `safe`: Provides you with the most recent Arbitrum block number that has achieved attestations from a two-thirds majority of Ethereum's validator set. This occurs when the Sequencer's batch is posted as an L1 block on Ethereum and then the batch transactions achieve `safe` finality there. While safe blocks are typically resistant to re-orgs, they can still be re-orged in the event of a significant L1 re-org. * `finalized`: Provides you with the most recent Arbitrum block number that is finalized on Ethereum. This means that the Sequencer's batch has been published\*\* \*\*as an L1 block on the Ethereum network and has reached a substantial depth, making it eligible for hard finality. Unlike `safe` blocks, `finalized` blocks are highly improbable to undergo re-orgs. To learn more about the different phases of an Arbitrum transaction, from client initiation to Layer 1 confirmation, check out [The Lifecycle of an Arbitrum Transaction](https://docs.arbitrum.io/tx-lifecycle). ### How can I list my token on the Arbitrum Bridge? The L2 token list used in the Arbitrum bridge is generated from the L1 tokens that are part of the token lists of [Uniswap](https://tokenlists.org/token-list?url=https%3A%2F%2Ftokens.uniswap.org), [Gemini](https://tokenlists.org/token-list?url=https%3A%2F%2Fwww.gemini.com%2Funiswap%2Fmanifest.json), [Coinmarketcap](https://api.coinmarketcap.com/data-api/v3/uniswap/all.json), or [Coingecko](https://tokens.coingecko.com/uniswap/all.json). This generation is valid for L1-native tokens bridged to L2 and for L2-native tokens that have been bridged to L1, as long as they are part of either of those lists. Currently, there isn't any L2-only token list. ### What is a testnet or a devnet? Testnets (or devnets) primarily serve developers who want to test their applications without using real mainnet funds. Arbitrum Sepolia is a testnet that offers the same full feature set as the mainnet network. It is also a "true" L2 that runs on top of the Sepolia testnet (L1), using it for security and settlement. Users can bridge any asset from the Sepolia testnet (L1) into the Arbitrum Sepolia testnet (and back!), using the official [bridge](https://bridge.arbitrum.io/). ### Is there any testnet available on Arbitrum? Yes, there's an Arbitrum Sepolia testnet (421614) that uses the Nitro tech stack and runs on top of Ethereum Sepolia. You can find more information [here](https://developer.arbitrum.io/public-chains). ### When was Arbitrum One upgraded from Classic to Nitro? Arbitrum One [was upgraded](https://medium.com/offchainlabs/its-nitro-time-86944693bf29) on August 31st, 2022 (block [22207818](https://arbiscan.io/block/22207818)), from the Classic stack to the improved [Nitro](https://developer.arbitrum.io/inside-arbitrum-nitro/) tech stack, maintaining the same state. ### Do Arbitrum chains support precompiles that are present on Ethereum? Yes, all Arbitrum chains support all precompiles that Ethereum supports, as well as others that are not present on Ethereum. Check the [precompiles reference page](https://docs.arbitrum.io/for-devs/dev-tools-and-resources/precompiles) for more information about Arbitrum specific precompiles. ### What's the contract code size limit in Arbitrum chains? As specified in [EIP-170](https://eips.ethereum.org/EIPS/eip-170), contracts of up to 24KB are deployable on Arbitrum chains. ### How can I find the L2 block(s) that corresponds to a given L1 block? First, you should be familiar with how block numbers behave on Arbitrum. You can find information about it in [Block numbers and time](https://docs.arbitrum.io/for-devs/concepts/differences-between-arbitrum-ethereum/block-numbers-and-time). When you query an RPC node for a transaction receipt or a block information, you obtain as part of the result the property `l1BlockNumber`, which is the L1 block number that the sequencer viewed when it processed the transaction. With that, although it might be computationally complex, you can binary search the L1 block number you are looking for, and get all L2 blocks that have that `l1BlockNumber`. If you want a more specific result, you can perform the same operation with the timestamp from the L1 block, instead of the actual block number. ### Why do some old transactions have extremely high gas prices when querying them? When Arbitrum One was running under the Arbitrum Classic stack (before Nitro), the gas price was an unbounded bid, so when requesting those transactions via RPC, you may obtain a very high amount in the `gasPrice` property. Instead of that, it is recommended to look at the `effectiveGasPrice` property from the transaction receipt. ### What is the WASM module root? The WASM module root is a 32-byte hash, which is a Merkelization of the Go replay binary and its dependencies. The replay binary is much too large to post on-chain, so this hash is set in the L1 rollup contract to determine the correct replay binary during fraud proofs. You can find more information in [How to Customize your Arbitrum chain's behavior](https://docs.arbitrum.io/launch-orbit-chain/how-tos/customize-stf#step-4-enable-fraud-proofs). ### Why do I get a "gas required exceeds allowance" when trying to estimate the gas costs of a request? During an `eth_estimateGas` call, the request will go through a simulation on the node. Therefore, if the transaction reverts or if there aren't enough funds in the wallet that's making the call (usually the `from` parameter), the `eth_estimateGas` request will return an error stating: `gas required exceeds allowance`. Ensure you have sufficient funds in your wallet, and that the gas fields of the request (if you're using them) are populated correctly. ### How can I verify that a child chain block has been processed as part of a specific assertion? If you want to verify that the latest confirmed (or created) assertion has processed a specific child chain block, you can follow these steps: 1. From the rollup contract, obtain the latest confirmed (or created) assertion through the function `latestConfirmed` (or `latestNodeCreated`). In this context, we refer to assertions as "nodes". 2. Obtain the node information through `getNode` 3. Find the `NodeCreated` event emission that occurred upon that node's creation. 4. In that `NodeCreated` event, there's an `assertion` property that contains the state of the chain before processing the specified blocks, and after processing them. Get the `afterState.globalState` property 5. That value contains a `bytes32Vals` array with the latest child chain block hash processed in the first element. You can find an example script in our [arbitrum-tutorials](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/l2-block-verification-in-assertion) repository. ### Why is the fee of some Classic transactions slightly different than the multiplication of gasLimit and effectiveGasPrice? Gas prices in Classic transactions worked differently from those in Nitro transactions. Classic transactions handled four different prices: L1 fixed, L1 calldata, L2 computation, and L2 storage. You can view all these prices in the **Advanced TxInfo** tab on Arbiscan (see an [example](https://arbiscan.io/tx/0xecfe992adc408d1458d97b6e066ea7f4169bf048e84cb7a4beaea6e9c54f07da#txninfo) here). When querying the receipt of a Classic transaction on a Nitro node, some calculation is needed to obtain an `effectiveGasPrice` that is close to (but not exactly) what those four prices represent. That's why if you multiply the `gasLimit` by the `effectiveGasPrice` you might end up with a transaction fee that is slightly different than the actual fee paid. To get the exact fees paid, you can query a Classic node, which will return all the accurate information in an object called `feeStats`. That object will contain all the information, split into four different gas fields: `prices`, `unitsUsed`, and `paid` (`which is price * unitsUsed`). ### How can I update the information of my bridged token on Arbiscan? If you have a native-L1 token that was bridged to L2 via the standard gateway, you might find that you can't claim ownership of the L2 contract of your token as it was generated by another contract. To update its information on Arbiscan (logo, socials, etc.), you can open a ticket through [Arbiscan support system](https://arbiscan.io/contactus) and request them to replicate the information of your token on L1 to L2. ### Why does my transaction revert with InvalidFEOpcode when using Foundry? Foundry and other similar development tools that enable chain forking, do not support Arbitrum precompiles. If your transaction is calling a precompile, it is likely that it will revert with `InvalidFEOpcode`. To rule out that possibility, it is recommended to send the transaction with a different tool. ### Why do I receive an "intrinsic gas too low" error when sending a transaction even with a high gas price? The error: `intrinsic gas too low` usually refers to not providing enough gas to pay for the L1 component of the transaction fees. This error typically occurs when a high enough gas limit is not set (instead of a gas price), due to how Arbitrum handles gas. You can find more information in the article [Understanding Arbitrum: 2-dimensional fees](https://medium.com/offchainlabs/understanding-arbitrum-2-dimensional-fees-fd1d582596c9) and the page [How to estimate gas](https://docs.arbitrum.io/devs-how-tos/how-to-estimate-gas). ### How can I interpret Arbitrum transaction traces? In Arbitrum, every block contains a system transaction of type [`ArbitrumInternalTxType`](https://docs.arbitrum.io/arbos/geth#ArbitrumInternalTx), which the ArbOS itself creates for specific state updates, such as the L1 base fee and the block number. These transactions are distinct from typical Ethereum transactions. They are exclusively generated by the ArbOS state transition function, rather than by external entities such as externally owned accounts (EOAs) or smart contracts. Despite having an `INVALID` opcode, these transactions are represented in the trace API to signify their presence in the block, even though an opcode within EVM execution does not trigger them. One of the key functionalities of `ArbitrumInternalTxType` involves managing value transfers related to acknowledging batch postings. When batches of transactions get submitted to Layer 1, a special cross-chain message is produced as a receipt, confirming the successful posting of the batch. The function `ApplyInternalTxUpdate` is responsible for processing and updating the system's state based on these crosschain messages, ensuring consistency and integrity across the Arbitrum network. Another functionality of `ArbitrumInternalTxType` transactions is handling the value held in retryables if discarded. Retryable transactions are transactions that can be "resubmitted" if they fail to execute on the destination chain. However, in certain scenarios, these retryable transactions might get discarded, for instance, due to expiration or other conditions. In such cases, the escrowed call value, which is the value associated with the retryable transaction, will be paid out to a specified `callValueRefundAddress` account. This address designation occurs during the initial submission of the transaction on the parent chain. The purpose of this mechanism is to ensure that funds associated with retryable transactions are not lost if they cannot be successfully executed, thereby maintaining the integrity and reliability of the Arbitrum network. To accurately reflect the value transfers associated with these transactions, we employ a unique `callType` parameter: `invalid`. Unlike conventional EVM calls (such as `delegatecall`, `call`, etc.), this designation signifies a different system transaction. It denotes a straightforward value transfer from one account to another, with no inherent invocation of smart contract logic. While technically feasible to conduct this transfer within a conventional call type, our deliberate choice of **invalid** aims to enhance the clarity and distinctiveness of this transaction type within the EVM ecosystem. Notably, the sender account remains consistent across all instances of this transaction type for the batch posting receipt version. However, it's important to note that for retryable transactions, the sender account differs as each retryable transaction escrows its value in a unique vault specific to that transaction. This unique vault ensures that funds associated with each retryable transaction are stored securely until either the transaction successfully executes or a refund is issued. **Note that:** * All pre-Nitro transactions are labeled as `ArbitrumLegacyTxType` by Nitro. * Traces are not available for pre-Nitro (Classic) transactions. ### Why do some blocks have a total gas limit that's over the standard block gas limit? The execution gas block limit of Arbitrum chains is 32 million. However, when querying a block, we may find a `gasLimit` value that exceeds the specified number. Finding it exceeds happens because this `gasLimit` field accounts for both execution gas and the corresponding gas limit of the L1 costs (you can find more information about the role of the L1 costs in a transaction's gas limit in [this article](https://medium.com/offchainlabs/understanding-arbitrum-2-dimensional-fees-fd1d582596c9)). The gas limit corresponding to L1 costs is practically unlimited, leading to the possibility of finding very high values on the gasLimit field of a block. The effective block gas limit (32 million) only accounts for execution gas limit. We can check the actual gas used for execution on a specific block by checking the gasUsed field. --- > For a complete page index, fetch # Arbitrum introduction ## What is Arbitrum? Arbitrum is a finance-native blockchain platform. It provides infrastructure for applications, tokenization, and dedicated blockchain environments. You can build on the public chains Arbitrum One and Arbitrum Nova, or you can launch your own Arbitrum chain and configure its execution, data availability, fee model, governance, and validation. Arbitrum runs on top of Ethereum. Your transactions cost less and the chain processes more of them per second, while Ethereum still settles the results and keeps the data available. This page explains the main parts of the platform: Arbitrum Rollup, AnyTrust, Nitro, Stylus, Arbitrum One, Arbitrum Nova, and Arbitrum chains. ![Two boxes, one above the other. The top box is an Arbitrum chain, which the docs call either the child chain or layer 2. An arrow labeled 'settles to' points down to the bottom box, Ethereum, which the docs call either the parent chain or layer 1.](/img/arbitrum-chain-naming.svg) Two pairs of names appear throughout this page. Ethereum is the parent chain, also called layer 1. An Arbitrum chain that settles to Ethereum is a child chain, also called layer 2. ## Why does Ethereum need help scaling? Nothing is wrong with Ethereum. Its limits follow from design choices that put decentralization and security first. That tradeoff is the scalability trilemma: a chain cannot maximize decentralization, security, and scalability at the same time. Ethereum optimizes the first two, which caps the third. ![A triangle with one property at each corner. Ethereum maximizes decentralization and security, shown in blue. Scalability, shown in dark grey, is capped as a result. Rollups like Arbitrum add scalability without weakening the other two.](/img/scalability-trilemma.svg) Rather than weaken security or decentralization, the Ethereum roadmap moves execution offchain to Rollups like Arbitrum. Ethereum then specializes in settlement and data availability. A Rollup orders many transactions into a batch, executes them on its own infrastructure, and posts compressed data and a state commitment back to Ethereum. You inherit Ethereum's security, throughput rises by orders of magnitude, and fees drop. To go deeper, read [How Arbitrum works](/how-arbitrum-works/inside-arbitrum-nitro.md). ## Why does Ethereum process so few transactions per second? Ethereum's low throughput follows from the protocol design. It is not a bug or a missing optimization. Ethereum nodes must agree on the current state, and they reach that agreement by having every node process every transaction. Ethereum is also an open, decentralized, peer-to-peer system, so anyone can run a node and validate the chain. Keeping that possible means keeping the work per node small. Together, these two requirements cap transactions per second (TPS). The roadmap answers this by having child chains like Arbitrum carry the throughput. ## How does Arbitrum solve this? Arbitrum does not make Ethereum faster. It lets you transact at much higher throughput while you still inherit Ethereum's security. Ethereum's bottleneck is that every node re-executes every transaction. Arbitrum separates two jobs that Ethereum combines: * **Execution.** Running transactions and updating state. * **Settlement and data availability.** Agreeing on the canonical result and keeping the data publicly retrievable. Arbitrum executes offchain and uses Ethereum for settlement and data availability. Ethereum nodes do not re-execute Arbitrum transactions. They store the compressed data and accept the result unless someone proves it wrong. ## How does Arbitrum prove that a result is correct? When a transaction reaches Arbitrum, the sequencer puts it in order. Arbitrum compresses that order and posts it to Ethereum. The posted order is the evidence of which transactions run, and that is where the name "Rollup" comes from. As long as Ethereum stays secure, anyone can read those transactions. If a result differs from the posted order, a validator can challenge it. Billions of dollars have moved through Arbitrum, and no fraudulent result has ever been confirmed. To learn how the dispute protocol works, read the [BoLD gentle introduction](/how-arbitrum-works/bold/gentle-introduction.md). ## Who validates the chain and raises challenges? Anyone can validate Arbitrum's chain state. Whoever does so is a validator. Most people do not run one, in the same way that most people do not run an Ethereum staking node. The fraud proof system needs only one honest validator to keep the chain secure, and that single validator can catch several malicious actors. This is what makes the system trustless: your funds do not depend on any one designated party. To learn about the validator types, read [Run a validator node](/run-arbitrum-node/more-types/run-validator-node.md). ## How does a fraud proof work? **In short:** two validators disagree about an executed transaction. Ethereum holds the posted data, so only one of them can be telling the truth. Re-executing every transaction on Ethereum would cost too much. Instead, each party bisects its history of commitments until both arrive at the single instruction they disagree about. Ethereum then acts as the arbiter and declares a winner. The batches that Arbitrum posts to Ethereum are the source of truth, and the challenge process, called BoLD, proves which validator is right. For the full protocol, read the [BoLD gentle introduction](/how-arbitrum-works/bold/gentle-introduction.md). ## Does the challenge period delay my transactions? There is a delay, but it applies to one action, not to everyday activity. | Action | Delay | | ---------------------------------------- | ----------------------- | | Withdraw funds from Arbitrum to Ethereum | Yes, typically 6.4 days | | Use a third-party fast bridge | No, for a fee | | Deposit funds from Ethereum to Arbitrum | No | The challenge period delays withdrawals back to Ethereum because that is the point where you cross a trust boundary. It does not affect the transactions you send inside Arbitrum. To learn how bridging works, read [Token bridging](/how-arbitrum-works/deep-dives/token-bridging.md). To move tokens yourself, follow the [Arbitrum bridge quickstart](/arbitrum-bridge/quickstart.md). ## Why are fees on Arbitrum lower? The word "optimistic" describes how Arbitrum verifies state: it treats an assertion as valid unless someone challenges it through BoLD. That design buys security and correctness rather than cost savings. The low fees come from five other things: 1. **Amortized Ethereum costs.** Arbitrum posts transactions in batches. A batch of 500 transactions spreads one posting cost across all 500. 2. **Compression.** Arbitrum compresses the data it posts, so each batch costs less. 3. **Blobs.** [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) gives Ethereum a separate data lane, priced independently of regular gas, that makes posting batch data cheaper. 4. **No global re-execution.** Ethereum nodes do not re-execute Arbitrum transactions. 5. **Single-sequencer execution.** One sequencer is active at a time, so computation runs on one machine instead of thousands. To go deeper, read [How Arbitrum works](/how-arbitrum-works/inside-arbitrum-nitro.md). ## Is using Arbitrum the same as using Ethereum? **In short:** yes, at lower cost and higher speed. You bridge funds in, use applications, and bridge funds out. Your wallets, applications, and addresses all work. Layer 2 protocols optimize for different goals. Arbitrum put Ethereum compatibility first, so you can use your existing Ethereum wallets, and you can build and deploy contracts with your existing Ethereum libraries and tooling. Arbitrum reaches that compatibility by running a fork of [Geth](/how-arbitrum-works/reference/geth.md), the most widely used Ethereum implementation, modified to work as a trustless child chain. Most of the code that runs on Arbitrum is the same code that runs on Ethereum. Offchain Labs calls this approach Nitro, and you can read the [Nitro codebase](https://github.com/OffchainLabs/nitro). For the differences that remain, read the [Comparison overview](/arbitrum-essentials/arbitrum-vs-ethereum/comparison-overview.md). ## What can you build on Arbitrum that you cannot build on Ethereum? Stylus keeps Nitro's Ethereum compatibility and adds a second virtual machine alongside the Ethereum Virtual Machine (EVM). You can write contracts in Rust, C, and C++, and they interoperate with your Solidity contracts. Stylus shipped in [ArbOS 32](/run-arbitrum-node/arbos-releases/arbos32.md) and runs on Arbitrum One, Arbitrum Nova, and Arbitrum chains. To try it, follow the [Stylus quickstart](/stylus/quickstart.md). You can also launch your own Arbitrum chain with a custom gas token. For example, your chain can charge gas in USDC. The gas token is one of many settings you control. To see the full list, read the [Arbitrum chain introduction](/launch-arbitrum-chain/overview/introduction.md). ## Does Arbitrum Rollup fit every use case? Arbitrum Rollup avoids centralization and extra trust assumptions, which makes it a strong default and a net gain for the Ethereum ecosystem. That decentralization has a price, and not every application needs to pay it. When your security requirements differ, another tool in the Arbitrum suite may fit better, for example an Arbitrum AnyTrust chain. ## What is AnyTrust? AnyTrust is a different data availability option. It works like an Arbitrum Rollup, which posts data in batches to Ethereum, with one change: a small committee keeps the data available instead. Everything else stays the same. Because the data stays offchain in the normal case, an AnyTrust chain charges much lower fees. In exchange, AnyTrust does not offer the same decentralization, trustlessness, and permissionless security guarantees as a Rollup. If someone raises a challenge, the AnyTrust chain reverts to Rollup mode. The security assumption is that at least two committee members are honest and will provide the data when it is needed. AnyTrust suits applications that need high throughput and do not need the full decentralization of a Rollup. To learn more, read the [AnyTrust protocol](/how-arbitrum-works/deep-dives/anytrust-protocol.md). ## How many Arbitrum chains are there? Many. Running multiple chains in parallel is a core advantage of offchain scaling. Here is a snapshot of the chains running today: * On Ethereum, as layer 2: * [Arbitrum One](https://portal.arbitrum.io/), an Arbitrum Rollup chain that the [Arbitrum Foundation](https://arbitrum.foundation) operates * [Arbitrum Nova](https://nova.arbitrum.io/), an AnyTrust chain that the [Arbitrum Foundation](https://arbitrum.foundation) operates * Arbitrum chains that developers launch and operate themselves * On an EVM layer 2, as layer 3: * Arbitrum chains on Arbitrum One and Arbitrum Nova * Arbitrum chains on other EVM layer 2 chains ![Arbitrum chains stack on top of each other. Ethereum sits at layer 1 and provides settlement and data availability. Four chains sit at layer 2 and settle to Ethereum: another EVM chain, Arbitrum One in its cyan brand color with its logo (a Rollup chain that posts its data to Ethereum), Arbitrum Nova in its orange brand color with its logo (an AnyTrust chain whose data a committee keeps), and an Arbitrum chain that you launch. At layer 3, you can run more Arbitrum chains on top of the other EVM chain, on Arbitrum One, and on Arbitrum Nova.](/img/arbitrum-chains-diagram.svg) You can launch your own Arbitrum chain as a layer 2 on Ethereum, or as a layer 3 on an EVM layer 2 chain. For a full list of the chains running today, see the [Arbitrum Portal](https://portal.arbitrum.io/orbit/ecosystem). Pick the chain that matches your security and cost requirements. To launch your own, read the [Arbitrum chain introduction](/launch-arbitrum-chain/overview/introduction.md). ## Who decides the future of Arbitrum? The Arbitrum governance system owns the Arbitrum chains. To learn how it works, read the [Arbitrum governance documentation](https://docs.arbitrum.foundation/). --- > For a complete page index, fetch # Economics of Disputes in Arbitrum BoLD The following document explains the economics and denial-of-service protection mechanisms built into Arbitrum BoLD. It covers trade-offs Arbitrum has to make to enable permissionless validation, explaining the key problems in an accessible way. For a higher-level introduction to BoLD, see the [BoLD gentle introduction](/how-arbitrum-works/bold/gentle-introduction.md). ## Background [Arbitrum One](https://arbitrum.io/) is currently one of the most widely used Ethereum scaling solutions, with [\~$14bn **USD** in total-value-locked](https://l2beat.com/scaling/projects/arbitrum) at the time of writing. Not only do its scaling properties, such as its 250ms block times, make it popular, but so do its security properties and approach to decentralization. Currently, Arbitrum One is governed by the Arbitrum DAO, one of the most active and robust onchain organizations. In the Fall of 2023, Offchain Labs announced [Arbitrum BoLD](https://medium.com/offchainlabs/bold-permissionless-validation-for-arbitrum-chains-9934eb5328cc), a new dispute resolution protocol built from the ground up that brings Arbitrum chains to the next level of decentralization. BoLD, which is an acronym for **Bo**unded **L**iquidity **D**elay, allows permissionless validation of Arbitrum chains. This new protocol enables chain owners to remove the list of permissioned validators for their chains, allowing anyone to challenge invalid claims made about Arbitrum states on their parent chain and potentially win. In this document, we'll explore the economics and trade-offs enabling permissionless validation. ## Settling Arbitrum states to Ethereum We often say that "Arbitrum chains settle their states to a parent chain", and we'll elaborate on what that means. All Arbitrum One transactions can be recreated by reading data from the parent chain (Ethereum), as compressed batches of all child chain transactions are frequently posted to Ethereum. Once a batched transaction gets included in a finalized block on Ethereum, its history will likely never revert on Arbitrum One. However, when Ethereum receives a batch of transactions, it does not know what the correct result of executing those transactions is. To verify the correct result, a separate process confirms batch correctness on Ethereum, known as the "assertion." For Arbitrum One specifically, approximately every hour, entities known as validators check the correctness of batches by following the Arbitrum chain. Validators can choose to become proposers and propose something called an "assertion", which attests to the validity of a batch, stating, "I have verified this batch." As Ethereum does not verify the correctness of Arbitrum One, it allows approximately seven days for anyone to dispute one of these assertions. Before the deployment of BoLD, a permissioned list of proposers existed who could post assertions and challenge assertions for all Arbitrum chains. Arbitrum BoLD enables any chain owner, such as the ArbitrumDAO, to remove this permissioned list. Note that validators who opt to post assertions are otherwise known as "assertion proposers". ### Withdrawing assets back to Ethereum from Arbitrum Users of Arbitrum One who have bridged assets from Ethereum can initiate the [withdrawal process](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md#asset-withdrawals) at any time. However, for this withdrawal to fully execute, its corresponding claim must match a confirmed assertion on Ethereum. For instance, if Alice starts a withdrawal transaction on Arbitrum One, it gets posted in a batch on Ethereum. Then, a validator will post an assertion about that batch on Ethereum an hour later. The assertion has a seven-day window in which anyone can dispute it. After that window passes, the protocol confirms the assertion, and Alice will receive her withdrawn assets on Ethereum, which she is free to use as she pleases. "Settling states" and having a seven-day dispute window are crucial to ensuring safe withdrawal of assets. Allowing anyone to dispute invalid claims and win keeps withdrawals protected by strong security guarantees without needing to trust a group of validators. This "permissionless validation" distinguishes optimistic rollups from sidechains. ### The dispute period The reason there is a dispute window for assertions about Arbitrum One on Ethereum is because Ethereum itself **has no knowledge about what is correct** on Arbitrum One. The two blockchains are different domains with different states. Ethereum, however, can be used as a neutral referee for parties to dispute claims about Arbitrum One. The dispute period is seven days because it is seen as the maximum period of time an adversary could delay Ethereum before social intervention, originally proposed by Vitalik Buterin. This window gives enough time for parties to catch invalid claims and challenge them accordingly. ### Dispute resolution times An actual dispute occurs if an honest party disagrees with an assertion on Ethereum and posts an assertion they know to be correct as a counter-claim, or if a dishonest party decides to post to Ethereum a spurious assertion they know to be wrong, after another assertion has already been posted. This second claim creates a "fork" in the chain of assertions, requiring a resolution process. We'll get into the high-level details of how disputes are resolved later in this document. Once an actual dispute is ongoing, it will also take time to resolve, as Ethereum does not know the correctness of Arbitrum One's states. Ethereum must then give sufficient time for parties to submit their proofs and declare a winner. The new Arbitrum BoLD protocol **guarantees a resolution to a dispute within seven days** so long as an honest party or parties are present to defend against invalid claims, and have access to enough resources to pay for the costs of participating in the protocol—for more details, see the [Preventing Spam](#preventing-spam) section below. As assertions have a dispute window of seven days, and disputes require an additional seven days to resolve, a dispute made at the last second would **delay assertion confirmation to a maximum of 14 days**, or two weeks. BoLD is the only dispute protocol we are aware of that guarantees this bound. ### The cost of delaying withdrawals Delaying withdrawals incurs opportunity costs and negatively impacts the user experience for those who want to withdraw their assets. In the happy case of no disputes, withdrawals already have a built-in seven-day delay. A dispute adds seven days to that delay. The problem is that disputes delay *all* pending withdrawals from Arbitrum One back to Ethereum, not just a single claim. As such, **disputing a claim must incur a cost for the initiator** that is proportional to the opportunity cost it imposes on Arbitrum users. #### Requiring a bond to validate By default, all Arbitrum nodes act as validators, monitoring the chain to verify assertions posted to the parent chain and flagging any invalid assertions. On Arbitrum One, running a validator, known as a [“watchtower” node](/run-arbitrum-node/more-types/run-validator-node.md#validation-strategies), is permissionless and incurs no additional cost beyond the infrastructure required for the node. Another type of validator, called a "proposer," performs additional tasks in addition to their regular duties as a validator. Proposers compute Arbitrum states and propose assertions to the parent chain. To prevent abuse and delays in withdrawals, proposers must make a security deposit or "bond" to gain the privilege of proposing assertions. This bond can be withdrawn once their latest assertion is confirmed, ending their responsibilities as a proposer. Arbitrum BoLD allows validators to become proposers and challengers without permission. Proposers must bond **ETH** to propose state assertions to the parent chain. Only one proposer is needed for chain progress, allowing most validators to verify assertions. In the event of disputes over state assertions, BoLD allows anyone to post a "challenge bond" of **ETH** to dispute invalid assertions, acting as a challenger in defense of the Arbitrum chain. For more details on different strategies validators can use refer to [How to run a validator](/run-arbitrum-node/more-types/run-validator-node.md). #### Pricing bonds Ensuring assertions are frequently posted is a requirement for Arbitrum; however, it should not be a privilege easily obtained, which is why the pricing of this "security deposit" is based on opportunity cost. To be highly conservative, we want to account for a "bank run"-like scenario, in which everyone wants to withdraw their assets from Arbitrum One at the same time. The [Arbitrum One bridge](https://etherscan.io/address/0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a) contains approximately $3.4B USD worth of assets at the time of writing on Oct 23rd, 2024. Assuming funds could earn a 5% APY if invested elsewhere, the opportunity cost of 1 extra week of delay in withdrawing them from Arbitrum One is approximately $3.27M USD. Given this scenario, we recommend a bond for assertion posters to be greater than $3.7M USD. Honest proposers can always withdraw their bond once their assertions are confirmed. However, adversaries stand to lose the entirety of their bond if they propose invalid assertions. A large bond size significantly enhances the economic security of the system along these two axes by increasing the cost of proposing and by ensuring that malicious actors will forfeit their entire bond if they are proven wrong by the protocol. Given that participation in BoLD is permissionless, we recommend that the size of bonds required to participate be high enough to disincentivize malicious actors from attacking Arbitrum One and to mitigate against spam (that would otherwise delay confirmations up to one challenge period). High bonding values do not harm decentralization because (1) trustless bonding pools can be deployed permissionlessly to open challenges and post assertions, and (2) any number of honest parties of unknown identities can emerge to bond their funds to the correct assertion and participate in the defense of Arbitrum at any time within a challenge. As with the current dispute resolution protocol, there are no protocol-level incentives for parties who opt in to participate in validating Arbitrum One with BoLD. While both of these bonds can be any **ERC-20** token and configured to any size, we recommend the use of the **WETH** **ERC-20** token and the following bond sizes for Arbitrum One: * Assertion bonds: 3600 **ETH** is required from validators to bond their funds to an assertion in the eventual hopes of having that assertion be confirmed by the Rollup protocol. This requirement is a one-time bond to start posting assertions. The bond is available for withdrawal once a validator’s assertion is confirmed and can also accumulate through a trustless bonding pool. * Challenge-bonds, per level: 555 **WETH** at the "big-step" level; 79 **WETH** at the "small-step" level—required from validators to open challenges against an assertion observed on the parent chain (Ethereum, in the case of Arbitrum One), for each level. Note that “level” corresponds to the level of granularity over which the [interactive bisection game](/how-arbitrum-works/bold/how-bold-bisection-works.md) gets played, starting at the block level, moving on to a range of WASM execution steps, and then finally to the level of a single execution step. For more details on the concept of "levels" in BoLD challenges, see [Challenge resolution](/how-arbitrum-works/bold/bold-technical-deep-dive.md#challenge-resolution) section in the Technical deep dive. We calculated these values carefully to optimize for the resource ratio (explained later) and gas costs in the event of an attack, as described in [BoLD whitepaper](https://arxiv.org/abs/2404.10491). This effectively means that an entity that has already posted a bond to propose an assertion does not need to post a separate assertion bond to challenge an invalid state assertion that they observe. To be clear, the validator would still require 555 **ETH** and 79 **ETH** for ongoing challenges. These additional challenge bond amounts are required to participate in the interactive dispute game (back and forth) and narrow down the disagreement to a single step of execution that can be proven on Ethereum. The 555 **ETH** and 79 **ETH** challenge bonds can accumulate via a trustless bonding pool, and do not all have to be provided by the validator that initiated the challenge. These bonds are refundable at the end of a challenge and can also be assembled by the community using a trustless bonding pool. #### Centralization concerns Requiring a high bond to post assertions about Arbitrum seems centralizing, as we are replacing an allowlist of validators with a system that requires substantial funds to participate. However, **BoLD ships with a [trustless bonding pool](/how-arbitrum-works/bold/bold-technical-deep-dive.md#trustless-bonding-pools)** for assertion posting. That is, any group of honest parties can pool funds into a simple contract that will post an assertion to Ethereum without needing to trust each other. Making it easy to pool funds to become a validator without needing trust to dispute invalid claims does not affect the safety or decentralization of BoLD. Optimizing for the unhappy case is more important than optimizing for the happy case. As there only needs to be one honest assertion poster, it falls into the security budget of the chain to set a high bond fee in order to become a proposer. It should be expensive to delay Arbitrum One withdrawals, and it should also have a high barrier to entry for performing this key responsibility. As long as disputes are trustless, and trustless pools are available in production, we claim the security properties of assertion posting hold equally. ## Resolving disputes One of the core properties BoLD achieves is providing a fixed upper bound for dispute resolution times. This section will discuss the constraints required to achieve this from first principles. ### Dispute game overview Every game between adversarial parties needs a referee: a neutral party that can enforce the rules to declare a fair winner. Arbitrum BoLD relies on Ethereum as its referee because of its properties as the most decentralized, censorship-resistant smart contract chain in the world. When a dispute occurs about Arbitrum One assertions on Ethereum, there is a protocol for resolving them. At its core, a dispute regards the blockhash of an Arbitrum One block at a specific height. Ethereum does not know which claim is correct and instead relies on a dispute resolution mechanism to be played. The game involves different parties asserting claims with supporting evidence to eventually [narrow down their disagreement to a single step of execution](/how-arbitrum-works/bold/how-bold-bisection-works.md) within the execution of a block, known as a one-step proof (OSP). Ethereum can then verify this OSP by itself and, as the neutral referee, declare a winner. The "rules" of the dispute involve parties making claims with proof to reach a single point of disagreement. Parties "narrow down" their claims via moves called bisections. After a party has made a bisection, there is nothing else left to do until another party comes in and counters it. The core of the system is that an honest party winning a one-step proof leaves the malicious party with no other moves to make. Once the honest party has accumulated enough time without being countered, it is declared the winner. Compared to other dispute protocols, however, BoLD is **not** a dispute between two specific Ethereum addresses, such as Alice and Bob. Instead, it is a dispute between an absolute, correct history vs. an incorrect one. Claims in BoLD are not attached to a particular address or validator but instead to Merkle commitments of an Arbitrum chain's history. If Alice and Charlie are both honest, and Bob is malicious, Alice and Charlie can play the game as part of a single "team". If Alice goes offline in the middle of a dispute-game, Charlie can continue resolving the game on behalf of the honest team because Charlie and Alice claim and make moves on the correct history. This distinction between correct and incorrect history is why we say BoLD enables "trustless cooperation," as there is no need for communication between honest parties. We believe that committing a set of chain history hashes, rather than a specific hash at a given moment, is crucial for securing dispute protocols. For more technical details on the BoLD dispute protocol, see the [Technical deep dive](/how-arbitrum-works/bold/bold-technical-deep-dive.md) or the [BoLD research whitepaper](https://arxiv.org/abs/2404.10491). ### Spamming the dispute game BoLD is a dispute game in which the assertion that has accumulated seven days "not-countered" wins. That is, parties have incentives to counter any new claims as soon as they appear to "block" their rivals from increasing their timers. For honest parties, responding to claims may sometimes require offchain computational work and, therefore, resources such as CPUs. However, malicious parties can make claims that are unfounded while honest parties do the actual work. Because malicious parties can submit incorrect claims that force honest parties to do work, there must be an economic cost associated with making moves in the dispute game. Said differently, we need a way to prevent **spam attacks** in dispute games. #### The cost of moves When pricing the bonds required to make claims within disputes, we consider the marginal costs that the honest party incurs for each claim a malicious party makes. The BoLD research paper includes information such as the number of adversary moves multiplied by the gas cost of making bisections and claims and some estimates of the offchain computational costs. We deem this the **marginal cost** of a party in a dispute. With BoLD, the space of disagreements between parties is of max size 2^69. As such, the dispute game has to be played at different levels of granularity to make it computationally feasible. Let's use an analogy: say we have two one-meter sticks that seem identical, and we want to determine where they differ. They appear identical at the centimeter level, so we need to go down to the millimeter level, then the micrometer level, and then figure out where they differ at the nanometer level. This is what BoLD does over the space of disputes. Parties play the same game at different levels of granularity. At the centimeter level, each centimeter could trigger a millimeter dispute, and each millimeter dispute could have many micrometer disputes, etc. It is possible to abuse the dispute pattern with spam, unless it is discouraged. #### Preventing spam Since Ethereum knows nothing about which claims are honest or malicious until a one-step proof is provided, how can the protocol detect and discourage spam? A key insight is that honest parties only need to make one honest claim. Honest parties will never spam and create thousands of conflicting claims with themselves. Given this, we can put a price tag on making moves by looking at something called the "resource ratio" between honest and malicious parties, as defined in the BoLD research paper. This ratio is the sum of the gas plus the bonding marginal costs incurred by the adversary for the honest party. This calculation means that certain values input into the equations can lead to **different ratios**. For instance, the adversary must pay **10 times** the marginal costs of the honest party. However, aiming to increase this ratio significantly by plugging in different values leads to higher costs for all parties. #### Dispute mini-bonds We require parties to lock up some capital called a "mini-bond" when making big claims in a dispute. These bonds are not needed when making bisection moves but are critical for posting an initial claim. Pricing these mini-bonds helps achieve a high resource ratio of dishonest parties to honest parties. > **NOTE** > > "Mini-bonds" is another term for "challenge-bonds" mentioned above in [Pricing bonds](#pricing-bonds). It is clear that if we can multiply the cost to the malicious party by some multiplier of the honest party, we will get significant security benefits. For instance, imagine that a one billion dollar attack can be defended by simply pooling together $10 million. Is it possible to achieve such a ratio? Let's explore the limitations of making the cost to malicious parties higher than that to the honest parties. If we aim to have a constant resource ratio `> 1`, we have to do the following: if the adversary makes `N` bonds at any level, they can force the honest party to make `N` bonds at the next level down, where the adversary can choose not to place any bonds at all. Regarding resource ratio, to make the adversary always pay 10x in staking, we need to make the bond amount at one level 10x more than the next (as we go "upward" from sub-challenges towards the assertion-level challenge). As there are multiple levels, the equations for the bond size include an exponential factor on the desired constant resource ratio `> 1`. Below, we plot the bond size vs. the resource ratio of malicious to honest costs. The source for these equations can be found in the research paper and is [represented in this calculator](https://www.desmos.com/calculator/digjlq4vly). If we desire a constant resource ratio of malicious to honest costs `> 1`, the required bond size in **ETH** increases as a polynomial at a particular challenge level. #### Trade-offs Having a 1000x resource ratio would be nice in theory, but it would, unfortunately, require a bond of 1M **ETH** ($2.56B USD at time of writing) to open a challenge in the first place, which is unreasonable. Instead, we can explore a more feasible ratio. The resource ratio will drive the price of disputes claims, impacting both honest and malicious parties. However, claims can **always be made** through a **trustless pool**. Honest parties can pool together funds to participate in disputes. #### The sweet spot Now that we've established that a higher resource ratio is better, albeit with some trade-offs, what is the optimal balance? We propose a resource ratio of 6.46 for Arbitrum One. While odd, this resource ratio considers the initial "bond" to become a proposer (mentioned earlier) and a worst-case scenario of 500 `gwei`/gas on the parent chain for posting assertions and making sub-challenge moves (i.e., if an attack were to happen, the malicious actor could choose to perform their attack during a period of elevated gas prices). Again, the ratio of malicious to honest costs should be high to deter attacks sufficiently. Under our current assumptions (500 `gwei`/gas) and proposed parameters (bond sizes, etc.), for every $6.46 spent by malicious parties attacking, only $1 is needed to defend it successfully in BoLD. Here's a [direct link to the calculations](https://www.desmos.com/calculator/usmdcuopme) where the X-axis is parent chain gas costs in `gwei` and the Y-axis is the resource ratio. Unfortunately, there is no "one size fits all" framework for choosing the resource ratio for your chain. Therefore, we recommend teams learn and understand the benefits and trade-offs of operating BoLD in a permissionless format—including performing the same type of economic risk analyses we have performed for Arbitrum One. ## Thinking about incentives Although we have made claims with hard numbers about how to price disputes and withdrawal delays in Arbitrum BoLD, we also took a step back and considered the theoretical assumptions we were making. Arbitrum One is a complex protocol utilized by various groups of people with diverse incentives. The research team at Offchain Labs has devoted considerable effort to studying the game theory of validators in optimistic Rollups. Honest parties represent everyone with funds onchain, and they have a significant amount to gain by winning the challenge, as they can prevent the loss of their assets rather than losing them. A proposed, more complex model, which considers all parties staking and their associated costs, ["Incentive Schemes for Rollup Validators"](https://arxiv.org/abs/2308.02880). The paper examines the incentives needed to get parties to check whether assertions are correct. It finds that there is no pure strategy, Nash equilibrium, and only a mixed equilibrium if there is no incentive for honest validators to participate. However, the research showed a pure strategy equilibrium can be reached if honest parties are incentivized to **check** results. The problem of honest validators' "free riding" and not checking is well-documented as the [verifier's dilemma](https://www.smithandcrown.com/glossary/verifiers-dilemma). We believe future iterations of BOLD could include "attention challenges" that reward honest validators for also doing their job. ### Service fee for “Active” proposers For Arbitrum BoLD's initial launch, we believe that chain owners should pay a service fee to active, top-level proposers as a way of removing the disincentive for participation by honest parties who bond their own capital and propose assertions for Arbitrum One. The fee should be denominated in **ETH** and should correlate to the annualized income that Ethereum mainnet validators receive, over the same time period. At the time of writing, the estimated annual income for Ethereum mainnet validators is approximately 3% to 4% of their bond (based on [CoinDesk Indices Composite Ether Staking Rate (CESR) benchmark](https://www.coindesk.com/indices/ether/cesr) and [Rated.Network](https://explorer.rated.network/network?network=mainnet\&timeWindow=7d\&rewardsMetric=average\&geoDistType=all\&hostDistType=all\&soloProDist=stake)). This service fee can be paid out upon an active proposer’s top-level assertion being confirmed on Ethereum and will be calculated using the duration of time that the proposer was considered active by the protocol. The procedure that calculates this will be handled offchain, using a procedure that will be published at a later date. BoLD makes it permissionless for any validator to become a proposer and also introduces a way to pay a service fee to honest parties for locking up capital to do so. Validators are not considered active proposers until they successfully propose an assertion with a bond. > **NOTE** > > We envision the Arbitrum Foundation (AF) running its own node as a proposer. This proposer's bonding capital will be funded by the AF and/or the DAO, and (unlike other proposers) will not earn a service fee since it is being run as a public good using the community's own money. In order to become an active proposer for an Arbitrum chain, post-BoLD, a validator has to propose a state assertion to its parent chain. For Arbitrum One and Nova, the state assertion is posted onto the parent chain (Ethereum). If they do not have an active bond on the parent chain, they must then attach a bond to their assertion in order to successfully post it. Subsequent assertions posted by the same address will simply move the already-supplied bond to their latest proposed assertion. Meanwhile, if an entity, say Bob, has posted a successor assertion to one previously made by another entity, Alice, then Bob would be considered by the protocol to be the current active proposer. Alice would no longer be considered the active proposer by the protocol, and once Alice’s assertion is confirmed, she will receive a refund of her assertion bond. There can only be one “active” proposer at any point in time. For Arbitrum One specifically, all eligible entities that wish to be paid this service fee by the Arbitrum Foundation must undergo the Arbitrum Foundation’s KYC process, as no AIP "may be in violation of applicable laws, in particular sanctions-related regulations." This is also written in the [ArbitrumDAO's Constitution](https://docs.arbitrum.foundation/dao-constitution#section-2-dao-proposals-and-voting-procedures). ### Rewards and Reimbursements for Defenders The service fee described above is meant to incentivize or reimburse an honest, active proposer for locking up their capital to propose assertions and advance the chain. Similarly, in the event of an attack, a bounty is proposed to be paid out to honest defenders using confiscated funds from malicious actors (in the event of a challenge). For Arbitrum One specifically, 1% (called the “defender’s bounty”) of the confiscated funds from a malicious actor is to be rewarded to honest parties who deposit a challenge bond and post assertions as part of a sub-challenge, proportional to the amount that a defender has put up to defend a correct state assertion during the challenge. This bounty applies to all challenges (block challenges, sub challenges, and one-step challenges). Note that any gas costs spent by honest parties to defend Arbitrum One during a challenge are 100% refundable by the Arbitrum Foundation. In this model, honest defenders and proposers of Arbitrum One are incentivized to participate, while malicious actors stand to lose everything they spent attacking Arbitrum One. We believe that chain owners interested in adopting BoLD for their own chain should follow a similar approach, as described above for Arbitrum One, to incentivize challenge participation (but not necessarily assertion proposing). In this design, defenders are only eligible for the defender's bounty if they deposit a challenge bond (for Arbitrum One, this is either 555 or 79 **ETH**, depending on the level), posted to an onchain assertion as part of a sub-challenge (i.e., not the top-level assertion), and have had their onchain sub-challenge assertion get confirmed by the protocol. For Arbitrum One, the calculation for the defender's bounty is conducted offchain by the Arbitrum Foundation, and payment will be made via an ArbitrumDAO governance vote (since confiscated funds go to an ArbitrumDAO-controlled address). Honest parties are not automatically rewarded with all the funds seized from malicious actors to avoid creating a situation where honest parties waste resources competing to be the first to make each honest move in the interactive, fraud-proof game. Additionally, BoLD resolves disputes by determining which top-level assertion is correct, without necessarily being able to classify every move as “honest” or “malicious” as part of the interactive fraud-proof game using offchain knowledge. Once all of a validator’s proposed assertions are confirmed, a validator can withdraw their bond in full. Additionally, the protocol will automatically handle refunds of challenge bonds for honest parties and confiscation of bonds from malicious parties in the event of a challenge. In other words, bonds put up by honest parties will always be returned, and the bonds of malicious parties will always be confiscated. For Arbitrum One specifically, parent chain gas costs for honest parties defending a challenge will be reimbursed by the Arbitrum Foundation through a procedure to be published at a later date. The chain owner could therefore consider the cost of incentivizing or lending the assets to a single honest proposer in the happy case as the **security budget of the chain**. For Arbitrum One specifically, all eligible entities who wish to be paid the defender's bounty from the ArbitrumDAO must undergo the Arbitrum Foundation’s KYC process as no AIP "may be in violation of applicable laws, in particular sanctions-related regulations". This is also written in the [ArbitrumDAO's Constitution](https://docs.arbitrum.foundation/dao-constitution#section-2-dao-proposals-and-voting-procedures). ## Conclusion This page summarizes the rationale behind choosing bond sizes and the cost of spam prevention in optimistic Rollup dispute protocols. We recommend that bond sizes be high enough to discourage challenges from being opened, as malicious parties will always stand to lose when playing the game. As Arbitrum BoLD does not tie disputes to specific addresses, honest parties can have trustless cooperation to resolve disputes if desired. We posit that making the cost of the malicious parties 10 times that of the honest party leads to desirable economic properties that help us reason about how to price bonds. We describe how a 6.46x ratio (which BoLD, as deployed, will achieve) represents a pragmatic point in the design space that strikes a balance between concerns about staking costs and concerns about spam. Finally, we examine a high-level game theory discussion of optimistic rollups and argue that solving the verifier's dilemma through incentives for honest validators is an important step towards this goal. The topic of further improvements and new economic and incentive models for BoLD are valuable and we believe it deserves the full focus and attention of the community in future proposals and discussions. Details around additional or new proposed economic or incentive models for BoLD will need continued research and development work. Still, the deployment of BoLD as-is represents a substantial improvement to the security of Arbitrum even without all economic-related concerns being fully resolved. --- > For a complete page index, fetch # BoLD: a technical deep dive ## Overview Arbitrum's current dispute protocol involves defending against challengers individually in a 1-vs-1 tournament setting. In contrast, BoLD enables an all-vs-all battle royale between Good and Evil, with a single winner always determined. This dynamic is made possible by BoLD's time-bounded, permissionless validation using deterministic Merkle proofs and hashes. This allows any party to bond in the correct state and prove their claim through interactive fraud proofs, ensuring that a single honest party bonding in the correct state will always prevail in disputes. Validators on Arbitrum can post their claim on the validity of state roots, known as **assertions**. Ethereum, of course, does not know anything about the validity of these Arbitrum state roots, but it *can* help prove their correctness. *Anyone* in the world can then initiate a challenge over any unconfirmed assertion to start the protocol’s game. The assertions being disputed concern block hashes of an Arbitrum chain at a given batch/inbox position. Given that Arbitrum chains are deterministic, there is only one correct history for all parties running the standard Nitro software. Using the notion of one-step proof, Ethereum can check whether someone is making a fraudulent assertion after an interactive game is played to narrow down a dispute. If a claim is honest, it can be confirmed on Ethereum after a 6.4-day period (although the DAO can change this period). If a claim is malicious, anyone who knows the correct Arbitrum state can successfully challenge it within that 6.4-day window *and always win* within a challenge period plus some small delta. The current implementation of BoLD involves both onchain and offchain components: 1. Rollup contracts to be deployed on Ethereum. 2. New challenge management contracts to be deployed on Ethereum. 3. [Honest validator software](https://github.com/OffchainLabs/nitro) equipped to submit assertions and perform challenge moves on any assertions it disagrees with. The honest validator is robust enough to win against malicious entities and always ensures honest assertions are the only ones confirmed onchain. ### Key terminology * **Arbitrum Rollup contracts:** The set of smart contracts on Ethereum parent chain that serve as both the data availability layer for Arbitrum and for confirming the rollup's state assertions after a challenge period has passed for each assertion made. * **Assertions:** A claim posted to the Arbitrum Rollup contracts on Ethereum (parent chain) about the Arbitrum (child chain) execution state. Each claim consumes messages from the Arbitrum Rollup inbox contract. Each assertion can be confirmed after a period of 6.4 days, and anyone can challenge it during that period. A BoLD challenge will add an additional upper bound of 6.4 days to confirm an assertion. Gaining the right to post assertions requires placing a large, one-time bond, which can get taken away in the case of losing a challenge to a competing assertion. * **Bonding:** Participants in the protocol need to bond a certain amount of **WETH** to gain the privilege of posting assertions to the Rollup contracts by locking up an **WETH** bond in the protocol contracts. Whenever someone wants to create a sub-challenge, they must also place a smaller bond called a challenge bond when they do so. Bonds, their rationale, and magnitude will be covered in greater detail in the [Opening challenges](#opening-challenges) and [Sub-challenges](#sub-challenges) sections below, as well as the [Economics of disputes](/how-arbitrum-works/bold/bold-economics-of-disputes.md) page. * **Bonding of funds:** Creating an assertion in the Rollup contracts requires the submitter to join the validator set by putting up a large bond as **WETH**. Subsequent assertions posted by the same party do not require more bonds. Instead, the protocol always considers validators bonded to their latest posted assertion. The bonded funds are taken away if another competing assertion is confirmed. When an assertion is confirmed, the associated bonded funds can be withdrawn. * **Chain bindings:** Software that can interact with an Ethereum node in order to make calls and transactions to the onchain contracts needed for participating in the protocol. We utilize go-ethereum’s abigen utilities to create Go bindings to interact with the contracts above, with a few more developer-friendly wrappers. * **ChallengeManager:** This is a contract that allows for initiating challenges on assertions and provides methods for anyone to participate in challenges in a permissionless fashion. BoLD will require a new `ChallengeManager` written in Solidity and deployed to Ethereum. The challenge manager contains entry points for making challenge moves, opening leaves, creating sub-challenges, and confirming challenges. * **Challenge manager client:** Software that can manage the life cycles of challenges that an active validator participates in. Validators need to be able to participate in multiple challenges at once and manage individual challenge vertices correctly to act upon, confirm, or reject them. * **Challenge period:** Window of time ([6.4 days on Arbitrum One](/arbitrum-essentials/reference/chain-params.md)) over which an assertion can be challenged, after which the assertion can be confirmed. This is configurable by the DAO. * **challenge protocol:** A set of rules through which a disagreement on an assertion is resolved using Ethereum as the final arbiter. Ethereum's VM can verify one-step proofs of deterministic computation that will confirm a challenge winner in Arbitrum's Rollup contracts once the challenge period has elapsed. * **Delay attacks:** In a [delay attack](/how-arbitrum-works/bold/gentle-introduction.md#bold-makes-withdrawals-safer-to-the-parent-chain), a malicious party (or group of parties) acts within the challenge protocol and tries to delay the confirmation of results back to the parent chain. Before BoLD, the previous challenge protocol allowed adversaries to do this by forcing the honest party to play 1-vs-1 games against them to delay confirmation. In contrast, BoLD has a proven, constant upper bound on confirmation times for assertions in Arbitrum, addressing the biggest flaw of the current challenge mechanism. BoLD validators don’t need to play 1-vs-1 challenges and instead can defend a single assertion against many malicious claims. With delay attacks solved, Arbitrum will be able to allow permissionless validation. * **Edge:** Edges are a portion of a claim made by a validator about the history of the chain from some end state all the way back to some initial state. Edges are the fundamental unit in a challenge. * **Fraud proofs:** Proofs that prove or disprove that an invalid state transition has taken place. These proofs are generated by challenge participants and are submitted to a chain's parent chain. For example, Arbitrum Rollups that settle onto Ethereum will have their proofs submitted to Ethereum and verified via a smart contract. In this case, these proofs allow Ethereum to be the final arbiter of disagreements over assertions in the Rollup contracts, which cannot be falsified by any parties as there is only a single, correct result of executing a `WASM` instruction on a pre-state. WASM is the assembly language that is used to represent programs whose execution is being disputed. In fact, Arbitrum, before and after BoLD, uses a slightly different language called WAVM when executing challenges. The difference is not important to this discussion, but for details, see the page outlining the differences between WASM and WAVM. * **Honest validator:** An entity that knows the correct state of the Arbitrum child chain and who may want to participate in creating assertions, confirming assertions, and/or challenging invalid assertions if they exist. More specifically, this entity must run an Arbitrum full node in `MakeNodes`, `Defensive`, `StakeLatest`, or `ResolveNodes` mode as described in the [How to run a validator](/run-arbitrum-node/more-types/run-validator-node.md). Note that there must always be an active proposer (i.e., a validator who actively submits new assertions) to advance the chain and who will need to run a validator in `MakeNodes` mode. * **OneStepProver:** A set of existing contracts that implement a miniature WASM VM capable of executing one-step-proofs of computation of the child chain state transition function. This is implemented in Solidity and exists on Ethereum. No changes to the `OSP` contracts are needed for BoLD. * **[Permissionless validation](/how-arbitrum-works/bold/gentle-introduction.md):** The ability for anyone to interact with the Arbitrum Rollup contracts on Ethereum to both post assertions and challenge others' assertions without needing permission. With the release of BoLD, the Rollup contracts on Arbitrum will no longer have a permissioned list of validators. * **Rollup contract:** This smart contract living on Ethereum allows validators to bond on state assertions about Arbitrum. It's known as [`RollupCore.sol`](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/RollupCore.sol), and Arbitrum chains use it to post assertions. BoLD requires several changes to how assertions work in this contract, and it now contains a reference to another contract called a `ChallengeManager`, which is new in BoLD. * **State manager backend:** Software that can retrieve child chain states and produce commitments to `WAVM` histories for Arbitrum based on an execution server. The validator client, described below, will have access to a state manager backend in order to make moves on challenge vertices. * **Timers:** Each unrivaled edge (that is, an edge without another competing edge) will have a timer that *ticks up*. Time in the protocol is measured using blocks of the first non-Arbitrum ancestor chain (i.e., Ethereum blocks for L2 chains and L3 chains settling to an Arbitrum chain, and parent chain blocks for chains settling to a non-Arbitrum L2 chain), and block numbers are used. An edge's timer stops ticking when a rival edge is created onchain. We'll give a more detailed definition later, but two edges are considered rivals if they agree on some prefix of a computation starting from the same state but disagree on the remainder of that computation. Most importantly, timers are used to confirm assertions when an unrivaled edge's timer and associated assertion reaches the specified challenge period. See the section on [Timers and Edges](/how-arbitrum-works/bold/bold-technical-deep-dive.md#timers) for more details. * **Validating bridge:** The smart contract that leverages Ethereum's security and censorship-resistance to unlock bridged assets from the child chain back to the parent. Assets can be unlocked after an assertion (showing that those assets have been bridged from the child chain) has been posted and confirmed. * **Validator client:** A validator client is software that knows the correct history of the Arbitrum child chain via a state manager backend and can create assertions on the parent chain about them by bonding a claim. A validator is also active in ensuring honest assertions get confirmed and participating in challenging those it disagrees with. In BoLD, an honest validator will also participate in challenges other validators are a part of to support other honest participants. It interacts with the onchain components via chain bindings described above. * **Validator software:** Software that has knowledge of the correct Arbitrum child chain state at any point. It tracks the onchain rollup contracts for assertions posted and will automatically initiate challenges on malicious assertions if configured to do so by the user. It will participate in new and existing challenges and make moves as required by the protocol to win against any number of malicious entities. Its goals are to ensure only honest assertions about Arbitrum's state are confirmed on Ethereum, and that honest assertions always get confirmed on Ethereum in a timely manner (within at most two challenge periods). All Arbitrum full nodes are watchtower validators by default. This means they do not post claims or assertions unless configured to do so but will warn in case invalid assertions are detected onchain. ### How BoLD uses Ethereum When it comes to implementing the protocol, BoLD needs to be deployed on a credibly-neutral, censorship-resistant backend to be fair to all participants. As such, Ethereum was chosen as the perfect candidate. Ethereum is currently the most decentralized, secure, smart contract blockchain to which the full protocol can be deployed, with challenge moves performed as transactions to the protocol’s smart contracts. A helpful mental model for understanding the system is that it uses Ethereum itself as the ultimate referee for deciding assertion results. Participants in the challenge protocol can disagree over the *results of child chain state transitions* and provide proofs to the protocol's smart contracts on Ethereum to determine which result is correct. Because computation is deterministic, there will always be a single correct result. ![Transaction lifecycle diagram showing various pathways for submitting transactions](/img/haw-transaction-lifecycle.svg) *From the **[Nitro whitepaper](https://github.com/OffchainLabs/nitro/blob/master/docs/Nitro-whitepaper.pdf)**. Parent chain blocks are “settled to the parent chain” after a 6.4 day period has elapsed and nobody has challenged their validity on Ethereum.* In effect, there is a miniature Arbitrum state-transition VM [deployed as an Ethereum smart contract](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/osp/OneStepProofEntry.sol) to prove which assertions are correct. However, computation on Ethereum is expensive, which is why this mini-VM is built to handle “one-step proofs” consisting of a single step of WebAssembly (WASM) code. The Arbitrum state transition logic, written in Golang, is also compiled to WASM and will therefore obtain the same results as the VM found in the onchain smart contract. The soundness of the protocol depends on the assumptions that computation is deterministic and equivalent between the onchain VM and the Golang state transition compiled to WASM. All actors in the protocol have a local state from which they can produce valid proofs, and all honest parties will have the same local state. Malicious entities, however, can deviate from the honest parties in attempts to confirm invalid states through the protocol. Both the protocol and the honest validator client’s job is then to allow honest parties to always win against any number of malicious participants by always claiming the absolute truth. ## Assertions A key responsibility for Arbitrum proposers is to regularly post claims about the Arbitrum chains’ state to Ethereum at certain checkpoints. These are known as assertions (and are sometimes called child chain state roots). Assertions contain information, most critically: 1. The child chain block hash being claimed 2. A "history commitment" combining hashes of the chain's intermediate state after every block the assertion covers. This is essentially a Merkle-like data structure, a tree from which the asserter only needs to provide the root when posting the assertion. 3. The batch number it corresponds to for the Arbitrum chain 4. The number of messages in the Arbitrum Sequencer inbox at the time the assertion is created The following assertion to be posted onchain must consume the specified number of inbox messages from the previous assertion. There is a required delay measured in blocks of the first non-Arbitrum ancestor chain (i.e., Ethereum blocks for L2 chains and L3 chains settling to an Arbitrum chain, and parent chain blocks for chains settling to a non-Arbitrum L2 chain) for assertion posting. Currently, this value is set to equal one hour for BoLD. Anyone can confirm assertions after a period of 6.4 days if they have not been challenged. In particular, assertions facilitate the process of [withdrawing from Arbitrum back to Ethereum](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md). Arbitrum withdrawals require specifying a blockhash, which must be confirmed as an assertion onchain. This is why withdrawals have a delay of 6.4 days if they are not actively challenged. Validators must become proposers in the rollup contract before being allowed to post assertions. For Arbitrum One and Arbitrum Nova, this involves placing a one-time bond of 3600 **WETH** that is locked in the contract until they choose to withdraw. Validators can only withdraw their bond if their latest posted assertion gets confirmed. Every assertion a validator posts will become their latest bonded assertion. Subsequent bonds are not needed to post more assertions, instead, the protocol “moves” a validator’s bonds to their latest posted assertion. Assertions form a chain where there can be forks. For instance, a validator might disagree on the history commitment of block state hashes, which an assertion contains. All Arbitrum Nitro nodes are configured to warn users if they observe an assertion they disagree with posted onchain. However, suppose a node is configured as a validator and has deposited a bond to the [`Rollup` contract](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/RollupCore.sol). In that case, that validator can post the correct rival assertion to any invalid one it just observed. The validator will also be able to initiate a challenge by posting a challenge bond and other data to the `ChallengeManager`, signaling it is disputing an assertion. #### Overflow assertions Given the mandatory delay of one hour between assertions posted onchain, and each assertion is a claim to a specific Arbitrum batch, there could be a very large number of blocks in between assertions. However, a single assertion only supports a maximum of 2^26 Arbitrum blocks since the previous assertion. If this value is overflowed, one or more follow-up overflow assertions needs to be posted to consume the rest of the blocks above the maximum. This overflow assertion will not be subject to the mandatory one-hour delay between assertions. #### Trustless bonding pools A large upfront assertion bond is critical for discouraging malicious actors from attacking Arbitrum and spamming the network (e.g., delay attacks), especially because malicious actors will always lose challenges and their entire bond. On the other hand, requiring such a high upfront assertion bond may be prohibitive for a single honest entity to put up—especially since the cost to defend Arbitrum is proportional to the number of malicious entities and ongoing challenges at any given point in time. To address this, there is a [contract](https://github.com/OffchainLabs/bold/blob/main/contracts/src/assertionStakingPool/AssertionStakingPoolCreator.sol) that anyone can use to deploy a trustless, bonding (or staking) pool as a way of crowdsourcing funds from others who wish to help defend Arbitrum, but who may otherwise not individually be able to put up the sizeable upfront bond itself. Anyone can deploy an assertion bonding pool using the `AssertionStakingPoolCreator.sol` contract as a means to crowdsource funds for bonding funds to an assertion. To defend Arbitrum using one of these pools, an entity would first deploy this pool with the assertion they believe is correct and wish to bond on to challenge an adversary's assertion. Then, anyone can verify that the claimed assertion is correct by running the inputs through their node's State Transition Function (STF). If other parties agree that the assertion is correct, they can deposit their funds into the contract. When enough funds have been deposited, anyone can trigger the creation of the assertion onchain to start the challenge in a trustless manner. Finally, once the dispute protocol confirms the honest parties' assertion, all involved entities will get their funds reimbursed and can withdraw. Note that with bonding pools, there is no minimum **WETH** requirement and once the entire bond amount is raised (either 3600, 555, or 79 **ETH** for Arbitrum One), then the assertion can be posted by anyone trustlessly. Additionally, there is an optional feature in the Nitro node validator software that enables both the automatic deployment of a bonding pool contract and depositing of funds to challenge an observed invalid assertion. Trustless bonding pools can also be created to open challenges and make moves on challenges without sacrificing decentralization. ### Opening challenges To initiate a challenge, there must first be a fork in the assertion chain within the Arbitrum Rollup contracts. However, a challenge's actual start involves creating an edge claim and posting it to the `ChallengeManager` contract on the parent chain. If there is a fork in the assertion chain (a "top-level" dispute), anyone can begin a challenge by creating a challenge edge. No bond is needed: each branch of the fork has a unique assertion, with an attached bond, and that assertion contains enough information to uniquely determine its challenge edge; that is, there is no "wiggle room" that an adversary can use to create multiple challenge edges from a single assertion (if this were possible, it could lead to spam attacks on the protocol). Anyone can open a sub-challenge on an assertion without needing to be a bonder in the Rollup contract, so long as they post a challenge bond and an edge claiming intent to start the challenge. This challenge bond is much lower than the one required to become an assertion proposer. Challenges are not tied to specific addresses or parties—instead, anyone can participate. Recall that a challenge is a fundamental disagreement about an assertion posted to the Arbitrum chain. At its core, validators essentially disagree about the blockhash at a certain block number, and the BoLD protocol allows them to interactively narrow down their disagreement via fraud proofs such that Ethereum can be the final referee and claim a winner. At its core, the disagreement between validators looks something like this: * Common parent assertion: `batch 5, blockhash 0xabc` * Alice’s assertion: `batch 10, blockhash 0x123` * Bob’s assertion: `batch 10, blockhash 0x456` Their disagreement is about an Arbitrum block somewhere between batch 5 and batch 10. Here’s how the actual challenge begins in this example: Validators have to fetch all blocks between batch 5 and batch 10 and create a Merkle commitment out of them as a Merkle tree with `2^26+1` leaves. If there are fewer than 2^26 blocks in between the assertions, the last block is repeated to pad the leaves of the tree to that value. Validators then create an “edge” data structure, which contains the following fields: * **start\_hash:** the start\_hash of the claimed assertion and is also the end\_hash of the previous assertion * **end\_hash:** the end hash of the last block in the child assertion that a validator claims is correct. * **merkle\_root:** the Merkle root that results from committing to a Merkle tree from the start block hash to the end block hash * **inclusion\_proofs:** Merkle proofs that the end hashes are indeed leaves of the Merkle tree committing to a root The concept of a history commitment is at the core of challenges and BoLD itself. The validators above provide a Merkle proof of their commitment to some history. In this case, all the Arbitrum block hashes from batch 5 to batch 10. Using this tree, validators can narrow their disagreement to a single block using Merkle proofs by iteratively bisecting the Merkle root and creating edges which have their own history commitments to each half of the tree. ### Challenge resolution The fundamental unit in a challenge is an edge data structure. #### Initiation The first validator to create an edge initiates a challenge. The smart contracts validate the Merkle inclusion proofs and hashes provided to prove this challenge is about a specific fork in the assertion chain in the Rollup contract. #### Bisections When an edge is created, it claims some history from point A to B, with which validators can agree or disagree. Other validators can claim some history from point A to B’, where B’ is a different end state. A history commitment is a Merkle commitment to a list of hashes. To narrow down a disagreement, validators have to figure out what exact hash they disagree with. To do this, the game essentially takes turns between validators playing binary search. Each move here is known as a “bisection” because each move splits a history commitment in half. For an interactive walkthrough that replays real onchain bisections, see [How BoLD bisection works](/how-arbitrum-works/bold/how-bold-bisection-works.md). For instance: Alice commits to 33 hashes with start = A, end = B Bob commits to 33 hashes with start = A, end = B’ Either of them can perform a “bisection” move on their edge. For instance, if Alice “bisects” her edge E, the bisection transaction will produce two children, E\_1 and E\_2. E\_1 commits to 17 hashes from height A to B/2, and E\_2 commits to 17 hashes from height B/2 to B. A validator can make a bisection move on an edge as long as that edge is “rivaled”, meaning that there is another edge with a conflicting claim. #### Sub-challenges The number of steps of execution at which validators could disagree within a single Arbitrum block has a max of 2^42. To play a game of bisections on this amount of hashes would be unreasonable from a space requirement, as each history commitment would require 4.35Tb worth of hashes. Instead, BoLD plays the bisection game over different levels of granularity of this space of 2^42 hashes that we call sub-challenges that can be viewed as recursive execution of the dispute resolution process. As a reminder, the bisection game is an iterative and interactive process. The first sub-challenge is at the block level and is where validators disagree over Arbitrum blocks between two assertions. The disagreeing validators create “edges” containing history commitments to all the blocks in between those two assertions, which is a max of `2^26` child chain blocks, and commence the bisection game. As they progressively narrow down to a single block of disagreement, the validators then begin the next phase of the challenge process by opening a sub-challenge over up to `2^19` **BigSteps**, which are each `2^23` steps of WASM execution. Once they reach a single disagreement at the BigStep level, they open a final sub-challenge over a maximum of `2^23` SmallSteps, which are each a single step of WASM execution. The bisection game is the same at each sub-challenge level, and opening a sub-challenge requires placing another “challenge bond”. The magnitudes of challenge bonds are different at each sub-challenge level. See [How BoLD bisection works](/how-arbitrum-works/bold/how-bold-bisection-works.md) for a visual replay of these levels in action. #### One step proof Once validators reach a single step of disagreement at the deepest sub-challenge level, they need to provide something called a **One Step Proof**, or OSP for short. This is a proof of WASM execution showing that executing the Arbitrum state transition function at machine hash A leads to machine hash B. The parent chain, like Ethereum is for Arbitrum One, then actually runs a WASM emulator using a smart contract for this step and will declare a winner. An evil party cannot forge a one-step proof, and unless there is a critical bug in the smart contract, the honest party will always win. At this point, the honest party’s one-step proven edge will be confirmed, and the evil party has no more moves to make. Next, the honest party’s “branch” of edges all the way from the top to the one-step proven edge will have an ever-increasing timer until the top edge is confirmed by time. #### Timers Once a validator creates an edge, and if it does not have any rival edge contesting it, that edge will have a timer that ticks up called its **unrivaled timer**. Time in the protocol is measured in blocks of the first non-Arbitrum ancestor chain (i.e., Ethereum blocks for L2 chains and L3 chains settling to an Arbitrum chain, and parent chain blocks for chains settling to a non-Arbitrum L2 chain), and block numbers are used. An edge's timer stops ticking when a rival edge is created onchain. Edges also have an **inherited timer**, which is the sum of its unrivaled timer + the minimum inherited timer of an edge's children (recursive definition). Once one of the top-level edges that initiated a challenge has achieved an inherited \`timer >= a CHALLENGE\_PERIOD (6.4 days)\`\`, it can be confirmed. At this point, its assertion can also be confirmed as its associated challenge has completed. A minor but important detail is that edges also inherit the time their claimed assertion was unrivaled. Feel free to read the [BoLD whitepaper](https://arxiv.org/abs/2404.10491) for more details around how timers are tracked. #### Cached timer updates An edge's "inherited timer" value exists onchain and can be updated via a transaction. Given it is a recursive definition, it can be updated via multiple transactions. First, the lowermost edges have their timers updated, then their parents, etc., up to the top. Validators can track information locally to avoid sending wasteful transactions and only propagate updates once they are confident their edge is confirmable by time. #### Confirmation Once an edge has a total onchain timer greater than or equal to a challenge period, it can be confirmed via a transaction. Not all edges need to be confirmed onchain, as simply the top-level block challenge edge is enough to confirm the claimed assertion and resolve a dispute. A challenge is not complete at the one-step proof. It is only complete once the claimed assertion of a challenge is confirmed by time. ### Bonding in challenges To create a challenge, there must be a fork in the Arbitrum assertion chain smart contract. A validator that wishes to initiate a challenge must then post an “edge” claiming a history of block hashes from the previous assertion to the claimed assertion they believe is correct. To do so, they need to put up some value called a "challenge bond". Note that to open a new assertion-level challenge, no challenge bond needs to be posted. This is because top-level assertions already contain enough information to uniquely determine their corresponding challenge edge (which contain a hash of this history), and have already been bonded on. Challenge bonds are named as such because they are bonds required for opening challenges. The mechanism of how challenge bond economics are decided is contained in the [Economics of Disputes](/how-arbitrum-works/bold/bold-economics-of-disputes.md), which also explains the cost profile and spam prevention in BoLD. In short, the actual cost of a bond encompasses many costs associated with participating in the dispute game. More information on the bond sizes and how they were calculated can be found in the [Economics of Disputes](/how-arbitrum-works/bold/bold-economics-of-disputes.md) document mentioned above. Each sub-challenge that is created requires depositing a challenge bond. For Arbitrum One, the first unrivaled edge’s bond is kept in the challenge manager contract on Ethereum, while any subsequent rival bonds are kept in an excess bond receiver address. Once a challenge is complete, all bonds for an honest party are automatically refunded in-protocol while all confiscated bonds are sent to the ArbitrumDAO treasury. It is important to not offer the majority of the bonds confiscated from dishonest parties to honest parties to avoid perverse incentives, such as grieving attacks in self-challenges or to discourage needless competition between honest parties. ### Reimbursements of bonds The reimbursement of assertion bonds and challenge bonds for honest parties will be handled “in-band” by the protocol. Please see [Economics of Disputes](/how-arbitrum-works/bold/bold-economics-of-disputes.md) for more information about this topic. ### Upgrade mechanism For BoLD to be deployed on an Arbitrum chain, an upgrade admin action needs to be taken using an `UpgradeExecutor` pattern. This is a smart contract that executes actions as the rollup owner. At the upgrade, the `RollupCore.sol` contract will be updated to a new BoLD one, and additional contracts needed for BoLD challenges, such as an `EdgeChallengeManager.sol`, will also be deployed to the parent chain. Next, assertions will then be posted to the new Rollup contract. During the upgrade period, there could have been a very large number of blocks posted in Arbitrum batches. For this purpose, BoLD assertions support the concept of an **overflow**, allowing us to efficiently handle this situation. > **CAUTION** — Withdrawals leading up to a BoLD upgrade > > The confirmation timing on any withdrawal that is in-flight when the BoLD upgrade is activated will be delayed until the first BoLD assertion is confirmed. This means that for any Arbitrum chain that upgrades to use BoLD, including Arbitrum One and Arbitrum Nova, all pending withdrawals to the parent chain, Ethereum, that were initiated *before* the upgrade will be delayed by one challenge period, plus the time between the withdrawal was initiated and the time that the BoLD upgrade takes place. This is because the upgrade effectively "resets" the challenge period for that are not yet finalized. > > For example, if the upgrade happened at time *t*, then a withdrawal initiated at a time *t-2* days will need to wait an additional *6.4* days for their withdrawal to be finalized, totaling 8.4 days of maximum delay. Withdrawals that finalize before the upgrade takes place at time *t* will be unaffected. In other words, the maximum delay a withdrawal will experience leading up to the upgrade is 12.8 days (two challenge periods). The upgrade pattern for an existing Arbitrum Rollup to a BoLD-enabled one is tested extensively and run as part of each of our pull requests in the BoLD repository [upgrade workflow on GitHub](https://github.com/OffchainLabs/bold/blob/c4e068b568ff662f49ed191c5c3188ea7b6138b2/.github/workflows/go.yml#L209). --- > For a complete page index, fetch # Overview of BoLD This introduction is for those who want to learn about BoLD: a new dispute protocol for optimistic Rollups that enables permissionless validation for Arbitrum chains. BoLD stands for Bounded Liquidity Delay and is active on Arbitrum One, Arbitrum Nova, and Arbitrum Sepolia. ## What exactly is BoLD? BoLD upgraded Arbitrum's former dispute protocol. Specifically, BoLD changed some of the rules used by validators to open and resolve disputes about Arbitrum’s state, ensuring that only valid states receive confirmation on an Arbitrum chain’s parent chain (Ethereum). The former dispute protocol leveraged fraud proofs for challenges and was limited to a set of allowlisted validators. BoLD enables anyone to participate in validating the chain state (including challenges) and enhances security around child-to-parent chain messaging (including withdrawals). Under BoLD, a bonded validator's responsibilities are to: * Post claims about an Arbitrum chain state to its parent chain (i.e., Ethereum) * Open challenges to dispute invalid claims made by other validators, and * Confirm valid claims by participating in and winning challenges BoLD unlocks permissionless validation, ensuring that disputes are resolved within a fixed period (currently equivalent to two challenge periods, plus a two-day grace period for the Security Council to intervene if necessary, and a small delta for computation), effectively removing the risk of delay attacks and making withdrawals to a parent chain more secure. BoLD accomplished this by introducing a new dispute system that allows any single entity to defend Arbitrum against malicious parties—effectively enabling anyone to validate, propose, and defend Arbitrum's chain state without permission. ## Why did Arbitrum need a new dispute protocol? In the past, working fraud proofs were limited to allowlisted validators, who could assert the state of the chain. BoLD further decentralizes the protocol by allowing **anyone** to challenge and win disputes, all within a fixed time period. Arbitrum chains will continue to use an interactive proving game between validators and fraud proofs for security, with the added benefit that it is completely permissionless and time-bounded to the same length as a single challenge period (6.4 days by default). BoLD can uniquely offer time-bound, permissionless validation because a correct state assertion isn't tied to the validator that bonds their capital to a claim. This feature, coupled with the fact that the child chain states are entirely deterministic, can be proven on Ethereum, meaning that any number of honest parties can rely on BoLD to prove that their claim is correct. Lastly, BoLD does not change the fact that only a single honest party is required to defend Arbitrum. ### BoLD enables Arbitrum to become a Stage 2 rollup Inspired by [Vitalik’s proposed milestones](https://ethereum-magicians.org/t/proposed-milestones-for-rollups-taking-off-training-wheels/11571), the team over at L2BEAT has assembled a widely recognized framework for evaluating the development of Ethereum Rollups. Both Vitalik and the [L2BEAT framework](https://medium.com/l2beat/introducing-stages-a-framework-to-evaluate-rollups-maturity-d290bb22befe) refer to the final stage of Rollup development as "**Stage 2: No Training Wheels**”. A critical criterion for being considered a Stage 2 Rollup is the ability to allow anyone to validate the child chain state and post fraud proofs to Ethereum without restrictions. This step is a key requirement for Stage 2 because it ensures [“that a limited set of entities does not control the system and instead is subject to the collective scrutiny of the entire community”](https://medium.com/l2beat/introducing-stages-a-framework-to-evaluate-rollups-maturity-d290bb22befe). BoLD enabled permissionless validation by allowing anyone to challenge incorrect Arbitrum state assertions, unlocking new avenues for participation in securing the network and fostering greater inclusivity and resilience. BoLD achieves this by guaranteeing that a single, honest entity, whose capital is bonded to the correct Arbitrum state assertion, will always prevail against malicious adversaries. ![Pie slice](/img/bold-l2beat-pie-chart.png) With BoLD at its core, Arbitrum charts a course toward Stage 2 Rollup recognition by addressing the currently yellow (above) State Validation wedge in [L2BEAT's risk analysis pie chart](https://l2beat.com/scaling/summary). BoLD contributes to a more permissionless, efficient, and robust rollup ecosystem. ### BoLD makes withdrawals safer to the parent chain In the past, there was a period following a state assertion, known as the “challenge period,” during which any validator could open a dispute over the validity of a given child chain state root. If no disputes occurred during the challenge period, the protocol would confirm the state root as valid. This challenge period is why you must wait \~1 week (6.4 days) to [withdraw assets from Arbitrum One](/arbitrum-essentials/bridging/withdraw/tokens.md). While this design uses working fraud proofs for security, it is susceptible to [delay attacks](https://medium.com/offchainlabs/solutions-to-delay-attacks-on-rollups-434f9d05a07a), where malicious actors continuously open disputes to extend that challenge period for as long as they’re willing to sacrifice bonds—effectively extending the challenge period indefinitely by an amount equal to the time it takes to resolve each dispute, one by one. This risk is not ideal nor safe, and is why validation for Arbitrum One and Nova was limited to a permissioned set of entities overseen by the Arbitrum DAO. ![BoLD safer withdrawals](/img/bold-safer-withdrawals-with-bold-2.png) BoLD addresses these challenges head-on by introducing a time limit on the existing Rollup protocol for resolving disputes, effectively ensuring that challenges conclude within a 6.4-day window (the DAO can change this window for Arbitrum One and Nova). This conclusion is possible due to two reasons: 1. BoLD’s design allows for challenges between the honest party and any number of malicious adversaries to happen in parallel, and 2. The use of a time limit that will automatically confirm the honest party’s claims if the challenger fails to respond. To summarize with an analogy and the diagram below: Arbitrum’s former dispute protocol assumed that any assertion that gets challenged must be defended against each unique challenger sequentially, like in a “*1v1 tournament*”. BoLD, on the other hand, enabled any single honest party to defend the correct state and guarantee a win, similar to an “*all-vs-all battle royale*” in which there must and will always be a single winner. ![Before and after with BoLD](/img/bold-before-vs-after-with-bold.png) > **NOTE** > > The timer/clocks above are arbitrary and instead represent the duration of challenges and the sequential nature of challenges today, but they can take place in parallel with BoLD. The durations of the challenges are independent of one another. ## How is this possible? The BoLD protocol provides the guardrails and rules for how validators challenge claims about the state of an Arbitrum chain. Since Arbitrum’s state is deterministic, there will always be only one correct state for a given input of onchain operations and transactions. The beauty of BoLD’s design ensures that disputes are resolved within a fixed time window, eliminating the risk of delay attacks and ultimately enabling anyone to bond their funds to and successfully defend the singular correct state of Arbitrum. Let’s dive into an overview of how BoLD actually works. 1. **An assertion is made:** Validators begin by taking the most recent confirmed [assertion](/how-arbitrum-works/deep-dives/assertions.md), called `Block A`, and assert that some number of transactions afterward, using Nitro's deterministic [State Transition Function (STF)](/how-arbitrum-works/reference/stf-inputs.md), will result in an end state, `Block Z`. If a validator claims that the end state represented by `Block Z` is correct, they will bond their funds to `Block Z` and propose that state to its parent chain. (For more details on how bonding works, see [BoLD technical deep dive](/how-arbitrum-works/bold/bold-technical-deep-dive.md)). If no one disagrees after a certain period, known as the challenge period, then the state represented by the assertion `Block Z` is confirmed as the correct state of an Arbitrum chain. However, if someone disagrees with the end state `Block Z`, they can submit a challenge. 2. **A challenge is opened:** When another validator observes and disagrees with the end state represented by `Block Z`, they can permissionlessly open a challenge by asserting and bonding capital to a claim on a different end state, represented by an assertion `Block Y`. At this point, there are now two asserted states: `Block A → Block Z` and `Block A → Block Y`. Each of these asserted states, at this point, is referred to as an edge, while a Merkle tree of asserted states from some start to endpoint (e.g., `Block A → Block Z`) is more formally known as a *history commitment*. It is important to note that Ethereum at this point has no notion of which edge(s) are correct or incorrect—edges are simply a portion of a claim made by a validator about the history of the chain from some end state all the way back to some initial state. Also note that because a bond posted by a validator is for an assertion rather than for the party that posted it, there can be any number of honest, anonymous parties who can open challenges to incorrect claims. It is important to note that bonds posted to open challenges get held in the Rollup contract. There is a prescribed procedure outlining the Arbitrum Foundation's expectations regarding the use of these funds; see Step 5 below for a summary. 3. **Multi-level, interactive dissection begins:** To resolve the dispute, the disagreeing entities will need to agree on what the *actual, correct* asserted state should be. It would be tremendously expensive to re-execute and compare everything from `Block A → Block Z` and `Block A → Block Y`, especially since there could be potentially millions of transactions in between `A`, `Z`, and `Y`. Instead, entities take turns [bisecting their respective history commitments](/how-arbitrum-works/bold/how-bold-bisection-works.md) until they arrive at a single step of instruction, where an arbiter, such as Ethereum, can declare a winner. Note that this system is very similar to how challenges are resolved on Arbitrum chains today—BoLD only changes some minor, but important, details in the [resolution process](/how-arbitrum-works/bold/bold-technical-deep-dive.md#challenge-resolution). Let’s dive into what happens next: * **Block challenges**: When a challenge gets opened, edges are referred to as level-zero edges since they are at the granularity of Arbitrum blocks. The disputing parties take turns bisecting their historical commitments until they identify the specific block on which they disagree. * **Big-step challenge:** Now that the parties have narrowed down their dispute to a single block, the back-and-forth bisection exercise continues within that block. Note that this block is agreed upon by all parties to be a state that follows the initial state but precedes the final state. This time, however, the parties will narrow down on a specific range of instructions for the State Transition Function within the block—essentially working towards identifying a set of instructions within which their disagreement lies. Currently, this range is 2^20 steps of `WASM` instructions, which is the assembly of choice for validating Arbitrum chains. * **One-step challenge:** Within that range of 2^20 instructions, the back-and-forth bisecting continues until all parties arrive at a single step of instruction that they disagree on. At this point, the parties agree on the initial state of Arbitrum before the step, but disagree on the end state immediately after. Remember that since Arbitrum’s state is entirely deterministic, there is only one correct end state. 4. **One-step proof:** Once a challenge is isolated down to a dispute about a single step, both parties run that step to produce, and then submit, a one-step proof to the OneStepProof smart contract on the parent chain (e.g., Ethereum). A one-step proof is a proof that a single step of computation results in a particular state. The smart contract on the parent chain will execute the disputed step to validate the correctness of the submitted proof from both parties. It is at this point that the honest party's proof will be deemed valid and its tree of edges will be confirmed by time, whereas the dishonest party's edges will be rejected due to a timeout. 5. **Confirmation:** Once the honest one-step edge is confirmed, the protocol will work on confirming or rejecting the parent edges until it reaches the level-zero edge of the honest party. With the honest party’s level-zero edge now confirmed, their assertion bond is refundable. Meanwhile, the dishonest party has its bonds removed to ensure that dishonesty is always punished. * There is another way that a level-zero edge can get confirmed: time. At each of the mini-stages of the challenge (block challenge, big-step challenge, one-step challenge), a timer increments upwards towards some challenge period, T, defined by BoLD. This timer begins ticking for a party when they submit their bisected history commitment, and it continues until their challenger submits their bisected history commitment in response. An edge is automatically confirmed if the timer reaches T. 6. Reimbursements for the honest party's parent chain gas costs and mini-bonds made at the other challenge levels are handled by the Arbitrum Foundation. That’s it! We’ve now walked through each step that validators will take to dispute challenges under the BoLD protocol. One final note is that each of the steps explained above can run concurrently, which is one of the reasons why BoLD can guarantee a resolution to disputes within a fixed time frame. ## Frequently asked questions about BoLD (FAQ): #### Q: How does bonding work? The entities responsible for posting assertions about Arbitrum state to Ethereum are called validators. If posting assertions were free, anyone could create conflicting assertions at will to delay withdrawals by 14 days instead of 7. As such, Arbitrum requires validators to put in a “security deposit”, known as a bond, to be allowed to post assertions. Validators can withdraw their bond as soon as their latest posted assertion receives confirmation, and they will then end their responsibilities. These bonds can be any **ERC-20** token. They should be set to a large enough value (e.g., 200 **WETH**) to make it economically infeasible for an adversary to attack an Arbitrum chain and to mitigate against spam (that would otherwise delay confirmations). Requiring a high bond to post assertions about Arbitrum seems centralized, as we are replacing an allowlist of validators with a system that requires a large financial commitment to participate. To address this, there is a [contract](https://github.com/OffchainLabs/BoLD/blob/main/contracts/src/assertionStakingPool/AssertionStakingPoolCreator.sol) that anyone can use to deploy a bonding pool as a way of crowdsourcing funds from others who wish to help defend Arbitrum but who may not individually be able to put up the large upfront bond itself. The use of bonding pools, coupled with the fact that there can be any number of honest anonymous parties ready to defend Arbitrum, means that these high bond values do not harm decentralization. #### Q: Why are the bond sizes so high for Arbitrum One? There are two types of “bonds” in BoLD: **assertion and challenge**. The sizes below are carefully calculated and set for Arbitrum One, using a variety of factors, including TVL, to optimize a balance between the cost for honest parties and the security of the protocol. As always, the exact bond sizes for an Arbitrum chain using BoLD is entirely up to the chain owner to decide, if they choose to adopt BoLD at all. **Assertion bond sizes** Assertion bond sizes are akin to a “security deposit” that an entity deposits to fulfill the role of a proposer (i.e., a validator who proposes state assertions to the parent chain). The bond sizes are high because the proposer assumes significant responsibility: they must ensure the chain progresses. Accordingly, the bond also acts as a deterrent to *delay attacks*, where the attacker would sacrifice the bond to cause roughly a week of delay in a group of withdrawals. If the bond is too small or free, there may not be sufficient deterrence against this type of attack. Validators who choose to be proposers can withdraw their bond as soon as the protocol has confirmed their most recent posted assertion. We expect there to be very few proposers for Arbitrum One, as only one is sufficient for the chain's safety and full functionality. **Challenge bond sizes** If someone disagrees with a posted assertion from a proposer, they can pool funds together to propose their assertion that represents the correct history of the chain. Upon doing so, a challenge between the two claims will begin. Anyone can participate in the challenge, as it is permissionless. To resolve a challenge, participants will incur compute and gas costs due to the [interactive fraud proof game](/how-arbitrum-works/bold/bold-technical-deep-dive.md#challenge-resolution). Additionally, certain moves within a challenge require an extra bond to prevent resource exhaustion and spam from adversaries. These moves within a challenge require smaller, **challenge bonds**. The proposed challenge bonds for Arbitrum One are 1,110 **ETH** to fully resolve a dispute, which will also be reimbursed upon confirmation of the protocol's assertions. The rationale for the specific challenge bond size is based on a concept known as a [“resource ratio,”](/how-arbitrum-works/bold/bold-economics-of-disputes.md#preventing-spam) defined as the cost ratio between an adversary and an honest party when participating in the interactive fraud-proof game. Selecting this value ensured that the malicious party would pay 10 times the honest party's marginal costs. This resource ratio, coupled with the fact that an honest party will always have its bonds refunded while a malicious party loses everything, helps prevent and deter attacks from the outset. To summarize with a scenario, this effectively means that defending against a $1B dollar attack would require \~$100M of bonds. The \~$100M would be reimbursed upon winning a challenge, while the $1B in bonds posted by an adversary would be forfeited. The proposal aims to send the confiscated funds to the treasury by setting the “excess state receiver” address to the [DAO’s treasury address](https://arbiscan.io/address/0xF3FC178157fb3c87548bAA86F9d24BA38E649B58). The trade-off is that the higher the resource ratio we want, the more expensive it is for both honest and dishonest parties to make claims in disputes. **Bonding pools as a way to allow people to participate in assertion posting** BoLD ships with trustless [bonding pools](/how-arbitrum-works/bold/bold-technical-deep-dive.md#trustless-bonding-pools) that allow any group of participants to pool their funds together to challenge a dishonest proposer, and win. That is, any group of entities can pool funds into a simple contract that will post an assertion to Ethereum without needing to trust each other. Upon observing an invalid assertion, validators have a challenge period (\~6.4 days) to pool funds in the contract and respond with a counterassertion. Making it easy to pool the funds to participate in the defense of the Arbitrum trustlessly improves decentralization and the safety of BoLD. #### Q: Does the bond requirement only mean that whales can validate Arbitrum One? Validating Arbitrum One is **free and accessible**. By default, all Arbitrum One nodes are watchtower validators, meaning they can detect and report invalid assertions posted to Ethereum. However, becoming an assertion proposer requires a bond, as without it, anyone could delay all Arbitrum-bridged assets by one week. However, BoLD allows anyone to propose assertions and also challenge invalid assertions via pool contracts, helping keep proposers accountable for their actions. #### Q: How does BoLD disincentivize malicious actors from attacking an Arbitrum chain? Honest parties will always be refunded, while malicious actors will always stand to lose 100% of their bond. Malicious actors stand to lose everything at each challenge. The BoLD delay is bounded, and additional challenges would not increase the delay of a particular assertion. #### Q: In the event of a challenge, what happens to the confiscated funds from malicious actors for Arbitrum One? Recall that BoLD enables any validator to put up a bond to propose assertions about the child chain state. These assertions about the child chain state are deterministic, so an honest party who posts a bond on the correct assertion will always win in disputes. In these scenarios, the honest party will eventually have their bonds reimbursed while the malicious actor will lose all of their funds. In BoLD, all costs spent by malicious actors are confiscated and sent to the Arbitrum DAO treasury. A small reward, called the Defender's Bounty, of 1% will go to entities who put down challenge bonds in defense of Arbitrum One. For the remaining funds, the Arbitrum DAO will have full discretion over how to use those confiscated from a malicious actor. Applications include, but are not limited to: * Using the confiscated funds to refund the parent chain gas costs to honest parties, * Rewarding or reimbursing the honest parties with some, or all, of the confiscated funds over the 1% Defender's Bounty, * Burning some, or all, of the confiscated funds, or * Keep some, or all, of the confiscated funds within the Arbitrum DAO Treasury As always, an Arbitrum chain can choose how it wishes to structure and manage confiscated funds from dishonest parties. #### Q: Why are honest parties not automatically rewarded with confiscated funds from a malicious actor? It’s tempting to think that rewarding the honest proposer in a dispute can only make the protocol stronger, but this turns out not to be true, because an adversary can sometimes profit by placing the honest bonds themselves. This situation creates perverse incentives that threaten BoLD's security. Here’s an example, from Ed Felten: 💡 *Suppose the top-level assertion bond is $5M. A delay attack, where the attacker aims to cause a delay of at least one week at minimum cost, costs the attacker $5M. If we change the protocol to give the honest proposer 20% of the confiscated bond, then the attack only costs $4M, because the attacker can post both bonds ($10M total) and get back the honest $5M bond, plus a $1M reward. So, the protocol is weaker against delayed griefing. We can compensate by increasing the top-level assertion bond to $6.25M, so delay griefing still costs $5M, but that increases the cost, to an honest party, required to defend against other attacks. The intuition that giving larger rewards for honest actions can only strengthen the protocol, though natural, turns out to be incorrect. The reason is that large rewards create new strategic options for the attacker, which might enable them to reduce their costs.* That said, there’s no harm in paying the honest proposer a fair interest rate on their bond, so they don’t suffer for having helped the protocol by locking up their capital in a bond. Therefore, the BoLD AIP proposes that honest parties be rewarded with 1% of the bonds confiscated from a dishonest party in the event of a challenge. This reward applies only to entities that deposit challenge bonds and participate in defending Arbitrum against a challenge. The exact amount rewarded to honest parties will be proportional to the amount the defender deposits into the protocol during a challenge, making bonding pool participants eligible. #### Q: Why is **ARB** not the bonding token used for BoLD on Arbitrum One? Although BoLD supports using an **ERC-20** token, Ethereum, specifically **WETH**, was chosen over **ARB** for a few reasons: 1. **Arbitrum One and Arbitrum Nova both inherit their security from Ethereum already.** Arbitrum One and Nova rely on Ethereum for both data availability and as the referee for determining winners during fraud-proof disputes. Ethereum’s value is also relatively independent of Arbitrum, especially when compared to **ARB**. 2. Adversaries might be able to exploit the potential instability of **ARB** when trying to win challenges. Suppose an adversary deposits their bond in **ARB** and can create an impression that they have a nontrivial chance of winning the challenge. This impression might drive down the value of **ARB**, which would decrease the adversary's cost to create more bonds (i.e., more spam) during the challenge, which in turn could increase the adversary's chances of winning (which would drive **ARB** lower, making the attack cheaper still for the adversary, etc.). 3. **Access to liquidity**: Ethereum has greater liquidity than **ARB**. In the event of an attack on Arbitrum, access and ease of pooling funds may become crucial. 4. **Fraud proofs are submitted to, and arbitrated on, the parent chain (Ethereum).** The bonding of capital to make assertions is done so on the parent chain (Ethereum), since Ethereum is the arbitrator of disputes. If BoLD were to use **ARB** instead of Ethereum, a large amount of **ARB** must be pre-positioned on the parent chain, which is more difficult to do when compared to pre-positioning Ethereum on the parent chain (Ethereum). An Arbitrum chain owner may choose to use any token they wish for bonding if they adopt and use BoLD permissionless validation. #### Q: Can the required token for the validator be set to **ARB**, and can network **ETH** revenues get distributed for validator incentives for Arbitrum One? Yes. The asset a validator uses to become a proposer in BoLD can be configured to any **ERC-20** token, including **ARB**. For Arbitrum One, **ETH** is used as a bond for various reasons mentioned above. The Arbitrum DAO can change this asset type at any time via a governance proposal. Should such an economic incentive model exist, the source and denomination of funds used to incentivize validators will be at the discretion of the Arbitrum DAO. Again, though, we don't see **ARB**-based bonding as a good idea at present; see the last question. #### Q: How are honest parties reimbursed for bonding their capital to help secure Arbitrum One? The Arbitrum DAO reimburses “active” proposers with a fair interest rate, as a way of removing the disincentive to participate, by reimbursing honest parties who bond their capital and propose assertions for Arbitrum One. The interest rate should be denominated in **ETH** and should be equal to the annualized yield that Ethereum mainnet validators receive, which at the time of writing, is an APR between 3% to 4% (based on [CoinDesk Indices Composite Ether Staking Rate (CESR)](https://www.coindesk.com/indices/ether/cesr) benchmark and [*Rated.Network*](https://explorer.rated.network/network?network=mainnet\&timeWindow=all\&rewardsMetric=average\&geoDistType=all\&hostDistType=all\&soloProDist=stake)). This interest is considered a reimbursement because this payment reimburses the honest party for the opportunity cost of locking up their capital and should not be perceived as a “reward”—for the same [reasons why the protocol does not reward honest parties with the funds confiscated from a malicious actor](#q-why-are-honest-parties-not-automatically-rewarded-with-confiscated-funds-from-a-malicious-actor)). These reimbursement payments can be paid out upon an active proposer’s honest assertion being confirmed on Ethereum and will be calculated and handled offchain by the Arbitrum Foundation. BoLD makes it permissionless for any validator to become a proposer and introduces a way to pay service fees to honest parties for locking up capital to do so. Validators are not considered as active proposers until they successfully propose an assertion *with* a bond. To become an active proposer for Arbitrum One post-BoLD, a validator must propose a child chain state assertion to Ethereum. If they do not have an active bond on the parent chain, they need to attach a bond to their assertion to post it successfully. Subsequent assertions posted by the same address will move the already-supplied bond to their latest proposed assertion. Meanwhile, if an entity, say Bob, has posted a successor assertion to one previously made by another entity, Alice, then Bob would be considered the current active proposer by the protocol. Alice will no longer be considered the active proposer by the protocol, and once her assertion is confirmed, she will receive a refund of her assertion bond. There can only be one “active” proposer at any point in time. The topic of economic and incentive models for BoLD on Arbitrum One is valuable. It deserves the full focus and attention of the community via a separate proposal or discussion, decoupled from this proposal to bring BoLD to mainnet. Details regarding proposed economic or incentive models for BoLD will require ongoing research and development. However, deploying BoLD as-is represents a substantial improvement to Arbitrum's security, even without addressing economic-related concerns. The DAO may, through governance, choose to fund other parties or modify this reimbursement model at any time. For Arbitrum chains, any economic model can be configured alongside BoLD if chain owners decide to adopt BoLD. #### Q: For Arbitrum One proposers, is the service fee applied to the amount bonded? If that’s the case, the **ETH** would be locked and thus unable to be used to generate yield elsewhere. So, which assets get used to create this yield for the service fee? Would it involve some **ETH** from the Arbitrum bridge? The proposed service fee should correlate to the annualized income that Ethereum mainnet validators receive over the same period. At the time of writing, the estimated annual income for Ethereum mainnet validators is approximately 3% to 4% of their bond (based on [CoinDesk Indices Composite Ether Staking Rate (CESR)](https://www.coindesk.com/indices/ether/cesr) benchmark and [*Rated.Network*](https://explorer.rated.network/network?network=mainnet\&timeWindow=all\&rewardsMetric=average\&geoDistType=all\&hostDistType=all\&soloProDist=stake)). The fee is applied to the total amount bonded over the duration of a proposer's activity. A validator must deposit **ETH** into the contracts on the parent chain to become a proposer. So those deposited funds will indeed be unable to be used for yield in other scenarios. The decision on the source of funds for the yield is entirely up to the ArbitrumDAO. #### Q: For Arbitrum One, will the offchain computation costs get reimbursed? (i.e., the costs for a validator computing the hashes for a challenge) Reimbursements will not be made for any offchain computation costs, as we view these as costs borne by all honest operators, alongside the maintenance and infrastructure costs that regularly arise from running a node. Our testing has demonstrated that the cost of running a sub-challenge in BoLD, the most computationally-heavy step, on an AWS r5.4xlarge EC2 instance, costs around USD $2.50 (\~$1 hour for one challenge with 2.5 hour duration) using [on-demand prices for U.S. East (N. Virginia)](https://instances.vantage.sh/aws/ec2/r5.4xlarge). Therefore, the additional costs from offchain compute are assumed to be negligible relative to the regular infra costs of operating a node. #### Q: How will BoLD impact Arbitrum Nova? Although this AIP proposes that both Arbitrum One and Nova upgrade to use BoLD, we recommend the removal of the [allowlist of validators for Arbitrum One while keeping Nova permissioned with a DAO-controlled allowlist of entities](https://docs.arbitrum.foundation/state-of-progressive-decentralization#allowlisted-validators) - unchanged from today. This decision comes from two reasons: First, Arbitrum Nova’s TVL is much lower than Arbitrum One’s TVL (\~$17B vs. \~$46M at the time of writing, from [L2Beat](https://l2beat.com/scaling/summary)). This lower TVL means that the high bond sizes necessary to prevent spam and delay attacks would comprise a significant share of Nova’s TVL, which we believe introduces a centralization risk, as very few parties would have an incentive to secure Nova. A solution here would be to lower the bond sizes, Second, the lower bond sizes increase the cost of delaying griefing attacks (where malicious actors delay the chain’s progress) and thereby undermine the chain's security. We believe enabling permissionless validation for Nova is not worth the capital requirement trade-offs, given the unique security model of AnyTrust chains. Notably, since Arbitrum Nova's security already depends on at least one DAC member providing honest data availability, trusting the same committee to have at least one member provide honest validation does not add a major trust assumption. This trust assumption requires all DAC members to run validators as well. If the DAC is also validating the chain, a feature the Offchain Labs team has been working on, Fast Withdrawals, would allow users to withdraw assets from Nova in \~15 minutes, or the time it takes to reach parent chain finality. This finality is made possible by the DAC, which attests to and instantly confirms an assertion. Fast Withdrawals will be the subject of a future forum post and snapshot vote. #### Q: When it comes to viewing the upfront assertion bond (to be a proposer) as the security budget for Arbitrum One, is it possible for an attacker to go above the security budget, and if so, what happens then? The upfront capital to post assertions (onchain action) is 3,600 **ETH**, with subsequent sub-challenge assertions requiring 555/79 **ETH** (per level). This cost applies to both honest proposers and malicious entities. A malicious entity can post multiple invalid top-level assertions and/or open multiple challenges, and the honest entity can It is critical to note that Arbitrum state transitions are entirely deterministic. An honest party bonded to the correct state assertion will receive all their costs refunded, while a malicious entity stands to lose everything. Additionally, BoLD’s design ensures that any party bonded to the correct. If a malicious entity wanted to attack Arbitrum, they would need to deposit 3600 **ETH** to propose an invalid state assertion. > **INFO** — Node Running Info > > * Anyone can run an Arbitrum node (today and post-BoLD) > * The default mode is `watchtower`, which chills out and watches the chain in action. It will alert you if it detects something wrong on the chain, but it takes no action. It requires **no** funds and **doesn't** take any onchain action. > * Other "modes" that nodes can run are: `stakeLatest`, `resolveNodes`, `makeNodes`, and `defensive`. All of these modes **require** funds and **will** take onchain action. > * Nodes running in this mode are considered validators because they validate what they see and take onchain action. > * Running a `watchtower` node is **not** a validator. > * Proposers are in a special role; they strictly run in `makeNodes` mode. This role means a proposer is someone running an Arbitrum node in `makeNodes` mode, also making them a validator. > * For more information about see [Running a Node](/run-arbitrum-node/overview.md) and [Validation strategies](/run-arbitrum-node/more-types/run-validator-node.md#validation-strategies). #### Q: How do BoLD-based L3s challenge periods operate, considering the worst-case scenario? To recap, both Arbitrum’s current dispute protocol and BoLD require assertions to be posted to the parent chain and employ interactive proving, which involves a back-and-forth between two entities until a single step of disagreement is reached. That single disputed step then gets submitted to contracts on the parent chain, which will eventually declare a winner. For L2s such as Arbitrum One, BoLD must be deployed on a credibly neutral, censorship-resistant backend to ensure fair dispute resolution. Ethereum is therefore the ideal candidate for deploying the BoLD protocol on L2s. But you might now be wondering: what about L3 Arbitrum chains that don't settle to Ethereum? Unlike L2s that settle to Ethereum, assertions on an L3's state need to be posted to an L2 either via (A) the L3 Sequencer or (B) the Delayed Inbox queue managed by the L2 sequencer on L2. If the parent chain (in this case, L2) is getting repeatedly censored or if the L2 sequencer is offline, every block-level assertion and/or sub-challenge assertion would need to wait 24 hours before they can bypass the sequencer (using the `SequencerInbox`'s [`forceInclusion` method](/how-arbitrum-works/deep-dives/transaction-lifecycle.md#force-inclusion-after-delays)). If this were to happen, challenge resolution would get delayed by a time *t* where *t* = (24 hours) \* number of moves for a challenge. To illustrate with sample numbers, if a challenge takes 50 sequential moves to resolve, then the delay would be 50 days! To mitigate the risk of this issue manifesting on Arbitrum chains, Offchain Labs has included a feature called [*Delay Buffer*](/how-arbitrum-works/deep-dives/sequencer.md#censorship-timeout) (also called *Censorship Timeout*) in BoLD’s 1.0.0 release. The *Delay Buffer* feature aims to limit the negative effects of: prolonged parent chain censorship, prolonged sequencer censorship, and/or unexpected sequencer outages. This buffer is configurable by setting a time threshold that decrements when unexpected delays occur. Once that time threshold is reached, the force inclusion window is reduced, effectively allowing entities to make moves without the 24-hour delay per move. Under reasonable parameterization, the sequencer could be offline/censoring for 24 hours twice, before the force inclusion window drops from 24 hours to a minimum inclusion time. The force inclusion window gradually (over weeks) replenishes to its original value over time as long as the sequencer is on "good behavior"—regularly sequencing messages without unexpected delays. We believe that the Delay Buffer feature provides stronger guarantees of censorship resistance for Arbitrum chains. Recommended parameter defaults for L3 Arbitrum chains that wish to use BoLD are available in the [BoLD configuration parameters table](/launch-arbitrum-chain/chain-config/validation/bold.md#table-of-arbitrum-bold-parameters). #### Q: What is the user flow for using the assertion bonding pool contract? > **WARNING** > > The autopooling feature is not available on Arbitrum Nitro as of yet. Anyone can deploy an assertion bonding pool using [`AssertionStakingPoolCreator.sol`](https://github.com/OffchainLabs/BoLD/blob/main/contracts/src/assertionStakingPool/AssertionStakingPoolCreator.sol) as a means to crowdsource funds to put up a bond for an assertion. To defend Arbitrum using a bonding pool, an entity would first deploy this pool with the assertion they believe is correct and wish to put up a bond to challenge an adversary's assertion. Then, anyone can verify that the claim is correct by running the inputs through their node's State Transition Function (STF). If other parties agree that the assertion is correct, then they can deposit their funds into the contract. When sufficient funds are available, anyone can permissionlessly trigger the creation of the assertion onchain to initiate the challenge. Finally, once the dispute protocol confirms the honest parties' assertion, all involved entities can receive reimbursement for their funds and withdraw. The Arbitrum Nitro node validation software also features an optional "auto pooling" option that automates the entire workflow for assertion bonding pool deployment and depositing funds into the pool. If "auto pooling" is activated and the private key controlling the validator has sufficient funds, a trustless pool will get deployed alongside an assertion of the available funds. If a validator with the "auto pooling" feature enabled sees an assertion onchain that it agrees with *and* a bonding pool already exists for that assertion. The validator will automatically deposit funds into the bonding pool to "join" others backing that onchain assertion in a trustless manner. #### Q: What type of hardware will be necessary to run a BoLD validator? The minimum hardware requirements for running a BoLD validator are still being researched and finalized. The goal, however, is that regular consumer hardware (i.e., a laptop) can effectively secure an Arbitrum chain using BoLD in the average case, by any honest party. #### Q: How do BoLD validators communicate with one another? Is it over a P2P network? BoLD validators for Arbitrum chains communicate directly with smart contracts on the parent chain (Ethereum), meaning that opening challenges, submitting bisected history commitments, one-step proofs, and confirmations are all adjudicated on the Ethereum blockchain. There is no P2P between validators. #### Q: For an L3 Arbitrum chain, secured using BoLD, that settles to Arbitrum One, does the one-step proof happen on the parent chain? Yes, it happens on Arbitrum One. #### Q: For Arbitrum One, does implementing BoLD reduce the scope or remove the need for the Arbitrum Security Council? BoLD can limit the scope of Arbitrum One and Nova’s reliance on the Security Council as it takes Arbitrum chains one step closer to complete decentralization. ## Related reading * [BoLD: a technical deep dive](/how-arbitrum-works/bold/bold-technical-deep-dive.md): Implementation details of the dispute protocol, including assertions, edges, sub-challenges, and the one-step proof. * [How BoLD bisection works](/how-arbitrum-works/bold/how-bold-bisection-works.md): Interactive visualization that replays real onchain bisections across the block, bigstep, and smallstep levels. * [Economics of disputes](/how-arbitrum-works/bold/bold-economics-of-disputes.md): Bond sizes, the resource ratio, and spam prevention in BoLD. * [Assertions deep dive](/how-arbitrum-works/deep-dives/assertions.md): How assertions structure validation and disputes in the Rollup contracts. * [How to run a validator](/run-arbitrum-node/more-types/run-validator-node.md): Validation strategies (watchtower, defensive, makeNodes, etc.) for participating in BoLD. --- > For a complete page index, fetch # How BoLD bisection works The [BoLD (Bounded Liquidity Delay) dispute protocol](/how-arbitrum-works/bold/gentle-introduction.md) resolves disagreements between validators through a process called [**bisection**](/how-arbitrum-works/bold/bold-technical-deep-dive.md#bisections). When two or more validators disagree about the state of the chain, they narrow down the exact point of disagreement by repeatedly splitting (bisecting) their claims until the dispute is reduced to a single step of execution, which can then be verified with a [**one-step proof**](/how-arbitrum-works/bold/bold-technical-deep-dive.md#one-step-proof) (OSP). The interactive visualization below replays real onchain events from a [ChallengeManager](/how-arbitrum-works/bold/bold-technical-deep-dive.md#challenge-resolution) contract on Arbitrum Sepolia, showing how edges are created, bisected across multiple [levels (block, bigstep, smallstep)](/how-arbitrum-works/bold/bold-technical-deep-dive.md#sub-challenges), and eventually resolved. Loading... --- > For a complete page index, fetch # AnyTrust Protocol AnyTrust is a variant of the [](/how-arbitrum-works/inside-arbitrum-nitro.md)Arbitrum Nitro technology that lowers costs by accepting a mild trust assumption. The Arbitrum protocol requires that all Arbitrum nodes, including [validators](/how-arbitrum-works/deep-dives/assertions.md) (nodes that verify the correctness of the chain and place bonds on correct results), have access to the data of every child chain transaction in the Arbitrum chain’s inbox. An Arbitrum Rollup provides data access by [posting the data (in batched, compressed form) on the parent chain](/how-arbitrum-works/deep-dives/sequencer.md#batch-posting), Ethereum, as blobs or calldata. The Ethereum gas used to pay for this is the largest component of the cost of using Arbitrum. AnyTrust relies instead on an external Data Availability Committee (DAC) to store data and provide it on demand. The DAC has N members, of which AnyTrust assumes at least two are honest. This means that if `N - 1` DAC members promise to provide access to some data, at least one of them must be honest. Since there are two honest members and only one failed to keep the promise, it follows that at least one of the promisers must be honest, and that honest member will provide data when needed to ensure the chain can properly function. ## Keysets A Keyset specifies the [Boneh–Lynn–Shacham (BLS)](https://en.wikipedia.org/wiki/BLS_digital_signature) public keys of DAC members and the number of signatures required for a Data Availability Certificate (DACert) to be valid. Keysets enable DAC membership changes and allow DAC members to change their keys. A Keyset contains: * The number of DAC members, and * For each DAC member, a BLS public key, and * The number of DAC signatures required. Keysets are identified by their hashes. A parent chain `KeysetManager` contract maintains a list of currently valid Keysets. The child chain’s `Owner` can add or remove Keysets from this list. When a Keyset becomes valid, the `KeysetManager` contract emits a parent-chain Ethereum event that includes the Keyset’s hash and full contents. This allows the contents to be recovered later by anyone, given only the Keyset hash. Although the API does not limit the number of Keysets that can be valid simultaneously, only one Keyset is normally valid. ## Data Availability Certificates (DACert) A central concept in AnyTrust is the Data Availability Certificate (hereafter, a "DACert"). A DACert contains: * The hash of a data block * An expiration time * Proof that the `N - 1` DAC members have signed the pair (hash, expiration time), consisting of: * The hash of the Keyset used in signing * A bitmap showing which DAC members signed * A BLS aggregated signature (over the BLS12-381 curve) proving that those parties signed. Because of the `2-of-N` trust assumption, a DACert constitutes proof that the block’s data (i.e., the preimage of the hash in the DACert) will be available from at least one honest DAC member, at least until the expiration time expires. In regular Arbitrum Nitro, the Arbitrum [](/how-arbitrum-works/deep-dives/sequencer.md)Sequencer posts data blocks on the parent chain as [blobs or calldata](/how-arbitrum-works/deep-dives/sequencer.md#submitting-to-the-sequencer-inbox-contract). The hashes of the data blocks are committed to the parent chain Inbox contract, allowing the data to be reliably read by the child chain code. AnyTrust gives the Sequencer two ways to post a data block on the parent chain: it can post the full data as above, or it can post a DACert proving availability of the data. The parent chain Inbox contract will reject any DACert that uses an invalid Keyset; the other aspects of DACert validity are checked by the child chain code. The child chain code that reads data from the inbox reads a full-data block as in ordinary Arbitrum Nitro. If it sees a DACert instead, it checks the DACert's validity, using the Keyset specified by the DACert (which is known to be valid because the parent chain inbox verified it). The child chain code verifies that: * The number of signers is equal to or greater than the number required by the Keyset * The aggregated signature is valid for the claimed signers * The expiration time is at least two weeks after the current child chain timestamp If the DACert is invalid, the child chain code discards it and proceeds to the next data block. If the DACert is valid, the child chain code reads the data block, which is guaranteed to be available because the DACert is valid. ## Data Availability Servers (DAS) DAC members run the Data Availability Server (DAS) software. The DAS exposes two APIs: * The Sequencer API, intended only for the Arbitrum chain’s Sequencer, is a JSON-RPC interface that enables the Sequencer to submit data blocks to the DAS for storage. Production deployments will typically block access to this API from callers other than the Sequencer. * The REST API, intended to be available to the world, is a RESTful HTTP(s)-based protocol that allows data blocks to be fetched by hash. This API is fully cacheable, and deployments may use a caching proxy or CDN to increase scale and protect against DoS attacks. Only DAC members have a reason to support the Sequencer API. We expect others to run the REST API, which is helpful (discussed below). The DAS software, based on configuration options, can store its data in local files, in a BadgerDB database, on Amazon S3, or redundantly across multiple backing data stores. The software also supports optional caching in memory (using Bigcache) or in a Redis instance. ## Sequencer-DAC interaction When the Arbitrum Sequencer produces a data batch that it wants to post using the DAC, it sends the batch's data, along with the expiration time (normally, three weeks in the future) via RPC to all DAC members in parallel. Each DAC member stores the data in its backing store, indexed by the data's hash. Then the member signs the pair (hash, expiration time) using its BLS key, and returns the signature with a success indicator to the Sequencer. Once the Sequencer has collected enough signatures, it can aggregate the signatures and create a valid DACert for the pair (hash, expiration time). The Sequencer then posts that DACert to the parent chain Inbox contract, making it available to the AnyTrust chain software on the child chain. If the Sequencer fails to collect enough signatures within a few minutes, it will abandon the attempt to use the DAC and will "[fall back to Rollup mode](/how-arbitrum-works/deep-dives/assertions.md)" by posting the full data directly to the parent chain, as it would do in a non-AnyTrust chain. The child chain software can understand both data posting formats (via DACert or full data) and will handle each one correctly. --- > For a complete page index, fetch # ArbOS ArbOS is the child-EVM virtual machine monitor (VMM—aka hypervisor) for the Arbitrum chain, providing the execution environment for the chain. It acts as a trusted “system glue” component within the [State Transition Function (STF)](/how-arbitrum-works/deep-dives/stf-gentle-intro.md). ArbOS is responsible for: ## 1. Managing network resources ArbOS allocates and tracks the resources needed to execute transactions on the child chain. ## 2. Block production ArbOS processes incoming [sequencer data batches](/how-arbitrum-works/deep-dives/sequencer.md#batch-posting) 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 features such as **ETH** and token [deposits](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md) and [withdrawals](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md). ## 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 while offering significant flexibility, allowing 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 a dual-gas-account model to cross-chain messaging—optimizing performance and flexibility. These innovations are realized through minimal, yet strategic modifications to Geth that integrate 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 Arbitrum Nitro leverages “Geth at the core” and ArbOS's custom enhancements 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 ArbOS implements precompiles using a Solidity interface paired with a Golang backend, connected through runtime reflection for type safety and ABI conformance. For full technical details of the precompile architecture, see the [ArbOS technical reference](/how-arbitrum-works/reference/arbos-reference.md#how-precompiles-work). For a complete list of precompiles, refer to the [precompile references](/arbitrum-essentials/precompiles/reference.md). ## Messages An [`L1IncomingMessage`](https://github.com/OffchainLabs/nitro/blob/4ac7e9268e9885a025e0060c9ec30f9612f9e651/arbos/incomingmessage.go#L54) represents an incoming [sequencer](/how-arbitrum-works/deep-dives/sequencer.md) message. A message includes one or more user transactions, depending on load, and is made into a [unique child chain block](https://github.com/OffchainLabs/nitro/blob/4ac7e9268e9885a025e0060c9ec30f9612f9e651/arbos/block_processor.go#L118). The child chain block may include additional system transactions while processing the message's user transactions. However, 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](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md). ## ArbOS state ArbOS's state is managed through `ArbosState` objects that abstract over the underlying key-value storage. Most of this state supports [precompiles](/arbitrum-essentials/precompiles/reference.md). Key components include `blockhashes` (recent parent chain block hashes), `l1PricingState` (parent chain fee tracking and batch poster reimbursement), and `l2PricingState` (child chain gas pricing via dual gas pools). For the full technical details of ArbOS state, pricing internals, and the per-block gas limit mechanism, see the [ArbOS technical reference](/how-arbitrum-works/reference/arbos-reference.md#arbos-state). For parent chain pricing specifics, see [Gas and fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#parent-chain-gas-pricing). ## Gas and fees ArbOS manages child chain gas pricing and fee collection for both child chain execution costs and parent chain data posting costs. The child chain uses a dynamic basefee mechanism with multiple gas targets over different time windows to handle demand spikes while maintaining long-term chain capacity. Transactions pay fees to both the batch poster (for parent chain data costs) and the network (for child chain execution). #### Comprehensive explanation of gas pricing * Child chain basefee calculation and gas targets * Parent chain calldata pricing and batch poster reimbursement * Fee estimation and collection mechanisms * The gas target's role in validator security See [Gas and fees](/how-arbitrum-works/deep-dives/gas-and-fees.md) for the complete breakdown, including detailed mathematical formulas and parent chain pricing internals. ## Stylus-specific differences [](/stylus/gentle-introduction.md)Stylus extends ArbOS to support WASM-based smart contracts alongside the EVM. When a transaction targets a Stylus contract, ArbOS routes execution to the WASM runtime, which uses host I/O calls for blockchain state access instead of EVM opcodes. Stylus contracts use a multi-dimensional gas model based on Ink units, with LRU caching to minimize execution overhead. For the full technical details of Stylus execution flow, caching, gas pricing, and Go-WASI integration, see the [ArbOS technical reference](/how-arbitrum-works/reference/arbos-reference.md#stylus-specific-differences). --- > For a complete page index, fetch # Assertions ## The Rollup chain assertions vs. child chain blocks The Rollup chain consists of assertions, which serve as checkpoints summarizing multiple child chain blocks. Assertions are what give Arbitrum its hard finality guarantee — for the broader soft- vs. hard-finality picture, see [Finality](/how-arbitrum-works/deep-dives/sequencer.md#finality). * Child chain blocks contain individual transaction data * Assertions provide state summaries recorded on Ethereum * Each assertion may represent multiple child chain blocks, optimizing gas costs and reducing Ethereum storage usage. Validators submit assertions by calling `createNewAssertion` in the Rollup contract. Assertions contain structured data known as `AssertionInputs`, which capture the before-state and after-state of execution for future validation. ## Contents of an assertion Each assertion consists of: * **Assertion number**: A unique identifier * **Predecessor assertion**: The last confirmed assertion * **Number of child chain blocks**: The total child chain blocks included * **Number of inbox messages**: Messages consumed during execution * **Output hash**: A cryptographic commitment to the resulting state Arbitrum ensures assertions are automatically confirmed or rejected based on protocol rules: 1. An assertion is confirmed if: * Its predecessor is the latest confirmed assertion * The dispute window has passed without challenges 2. An assertion is rejected if: * Its predecessor assertion is invalid * A conflicting assertion has been confirmed For more details on how the Rollup chain works under BoLD, the [gentle introduction](/how-arbitrum-works/bold/gentle-introduction.md) provides an overview that touches on the Rollup chain. > **NOTE** — Validators and proposers serve different roles > > Validators validate transactions by computing the next chain state using the chain's [STF](/how-arbitrum-works/deep-dives/stf-gentle-intro.md), whereas proposers can also assert and challenge the chain state on the parent chain. Except for the assertion number, the assertion's contents are merely claims by its proposer. Arbitrum doesn't know at first whether any of these fields are correct. The protocol should eventually confirm the assertion if all of these fields are correct. The protocol should eventually reject the assertion if any of these fields are incorrect. An assertion implicitly claims that its predecessor is correct, meaning it also asserts the correctness of the entire chain's history: a sequence of ancestor assertions that reaches back to the chain's genesis. An assertion also implicitly claims that its older siblings (other assertions with the same predecessor) are incorrect, if any exist. If two assertions are siblings, and the older sibling is correct, then the younger sibling is considered incorrect, even if everything else in the younger sibling is true. The assertion is assigned a deadline, which indicates how much time other validators have to respond to it. For an assertion `R` with no older siblings, the deadline will equal the time when the assertion posts, plus an interval of time known as the challenge period; subsequent younger siblings will have the same deadline as their oldest sibling (`R`). You don't need to do anything if you're a validator and agree that an assertion is correct. If you disagree with an assertion, you can post another assertion with a different result, and you'll probably end up in a challenge against the party that proposed the first assertion (or another party acting in support of that assertion). For the bisection protocol used to resolve such challenges, see [How BoLD bisection works](/how-arbitrum-works/bold/how-bold-bisection-works.md). ## Delays Even if the Assertion Tree has multiple conflicting assertions and multiple disputes are in progress, validators can continue making new assertions. Honest validators will build on one valid assertion (intuitively, an assertion is also an implicit claim of the validity of all its parent assertions). Likewise, users can continue transacting on the child chain, as transactions will still post to the chain’s inbox. The only delay users experience during a dispute is in their [child-to-parent chain messages](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md) (i.e., withdrawals). A key property of BoLD is that, in the common case, their withdrawals (messages) will only experience a delay of one challenge period. In the event of a dispute, withdrawals (messages) will be delayed by no more than two challenge periods, regardless of the adversaries’ behavior during the challenge. ## Staking and validator incentives Arbitrum requires validators to bond **ETH** as a security deposit to ensure honest participation and prevent malicious behavior. This mechanism enforces economic accountability (for a deeper analysis of bond sizing and dispute incentives, see [The economics of disputes in BoLD](/how-arbitrum-works/bold/bold-economics-of-disputes.md)): * **Proposers** (validators submitting assertions) must bond **ETH** to support their claims. * **Challenges** against incorrect assertions result in bond forfeiture for dishonest validators. * **Successful challengers** receive a portion of the dishonest validator's bond as a reward. Validators can adopt different roles: 1. **Active validators**: Regularly propose new assertions. 2. **Defensive validators**: Monitor the network and challenge incorrect assertions. 3. **Watchtower validators**: Passively observe and raise alarms when fraud is detected. The protocol design requires only one honest validator to secure the system, making Arbitrum trustless and resistant to Sybil attacks. ## Staking mechanism Some validators will act as bonders at any given time, while others remain passive. Bonders deposit **ETH** bonds into Arbitrum's smart contracts, which are forfeited if they lose a challenge. > **NOTE** > > Arbitrum Nitro chains accept **ETH** as the only collateral for staking. A single bond can secure a sequence of assertions, meaning a validator's bond applies to multiple checkpoints of the chain's history. This checkpoint allows efficient resource use while maintaining security. A validator must be bonded to its predecessor to create a new assertion. The bond ensures that validators have economic risk in any assertion they make. ## Handling disputes and delays Multiple disputes may be active simultaneously if conflicting assertions arise in the Assertion Tree. However, Arbitrum's protocol ensures that: * **Honest validators can continue asserting**, building on the last correct assertion. * **Users can keep transacting** on the child chain without disruption. * **Child-to-parent chain withdrawals** may experience delays; typically, withdrawals experience a single challenge period (6.4 days). A key property of BoLD is that, in the common case, withdrawals (messages) will experience only a one-challenge-period delay. In the event of a dispute, withdrawals will be delayed by no more than two challenge periods, regardless of the adversaries' behavior during the challenge. Despite these delays, Arbitrum guarantees that honest assertions always succeed, maintaining Ethereum-level security. ## Verifying a child chain block in a confirmed assertion You can programmatically check whether a specific child chain block has been included in a confirmed assertion by querying the Rollup contract on the parent chain. This is useful for applications that need to verify finality beyond soft confirmation. The Rollup contract lives on the parent chain, but its address is exposed via the child chain's network metadata. The example below uses the child chain to look the address up, then attaches it to the parent provider for reads. ```ts import { Contract } from 'ethers'; import { getArbitrumNetwork } from '@arbitrum/sdk'; import { BoldRollupUserLogic__factory } from '@arbitrum/sdk/dist/lib/abi-bold/factories/BoldRollupUserLogic__factory'; // Look up the child chain's network metadata to find the Rollup address. const childNetwork = await getArbitrumNetwork(childProvider); // The Rollup contract is deployed on the parent chain, so attach it to parentProvider. const rollup = new Contract(childNetwork.ethBridge.rollup, BoldRollupUserLogic__factory.abi, parentProvider); // Get the latest confirmed assertion hash. const assertionHash = await rollup.latestConfirmed(); ``` To resolve `assertionHash` to a specific child chain block, query the Rollup's `AssertionCreated` events for that hash and decode the `afterState` — it contains the last processed `GlobalState` (block number and send count). See the [block-verification tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/block-verification-in-parent-chain-assertion) for a complete working example. --- > For a complete page index, fetch # Gas and fees Arbitrum uses gas to track the execution cost on an Arbitrum Nitro chain. It works the same as Ethereum gas, in the sense that every EVM instruction costs the same amount of gas as it would on Ethereum. There are two parties a user pays when submitting a transaction: * **Poster**: If reimbursable, covers the parent chain resources such as the parent chain calldata needed to post the transaction. * **Network fee account**: Covers the child chain's resources, including computation, storage, and other burdens that child chain nodes must bear to service transactions. The parent chain component is the product of the transaction's estimated contribution to its batch's size—computed using [Brotli](/how-arbitrum-works/deep-dives/sequencer.md#compression) on the transaction alone—and the child chain's view of the parent chain data price. This value dynamically adjusts over time to ensure the batch poster is ultimately fairly compensated. The child chain component consists of the traditional fees Geth would pay to bonders in a vanilla parent chain, such as the computation and storage charges that apply to the [](/how-arbitrum-works/deep-dives/stf-gentle-intro.md)State Transition Function (STF). [](/how-arbitrum-works/deep-dives/arbos.md)ArbOS charges additional fees for executing its child-chain-specific [precompiles](/arbitrum-essentials/precompiles/overview.md), whose fees are dynamically priced based on the resources used during execution. The following sections detail how parent and child chain fees are calculated. For a practical guide on estimating gas for your transactions, see [How to estimate gas in Arbitrum](/arbitrum-essentials/how-to-estimate-gas.md). ## Parent chain gas pricing [ArbOS](/how-arbitrum-works/deep-dives/arbos.md) dynamically prices the parent chain gas, with the price adjusting to ensure that the amount collected in the parent chain gas fees is as close as possible to the costs that must be covered, over time. ### Challenges in pricing parent chain resources There are two main challenges in accurately pricing parent chain resources: #### 1. Apportioning batch costs among transactions * **Compression complexity**: The data posted to the parent chain is compressed using a general-purpose compression algorithm ([Brotli](/how-arbitrum-works/deep-dives/sequencer.md#compression)). The effectiveness of compression depends on shared patterns among transactions in a batch. * **Contribution estimation**: It's difficult to determine how much a specific transaction contributes to the batch's overall compressibility. * **Ideal vs. practical**: Ideally, transactions that enhance compressibility would get charged less, but there's no efficient way to calculate this precisely within the constraints of the STF. #### 2. Assessing parent chain fees at sequencing time * **Determinism requirement**: The parent chain fee for a transaction must be known when the transaction is sequenced to maintain the STF's determinism. * **Future uncertainty**: At sequencing time, the actual cost of the batch is unknown because it depends on the parent chain base fee at the future time of batch posting and the remaining contents of the batch (which affect its size and compressibility). * **Impossibility of exact charges**: Charging based on future information is not feasible, so the system must rely on estimations. ### Nitro's approach To overcome these challenges, Arbitrum Nitro implements a two-fold strategy: 1. **Estimated relative footprint**: An estimated size is calculated for each transaction, measured in data units, to approximate its impact on batch size. 2. **Adaptive fee per data unit**: A dynamic fee per data unit adjusts over time to align collected fees with actual costs. ### Parent chain costs There are two types of parent chain costs: batch posting costs and rewards. Batch posting costs reflect the actual cost a batch poster pays to post batch data on the parent chain. Whenever a batch is posted, the parent chain contract that records it will send a special "batch posting report" message to the child chain ArbOS, reporting who paid for the batch and the parent chain basefee at the time. This message is placed in the chain's [](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#transacting-via-the-delayed-inbox)delayed inbox so that it will be delivered to the child chain ArbOS after some delay. When a batch posting report message arrives at the child chain, ArbOS computes the cost of the referenced batch by multiplying the reported basefee by the batch's data cost. (ArbOS retrieves the batch's data from its inbox state and computes the parent chain gas the batch would have used by counting the number of zero bytes and non-zero bytes in the batch.) The pricer records the resulting cost as funds due to the party who is reported to have submitted the batch. The second type of parent chain cost is an optional (per chain) per-unit reward for handling transaction calldata. In general, the reward might be paid to the [](/how-arbitrum-works/deep-dives/sequencer.md)Sequencer, or to members of the Data Availability Committee in an [AnyTrust chain](/how-arbitrum-works/deep-dives/anytrust-protocol.md), or to anyone else who incurs per-calldata-byte costs on behalf of the chain. The reward is a fixed number of wei per data unit, and is paid to a single address. The parent chain pricer keeps track of funds due to the reward address based on the number of data units processed so far. This amount is updated whenever a batch posting report arrives at the child chain. ### Apportioning costs among transactions To approximate each transaction's contribution to parent chain costs: * Each transaction is individually compressed using the Brotli compressor at its lowest compression level (fastest setting). This reduces computational overhead within the STF. * The size of the compressed transaction is multiplied by 16 (since Ethereum charges 16 gas per non-zero byte). This product represents the transaction's estimated footprint in data units. * While not exact, this method approximates the transaction's size after full batch compression and is computationally efficient for real-time processing. ### Parent chain calldata fees The parent chain calldata fees exist because the Sequencer, or the batch poster that posts the Sequencer's transaction batches on Ethereum, incurs parent chain gas costs to post transactions on Ethereum as calldata. Funds collected in the parent chain calldata fees are credited to the batch poster to cover its costs. Every transaction that comes in through the Sequencer will pay a parent chain calldata fee. Transactions that come in through the delayed inbox do not pay this fee because they don't add to batch posting costs—but these transactions pay gas fees to Ethereum when they are put into the delayed inbox. The parent chain pricing algorithm assigns a parent chain calldata fee to each Sequencer transaction. First, it computes the transaction's size, an estimate of how many bytes the transaction will add to the compressed batch it is in; the formula includes an estimate of the transaction's compressibility. Second, it multiplies the computed size estimate by the current price per estimated byte to determine the transaction's parent chain calldata cost in `wei`. Finally, it divides this cost by the current child chain basefee to convert the fee into child chain gas units. The result is reported as the "poster fee" for the transaction. The price per estimated byte is set by a dynamic algorithm that compares the total parent-chain calldata fees collected to the total fees actually paid by batch posters and tries to bring the two as close to equality as possible. If batch poster costs are less than fee receipts, the price will increase; if they exceed fee receipts, the price will decrease. ### Adaptive pricing algorithm To align collected fees with actual costs, Arbitrum uses an adaptive algorithm with two primary goals: 1. **Cost alignment**: Minimize the long-term difference between collected fees and the Sequencer's parent chain costs. 2. **Stability**: Avoid sudden fluctuations in the data price, ensuring a stable fee environment. #### Pricer components The pricer module within ArbOS tracks: * **Amount owed to the Sequencer**: The cumulative parent chain costs incurred by the Sequencer for batch posting. * **Reimbursement fund**: Collects all funds charged to transactions for parent chain fees. Acts as a pool to reimburse the Sequencer. * **Data unit count**: The total number of recent data units processed, which increases with each transaction's estimated data units. * **Current parent chain data unit price**: The adaptive fee per data unit expressed in `wei`. #### Algorithm for price adjustment When the Sequencer posts a batch to the parent chain inbox: 1. **Batch posting report generation**: The parent chain inbox inserts a "batch posting report" transaction into the chain's Delayed Inbox. After a delay, this report is processed by ArbOS's pricer module. 2. **Processing the batch posting report**: * **Compute batch cost**: ArbOS calculates the actual cost of posting the batch by retrieving the batch data from the inbox state and counting zero and non-zero bytes to determine parent chain gas usage. The cost is added to the amount owed to the Sequencer. * **Update data units**: Calculate the data units assigned to this update $(U_{\text{upd}})$: $$ U_{\text{upd}} = U \times \frac{T_{\text{upd}} - T_{\text{prev}}}{T - T_{\text{prev}}} $$ Where $U$ is total recent data units, $T$ is the current time, $T_{\text{upd}}$ is the time when the update occurred, and $T_{\text{prev}}$ is the time of the previous update. Subtract $U_{\text{upd}}$ from the total $U$. * **Reimburse the Sequencer**: Pay the Sequencer from the reimbursement fund. The amount paid is the lesser of the amount owed or the fund balance. Deduct the paid amount from both the reimbursement fund and the amount owed. * **Compute surplus and derivative**: Surplus ($S$): $$ S = \text{Reimbursement Fund Balance} - \text{Amount Owed} $$ Derivative of surplus ($D$): $D = \frac{S - S_{\text{prev}}}{U_{\text{upd}}}$ where $S_{\text{prev}}$ is the surplus at the previous update. * **Compute derivative goal ($D'$)**: Establish a target derivative to eliminate surplus over time: $D' = -\frac{S}{E}$ where $E$ is the equilibration constant (time horizon for balancing surplus). * **Adjust price ($\Delta P$)**: Calculate the change in the data unit price: $$ \Delta P = \frac{(D' - D) \times U_{\text{upd}}}{\alpha + U_{\text{upd}}} $$ Where $\alpha$ is a smoothing parameter to prevent abrupt changes. Update the price: $$ P = \max(0, P_{\text{prev}} + \Delta P) $$ * **Outcome**: The adaptive algorithm adjusts the parent chain data unit price to align collected fees with actual costs, ensuring that the Sequencer gets fairly reimbursed while avoiding surpluses or deficits. #### Compression levels For an operational overview of how the Sequencer applies these levels, see [Compression](/how-arbitrum-works/deep-dives/sequencer.md#compression). Dynamic adjustments of compression levels are based on backlog size ($B$): * **Compression level ($CL$)** (where $UC$ is the user-configured compression level): * For $B \leq 20$: $CL = \min(6, UC)$ * For $20 < B < 60$: $CL = UC$ * For $B > 60$: $CL = \min(4, UC)$ * **Recompression level ($RL$)**: * For $B < 40$: $RL = UC$ * For $B \geq 40$: $RL = \min(6, UC)$ Recompression of existing batch segments may occur if the batch exceeds maximum size limits or hasn't been properly closed. ### Parent chain fee collection A transaction is charged for the parent chain gas if and only if it arrived as part of a sequencer batch. This means that someone would have paid for the parent chain gas to post the transaction on the parent chain. The estimated cost of posting a transaction on the parent chain is the product of the transaction's estimated size and the current parent chain gas basefee. This estimated cost is divided by the current child-chain gas used for the parent-chain operation (see [Understanding Arbitrum 2-dimensional fees](https://medium.com/offchainlabs/understanding-arbitrum-2-dimensional-fees-fd1d582596c9) for more information). The estimated size is measured in the parent chain gas. It is calculated as follows: first, compress the transaction's data using the Brotli-zero algorithm, then multiply the size of the result by 16 (because the parent chain charges 16 gas per byte—the parent chain charges less for bytes that are zero, but that doesn't apply here). Brotli-zero is used to reward users for posting compressible transactions. Ideally, we would like to reward for posting transactions that contribute to the compressibility (using the Brotli compressor) of the entire batch, but that is a difficult notion to define and, in any case, would be too expensive to compute at the child chain. Brotli-zero is an approximation that is cheap to compute. Parent chain gas fee funds collected from transactions are transferred to a special [`L1PricerFundsPool`](https://github.com/OffchainLabs/nitro/blob/3f4939df1990320310e7f39e8abb32d5c4d8045f/arbos/l1pricing/l1pricing.go#L46) account, so that account's balance represents the funds collected and available to pay for costs. The parent chain pricer also records the total number of "data units" (the sum of the estimated sizes, multiplied by 16) received. ## Child chain gas pricing The child chain gas price on a given Arbitrum chain has a set floor, which can be queried via [`ArbGasInfo`](/arbitrum-essentials/precompiles/reference.md#arbgasinfo)'s `getMinimumGasPrice` method. ### Estimating child chain gas Calling the Arbitrum Node's `eth_estimateGas` RPC returns 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 **ETH** is required for the transaction to succeed. This means that, for a given operation, the value returned by `eth_estimateGas` will change over time (as the parent chain's calldata price fluctuates). (See [2-dimensional fees](https://medium.com/offchainlabs/understanding-arbitrum-2-dimensional-fees-fd1d582596c9) and [How to estimate gas in Arbitrum](/arbitrum-essentials/how-to-estimate-gas.md) for more.) ### Child chain gas fees Child chain gas fees work similarly to Ethereum: a transaction consumes gas, which is multiplied by the current basefee per gas to determine the child chain transaction fee (denominated in gwei). The basefee is set by a pricing algorithm that governs fees across multiple adjustment windows. This algorithm uses several gas targets, each paired with its own adjustment window. Higher targets with shorter windows (for example, 60 Mgas/s over nine seconds) absorb transient demand spikes without triggering aggressive fee increases. Lower targets with longer windows address slower traffic, with the lowest target measured over 86,400 seconds (one day), establishing the chain's long-term capacity. From the users' perspective, this means that the network will charge an increasing amount for each additional amount of gas consumed above the target. For example, a 10% fee is levied for going 1% over the target. The fee rises to 15% if you go to 2% over the target, and so on. The algorithm tracks a gas backlog for each target. Whenever a transaction consumes gas, that gas is added to the backlog. Each second, the corresponding gas target is subtracted from its backlog (the backlog cannot go below zero). If the backlog grows, the basefee increases exponentially to discourage usage; if the backlog shrinks, the basefee decreases to incentivize more demand. This behavior is intended to bring the chain's usage to an equilibrium defined by the long-term gas target. This layered structure creates a dampening effect: overlapping windows smooth volatility at their respective timescales, reducing peak gas prices during congestion while preserving responsiveness to genuine capacity constraints. For more details on how gas targets affect chain security and validator assumptions, see [the gas target](#the-gas-target) section below. ### Child chain tips The sequencer prioritizes transactions on a first-come, first-served basis (chains running [Timeboost](/how-arbitrum-works/timeboost/gentle-introduction.md) modify this for express-lane transactions). 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 cost of the subsequent transaction's execution [will be included](https://github.com/OffchainLabs/go-ethereum/blob/d52739e6d54f2ea06146fdc44947af3488b89082/internal/ethapi/api.go#L999) in the gas estimation via the node's RPC. A transaction's gas estimate can only be found if all the transactions succeed at a given gas limit. This is especially important when working with retryables and scheduling redeem attempts. Because a call to [`redeem`](/arbitrum-essentials/precompiles/reference.md#arbretryabletx) donates all of the call's gas, doing multiple calls requires limiting the amount of gas provided to each subcall. Otherwise, the first will take all 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`](/arbitrum-essentials/nodeinterface/reference.md) and similarly requires the auto-redeem attempt to succeed. ### The gas target The security of Arbitrum Nitro chains depends on the assumption that when one [](/how-arbitrum-works/deep-dives/assertions.md)validator creates an [assertion](/how-arbitrum-works/deep-dives/assertions.md), other validators check it and respond with a correct assertion or a [](/how-arbitrum-works/bold/gentle-introduction.md)challenge if it's wrong. This requires that the other validators have the time and resources to check each assertion quickly enough to issue a timely challenge. The Arbitrum protocol takes this into account when setting deadlines for assertions. This sets an effective gas target for executing an Arbitrum 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 gas target, their deadlines will get farther and farther into the future. Due to the limit enforced by the Rollup protocol contracts on how far in the future a deadline can be, this will eventually slow down new assertions, thereby enforcing the effective gas target. Setting the gas target accurately depends on estimating the time required to validate an assertion with some accuracy. Any uncertainty in estimating validation time will force us to set the gas target lower, to be safe. And we do not want to set the gas target lower, so we try to enable accurate estimation. ## Total fee and gas estimation The total fee charged to a transaction is the child chain basefee multiplied by the sum of the child chain gas used and the parent chain calldata charge. As on Ethereum, a transaction will fail if it does not supply enough gas or specifies a basefee limit below the current basefee. Ethereum also allows a "tip," but Nitro ignores this field and never collects any tips. ### Allocating funds and paying what is owed When a batch posting report is processed in the child chain, the pricer allocates some of the collected funds to cover costs incurred. To allocate funds, the pricer considers three timestamps: * `currentTime` is the current time, when the batch posting report message arrives at the child chain * `updateTime` is the time at which the reported batch was submitted (which will typically be around 20 minutes before `currentTime`) * `lastUpdateTime` is the time of submission for the previous reported batch The pricer computes an allocation fraction `F = (updateTime-lastUpdateTime) / (currentTime-lastUpdateTime)` and allocates a fraction `F` of funds in the `L1PricerFundsPool` to the current report. The intuition is that the pricer knows how many funds have been collected between `lastUpdateTime` and `currentTime`, and we want to figure out how many of those funds to allocate to the interval between `lastUpdateTime` and `updateTime`. The given formula is correct if we assume that funds arrived at a uniform rate over the interval between `lastUpdateTime` and `currentTime`. The pricer similarly allocates a portion of the total data units to the current report. Now the pricer pays out the allocated funds to cover the rewards due and the amounts due to batch posters, thereby reducing the balance due to each party. If the allocated funds aren't sufficient to cover everything that is due, some amount will remain due. If the amount due is covered with the allocated funds, any remaining funds are returned to the `L1PricerFundsPool`. ### Getting parent chain fee info The parent chain gas basefee can be queried via [`ArbGasInfo.getL1BaseFeeEstimate`](/arbitrum-essentials/precompiles/reference.md#arbgasinfo). To estimate the parent chain fee a transaction will use, call [`NodeInterface.gasEstimateComponents()`](/arbitrum-essentials/nodeinterface/reference.md) or [`NodeInterface.gasEstimateL1Component()`](/arbitrum-essentials/nodeinterface/reference.md). Arbitrum transaction receipts include a `gasUsedForL1` field that shows the amount of gas used on the parent chain, in units of the child chain's gas. ### Adjusting the parent chain gas basefee After allocating funds and paying what is owed, the parent chain pricer adjusts the parent chain gas basefee. The goal of this process is to find a value that, over time, causes the amount collected to equal the amount owed. The algorithm first computes the surplus (funds in the `L1PricerFundsPool`, minus total funds due), which might be negative. If the surplus is positive, the parent chain gas basefee is reduced so that exactly the surplus will reduce the amount collected over a fixed future interval. If the surplus is negative, the basefee is increased so that the shortfall is eliminated over the same fixed future interval. A second term is added to the parent chain gas basefee, based on the derivative of the surplus (surplus at present, minus the surplus after the previous batch posting report was processed). This term, which is multiplied by a smoothing factor to reduce fluctuations, will reduce the basefee if the surplus is increasing, and increase the basefee if the surplus is shrinking. ## Effective block gas limit Post-Fusaka, the block gas limit and transaction filtering have changed. Instead of using a hard limit of 32M (Arbitrum One), a new "Effective Block Gas Limit" has been introduced. For the underlying gas-pool and per-block-limit mechanics in ArbOS, see the [ArbOS technical reference](/how-arbitrum-works/reference/arbos-reference.md#arbos-state). ### Filtering ArbOS 51 also introduces a `MaxTxGasLimit`. The State Transition Function (STF) will be relaxed to allow the final transaction in a block to use up to the `MaxTxGasLimit` even if it would cause the block to exceed the `MaxBlockGasLimit`. > **NOTE** > > The `MaxTxGasLimit` is set by implementing [EIP-7825](https://eips.ethereum.org/EIPS/eip-7825) from the Fusaka hard fork. This means that the "Effective Block Gas Limit" is really `MaxBlockGasLimit` + `MaxTxGasLimit` (64M for Arbitrum One). In previous versions of ArbOS, the STF would skip transactions from the sequencer feed if the transaction's `GasLimit` (set by the user) minus the L1 data posting gas exceeded the gas remaining in the block (without executing the transaction to see how much L2 gas it actually used). The new algorithm is more efficient because the STF doesn't need to continually search the transaction queue for one that fits in the remaining block gas, and can just keep adding transactions until the unused block gas is `0`. > **NOTE** > > This change does not affect the `GasTarget`, and therefore does not affect how much overall gas per second the chain will use—only how transactions using that gas could be divided between different blocks. --- > For a complete page index, fetch # Bridging from a parent chain to a child chain > **TIP** — Looking for implementation guides? > > This document explains the protocol-level concepts of parent-to-child messaging. For practical, step-by-step instructions on implementing bridging and messaging, see [How to bridge from parent chain to child chain](/arbitrum-essentials/bridging/deposit/eth-and-messages.md). In the [Bypassing the Sequencer](/how-arbitrum-works/deep-dives/transaction-lifecycle.md#bypassing-the-sequencer) section, we introduced an alternative way for users to submit transactions to a child chain by going through the parent chain's Delayed Inbox contract instead of sending them directly to the [](/how-arbitrum-works/deep-dives/sequencer.md)Sequencer. This approach is one example of a parent-to-child messaging path. More broadly, parent-to-child chain messaging covers all ways to: * Submit a child chain bound transaction from a parent chain * Deposit **ETH** or native tokens from a parent chain to a child chain * Send arbitrary data or instructions from a parent chain to a child chain We generally categorize these parent-to-child chain messaging methods as follows: 1. **Native token bridging**: Refers to depositing a child chain's native token from the parent chain to the child chain. Depending on the type of Arbitrum chain, this can include: * \***\*ETH** Bridging\*\*: For Arbitrum chains that use **ETH** as their gas token, users can deposit \*\*ETH\*\* onto a child chain via the Delayed Inbox. * **Custom gas token bridging**: For Arbitrum chains that use a custom gas token, users can deposit that chain's native token to a child chain using the same mechanism. 2. **Transaction via the Delayed Inbox**: As described in the [Bypassing the Sequencer](/how-arbitrum-works/deep-dives/transaction-lifecycle.md#bypassing-the-sequencer) section, this method allows users to send transactions through the parent chain. It includes two sub-types of messages: * **Unsigned messages**: General arbitrary data or function calls * **Signed messages**: Messages that include a signature, enabling certain authenticated actions 3. **Retryable tickets** are Arbitrum's canonical mechanism for creating parent-to-child messages–transactions initiated on a parent chain that trigger execution on a child chain. This method contains the following functionality: * **General retryable messaging**: For sending arbitrary data or calls from a parent-to-child chain. * **Customized feature messaging** (e.g., token bridging): Leveraging retryable tickets (and other messaging constructs) for specialized actions, such as bridging tokens from a parent-to-child chain. This section will explore these categories in detail and explain how they work. The diagram below illustrates the various paths available for parent-to-child chain communication and asset transfers. ![Parent to child messaging](/img/haw-l1-to-l2.svg) ## Native token bridging Native token bridging refers to depositing a chain's native currency (the token used to pay gas fees) from the parent chain to the child chain. This follows a different, simpler process than **ERC-20** token bridging through the canonical token bridge. Arbitrum chains can use **ETH** or any other **ERC-20** tokens as their gas fee currency. Arbitrum One and Nova use **ETH** as their native token, while some Arbitrum chains opt for a custom gas token. For more details about chains that use custom gas tokens, refer to the [Custom gas token SDK](/arbitrum-essentials/bridging/configure-token-gateway/custom.md). Whether a chain uses **ETH** or a custom gas token, users can deposit tokens from a parent chain (for Arbitrum One, Ethereum) into a child chain. Below, we describe how to deposit **ETH** on chains that use **ETH** as the native gas token. The process for depositing custom gas tokens follows the same steps, except it uses the chain’s Delayed Inbox contract. ### Depositing native tokens A special message type exists for simple native token deposits from parent-to-child chains. The `Inbox` contract's `depositEth` method provides this functionality for chains using **ETH**, while custom gas token chains use similar mechanisms through their Delayed Inbox contract. For step-by-step instructions on depositing native tokens, see [How to bridge from parent chain](/arbitrum-essentials/bridging/deposit/eth-and-messages.md#bridging-eth-to-a-child-chain). #### How deposits work When you call `Inbox.depositEth`, the **ETH** is sent to the bridge contract on the parent chain. The bridge then "credits" the deposited amount to the designated address on the child chain. From the L1 perspective, the funds are held in Arbitrum’s bridge contract on your behalf. A diagram illustrating this deposit process is below: Note on caller type and aliasing: * **If the parent chain caller is an Externally Owned Account (EOA)**: * The deposited **ETH** will appear in the same EOA address on the child chain. * **If the parent chain caller is a contract**: * The **ETH** will get deposited into the contract's aliased address on the child chain. In the next section, we will cover [Address aliasing](#address-aliasing). * **If the caller is a `7702-enabled account` (EOA with temporary contract code)**: * The **ETH** goes to the aliased address, similar to contracts. This is due to the presence of runtime code during execution, and ensures consistent aliasing behavior post-EIP-7702. ![Address aliasing](/img/haw-aliasing.svg) ### Address aliasing All unsigned messages submitted through the Delayed Inbox have their sender addresses "aliased" when executed on the child chain. Instead of returning the parent chain sender's address as `msg.sender`, the child chain sees the "child alias" of that address. Formally, the child alias calculation is: ```solidity Child_Alias = Parent_Contract_Address + 0x1111000000000000000000000000000000001111 ``` #### Why aliasing? Address aliasing in Arbitrum is a security measure that prevents cross-chain exploits. Without it, a malicious actor could impersonate a contract on a child chain by simply sending a message from that contract's parent chain address. By introducing an offset, Arbitrum ensures that child-chain contracts can distinguish between parent-chain contract calls and those from child-chain native addresses. #### Computing the original parent chain address If you need to recover the original parent chain address from an aliased child chain address onchain, you can use Arbitrum's `AddressAliasHelper` library. This library allows you to translate between the aliased child address and the original parent address in your contract logic. ```solidity modifier onlyFromMyL1Contract() override { require(AddressAliasHelper.undoL1ToL2Alias(msg.sender) == myL1ContractAddress, "ONLY_COUNTERPART_CONTRACT"); _; } ``` ## Transacting via the Delayed Inbox Arbitrum provides a *Delayed Inbox* contract on the parent chain that can deliver arbitrary messages to the child chain. This functionality is important for two reasons: 1. **General cross-chain messaging**: Allows a parent-chain EOA or contract to send messages or transactions to a child chain. This functionality is critical for bridging assets (other than the chain's native token) and performing cross-chain operations. 2. **Censorship resistance**: It ensures the Arbitrum chain remains censorship-resistant, even if the Sequencer misbehaves or excludes certain transactions; refer to [Bypassing the Sequencer](/how-arbitrum-works/deep-dives/transaction-lifecycle.md#bypassing-the-sequencer) for more details. Users can send child chain transactions through the Delayed Inbox in two primary ways: 1. [General child chain messaging](#general-child-chain-messaging) 2. [Retryable tickets](#retryable-tickets) ### General child chain messaging Any message sent via the Delayed Inbox can ultimately produce a transaction on the child chain. These messages may or may not include a signature. * **Signed messages**: Signed by an EOA on the parent chain. This signature proves the sender is an EOA rather than a contract, preventing certain cross-chain exploits and bypassing the need for aliasing. * **Unsigned messages**: These do not include an EOA's signature. For security reasons, the sender’s address on the child chain must be *aliased* when the message gets executed; see the [Address aliasing](#address-aliasing) section for details. Below, we describe the Delayed Inbox methods for each scenario. ### Signed messages Signed messages let a parent chain EOA prove ownership of an address, ensuring the child chain transaction executes with `msg.sender` set to the *signer's* address on the child chain (rather than an alias). This mechanism is beneficial for bypassing the Sequencer if: * You want to force-include a transaction on a child chain in case of Sequencer downtime or censorship. * You need an operation on a child chain that explicitly requires EOA authorization (e.g., a withdrawal). When submitting through the Delayed Inbox, a child chain transaction signature gets included in the message's calldata. Because it matches the EOA's signature, the child chain can safely treat the signer's address as the sender. The Delayed Inbox provides two methods for signed messages: `sendL2Message` (more flexible, can be called by EOAs or contracts) and `sendL2MessageFromOrigin` (cheaper gas costs, EOA-only). For implementation details, see [Sending signed messages](/arbitrum-essentials/bridging/deposit/eth-and-messages.md#sending-signed-messages). ### Unsigned messages Unsigned messages allow a parent chain sender to specify transaction parameters without an EOA signature. Because there is no signature, **the sender's address must be aliased on the child chain** (see the [Address aliasing](#address-aliasing) section for the rationale). The Delayed Inbox provides methods for unsigned messages from both EOAs and contracts, with variants for whether funds are transferred from the parent chain or drawn from the child chain balance. For implementation details and method signatures, see [Sending unsigned messages](/arbitrum-essentials/bridging/deposit/eth-and-messages.md#sending-unsigned-messages). ### Message types [](/how-arbitrum-works/inside-arbitrum-nitro.md)Arbitrum Nitro defines various **message types** to distinguish between the categories described above (signed vs. unsigned, EOAs vs. contracts, etc.). These message types help the protocol route and process each incoming message securely. For the full list of message-type identifiers used by ArbOS, see [`L1IncomingMessage`](https://github.com/OffchainLabs/nitro/blob/4ac7e9268e9885a025e0060c9ec30f9612f9e651/arbos/incomingmessage.go#L54) and the [ArbOS messages section](/how-arbitrum-works/deep-dives/arbos.md#messages). > **NOTE** > > Refer to the [Address aliasing](#address-aliasing) discussion for more background. This mechanism ensures that a parent chain contract can't impersonate a child chain address unless it provides a valid signature as an EOA. ## Retryable tickets Retryable tickets are Arbitrum's canonical method for creating parent-to-child chain messages—parent-chain transactions that initiate a message to be executed on a child chain. A retryable is submittable for a fixed cost (dependent only on its calldata size) paid at the parent chain. Critically, the ticket's *submission* on the parent chain is separable and asynchronous from its *execution* on the child chain. This design provides atomicity for cross-chain operations: if the parent chain transaction to request submission succeeds (doesn't revert), then the execution of the retryable on the child chain has a strong guarantee to eventually succeed. For step-by-step instructions on creating retryable tickets, including parameter estimation and SDK usage, see [Creating retryable tickets](/arbitrum-essentials/bridging/deposit/eth-and-messages.md#creating-retryable-tickets). ### Retryable ticket lifecycle The lifecycle of a retryable ticket involves three stages: submission, automatic redemption, and manual redemption. #### Submission Creating a retryable ticket is initiated with a call to the `createRetryableTicket` function of the inbox contract. Key parameters include the destination address, call value, gas limits, refund addresses, and calldata. The ticket requires sufficient funds to cover both submission costs and gas for child chain execution. Upon successful submission, a unique `TicketID` is created and the [`ArbRetryableTx`](/arbitrum-essentials/precompiles/reference.md#arbretryabletx) precompile emits a `TicketCreated` event. #### Automatic redemption Upon successful ticket creation, the system checks if conditions are met for automatic redemption: the user's child chain balance must cover the gas costs, and the provided `maxFeePerGas` must meet or exceed the child chain base fee. If these conditions are met, the system automatically attempts to execute the ticket (auto-redeem). If auto-redemption succeeds, the ticket executes immediately with the original parameters, and excess fees are refunded. If auto-redemption fails (for example, due to insufficient gas or an increase in gas prices), the ticket remains in the retryable buffer for up to one week, allowing for manual redemption. #### Manual redemption If automatic redemption fails, anyone can manually redeem the ticket by calling the [`ArbRetryableTx`](/arbitrum-essentials/precompiles/reference.md#arbretryabletx) precompile's `redeem` method. The manual redemption attempt donates its call gas to the execution, and the gas limit is not constrained by the original ticket parameters. This allows tickets to be retried with different gas conditions. Tickets remain in the retryable buffer for one week. If not successfully redeemed within this period, the ticket expires and is automatically discarded. However, tickets can be kept alive indefinitely by paying a fee to extend the lifetime for another full period before expiration. If a ticket expires without successful redemption, the escrowed `callValue` is refunded to the `callValueRefundAddress` specified during submission. This protects user funds even if execution never succeeds. #### Receipt types Retryable tickets produce two types of receipts: * **Ticket creation receipt**: Confirms successful ticket creation and includes a `TicketCreated` event with the `ticketId` * **Redeem attempt receipt**: Records each redemption attempt and includes a `RedeemScheduled` event Only one successful redemption can occur per ticket. Multiple failed redemption attempts each produce a receipt until one succeeds. ## Token bridging Retryable tickets power Arbitrum's canonical token bridge. For the full token bridge architecture, including how **ETH** and **ERC-20** tokens are bridged between layers, see the [Token bridging overview](/how-arbitrum-works/deep-dives/token-bridging.md). --- > For a complete page index, fetch # Child to parent chain messaging > **TIP** — Looking for implementation guides? > > This document explains the protocol-level concepts of child-to-parent messaging. For practical, step-by-step instructions on implementing withdrawals and messaging, see [How to bridge to parent chain from child chain](/arbitrum-essentials/bridging/withdraw/eth-and-messages.md). Arbitrum's outbox system allows arbitrary child-to-parent chain contract calls, i.e., messages initiated from the child chain that will eventually resolve through execution on the parent chain. Child-to-parent chain messages (i.e., outgoing messages) bear some things in common with Arbitrum’s [parent chain to child chain messages](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md), which are “in reverse” with differences, which we’ll explore in this section. ## Protocol flow Messages from child to parent chains are included in Arbitrum’s Rollup state and finalized through the Rollup protocol. The process follows these steps: 1. **Message creation on child chain**: * To initiate a child-to-parent chain message, a user or contract calls the `sendTxToL1` method on the [`ArbSys`](/arbitrum-essentials/precompiles/reference.md#arbsys) precompile. 2. **Message inclusion in an assertion**: * The message is batched with other transactions and included in a [Rollup assertion](/how-arbitrum-works/deep-dives/assertions.md). * Assertions are submitted to the Rollup contract on the parent chain and enter the [dispute window](/how-arbitrum-works/deep-dives/assertions.md) (6.4 days). 3. **Assertion confirmation**: * If the assertion remains unchallenged after the dispute window, the Rollup contract finalizes the assertion. * The assertion's Merkle root gets posted to the outbox contract on the parent chain. 4. **Execution on the parent chain**: * Once the assertion is confirmed, anyone can execute the message on the parent chain by proving its inclusion. * Execution is possible via `Outbox.executeTransaction`, which accepts a Merkle proof that the message exists in the finalized assertion. ## Sending and executing messages Child-to-parent messages are sent via the [`ArbSys`](/arbitrum-essentials/precompiles/reference.md#arbsys) precompile's `sendTxToL1` method. After the approximately 7-day challenge period, messages can be executed on the parent chain using the `Outbox.executeTransaction` method with a Merkle proof obtained from the [`NodeInterface`](/arbitrum-essentials/nodeinterface/reference.md) contract. For step-by-step implementation instructions, see [How to bridge to parent chain](/arbitrum-essentials/bridging/withdraw/eth-and-messages.md). ## Protocol design details ### Constant overhead for node confirmation * Calling `confirmNode` on the Rollup contract has constant gas overhead, regardless of the number of messages in the assertion. * This confirmation ensures malicious actors cannot grief the network by submitting assertions with many outgoing messages. ### Why child to parent chain messages require manual execution Unlike [retryable tickets](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md), which can execute automatically with pre-funded gas, child-to-parent chain messages must be executed manually because Ethereum (i.e., the parent chain) does not support scheduled execution. However, applications can implement execution markets that allow third parties to execute messages for a fee. ### Persistence and expiry * **Child-to-parent chain messages**: Persist indefinitely on the parent chain once included in the outbox. ## Parent-to-child chain message lifecycle Each message progresses through three primary states: | Stage | Description | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Posted on child chain | The message is sent via `ArbSys.sendTxToL1`. | | Waiting for finalization | The assertion containing the message is in the [](/how-arbitrum-works/deep-dives/assertions.md)challenge period (\~6.4 days) | | Confirmed and executable on the parent chain | If no [](/how-arbitrum-works/bold/gentle-introduction.md)fraud proof is submitted, the assertion is confirmed, and the message is available for execution in the outbox. | ## Asset withdrawals ### **ETH** withdrawals Arbitrum has a canonical bridge design and architecture, which we explain in detail in the [Token bridging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#token-bridging) section of the **Bridging from a parent chain to a child chain** article. This section explains how the Arbitrum canonical bridge works for child-to-parent chain token bridging. The [`ArbSys`](/arbitrum-essentials/precompiles/reference.md#arbsys) precompile provides a `withdrawEth` convenience method for withdrawing **ETH** from the child chain. This method burns the **ETH** on the child chain and creates a child-to-parent message. Like all child-to-parent messages, it requires execution on the parent chain after the challenge period via `Outbox.executeTransaction`. For implementation details, see [Withdrawing **ETH**](/arbitrum-essentials/bridging/withdraw/tokens.md). ### **ERC-20** token withdrawals Token withdrawals use Arbitrum's canonical bridge architecture, detailed in the [Token bridging overview](/how-arbitrum-works/deep-dives/token-bridging.md). The process flows through the [`L2GatewayRouter`](/how-arbitrum-works/deep-dives/token-bridging.md#canonical-token-bridge-implementation) to the appropriate gateway contract (`L2ArbitrumGateway`), which burns the child chain tokens and creates a message to the parent chain gateway. After the challenge period, the message can be executed on the parent chain to release tokens from escrow. For implementation details, see [Withdrawing **ERC-20** tokens](/arbitrum-essentials/bridging/withdraw/tokens.md). --- > For a complete page index, fetch # The Sequencer and Censorship Resistance The Sequencer is a pivotal component of the Arbitrum stack, responsible for efficiently ordering and processing transactions. It plays a crucial role in providing users with fast transaction confirmations while maintaining the security and integrity of the blockchain. In Arbitrum, the Sequencer orders incoming transactions and manages the batching, compression, and posting of transaction data to the parent chain, optimizing costs and performance. ![Sequencer operations](/img/haw-sequencer-operations.svg) In this section, we will explore the Sequencer's operations in detail. The topics covered include: * [Sequencing and broadcasting (Sequencer Feed)](#sequencing-and-broadcasting): An overview of the real-time transaction feed provided by the Sequencer, which allows nodes to receive instant updates on the transaction sequence. * [Batch posting](#batch-posting): How the Sequencer groups transactions into batches, compresses them to reduce data size, and sends them to the Sequencer Inbox contract on the parent chain. This section also delves into the parent chain pricing model and how it affects transaction costs: * [Batching](#batching-and-compression) * [Compression](#compression) * [Submitting to the Sequencer Inbox contract](#submitting-to-the-sequencer-inbox-contract) * [Finality](#finality): Understanding how transaction finality is achieved in Arbitrum through soft and hard finality mechanisms, ensuring that transactions are confirmed securely and reliably (not as a Sequencer task). * [Censorship Timeout](#censorship-timeout): A brief introduction to a special feature that aims to limit the negative effects of prolonged sequencer censorship or unexpected sequencer outages. By examining these aspects, you will understand the Sequencer’s role within the Arbitrum ecosystem, including how it enhances transaction throughput, reduces latency, and maintains a fair and decentralized network. ## Sequencing and broadcasting The Sequencer feed is a critical component of the Nitro architecture. It enables real-time dissemination of transaction data as they are accepted and ordered by the Sequencer. It allows users and nodes to receive immediate updates on transaction sequencing, facilitating rapid transaction confirmations and enhancing the network’s overall responsiveness. ### How the Sequencer publishes the sequence The Sequencer communicates the transaction sequence through two primary channels: 1. **Real-time sequencer feed**: A live broadcast that publishes transactions instantly as they are sequenced. Nodes and clients subscribed to this feed receive immediate notifications, allowing them to process transactions without delay. 2. **Batches posted on the parent chain**: At regular intervals, the Sequencer aggregates transactions and posts them to the parent chain for finality. Refer to the Batch posting section for detailed information on this process. ![Sequencer feed](/img/haw-sequencer-feed.svg) ### Real-time Sequencer feed The real-time feed represents the Sequencer’s commitment to process transactions in a specific order. By subscribing to this feed, nodes and clients can: * **Receive immediate notifications**: Get instant updates on newly sequenced transactions and their ordering. * **Process transactions promptly**: Utilize the sequenced transactions to update the state locally, enabling rapid application responses and user interactions. * **Benefit from soft finality**: Gain provisional assurance about transaction acceptance and ordering before the parent chain reaches finality. This mechanism is particularly valuable for applications requiring low latency and high throughput, such as decentralized exchanges or real-time gaming platforms. ### Soft finality and trust model “Soft finality” refers to the preliminary confirmation of transactions based on the Sequencer’s real-time feed. Key aspects include: * **Dependence on Sequencer integrity**: The feed’s accuracy and reliability depend on the Sequencer operating honestly and without significant downtime. * **Immediate user feedback**: Users can act on transaction confirmations swiftly, improving the user experience. * **Eventual consistency with the parent chain**: While the real-time feed provides quick updates, ultimate security, and finality are established once transactions are posted to and finalized on the parent chain. Refer to the Finality section for an in-depth discussion. Understanding this trust model is essential. While we expect the Sequencer to behave correctly, users and developers should know that soft finality depends on this assumption. In scenarios where absolute certainty is required, parties may wait for transactions to achieve finality on the parent chain. ### Role of the Sequencer feed in the network The Sequencer feed serves several vital functions within the Arbitrum ecosystem: * **State synchronization**: Nodes use the feed to keep their state up to date with the latest network state, ensuring consistency across the decentralized platform. * **Application development**: Developers can build applications that respond instantly to network events, enabling features such as live updates, instant notifications, and real-time analytics. * **Ecosystem transparency**: The feed promotes transparency and trust within the community by providing visibility into transaction sequencing and network activity. ### Considerations and limitations While the Sequencer feed offers significant advantages, consider the following: * **Reliance on Sequencer availability**: The effectiveness of the real-time feed depends on the Sequencer’s uptime and responsiveness. Network issues or Sequencer downtime can delay transaction visibility. * **Provisional nature of soft finality**: Until transactions reach finality on the parent chain, there is a small risk that the provisional ordering provided by the feed could change in exceptional circumstances. * **Security implications**: For high-stakes transactions where security is paramount (e.g., deposits and withdrawals on centralized exchanges), users may prefer to wait for confirmation on the parent chain, even with the longer latency. Developers and users should design their applications and interactions with these factors in mind, choosing the appropriate balance between speed and certainty based on their requirements. ### Delayed messages on the Sequencer feed As illustrated in the diagram above, the Sequencer feed not only sends child-chain transactions posted directly to the Sequencer, but also incorporates parent-chain-submitted child-chain transactions. These include [child chain messages submitted on the parent chain](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#transacting-via-the-delayed-inbox) and [retryable transactions](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#retryable-tickets). The Sequencer agent monitors the finalized messages submitted to the parent chain’s [Delayed Inbox contract](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md). Once finalized, it processes them as incoming messages to the feed, ensuring they are added as ordered transactions. The Nitro node can be configured to add delayed inbox transactions immediately after they are submitted to the parent chain, even before finalization. However, this approach introduces the risk of a reorg on the child chain—if the transaction fails to finalize on the parent chain. To mitigate this risk, on Arbitrum One and Nova, the Sequencer includes these transactions in the feed only after they are finalized on the Ethereum chain. You can also explore how the feed sends incoming messages via WebSocket and learn how to extract message data from the feed on this page: [Read Sequencer Feed](/run-arbitrum-node/sequencer/read-sequencer-feed.md). ## Batch posting Batch posting is a fundamental process for the Sequencer's operation in Arbitrum. It involves collecting multiple child chain transactions, organizing them into batches, compressing the data to reduce size, and sending these batches to the Sequencer Inbox contract on the parent chain. This mechanism is crucial for ensuring that transactions are securely recorded on the parent chain blockchain while optimizing for costs and performance. In this section, we will explore the batch posting process in detail, covering the following topics: * **Batching**: How the Sequencer groups incoming transactions into batches for efficient processing and posting. * **Compression**: The methods used to compress transaction data, minimizing the amount of data that needs to be posted on the parent chain and thereby reducing costs. * **Sending to Sequencer Inbox contract**: The procedure for submitting compressed batches to the Sequencer Inbox contract on the parent chain, ensuring secure and reliable recording of transactions. Understanding batch posting is essential for grasping how Arbitrum achieves scalability and cost-efficiency without compromising security. By delving into these subtopics, you’ll gain insight into the Sequencer’s role in optimizing transaction throughput and minimizing fees, as well as the innovative solutions implemented to address the challenges of parent chain data pricing. ## Batching and compression The Sequencer in Arbitrum is critical in collecting and organizing child chain transactions before posting them to the parent chain. The batching process is designed to optimize for both cost efficiency and timely transaction inclusion. ![Batching](/img/haw-batching.svg) #### Transaction collection and ordering: * **Continuous reception**: The Sequencer continuously receives transactions submitted by users. * **Ordering**: Transactions are ordered by the order in which they are received, ensuring deterministic transaction order. Chains can override this default first-come-first-served policy with [Timeboost](/how-arbitrum-works/timeboost/gentle-introduction.md), which auctions an "express lane" for faster inclusion. * **Buffering**: Received transactions are temporarily stored in a buffer awaiting batch formation. #### Batch formation criteria: * **Size thresholds**: Batch formation occurs when accumulated transactions reach a predefined size limit. This limit ensures that the fixed costs of posting data to the parent chain are amortized over more transactions, improving cost efficiency. * **Time constraints**: The Sequencer also monitors the elapsed time since the last batch update to prevent undue delays. Upon reaching the maximum time threshold, the Sequencer will create a batch with the transactions collected so far, even if the batch doesn’t meet the size threshold. #### Batch creation process: * **Aggregation**: Once the batch-formation criteria (size or time threshold) are satisfied, the Sequencer aggregates buffered transactions into a single batch. * **Metadata inclusion**: The batch includes all necessary metadata for all transactions. * **Preparation for compression**: Batch preparation for the compression stage begins, with techniques that minimize data size before posting to the parent chain. The batching mechanism allows the Sequencer to efficiently manage transactions by balancing the need for cost-effective parent chain posting with the requirement for prompt transaction processing. By strategically grouping transactions into batches based on size and time criteria, the Sequencer reduces per-transaction costs and enhances the overall scalability of the Arbitrum network. ### Compression The Sequencer employs compression when forming transaction batches to optimize the data and cost of batches posted to the parent chain. Arbitrum uses the Brotli compression algorithm due to its high compression ratio and efficiency, crucial for reducing parent chain posting costs. ![Compression](/img/haw-compression.svg) ### Compression level in the Brotli algorithm Brotli’s compression algorithm includes a parameter: **compression level**, which ranges from **0 to 11**. This parameter allows you to balance two key factors: * **Compression efficiency**: Higher levels result in greater size reduction. * **Computational cost**: Higher levels require more processing power and time. As the compression level increases, you can achieve better compression ratios at the expense of longer compression times. ### Dynamic compression level setting The compression level on Arbitrum is dynamically adjusted based on the current backlog of batches waiting to be posted to the parent chain by the Sequencer. In scenarios where multiple batches are queued in the buffer, it is possible to adjust the compression level to improve throughput dynamically. When the buffer becomes overloaded with overdue batches, the compression level decreases. For the exact backlog thresholds and how compression interacts with parent chain pricing, see [Compression levels](/how-arbitrum-works/deep-dives/gas-and-fees.md#compression-levels). This trade-off prioritizes speed over compression efficiency, enabling faster processing and transmission of pending batches. Doing so clears the buffer more quickly, ensuring smoother overall system performance. Now that transactions are batched and compressed, they pass to the batch poster for transmission to the parent chain. ## Submitting to the Sequencer Inbox contract After batching and compressing transactions, the Sequencer posts these batches to the parent chain to ensure security and finality. This process involves the **batch poster**, an Externally Owned Account (EOA) controlled by the Sequencer. The batch poster is responsible for submitting the compressed transaction batches to the Sequencer Inbox contract on the parent chain. Batch posting costs are reimbursed through ArbOS's parent-chain pricer — see [Parent chain gas pricing](/how-arbitrum-works/deep-dives/gas-and-fees.md#parent-chain-gas-pricing) and [ArbOS state](/how-arbitrum-works/deep-dives/arbos.md#arbos-state). There are two primary methods the Sequencer uses to send batches to the parent chain, depending on whether the chain supports [EIP-4844 (Proto-Danksharding)](https://eips.ethereum.org/EIPS/eip-4844) and the current network conditions: ![Submit to Sequencer inbox](/img/haw-submit-to-sequencer-inbox.svg) ### 1. Using blobs with `addSequencerL2BatchFromBlobs` * **Default approach**: When the parent chain supports EIP-4844, the Sequencer utilizes blob transactions to post batches efficiently. * **Method**: The batch poster calls the `addSequencerL2BatchFromBlobs` function of the Sequencer Inbox contract. * **Process**: * Batch data gets included as blobs—large binary data structures optimized for scalability. * The transaction includes metadata about the batch but does not include the batch data itself in the calldata. * **Benefits**: * **Cost efficiency**: Blobs allow cheaper data inclusion than calldata, reducing gas costs. * **Scalability**: Leveraging blobs enhances the network's ability to handle large volumes of transactions. ### 2. Using calldata with `addSequencerL2Batch` * **Alternative approach**: If the **Blob Base Fee** is significantly high or the blob space is constrained during batch posting, the Sequencer may opt to use calldata. * **Method**: The batch poster calls the `addSequencerL2Batch` function of the Sequencer Inbox contract. * **Process**: * The compressed batch transactions are included directly in the transaction's calldata. * **Considerations**: * **Cost evaluation**: The Sequencer dynamically assesses whether using calldata is more cost-effective than blobs based on current gas prices and blob fees. * **Compatibility**: If the parent chain does not support EIP-4844, this method is the default and only option for batch posting. > **NOTE** > > The Sequencer continuously monitors network conditions to select the most cost-effective batch posting method, ensuring optimal operation under varying conditions. ### Authority and finality * **Exclusive access**: Only the Sequencer can call these methods on the Sequencer Inbox contract. This exclusivity ensures that no other party can include messages directly. * **Soft-confirmation receipts**: The Sequencer’s unique ability to immediately process and include transactions allows it to provide users with instant, “soft-confirmation” receipts * **Parent chain finality**: Once batches post, the transactions achieve parent-chain-level finality, secured by the parent chain’s consensus mechanism. By efficiently sending compressed transaction batches to the Sequencer Inbox contract using the most cost-effective method available, the Sequencer ensures transactions are securely recorded on the parent chain. This process maintains the integrity and reliability of the network, enabling users to perform fast, secure transactions. ## Finality Finality in blockchain systems refers to the point at which a transaction becomes irreversible and is permanently included in the blockchain’s ledger. In the context of Arbitrum’s Nitro architecture, understanding finality is crucial for developers and users to make informed decisions about transaction confirmations, security guarantees, and application design. Arbitrum offers two levels of finality: 1. **Soft finality**: Provided by the Sequencer’s real-time feed, offering immediate but provisional transaction confirmations. 2. **Hard finality**: Occurs when transactions are included in batches posted to and finalized on the parent chain, providing strong security assurances. Final settlement on the parent chain happens via [assertions](/how-arbitrum-works/deep-dives/assertions.md) confirmed by the Rollup protocol. This section explores the concepts of soft and hard finality, their implications, trust considerations, and guidance for utilizing them effectively within the Arbitrum network. ### Soft finality Soft finality refers to the preliminary confirmation of transactions based on the Sequencer’s real-time feed. Key characteristics include: * **Immediate confirmation**: Transactions are confirmed almost instantly as they are accepted and ordered by the Sequencer. * **Provisional assurance**: The confirmations are provisional and rely on the Sequencer’s integrity and availability. * **High performance**: Enables applications to offer rapid responses and real-time interactions, enhancing user experience. #### Advantages of soft finality * **Low latency**: Users receive immediate feedback on transaction status. * **Optimized for speed**: Ideal for applications where responsiveness is critical. * **Improved user experience**: Reduces waiting times and uncertainty. #### Limitations of soft finality * **Trust dependency**: Relies on the Sequencer’s honesty and ability to maintain uptime. * **Potential for reordering**: In rare cases, if the Sequencer acts maliciously or encounters issues, the provisional ordering could change. * **Not suitable for high-value transactions**: For transactions requiring strong security guarantees, soft finality may not suffice. ### Hard finality Hard finality occurs when batched transactions get posted to the parent chain. Key characteristics include: * **Strong security guarantees**: When included in blocks on the parent chain, transactions inherit the parent chain’s security assurances. * **Irreversibility**: Once finalized, transactions are immutable and cannot be altered or reversed. * **Data availability**: All transaction data is recorded onchain, ensuring transparency and verifiability. #### Advantages of hard finality * **Maximum security**: Protected by the robustness of the parent chain’s consensus mechanism. * **Trust minimization**: This does not require trust in the Sequencer; the underlying blockchain provides security. * **Suitable for high-value transactions**: Ideal for scenarios where security and immutability are paramount. #### Limitations of hard finality * **High latency**: Achieving hard finality takes longer because the parent chain must process and finalize batches. The challenge protocol that backs this guarantee is described in [BoLD](/how-arbitrum-works/bold/gentle-introduction.md). * **Cost considerations**: Posting batches to the parent chain incurs fees, potentially increasing transaction costs. ### Trust considerations Understanding the trust assumptions associated with each level of finality is essential: * **Soft finality trust model**: * **Reliance on the Sequencer**: Users must trust that the Sequencer operates honestly, sequences transactions correctly, and remains available. * **Risk of misbehavior**: If the Sequencer acts maliciously, it could reorder or censor certain transactions before they achieve hard finality. * **Hard finality trust model**: * **Reliance on the parent chain**: Security is based on the consensus and integrity of the parent chain. * **Reduced trust in Sequencer**: Even if the Sequencer misbehaves, transactions included in posted batches are secured once finalized on the parent chain. ### Application implications Developers and users should consider the appropriate level of finality based on their specific use cases: * **When to rely on soft finality**: * **Low-risk transactions**: For transactions where the potential impact of reordering or delays is minimal. * **User experience priority**: Applications where responsiveness and immediacy enhance user engagement, such as gaming or social platforms. * **Frequent transactions**: Scenarios involving a high volume of small transactions where waiting for hard finality is impractical. * **When to require hard finality**: * **High-value transactions**: Financial transfers, large trades, or any transaction where security is critical. * **Regulatory compliance**: Situations requiring strict adherence to security standards and auditable records. * **Centralized exchanges (CeXs)**: For deposit and withdrawal operations where certainty of transaction finality is mandatory. ## Censorship Timeout As mentioned in the original [Arbitrum BoLD Forum Post](https://forum.arbitrum.foundation/t/aip-bold-permissionless-validation-for-arbitrum/23232/70?), the initial release of [Arbitrum BoLD](/how-arbitrum-works/bold/gentle-introduction.md) will come with a feature called ”Censorship Timeout” (originally called “Delay Buffer”). For Arbitrum One and Nova, it is proposed that this feature be enabled by default alongside BoLD’s upgrade. Censorship Timeout aims to limit the negative effects of: * Prolonged sequencer censorship, and/or, * Unexpected sequencer outages. ### How the Censorship Timeout feature works To explain how this feature improves the security of chains settling to Arbitrum One and Nova, consider a scenario where an L3's parent chain sequencer (the L2 sequencer) is censoring or offline. In such a case, every assertion and/or sub-challenge move would need to wait 24 hours before bypassing the L2 sequencer (using the `SequencerInbox`'s [`forceInclusion`](/how-arbitrum-works/deep-dives/transaction-lifecycle.md#force-inclusion-after-delays) method described here). In this scenario, challenge resolution would be delayed by a time `t` where `t = (24 hours) * number of moves for a challenge`. To illustrate with sample numbers, if a challenge takes 50 sequential moves to resolve, then the delay would be 50 days. The Censorship Timeout feature mitigates this by lowering the force inclusion threshold when unexpected delays in message inclusion occur due to one (or all) of the above-mentioned cases of censorship or a sequencer outage, enabling entities to make moves without the 24-hour delay-per-move. The force inclusion window is the lesser of `delayBuffer` and `delayBlocks`, where `delayBlocks` is a constant currently set to 24 hours, and `delayBuffer` ranges from 30 minutes to 48 hours. The `delayBuffer` value “grows and shrinks” depending on how long the Sequencer is offline or censoring transactions. As a way to measure this behavior, the `delayBuffer` is decremented by the difference between a delayed message’s delay beyond the `threshold` and how long it has been delayed (i.e., when some delayed messages are delayed by more than the `threshold`, the difference between the messages’ delay and the `threshold` is removed from the buffer). For example, if the threshold is 30 minutes and a message was delayed by 32 minutes, the `delayBuffer` is decremented by 2 minutes. The `threshold` is set to 30 minutes on Arbitrum One and 1 hour on Arbitrum Nova. The `delayBuffer` replenishes at a linear rate when the sequencer is operating correctly at a nominal rate of one minute for every 20 minutes in which no messages are delayed beyond the `threshold`. Below are the initial, proposed parameter values for the Censorship Timeout feature for Arbitrum One and Nova: * `delay buffer` = 14400 parent chain (Ethereum) blocks (2 days) * `threshold` = 150 L1 Ethereum blocks (30 minutes) for Arbitrum One and 300 parent chain (Ethereum) blocks (one hour) for Arbitrum Nova * `replenish rate` = 5% (meaning one day is replenished every 20 days or roughly a 95% uptime) We believe that the Censorship Timeout feature provides stronger guarantees of censorship resistance for Arbitrum chains—especially those that settle to Arbitrum One or Arbitrum Nova. As always, chain owners can decide whether to utilize this feature for their chain and can also change the default parameters as they see fit for their use case. ### Decentralized fair sequencing Arbitrum’s long-term vision includes transitioning from a centralized Sequencer to a decentralized, fair sequencing model. In this framework, a committee of servers (or validators) collectively determines transaction ordering, ensuring fairness, reducing the influence of any single party, and making it more resistant to manipulation. By requiring a supermajority, this approach distributes sequencing power among multiple honest participants, mitigates the risks of front-running or censorship, and aligns with broader blockchain principles of enhanced security, transparency, and decentralization. The [Timeboost](/how-arbitrum-works/timeboost/gentle-introduction.md) ordering policy is designed to be compatible with decentralized sequencing. --- > For a complete page index, fetch # Sequencer architecture and transaction flow This deep dive follows a transaction through a single Sequencer instance: how it arrives, how it waits in the transaction queue, how blocks get created, and how the result reaches the rest of the network. It is aimed at chain operators and integration partners who need to reason about queueing, timeouts, and block timing. Two companion pages cover the surrounding context, and this page assumes you have read them: * [The Sequencer and censorship resistance](/how-arbitrum-works/deep-dives/sequencer.md) explains the Sequencer's role, the real-time feed, batch posting, and finality. * [Transaction lifecycle](/how-arbitrum-works/deep-dives/transaction-lifecycle.md) explains the different ways to submit a transaction, including bypassing the Sequencer entirely. > **NOTE** — Scope of this page > > This page describes the internals of a single Sequencer instance. For the behavior of Arbitrum One's public sequencer endpoint (latency expectations, retries, and fallback patterns), see [RPC endpoints and providers](/arbitrum-essentials/reference/node-providers.md). For running multiple redundant Sequencers with Redis-based coordination, see [How to set up a high-availability sequencer](/launch-arbitrum-chain/run-a-node/high-availability-sequencer.md) and [How to run a Sequencer Coordinator Manager](/run-arbitrum-node/sequencer/run-sequencer-coordination-manager.md). All code references below point to [Nitro `v3.11.0`](https://github.com/OffchainLabs/nitro/tree/a618155919315241665356fe60f3cd00d66d5e46). ## Transaction flow at a glance ![Transaction flow from a user through an RPC node into the Sequencer's bounded transaction queue, FIFO into block creation, then out to the sequencer feed and, via the batch poster, to the sequencer inbox on the parent chain where validators verify it](/img/haw-sequencer-transaction-flow.svg) 1. A user submits a signed transaction to any RPC node on the chain. 2. The RPC node does not execute or pool the transaction; it forwards it to the Sequencer. 3. The Sequencer places the transaction in a bounded, in-memory queue. 4. The block creation loop drains the queue in FIFO order and executes transactions one at a time to build a block. 5. The new block is published on the Sequencer Feed for real-time consumers, and the batch poster later posts the compressed sequence to the sequencer inbox on the parent chain. 6. Validators re-execute the sequence posted on the parent chain to verify and assert the chain's state. > **NOTE** — Validators do not accept user transactions > > Validators only read the ordered transactions from the parent chain and re-execute them locally to compute the chain's state. They have no transaction queue and no way to accept, order, or process a user transaction directly. The only ingestion point for user transactions is the Sequencer, and RPC nodes exist to forward transactions to it. (The one exception, submitting through the delayed inbox on the parent chain, is covered in [Transaction lifecycle](/how-arbitrum-works/deep-dives/transaction-lifecycle.md).) ## How transactions reach the Sequencer Only one node on the chain actively sequences transactions at any given time (in a high-availability setup, several sequencer-capable nodes may run behind a coordinator, which picks the active one; see the scope note above). Every other node runs a forwarder: when it receives a transaction over RPC, it immediately relays the raw transaction to the Sequencer's endpoint instead of processing it locally ([`forwarder.go`](https://github.com/OffchainLabs/nitro/blob/a618155919315241665356fe60f3cd00d66d5e46/execution/gethexec/forwarder.go#L126)). The target is configured with `--execution.forwarding-target`; for known chains, Nitro fills it in automatically from the chain's configuration when the node is not a sequencer. A non-sequencer node with no forwarding target (explicitly set to `"null"`) simply drops incoming transactions. Unlike Ethereum, there is **no traditional mempool**. Transactions are not gossiped between nodes, do not sit in a public pending pool, and are not reordered by gas price. The Sequencer receives transactions directly into an in-memory queue and processes them on a first-come, first-served basis. First-come, first-served is the default ordering policy, and the FIFO behavior described on this page assumes it. Chains that enable Timeboost modify the ordering: transactions from the current express lane controller are sequenced as soon as they arrive, while every other transaction has its arrival timestamp delayed (by default, 200 milliseconds) before taking its place in the queue. To learn how the express lane and its auction work, see [Timeboost's gentle introduction](/how-arbitrum-works/timeboost/gentle-introduction.md). ## The transaction queue The heart of the intake path is a bounded, in-memory queue, sized by `--execution.sequencer.queue-size` (default `1024`) ([`sequencer.go#L461`](https://github.com/OffchainLabs/nitro/blob/a618155919315241665356fe60f3cd00d66d5e46/execution/gethexec/sequencer.go#L461)). Its behavior has three distinct zones: * **Inside the queue**: transactions are ordered strictly **FIFO**. Whatever enters the queue first gets sequenced first. * **At the queue boundary, when the queue is full**: a transaction has "reached the Sequencer but is not yet queued." The Sequencer holds the submission open and waits for a slot to free up ([`sequencer.go#L731-L735`](https://github.com/OffchainLabs/nitro/blob/a618155919315241665356fe60f3cd00d66d5e46/execution/gethexec/sequencer.go#L731-L735)). Ordering among these waiting transactions is **not guaranteed**: when a slot frees up, which waiting transaction claims it depends on runtime scheduling, not arrival order. * **Rejected**: if a transaction cannot be queued and sequenced before its deadline (see below), it is rejected and never executes. ### Queue timeout and `context deadline exceeded` Every transaction receives a deadline when it arrives, set by `--execution.sequencer.queue-timeout` (default `12s`) ([`sequencer.go#L557-L560`](https://github.com/OffchainLabs/nitro/blob/a618155919315241665356fe60f3cd00d66d5e46/execution/gethexec/sequencer.go#L557-L560)). The deadline is enforced at two points: 1. **While waiting to enter a full queue**: if no slot frees up before the deadline, the submission fails immediately. 2. **Again at dequeue time**: when the block creation loop pops a transaction from the queue, it first checks whether the transaction's deadline already expired while it sat in the queue, and rejects it if so ([`sequencer.go#L1360-L1364`](https://github.com/OffchainLabs/nitro/blob/a618155919315241665356fe60f3cd00d66d5e46/execution/gethexec/sequencer.go#L1360-L1364)). In both cases, the caller receives an error containing the string `context deadline exceeded`. This error means exactly one thing: the chain could not sequence the transaction within `queue-timeout`, and the transaction **was not executed**. For a user or integration partner, the correct response is: * Treat it as backpressure, not as a permanent failure. It is safe to resubmit the same signed transaction (same nonce) with a backoff. * Make sure your client-side HTTP timeout is longer than the chain's `queue-timeout`; otherwise your client gives up before the Sequencer reports the outcome. * If you see this error persistently rather than in bursts, the chain's intake is saturated: see [Tuning guidance for chain operators](#tuning-guidance-for-chain-operators) below. ## Block creation and timing The Sequencer produces blocks from a single loop: attempt to create a block; if a block was produced, wait until `max-block-speed` has elapsed since the attempt started before trying again; if not, retry immediately ([`sequencer.go#L1773-L1781`](https://github.com/OffchainLabs/nitro/blob/a618155919315241665356fe60f3cd00d66d5e46/execution/gethexec/sequencer.go#L1773-L1781)). `--execution.sequencer.max-block-speed` (default `250ms`) is therefore the **minimum delay between blocks**, which caps block production at four blocks per second by default. It is not a block time: * **When the queue is empty**, the block creation routine simply blocks waiting for the next transaction to arrive ([`sequencer.go#L1314-L1341`](https://github.com/OffchainLabs/nitro/blob/a618155919315241665356fe60f3cd00d66d5e46/execution/gethexec/sequencer.go#L1314-L1341)). No transactions means no blocks: the Sequencer does not produce empty blocks, and `createBlock` reports that no block was made unless at least one transaction was successfully sequenced. * **When blocks are heavy**, the actual interval stretches beyond `max-block-speed`, because execution itself takes time and the timer only sets a floor. In short, **there is no fixed block time on the child chain**. Block timestamps and block numbers advance with demand, which is why time-based logic in contracts should never assume a constant block interval. Within one block creation pass, the Sequencer pulls the first transaction (waiting for it if necessary), then keeps draining additional queued transactions for a short window (`--execution.sequencer.read-from-tx-queue-timeout`, default `10ms`) before sealing the set into a block. Transactions that don't fit in the block's gas limit are pushed to an internal retry queue and get first priority in the next block. ### Execution is strictly sequential When building a block, transactions are executed **one at a time** through the state transition function ([`block_processor.go#L372-L407`](https://github.com/OffchainLabs/nitro/blob/a618155919315241665356fe60f3cd00d66d5e46/arbos/block_processor.go#L372-L407)), under a lock that ensures only one block is ever being built at once. There is no parallel execution: total throughput is bounded by single-threaded EVM execution speed and the per-block gas limit, not by how fast transactions can be queued. ## What happens during a surge Suppose 100,000 transactions arrive at effectively the same moment, with default settings: 1. The first `1024` transactions occupy the queue. The rest wait at the queue boundary, each holding its own 12-second deadline, with no ordering guarantee among them. 2. Every `250ms` or more, the Sequencer drains a batch of queued transactions into a block, executing them sequentially. Freed slots are claimed by waiting transactions. 3. Any transaction that cannot make it through the queue and into a block within its 12-second deadline fails with `context deadline exceeded`. The queue is deliberately a short buffer, not a mempool: with default settings it never holds more than about 12 seconds' worth of work. Everything beyond what the chain can execute in that window is shed back to the submitter, who is expected to retry. This keeps latency bounded and predictable for the transactions that do get in, at the cost of pushing burst-absorption out to the edges (RPC clients, or an operator-run relayer, described below). ## From block to the rest of the network As soon as a block is created, the transaction streamer hands the new message to the broadcaster, which publishes it over WebSocket on the sequencer feed ([`transaction_streamer.go#L1307`](https://github.com/OffchainLabs/nitro/blob/a618155919315241665356fe60f3cd00d66d5e46/arbnode/transaction_streamer.go#L1307)). Full nodes and [feed relays](/run-arbitrum-node/run-feed-relay.md) consume the feed to give sub-second soft confirmations; see [How to read the sequencer feed](/run-arbitrum-node/sequencer/read-sequencer-feed.md). Independently, the batch poster compresses the sequenced transactions and posts them to the sequencer inbox on the parent chain, at which point they inherit the parent chain's finality. Validators then re-execute the posted sequence and assert the resulting state. [The Sequencer and censorship resistance](/how-arbitrum-works/deep-dives/sequencer.md#sequencing-and-broadcasting) covers the feed, batch posting, and the finality trade-offs in detail, and [Assertions](/how-arbitrum-works/deep-dives/assertions.md) covers validation. ## Tuning guidance for chain operators For chains with different traffic profiles than Arbitrum One, the queue parameters are the primary tuning surface: * `--execution.sequencer.queue-size` (default `1024`): raising it lets the Sequencer absorb larger instantaneous bursts without making submitters wait at the queue boundary. The limit to keep in mind: the queue-timeout deadline keeps counting while a transaction sits in the queue, so a queue deeper than what the chain can execute within `queue-timeout` only moves rejections from enqueue time to dequeue time. Size the queue to roughly what your chain can drain in one timeout window. * `--execution.sequencer.queue-timeout` (default `12s`): raising it lets transactions ride out longer spikes at the cost of slower failure feedback and longer-held connections; lowering it makes overload fail fast. Whatever you choose, communicate it to integration partners so their client timeouts and retry logic stay consistent with it. * `--execution.sequencer.max-block-speed` (default `250ms`): lowering it raises the block production cap and reduces best-case latency; it does not increase execution throughput, which stays bounded by sequential execution and the per-block gas limit. ### Operator-run relayer cache If your workload has sustained bursts that no reasonable `queue-size`/`queue-timeout` setting absorbs (for example, game events or airdrops that generate far more than one timeout window's worth of transactions at once), the standard pattern is a **relayer cache in front of the Sequencer**: an operator-run service that accepts transactions immediately, holds them durably, and submits them to the Sequencer at the rate the queue drains, retrying on `context deadline exceeded`. This pattern complements rather than replaces queue tuning: `queue-size` and `queue-timeout` define how much burst the Sequencer itself absorbs, while the relayer holds everything beyond that and controls its own submission order and retry policy. The trade-off is that transactions waiting in the relayer have no onchain ordering guarantee until they actually enter the Sequencer's queue, so the relayer becomes a trusted component of your chain's ingestion path. ## Related resources * [The Sequencer and censorship resistance](/how-arbitrum-works/deep-dives/sequencer.md): feed, batch posting, finality, and the delayed inbox escape hatch * [Transaction lifecycle](/how-arbitrum-works/deep-dives/transaction-lifecycle.md): all submission pathways, including bypassing the Sequencer * [RPC endpoints and providers](/arbitrum-essentials/reference/node-providers.md): public endpoint behavior, retries, and fallback patterns * [How to set up a high-availability sequencer](/launch-arbitrum-chain/run-a-node/high-availability-sequencer.md): redundant Sequencers with Redis-based coordination * [How to run a Sequencer Coordinator Manager](/run-arbitrum-node/sequencer/run-sequencer-coordination-manager.md): managing the sequencer priority list * [How to read the sequencer feed](/run-arbitrum-node/sequencer/read-sequencer-feed.md) and [How to run a feed relay](/run-arbitrum-node/run-feed-relay.md) --- > For a complete page index, fetch # A gentle introduction The State Transition Function (STF) determines how blockchain systems change state during transaction processing. The STF takes the current blockchain state (account balances, smart contract data, and ledger information) and an input (a transaction or a block) to compute the new state deterministically. This deterministic property ensures all network nodes reach the same result, maintaining consensus. With Arbitrum, the STF plays an even more pivotal role. Arbitrum executes transactions offchain in batches, periodically submitting summaries to the parent chain. This approach leverages offchain computation to achieve higher throughput and lower gas costs while maintaining Ethereum’s security. To safeguard against incorrect or malicious offchain execution, Arbitrum employs a challenge mechanism called fraud proofs. If a dispute arises, the STF can be recomputed step-by-step onchain, enabling the network to verify the validity of offchain computations and ensure that errors or fraudulent behavior are detected and corrected. For how disputes are resolved in practice, see [Assertions](/how-arbitrum-works/deep-dives/assertions.md) and the [BoLD gentle introduction](/how-arbitrum-works/bold/gentle-introduction.md). The [](/how-arbitrum-works/inside-arbitrum-nitro.md)Arbitrum Nitro stack's STF mirrors Ethereum's STF with key modifications for Arbitrum chain requirements. The function processes ordered transactions and outputs the updated state from the transaction batch. [](/stylus/gentle-introduction.md)Stylus expands Arbitrum’s execution model beyond the Ethereum Virtual Machine (EVM) by adding WebAssembly (WASM)—based execution, allowing high-performance contracts in Rust, C, and C++ to run alongside traditional EVM contracts (Solidity). This integration of Stylus introduces several modifications to the STF, including: ## Stylus-specific transaction processing A modified version of Geth that recognizes and processes Stylus transactions, ensuring proper inclusion in state transitions. ## Execution in a WASM runtime Stylus transactions execute in [](/how-arbitrum-works/deep-dives/arbos.md)ArbOS's WASM runtime instead of the EVM, enabling faster execution and more efficient computation. ## Stylus gas accounting and pricing Unlike standard EVM transactions, Stylus transactions introduce new gas pricing models that account for factors such as opcode pricing, host I/O operations, and Ink usage costs. ## Interoperability with the EVM Stylus contracts can interact with Solidity contracts, enabling hybrid applications that leverage EVM and WASM execution environments. These Stylus-related changes aim to maintain compatibility with Ethereum’s execution model while introducing a more efficient, flexible, and scalable alternative for smart contract development. The following sections cover [STF inputs](/how-arbitrum-works/reference/stf-inputs.md), node processing, and implementation rules, highlighting [differences between Ethereum and Arbitrum](/arbitrum-essentials/arbitrum-vs-ethereum/comparison-overview.md), and Stylus execution environments. [Stylus-specific execution tasks handled within ArbOS](/how-arbitrum-works/deep-dives/arbos.md) will be covered separately, focusing on host I/O operations, caching, and WASM memory management. --- > For a complete page index, fetch # Token bridging The Arbitrum protocol itself has no notion of token standards and gives no built-in advantage or special recognition to any particular token bridge. In this article, we describe the "canonical bridge," which was implemented by Offchain Labs, and should be the primary bridge most users and applications use; it is (effectively) a decentralized app with contracts on both Ethereum (parent chain) and Arbitrum (child chain) that leverages Arbitrum's [cross-chain messaging protocol](/arbitrum-essentials/bridging/cross-chain-messaging.md) to achieve basic desired token-bridging functionality. We recommend that you use it! For the protocol-level mechanics this bridge sits on top of, see [Bridging from a parent chain to a child chain](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md) and [Child to parent chain messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md). ## Design rationale In our token bridge design, we use the term "gateway" as per [this proposal](https://ethereum-magicians.org/t/outlining-a-standard-interface-for-cross-domain-erc20-transfers/6151); i.e., one of a pair of contracts on two different domains (i.e., Ethereum and an Arbitrum chain), used to facilitate cross-domain asset transfers. We now describe some core goals that motivated the design of our bridging system. ### Custom gateway functionality For many **ERC-20** tokens, "standard" bridging functionality is sufficient, which entails the following: a token contract on Ethereum is associated with a "paired" token contract on Arbitrum. Depositing a token involves escrowing a certain amount of the token in a parent chain bridge contract, and minting the same amount at the paired token contract on a child chain. Then, on the child chain, the paired contract behaves much like a normal **ERC-20** token contract. Withdrawing entails burning a specific amount of the token in the child chain contract, which can be claimed later from the parent chain bridge contract. Many tokens, however, require custom gateway systems, the possibilities of which are hard to generalize, e.g.,: * Tokens which accrue interest to their holders need to ensure that the interest is dispersed properly across layers, and doesn't simply accrue to the bridge contracts * Our cross-domain **WETH** implementations require tokens to be wrapped and unwrapped as they move across layers. Thus, our bridge architecture must allow not just the standard deposit and withdrawal functionalities, but also for new, custom gateways to be dynamically added over time. ### Canonical child chain representation per parent chain token contract Having multiple custom gateways is beneficial, but we also want to avoid a situation in which a single parent chain token that utilizes our bridging system can be represented at multiple addresses/contracts on the child chain, as this adds significant friction and confusion for users and developers. Thus, we need a way to track which parent chain token uses which gateway, and in turn, to have a canonical address oracle that maps the tokens' addresses across the Ethereum and Arbitrum domains. ## Canonical token bridge implementation With this in mind, we provide an overview of our token bridging architecture. Our architecture consists of three types of contracts: 1. **Asset contracts**: These are the token contracts themselves, i.e., an **ERC-20** on the parent chain and its counterpart on Arbitrum. 2. **Gateways**: Pairs of contracts (one on the parent chain, one on the child chain) that implement a particular type of cross-chain asset bridging. 3. **Routers**: Exactly two contracts (one on the parent chain, one on the child chain) that route each asset to its designated gateway. ![](/img/apps-gatewayUML.svg) All Ethereum-to-Arbitrum token transfers are initiated via the router contract on the parent chain, specifically the `L1GatewayRouter` contract. `L1GatewayRouter` forwards the token's deposit call to the appropriate gateway contract on the parent chain, the `L1ArbitrumGateway` contract. `L1GatewayRouter` is responsible for mapping the parent chain token addresses to L1Gateway contracts, thus acting as a parent/child chain address oracle and ensuring each token corresponds to only one gateway. The `L1ArbitrumGateway` then communicates to its counterpart gateway contract on the child chain, the `L2ArbitrumGateway` contract (typically/expectedly via [retryable tickets](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md)). ![](/img/apps-bridge_deposits.png) Similarly, Arbitrum-to-Ethereum transfers initiate via the router contract on the child chain, specifically the `L2GatewayRouter` contract, which in turn calls the token's gateway contract on the child chain. This `L2ArbitrumGateway` contract in turn communicates to its corresponding gateway contract on the parent chain, the `L1ArbitrumGateway` contract (typically/expectedly via [sending child-to-parent messages to the outbox](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md)). ![](/img/apps-bridge_withdrawals.png) For any given gateway pairing, we require that calls initiate through the corresponding router (`L1GatewayRouter` or `L2GatewayRouter`), and that the gateways conform to the [`TokenGateway`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/libraries/gateway/TokenGateway.sol) interfaces; the `TokenGateway` interfaces should be flexible and extensible enough to support any bridging functionality a particular token may require. ## The standard **ERC-20** gateway By default, any **ERC-20** token on a parent chain that isn't registered to a gateway can be bridged permissionlessly through the `StandardERC20Gateway`. You can use the [bridge UI](https://bridge.arbitrum.io/) or follow the instructions in [How to bridge tokens via Arbitrum’s standard **ERC-20** gateway](/arbitrum-essentials/bridging/deposit/eth-and-messages.md) to bridge a token to a child chain via this gateway. ### Example: Standard `Arb-ERC-20` deposit and withdraw To help illustrate what this all looks like in practice, let's go through the steps of what depositing and withdrawing `SomeERC20Token` via our standard **ERC-20** gateway looks like. Here, we're assuming that `SomeERC20Token` has already been registered in the `L1GatewayRouter` to use the standard **ERC-20** gateway. #### Deposits 1. A user calls `L1GatewayRouter.outboundTransferCustomRefund` **\[1]** (with `SomeERC20Token`'s parent chain address as an argument). 2. `L1GatewayRouter` looks up `SomeERC20Token`'s gateway, and finds that it's the standard **ERC-20** gateway (the `L1ERC20Gateway` contract). 3. `L1GatewayRouter` calls `L1ERC20Gateway.outboundTransferCustomRefund`, forwarding the appropriate parameters. 4. `L1ERC20Gateway` escrows the tokens sent and creates a retryable ticket to trigger `L2ERC20Gateway`'s `finalizeInboundTransfer` method on the child chain. 5. `L2ERC20Gateway.finalizeInboundTransfer` mints the appropriate amount of tokens at the `arbSomeERC20Token` contract on the child chain. ❗️ *\[1] Please keep in mind that some older custom gateways might not have `outboundTransferCustomRefund` implemented, and `L1GatewayRouter.outboundTransferCustomRefund` does not fallback to `outboundTransfer`. In those cases, please use the function `L1GatewayRouter.outboundTransfer`.* > **INFO** > > `arbSomeERC20Token` is an instance of [`StandardArbERC20`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/StandardArbERC20.sol), which includes `bridgeMint` and `bridgeBurn` methods only callable by the `L2ERC20Gateway`. #### Withdrawals 1. On Arbitrum, a user calls `L2GatewayRouter.outBoundTransfer`, which in turn calls `outBoundTransfer` on arbSomeERC20Token's gateway (i.e., `L2ERC20Gateway`). 2. This burns `arbSomeERC20Token` tokens, and calls `ArbSys` with an encoded message to `L1ERC20Gateway.finalizeInboundTransfer`, which will eventually execute on the parent chain. 3. After the dispute window expires and the assertion with the user's transaction is confirmed, a user can call `Outbox.executeTransaction`, which in turn calls the encoded `L1ERC20Gateway.finalizeInboundTransfer` message, releasing the user's tokens from the `L1ERC20Gateway` contract's escrow. ## The Arbitrum generic-custom gateway Just because a token has requirements beyond the offerings of the standard **ERC-20** gateway, that doesn't necessarily mean that a unique gateway needs to be tailor-made for the token in question. Our generic-custom gateway is flexible enough to be suitable for most (but not necessarily all) custom fungible token needs. As a general rule: **If your custom token can increase its supply (i.e., mint) directly on the child chain, and you want the child chain-minted tokens to be withdrawable back to the parent chain and recognized by the parent chain contract, it will probably require its own special gateway. Otherwise, the generic-custom gateway is likely the right solution for you!** Some examples of token features suitable for the generic-custom gateway: * A child chain token contract upgradable via a proxy * A child chain token contract that includes address allowlisting/denylisting * The deployer determines the address of the child chain token contract ### Setting up your token with the generic-custom gateway Follow the steps below to set up your token for use with the generic-custom gateway. You can also find more detailed instructions on the page [How to bridge tokens via Arbitrum’s generic-custom gateway](/arbitrum-essentials/bridging/overview.md). **0. Have a parent chain token** Your token on the parent chain should conform to the [ICustomToken](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/ICustomToken.sol) interface (see [`TestCustomTokenL1`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/test/TestCustomTokenL1.sol) for an example implementation). Crucially, it must have an `isArbitrumEnabled` method in its interface. **1. Deploy your token on Arbitrum** Your token should conform to the minimum [`IArbToken`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/arbitrum/IArbToken.sol) interface; i.e., it should have `bridgeMint` and `bridgeBurn` methods only callable by the `L2CustomGateway` contract, and the address of its corresponding Ethereum token accessible via `l1Address`. For an example implementation, see [`L2GatewayToken`](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/libraries/L2GatewayToken.sol). > **INFO** — Token compatibility with available tooling > > If you want your token to be compatible out of the box with all the tooling available (e.g., the [Arbitrum bridge](https://bridge.arbitrum.io/)), we recommend that you keep the implementation of the `IArbToken` interface as close as possible to the [L2GatewayToken](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/libraries/L2GatewayToken.sol) implementation example. > > For example, if an allowance check is added to the `bridgeBurn()` function, the token will not be easily withdrawable through the Arbitrum bridge UI, as the UI does not prompt an approval transaction of tokens by default (it expects the tokens to follow the recommended `L2GatewayToken` implementation). **2. Register your token on the parent chain to your token on the child chain via the `L1CustomGateway` contract** Have your parent chain token's contract make an external call to `L1CustomGateway.registerTokenToL2`. Performing the registration can be completed as a chain-owner registration via an [Arbitrum DAO](https://forum.arbitrum.foundation) proposal. **3. Register your token on the parent chain to the `L1GatewayRouter`** After your token's registration to the generic-custom gateway is complete, have your parent chain token's contract make an external call to `L1GatewayRouter.setGateway`; performing the registration can also be completed as a chain-owner registration via an [Arbitrum DAO](https://forum.arbitrum.foundation) proposal. > **INFO** — We are here to help > > If you have questions about your custom token needs, please feel free to reach out to us on our [Discord server](https://discord.gg/arbitrum). ## Other flavors of gateways Note that in the system described above, one pair of gateway contracts handles the bridging of many **ERC-20**'s; i.e., many **ERC-20**'s on the parent chain are each paired with their own **ERC-20**'s on Arbitrum via a single gateway contract pairing. Other gateways may have different relationships with the contracts that they bridge. Take our wrapped Ether implementation for example: here, a single **WETH** contract on the parent chain is connected to a single **WETH** contract on the child chain. When transferring **WETH** from one domain to another, the parent/child chain gateway architecture is used to unwrap the **WETH** on domain A, transfer the now-unwrapped Ether, and then re-wrap it on domain B. This process ensures that **WETH** can behave on Arbitrum in the same way users are accustomed to it behaving on Ethereum, while ensuring that all **WETH** tokens are always fully collateralized on the layer on which they reside. Regardless of a token's complexity in bridging needs, it is possible to create a gateway to accommodate it within our canonical bridging system. You can find an example of implementation of a custom gateway in the page [How to bridge tokens via a custom gateway](/arbitrum-essentials/bridging/overview.md). ## Demos Our [How to bridge tokens](/arbitrum-essentials/bridging/overview.md) section provides an example of interacting with Arbitrum's token bridge via the [Arbitrum SDK](https://github.com/OffchainLabs/arbitrum-sdk). ## A word of caution on bridges (aka, "I've got a bridge to sell you") Cross-chain bridging is an exciting design space; alternative bridge designs can potentially offer faster withdrawals, interoperability with other chains, and different trust assumptions with their own potentially valuable UX tradeoffs, etc. They can also potentially be completely insecure and/or outright scams. Users should treat other, non-canonical bridge applications the same way they treat any application running on Arbitrum, and exercise caution and due diligence before entrusting them with their value. --- > For a complete page index, fetch # Transaction lifecycle on Arbitrum You can submit transactions to Arbitrum through two main pathways: 1. **Through the [](/how-arbitrum-works/deep-dives/sequencer.md)Sequencer**: The standard method for most transactions. 2. **Bypassing the Sequencer**: Using the [](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#transacting-via-the-delayed-inbox)Delayed Inbox contract on the parent chain. This guide explains both methods, when to use each, and how they flow through Arbitrum’s transaction lifecycle. ## Transaction submission methods ### Sequencer pathways You can send transactions to the Sequencer through four methods: * Public RPC endpoints * Third-party RPC providers * Self-hosted Arbitrum nodes * Direct Sequencer endpoint The first three options route through a load balancer, while the Sequencer endpoint connects directly. ### Non-sequencer pathway You can also submit transactions directly to the Delayed Inbox contract on the parent chain. This method works even when the Sequencer is unavailable, providing an additional layer of censorship resistance. The following diagram shows the different ways you can submit transactions to Arbitrum: ![Transaction lifecycle diagram showing various pathways for submitting transactions](/img/haw-transaction-lifecycle.png) ## Submitting transactions to the Sequencer The Sequencer processes most transactions on Arbitrum. You have four options for sending transactions to the Sequencer, each with different benefits depending on your needs. ### Public RPC Arbitrum provides public RPC endpoints for Arbitrum One, Arbitrum Nova, and Arbitrum Sepolia. These endpoints are rate-limited, making them best for: * Testing and development * Light usage and general interactions * Applications with low transaction volume For more details on the specific RPC endpoints for each chain, please see [this section](https://docs.arbitrum.io/build-decentralized-apps/reference/node-providers#arbitrum-public-rpc-endpoints) of the documentation. ### Third-party RPC Third-party node providers offer enhanced performance and higher rate limits. Many providers that support Ethereum also support Arbitrum chains. Use third-party RPCs when you need: * Higher throughput * Better performance * More reliable uptime * Advanced features like analytics You can find a list of supported third-party providers [here](https://docs.arbitrum.io/build-decentralized-apps/reference/node-providers#third-party-rpc-providers). ### Arbitrum nodes Running your own Arbitrum node provides maximum control and privacy. Your transactions connect to the Sequencer through the [](/how-arbitrum-works/deep-dives/sequencer.md#sequencing-and-broadcasting)Sequencer Feed. Consider this option if you need: * Complete control over transaction handling * Maximum privacy * Custom node configurations Please see the [Arbitrum Node](https://docs.arbitrum.io/run-arbitrum-node/overview) documentation to learn more about setting up and running a node. ### Sequencer endpoint The Sequencer endpoint provides the fastest path to transaction submission by bypassing the load balancer. This endpoint only supports: * `eth_sendRawTransaction` * `eth_sendRawTransactionConditional` Use this method when you need the lowest possible latency for time-sensitive transactions. The following diagram shows the four methods for submitting transactions to the Sequencer: ![Submit transaction to the Sequencer](/img/haw-submit-tx-to-sequencer.svg) ## Bypassing the Sequencer You can submit transactions directly to the [Delayed Inbox](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#transacting-via-the-delayed-inbox) contract on the parent chain without using the Sequencer. This method provides: * **Censorship resistance**: Transaction inclusion even if the Sequencer refuses to process it * **Availability guarantee**: Transactions work even when the Sequencer is offline * **Decentralization**: Reduces dependency on the Sequencer There are two paths your transaction can take through the Delayed Inbox. When you submit a transaction to the Delayed Inbox, two things can happen: 1. **Automatic processing**: The Sequencer picks up your transaction and includes it in the normal transaction flow 2. **Force inclusion**: If 24 hours pass without processing, you can call the `forceInclude` function on the `SequencerInbox` contract to guarantee inclusion. The [Censorship Timeout](/how-arbitrum-works/deep-dives/sequencer.md#censorship-timeout) feature can shorten this window when the Sequencer is offline or censoring. This two-path system ensures your transaction will always be processed, even if the Sequencer is unresponsive or censoring transactions. ![Bypassing the Sequencer](/img/haw-bypassing-the-sequencer.svg) ### Using the Delayed Inbox To submit a transaction through the Delayed Inbox: 1. **Construct your transaction** and serialize it 2. **Call [`sendL2Message`](https://github.com/OffchainLabs/nitro-contracts/blob/fbbcef09c95f69decabaced3da683f987902f3e2/src/bridge/AbsInbox.sol#L150)** with your serialized transaction data. For the difference between signed and unsigned message variants, see [Signed messages](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#signed-messages) and [Unsigned messages](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#unsigned-messages). 3. **Wait for processing** or force inclusion after 24 hours ### Force inclusion after delays If your transaction doesn't get processed within 24 hours, call the [`forceInclusion`](https://github.com/OffchainLabs/nitro-contracts/blob/fbbcef09c95f69decabaced3da683f987902f3e2/src/bridge/SequencerInbox.sol#L284) function on the `SequencerInbox` contract. This function ensures that transactions get included regardless of the Sequencer's status. ### Using the Arbitrum SDK The Arbitrum SDK simplifies Delayed Inbox interactions through the [`InboxTools`](https://github.com/OffchainLabs/arbitrum-sdk/blob/792a7ee3ccf09842653bc49b771671706894cbb4/src/lib/inbox/inbox.ts#L64C14-L64C24) class: | Method | Purpose | | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | [`sendChildSignedTx`](https://github.com/OffchainLabs/arbitrum-sdk/blob/792a7ee3ccf09842653bc49b771671706894cbb4/src/lib/inbox/inbox.ts#L401C16-L401C33) | Submit transaction to Delayed Inbox | | [`forceInclude`](https://github.com/OffchainLabs/arbitrum-sdk/blob/792a7ee3ccf09842653bc49b771671706894cbb4/src/lib/inbox/inbox.ts#L367) | Force transaction inclusion after 24 hours | | [`signChildTx`](https://github.com/OffchainLabs/arbitrum-sdk/blob/792a7ee3ccf09842653bc49b771671706894cbb4/src/lib/inbox/inbox.ts#L429) | Helper for transaction signing | --- > For a complete page index, fetch # Inside Arbitrum Nitro ## Transaction processing journey on Arbitrum As a developer initiating a transaction on Arbitrum, gaining a clear understanding of the end-to-end flow—from initial submission through to finality—is helpful, but it isn't required. This overview methodically traces the transaction lifecycle, emphasizing how Arbitrum’s architecture ensures precise, efficient, and secure handling at every stage. This article covers the complete Arbitrum Nitro stack, beginning with the Sequencer, which is responsible for transaction ordering, advancing to the State Transition Function (STF) for execution, and culminating in validation mechanisms that uphold integrity. ![Transaction lifecycle](/img/haw-transaction-lifecycle.png) Transaction lifecycle Along the way, you'll learn about Arbitrum's ability to deliver security on par with Ethereum, while achieving fee reductions by a factor of ten and transaction speeds accelerated by a factor of 100 using optimized mechanisms. ## The foundation: Arbitrum's core architecture overview Before delving into the journey of a transaction, it's important to outline the foundational architecture that powers Arbitrum's operations. Central to the architecture is a simple yet powerful principle: deterministic state transitions, which ensure consistent outcomes across the network. The system revolves around three core components: ![Original napkin sketch drawn by Arbitrum co-founder Ed Felten](/img/haw-eds-napkin-drawing.png)Original napkin sketch drawn by Arbitrum co-founder Ed Felten Transactions first enter the Inbox, serving as the primary gateway into the ecosystem. From there, the STF processes them deterministically, applying rules that guarantee reproducibility. Finally, the Outputs component generates the resulting data and state updates, completing the cycle. The deterministic model ensures that identical inputs will always produce the same outputs, among honest nodes, forming the bedrock of Arbitrum’s security. It supports efficient fraud proofs and dispute resolution, enabling verification of execution correctness without re-running entire transactions, thereby minimizing computational overhead. For foundational concepts about the STF, see the [State Transition Function gentle introduction](/how-arbitrum-works/deep-dives/stf-gentle-intro.md). ## Step 1: Submitting a transaction The submission process for a transaction on Arbitrum starts with the user sending the request, with options tailored to balance factors such as speed, control, and reliability based on specific needs. Most transactions route through the Sequencer, a specialized node that orders transactions and issues quick confirmations. This path accommodates various submission methods, including [public RPC](/how-arbitrum-works/deep-dives/transaction-lifecycle.md#public-rpc) for development and light usage, [third-party RPCs](/how-arbitrum-works/deep-dives/transaction-lifecycle.md#third-party-rpc) for improved throughput, [direct Sequencer endpoints](/how-arbitrum-works/deep-dives/transaction-lifecycle.md#sequencer-endpoint) for minimal latency in critical operations, and [self-hosted Arbitrum nodes](/run-arbitrum-node/run-full-node.md) for ultimate privacy and customization. Alternatively, to mitigate the risks of exclusion or delays by the Sequencer, users can submit directly to the Delayed Inbox contract on Ethereum, thereby bolstering system resilience. In this mechanism, non-Sequencer transactions enter a dedicated queue, where a well-functioning Sequencer typically integrates them within about ten minutes. If delayed for more than 24 hours, any network participant can [force inclusion](/how-arbitrum-works/deep-dives/transaction-lifecycle.md#force-inclusion-after-delays) into the main inbox, limiting the Sequencer's ability to block transactions permanently—it can only add a temporary delay. While the Sequencer route offers faster soft finality and streamlines workflows, the Delayed Inbox path doubles processing time but emphasizes censorship resistance (blocking transactions). Overall, the Sequencer’s commitment to ordered inclusion provides “soft finality” for a responsive experience, complemented by these alternatives for robust, flexible operations across applications. For more technical detail about the transaction lifecycle, refer to the [Transaction Lifecycle deep dive](/how-arbitrum-works/deep-dives/transaction-lifecycle.md). ## Step 2: Ordering and broadcasting: the Sequencer Once a transaction reaches the Sequencer, it integrates into a refined system for ordering and broadcasting, designed to maximize performance while upholding security. The Sequencer immediately shares the transaction through its real-time feed, offering instant network-wide visibility. This feed delivers immediate confirmation of acceptance and sequencing, keeping all nodes synchronized with the latest order and enabling soft finality, allowing users to proceed with confidence based on the Sequencer’s reliable commitment. To further optimize, the sequencer [groups transactions into batches](/how-arbitrum-works/deep-dives/sequencer.md#batching-and-compression) rather than processing them individually, reducing costs and boosting efficiency. Batches form when transactions accumulate to a predefined size or after a set time interval to prevent lags. The data is then compressed using the [Brotli algorithm](/how-arbitrum-works/deep-dives/sequencer.md#compression-level-in-the-brotli-algorithm), with the compression level dynamically adjusted from 0 to 11 based on congestion. Higher compression reduces Layer 1 posting expenses at the cost of increased computation, while the system prioritizes speed during heavy backlogs. After batches and compression, the data posts to Ethereum through the Sequencer Inbox contract using one of two methods: 1. The default blob transactions under [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) provide cost-effective and scalable data inclusion when supported by Ethereum. 2. As a fallback, calldata transactions embed data directly, ensuring compatibility even if blob fees rise or [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) blobs are unavailable. This process yields 10-100 times the cost savings over individual postings and adapts to network conditions for consistent efficiency. For a deeper look at how the Sequencer orders, batches, and posts transactions, see the [Sequencer deep dive](/how-arbitrum-works/deep-dives/sequencer.md). For details on how parent chain costs are calculated and priced, including the adaptive pricing algorithm, see the [Gas and fees deep dive](/how-arbitrum-works/deep-dives/gas-and-fees.md#parent-chain-gas-pricing). ## Step 3: Execution phase: State Transition Function With ordering and batching complete, execution shifts to the State Transition Function (STF), the core of Arbitrum's processing engine. For foundational concepts about how the STF works, see the [State Transition Function gentle introduction](/how-arbitrum-works/deep-dives/stf-gentle-intro.md). Arbitrum ensures full Ethereum Virtual Machine (EVM) compatibility via a three-layer architecture. At the base, the Geth core handles EVM execution, aligning behaviors with Ethereum and drawing on its extensively tested code for security. For more technical details about Geth, refer to the [Geth deep dive](/how-arbitrum-works/reference/geth.md). Above this, ArbOS—the Arbitrum operating system—adds child chain features such as cross-chain messaging, fee management, gas pricing, deposit and withdrawal handling, and advanced tooling, such as Stylus. The top layer, the node interface, manages RPC connections and APIs, providing Ethereum-like functionality for clients. In the STF, transactions follow a structured workflow: first, ArbOS validates formatting and funds, then charges gas for Layer 2 execution and Layer 1 posting. Then Geth executes per EVM standards. Finally, ArbOS updates state and cross-chain elements, generating receipts and logs to finalize the process. For technical details about ArbOS, refer to the [ArbOS deep dive documentation](/how-arbitrum-works/deep-dives/arbos.md) and the [ArbOS technical reference](/how-arbitrum-works/reference/arbos-reference.md). ![Geth sandwich](/img/haw-geth-sandwich.svg)Geth sandwich For Stylus contracts that use [WebAssembly](/stylus/concepts/webassembly.md) (WASM), execution is diverted to a WASM runtime, where contracts access state via specialized host I/O calls. This diversion yields 10-70 times faster performance than EVM equivalents and full interoperability for mixed calls. The Geth base lets Ethereum apps run unchanged, while ArbOS and Stylus enable Layer 2 optimizations and high-performance innovations. Consult the [State Transition Function deep dive](/how-arbitrum-works/reference/stf-inputs.md) for further mechanics. ## Step 4: Finality At this stage, the transaction achieves two complementary levels of finality, each tailored to Arbitrum’s security needs: 1. Soft finality emerges immediately upon inclusion in the Sequencer Feed, offering instant acceptance feedback, a commitment to order, and the ability to act without wait times—as noted in the submission phase. This "soft finality" relies on the Sequencer's trustworthiness for usability but lacks cryptographic backing. 2. Hard finality, conversely, solidifies when the batch is posted and is confirmed on Ethereum, inheriting its consensus security, ensuring public data availability, and making the transaction irreversible. This process typically takes 10-20 minutes, which varies depending on Ethereum’s block times and batch frequency. Hard finality depends on Rollup assertions being confirmed on Ethereum. The dual model combines quick "soft finality" for a better user experience with Ethereum-level safeguards for hard finality, along with censorship-resistant paths for assured inclusion. This balance delivers immediate feedback alongside strong protections, which is ideal for high-stakes transactions. For more on how assertions work, see the [Assertions deep dive](/how-arbitrum-works/deep-dives/assertions.md) page. ## Step 5: Validation and dispute resolution Following execution, Arbitrum verifies correctness through its validation and dispute systems. Central to this is the BoLD (Bounded Liquidity Delay) protocol, which is an advanced dispute framework that enables permissionless validation. Unlike conventional optimistic Rollups, BoLD permits any participant to validate without approval, while ensuring dispute resolution within bounded timeframes to prevent indefinite delays. BoLD facilitates a Challenge-based defense where honest parties can protect the chain's state against malicious actors. The individual who raises the dispute and the validator will [narrow the conflict to a single execution step](/how-arbitrum-works/bold/how-bold-bisection-works.md) via supporting claims, which culminate in a one-step proof (OSP) that Ethereum, as an impartial arbiter, verifies to determine the outcome. While BoLD's intricacies—such as its multi-round challenge games and economic incentives—are extensive, they enhance the chain's decentralization and resilience. Validation occurs through assertions submitted to the Rollup contract. #### BoLD references * For details on how assertions structure validation and disputes, see the [Assertions deep dive](/how-arbitrum-works/deep-dives/assertions.md). * For an introduction to BoLD, see the [BoLD gentle introduction](/how-arbitrum-works/bold/gentle-introduction.md). * For technical implementation details, see the [BoLD technical deep dive](/how-arbitrum-works/bold/bold-technical-deep-dive.md). * For how the bisection protocol narrows disputes to a single step, see [How BoLD bisection works](/how-arbitrum-works/bold/how-bold-bisection-works.md). * For economic considerations, see the [Economics of disputes documentation](/how-arbitrum-works/bold/bold-economics-of-disputes.md). ## Step 6: Bridging—Cross-chain communication Many transactions involve asset or data transfers between Ethereum and Arbitrum, which are managed through secure bridging protocols (see the [Token bridging deep dive](/how-arbitrum-works/deep-dives/token-bridging.md) for the architecture overview). For parent-to-child transfers from Ethereum to Arbitrum, options include native token bridging for direct **ETH** [deposits](/arbitrum-essentials/bridging/deposit/tokens.md), **ERC-20** transfers via the canonical bridge, or support for [custom gas tokens](/launch-arbitrum-chain/chain-config/costs/custom-gas-token-rollup.md). [Retryable tickets](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#retryable-tickets) enable atomic operations with guaranteed retries if execution fails, featuring predictable gas costs and a one-week validity period redeemable by anyone. Direct messaging handles signed EOA messages with verification or unsigned contract messages using [address aliasing](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#address-aliasing) for security. See the [Parent-to-child messaging deep dive](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md) for details. Child-to-parent transfers from Arbitrum to Ethereum begin with creating a message via [`ArbSys.sendTxToL1()`](/arbitrum-essentials/precompiles/reference.md#arbsys), followed by inclusion in a Rollup Assertion, a 6.4-day challenge period, and manual Layer 1 execution by any party (see [withdrawing tokens](/arbitrum-essentials/bridging/withdraw/tokens.md) for a hands-on guide). Messages are validated via Merkle proofs, persist indefinitely until executed, and require manual triggering due to Ethereum's constraints. For details on how Rollup assertions work and their role in finality, see the [Assertions deep dive](/how-arbitrum-works/deep-dives/assertions.md). See the [Child-to-parent messaging deep dive](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md) documentation for more details on how cross-chain messages work. The [canonical bridge architecture](/how-arbitrum-works/deep-dives/token-bridging.md#canonical-token-bridge-implementation) comprises asset contracts on both chains, gateway pairs for specific logic, and routers to direct traffic flow. It securely locks tokens on one chain while minting equivalents on the other, with a seven-day challenge period to safeguard withdrawals. This setup fosters unified interactions while safeguarding the integrity of each chain. ## Step 7: The economics of execution: gas and fees ### Fees Fees accumulate throughout the transaction lifecycle to fund processing (computation) and security. Arbitrum’s dual-fee model separates child chain gas fees, which cover the cost of EVM computation and storage with [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)-style dynamic pricing that adjusts for congestion, from parent chain data fees (blobs or calldata). Parent chain data fees account for posting data to Ethereum in compressed batches and apply only to Sequencer submissions. For a complete breakdown of how fees are calculated and collected, including the dynamic pricing algorithm and both parent and child chain components, refer to the [Gas and fees deep dive](/how-arbitrum-works/deep-dives/gas-and-fees.md). ### Gas A gas target is an optimal gas consumption rate per second. Exceeding this rate triggers [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) fee escalations to prevent node overloads and maintain chain activity. This rate prioritizes high-value transactions during peak periods while deterring spam, ensuring that validators and the Sequencer remain operational, even though parent contracts handle security. You can read more about [how the child chain gas fees are calculated](/how-arbitrum-works/deep-dives/gas-and-fees.md#child-chain-gas-fees) in the gas and fees deep dive. Fee calculation during execution involves assessing child-chain gas using EVM standards, estimating the parent-chain batch impact, applying current pricing, and collecting **ETH** totals. This model effectively covers all costs, with the gas target reinforcing security by keeping validation in sync. For practical gas estimation in decentralized apps, see [How to estimate gas](/arbitrum-essentials/how-to-estimate-gas.md). ## Step 8: Advanced features ### Stylus Arbitrum extends beyond standard Layer 2 capabilities with features like Stylus, which supports smart contracts written in languages such as Rust, C, and C++ via WASM. It offers [10-70 times faster execution and 100-500 times better memory efficiency](/stylus/concepts/vm-differences.md) than the EVM, with full cross-call interoperability. Stylus runs in the co-located [WASM VM](/stylus/concepts/webassembly.md) using host I/O for state access. For more details, refer to the [Stylus gentle introduction](/stylus/gentle-introduction.md) page, or jump into the [Stylus quickstart](/stylus/quickstart.md) to build a contract. ### Timeboost Timeboost refines ordering to capture MEV (maximum extractable value) for chain operators, protects users from attacks such as front-running, reduces spam-related congestion, and enables customizable policies. Timeboost preserves fast block times (250ms default). #### Timeboost references * For an introduction to Timeboost, see the [Timeboost gentle introduction](/how-arbitrum-works/timeboost/gentle-introduction.md). * For implementation details and usage instructions, see the [How to use Timeboost](/how-arbitrum-works/timeboost/how-to-use-timeboost.md) documentation. * For troubleshooting guidance, see the [Timeboost troubleshooting](/how-arbitrum-works/timeboost/troubleshoot-timeboost.md) and [FAQ](/how-arbitrum-works/timeboost/timeboost-faq.md) pages. ### AnyTrust AnyTrust enables cost-optimized data availability with a mild trust model that relies on a Data Availability Committee (DAC) of `N` members, where at least two are assumed honest. It uses BLS-signed Data Availability Certificates (DACerts) and falls back to Layer 1 posting if needed. Keysets define member keys and thresholds; DACerts include hashes, expirations, and signatures; and servers support various storage, such as local files or Amazon S3. The Sequencer sends batches to the committee, gathers signatures for DACerts, and posts them to Layer 1, defaulting to full data if the required signatures are not present. Ideal for low-cost apps like gaming. For a more complete understanding of AnyTrust, refer to the [AnyTrust protocol documentation](/how-arbitrum-works/deep-dives/anytrust-protocol.md). For the operator-side view of running a DAS, see the [data availability node guide](/run-arbitrum-node/data-availability.md). ## Related topics In this overview, you've learned about the complete transaction journey through Arbitrum Nitro. For a deeper exploration of specific topics, refer to the resources below: ### Deep dives * [Transaction Lifecycle](/how-arbitrum-works/deep-dives/transaction-lifecycle.md): Detailed transaction lifecycle from submission to finality * [Sequencer](/how-arbitrum-works/deep-dives/sequencer.md): How the Sequencer orders, batches, and posts transactions * [Gas and fees](/how-arbitrum-works/deep-dives/gas-and-fees.md): Gas pricing, fee calculations, and adaptive pricing algorithm * [State Transition Function: Gentle Introduction](/how-arbitrum-works/deep-dives/stf-gentle-intro.md): Foundational concepts of the STF * [Geth](/how-arbitrum-works/reference/geth.md): Geth core and EVM compatibility * [ArbOS](/how-arbitrum-works/deep-dives/arbos.md): Arbitrum Operating System features and capabilities * [State Transition Function inputs](/how-arbitrum-works/reference/stf-inputs.md): Message types and STF processing mechanics * [Assertions](/how-arbitrum-works/deep-dives/assertions.md): Rollup assertions and validation structure * [Parent-to-child messaging](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md): L1 to L2 messaging and bridging * [Child-to-parent messaging](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md): L2 to L1 messaging and withdrawals * [Token bridging](/how-arbitrum-works/deep-dives/token-bridging.md): Canonical bridge architecture, gateways, and routers * [AnyTrust protocol](/how-arbitrum-works/deep-dives/anytrust-protocol.md): Data availability committee and cost optimization ### BoLD (Bounded Liquidity Delay) * [BoLD: Gentle introduction](/how-arbitrum-works/bold/gentle-introduction.md): Overview of permissionless validation * [BoLD technical deep dive](/how-arbitrum-works/bold/bold-technical-deep-dive.md): Implementation details of the dispute protocol * [How BoLD bisection works](/how-arbitrum-works/bold/how-bold-bisection-works.md): Narrowing disputes to a single execution step * [Economics of disputes](/how-arbitrum-works/bold/bold-economics-of-disputes.md): Bonding, incentives, and cost considerations ### Timeboost * [Timeboost: Gentle Introduction](/how-arbitrum-works/timeboost/gentle-introduction.md): MEV capture and transaction ordering --- > For a complete page index, fetch # ArbOS technical reference This page provides a technical reference for ArbOS internals. For a conceptual overview of ArbOS and its role in the Arbitrum stack, see [ArbOS](/how-arbitrum-works/deep-dives/arbos.md). ## 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](https://github.com/OffchainLabs/nitro/blob/bc6b52daf7232af2ca2fec3f54a5b546f1196c45/precompiles/precompile.go#L379) uses this generated file to check the type safety of each precompile's implementer. [The installer](https://github.com/OffchainLabs/nitro/blob/bc6b52daf7232af2ca2fec3f54a5b546f1196c45/precompiles/precompile.go#L379) 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. You can add additional configurations, such as restricting a precompile's methods to be callable only by the chain owner, by adding precompile wrappers like `ownerOnly` and `debugOnly` to their [installation entry](https://github.com/OffchainLabs/nitro/blob/bc6b52daf7232af2ca2fec3f54a5b546f1196c45/precompiles/precompile.go#L403). Precompile methods are called, dispatched, and recorded 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](https://github.com/OffchainLabs/nitro/blob/bc6b52daf7232af2ca2fec3f54a5b546f1196c45/precompiles/precompile.go#L438) values. Each time a transaction calls a method of a child chain-specific precompile, ArbOS creates a [`call context`](https://github.com/OffchainLabs/nitro/blob/f11ba39cf91ee2cbf07d67d0e6c38015d94e704/precompiles/context.go#L26) to track and record the gas burned. For convenience, it also provides access to the public fields of the underlying [`TxProcessor`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/tx_processor.go#L38). For how the `TxProcessor` carries Arbitrum-specific hooks through the EVM lifecycle, see the [Geth reference](/how-arbitrum-works/reference/geth.md#hooks). 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](/arbitrum-essentials/precompiles/reference.md). ## ArbOS state ArbOS's state is viewed and modified via [`ArbosState`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/arbosState/arbosstate.go#L36) objects, which provide convenient abstractions for working with the underlying data of its [`backingStorage`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/storage/storage.go#L51). The backing storage's [keyed subspace strategy](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/storage/storage.go#L21) makes [`ArbosState`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/arbosState/arbosstate.go#L36)'s convenient getters and setters possible, minimizing the need to work directly with the specific keys and values of the underlying storage's [`stateDB`](https://github.com/OffchainLabs/go-ethereum/blob/0ba62aab54fd7d6f1570a235f4e3a877db9b2bd0/core/state/statedb.go#L66). Because two [`ArbosState`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/arbosState/arbosstate.go#L36) objects with the same [`backingStorage`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/storage/storage.go#L51) contain and mutate the same underlying state, different `ArbosState` objects can provide different views of ArbOS's contents. [`Burner`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/burn/burn.go#L11) 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 they receive a mutating request. Others demand that the caller have elevated privileges. Meanwhile, others dynamically charge users when doing stateful work. [`OpenArbosState()`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/arbosState/arbosstate.go#L57) chooses this view for safety when it creates the object, and the view can't change afterward. [`arbosVersion`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/arbosState/arbosstate.go#L37), [`upgradeVersion`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/arbosState/arbosstate.go#L38) and [`upgradeTimestamp`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/arbosState/arbosstate.go#L39) ArbOS upgrades are scheduled to happen [when finalizing the first block](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/block_processor.go#L350) after the `upgradeTimestamp`. Most of ArbOS's state exists to facilitate its [precompiles](/arbitrum-essentials/precompiles/reference.md). The remaining parts are detailed below. ### [`blockhashes`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/blockhash/blockhash.go#L15) This component maintains the last 256 parent chain block hashes in a circular buffer. This component allows the [`TxProcessor`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/tx_processor.go#L38) to implement the `BLOCKHASH` and `NUMBER` opcodes and supports the precompile methods that involve the [](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md)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`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/internal_tx.go#L24) [`ArbitrumInternalTx`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/internal_tx.go) included as the first transaction of the block. ### [`l1PricingState`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/l1pricing/l1pricing.go#L16) In addition to supporting the [`ArbAggregator precompile`](/arbitrum-essentials/precompiles/reference.md#arbaggregator), 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 batches 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 parent chain pricing, see [Gas and fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#parent-chain-gas-pricing). ### [`l2PricingState`](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/l2pricing/l2pricing.go#L14) 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 gas target. 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`, aren't accounted for in the pools since they don't directly impact the computational workload of network actors. Instead, the design of the gas target mechanism regulates execution resources to ensure consistent system performance and synchronization. While much of this state is accessible through the [`ArbGasInfo`](/arbitrum-essentials/precompiles/reference.md#arbgasinfo) and [`ArbOwner`](/arbitrum-essentials/precompiles/reference.md#arbowner) precompiles, most changes are automatic and happen during [block production](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/block_processor.go#L77) and the [transaction hooks](/how-arbitrum-works/reference/geth.md#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](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/block_processor.go#L336) of the time passed as ArbOS [finalizes the block](https://github.com/OffchainLabs/nitro/blob/fa36a0f138b8a7e684194f9840315d80c390f324/arbos/block_processor.go#L350). ArbOS's larger gas pool [determines](https://github.com/OffchainLabs/nitro/blob/2ba6d1aa45abcc46c28f3d4f560691ce5a396af8/arbos/l2pricing/pools.go#L98) the per-block gas limit, setting a dynamic [upper limit](https://github.com/OffchainLabs/nitro/blob/2ba6d1aa45abcc46c28f3d4f560691ce5a396af8/arbos/block_processor.go#L146) on the amount of compute gas a child chain block may have. This limit is always enforced, though it's done in the [`GasChargingHook`](/how-arbitrum-works/reference/geth.md#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 don't 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. A per-block gas limit is necessary 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-clock 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](https://github.com/OffchainLabs/nitro/blob/2ba6d1aa45abcc46c28f3d4f560691ce5a396af8/arbos/block_processor.go#L166) 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 won't ever run out, a block's transactions use the [same Geth gas pool](https://github.com/OffchainLabs/nitro/blob/2ba6d1aa45abcc46c28f3d4f560691ce5a396af8/arbos/block_processor.go#L199) to maintain the invariant that the pool decreases monotonically after each transaction. Block headers [use the Geth block limit](https://github.com/OffchainLabs/nitro/blob/2ba6d1aa45abcc46c28f3d4f560691ce5a396af8/arbos/block_processor.go#L67) for internal consistency and to ensure gas estimation works. They are distinct from the [`gasLeft`](https://github.com/OffchainLabs/nitro/blob/2ba6d1aa45abcc46c28f3d4f560691ce5a396af8/arbos/block_processor.go#L146) 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](https://github.com/OffchainLabs/nitro/blob/faf55a1da8afcabb1f3c406b291e721bfde71a05/arbos/block_processor.go#L328) of compute gas. ArbOS doesn't need to persist `gasLeft` because its pool induces a revert, and transactions use the Geth block limit during EVM execution. ## Stylus-specific differences This section details how [](/stylus/gentle-introduction.md)Stylus integrates into the [](/how-arbitrum-works/deep-dives/stf-gentle-intro.md)State Transition Function (STF), covering execution flow, message handling, caching, and interactions with [](/how-arbitrum-works/deep-dives/arbos.md)ArbOS and [](/how-arbitrum-works/reference/geth.md)Geth. ### 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 [](/how-arbitrum-works/deep-dives/sequencer.md)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 chain 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. ### 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)** * Measured 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. ### Interaction with ArbOS and Geth | Execution Stage | Handled By | | ---------------------- | --------------------------------------------------------- | | Transaction submission | **Geth** (identifies target contract) | | Stylus execution | **ArbOS** (switches to WASM runtime) | | Host I/O calls | **ArbOS** (handles storage, call data, context retrieval) | | State commitment | **Geth and ArbOS** (finalizes updates, commits to state) | ### 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. --- > For a complete page index, fetch # Finality and chain reorganizations This reference explains how finality works on Arbitrum chains, why and when a chain can reorganize (reorg), how deep a reorg can go, and which confirmation level an indexer should wait for before treating data as irreversible. This is intended for developers building indexers, block explorers, bridges, and custodial integrations, and it applies to Arbitrum One, Nova, and Dedicated Blockchains (Arbitrum chains)—including AnyTrust chains that settle to another Arbitrum chain. For a step-by-step deposit and withdrawal detection procedure, see the [Exchange integration checklist](/launch-arbitrum-chain/integrations/exchange-integration-checklist.md). This page provides the finality and reorg model that the checklist builds on. ## The three confirmation levels An Arbitrum node exposes three levels of confirmation, surfaced as the standard Ethereum JSON-RPC block tags. Each maps to a different point in the transaction lifecycle and carries a different reversibility guarantee. | Level | Block tag | What it means | Can it be reverted? | | ------------------ | ----------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | Soft confirmation | `latest` | The Sequencer has ordered and executed the transaction and emitted it on the Sequencer feed. | Yes—this is a promise from the Sequencer, not yet backed by the parent chain. | | Parent-chain safe | `safe` | The batch containing the block has been posted to the parent chain and reached the parent chain's `safe` block. | Only by a deep parent-chain reorg. | | Parent-chain final | `finalized` | The batch containing the block has been posted to the parent chain and reached the parent chain's `finalized` block. | Only if the parent chain itself reverts a finalized block (on Ethereum, economically infeasible). | The load-bearing rule An Arbitrum block is only as final as the parent-chain block that carries its batch. Soft confirmation is fast (sub-second) but reversible. Hard finality—the `safe` and `finalized` tags—is inherited from the parent chain and is what makes a block reorg-proof. ### What "seconds to finality" really means On an AnyTrust chain it is common to assume finality is achieved in seconds, as soon as the Data Availability Committee (DAC) signs the block's data. That describes **soft confirmation and data availability**, not hard finality: * The Sequencer executes and broadcasts the queued transactions immediately—this is your sub-second soft confirmation. * The DAC signing a Data Availability Certificate (DACert) guarantees the data *is retrievable*, so the batch can be posted compactly. It does not settle the block against the parent chain. * Hard finality still requires the batch (or DACert) to be posted to the parent chain's Sequencer Inbox and for that parent-chain block to finalize. So on your chain, "seconds" is the soft-confirmation latency; the reorg-proof guarantee tracks your parent chain's finality, which in turn tracks Ethereum finality. ## How the node derives `safe` and `finalized` A Nitro node is split into a consensus side (reads the parent chain) and an execution side (runs the EVM and serves RPC). Finality flows from the parent chain into the child chain's `safe`/`finalized` heads: 1. The node reads the parent chain's own `safe` and `finalized` blocks over RPC. It does not compute parent-chain finality itself—it defers to the parent chain's block tags. (Finality data is only used when the parent chain serves those tags—for example Ethereum proof-of-stake, another Arbitrum chain, or a chain built on another stack that exposes them—and when the node is configured to read them, via `node.parent-chain-reader.use-finality-data`, which defaults to `true`.) 2. For each parent-chain `safe`/`finalized` block, the node finds the newest sequencer batch that was posted **at or before** that parent-chain block. 3. The last child-chain block in that batch becomes the child chain's `safe` (or `finalized`) head, which is what the child-chain RPC serves for those tags. In other words, a child-chain block is `finalized` when the parent-chain block that carries its batch is itself `finalized`. Optionally, a chain can be configured to further hold `safe`/`finalized` back until a local validator has validated the block. Block-tag availability on Dedicated Blockchains The `safe` and `finalized` tags are always available on Arbitrum One and Nova. On a self-managed chain, they are available only when your parent chain exposes finality data and your node is configured to track it. Confirm the behavior on your own chain before relying on `finalized`, and document the expected confirmation latency for your integrators. ## Per-chain-type guarantees The confirmation *model* is identical across chains; what differs is which parent chain finality is inherited from, and therefore the latency. | Chain type | Parent chain | `finalized` inherits from | Approximate hard-finality latency | | -------------------------------------------- | ------------------ | -------------------------------------------------- | ----------------------------------------------------------------------- | | Arbitrum One / Nova | Ethereum | Ethereum `finalized` (\~2 epochs, \~13 min) | Time to post the batch, plus Ethereum finality. | | Arbitrum chain atop Ethereum (L2) | Ethereum | Ethereum `finalized` | Same as above. | | Arbitrum chain atop an L2 (L3, e.g. on Base) | The L2 (e.g. Base) | The L2's `finalized`, which itself tracks Ethereum | The L2's finality latency, which is bounded below by Ethereum finality. | For an L3 on Base, `finalized` on your chain cannot be more final than Base's `finalized`, and Base's `finalized` ultimately tracks Ethereum. The DAC does not shorten this—it only affects how data is made available, not how the parent chain finalizes. ### Parent chains outside the Ethereum and Arbitrum stacks A few chains settle to a parent chain that is neither Ethereum nor an Arbitrum chain; in practice this means an OP Stack chain such as Base. The confirmation model is unchanged—your node still defers to the parent chain's `safe` and `finalized` tags—but the meaning of those tags is defined by that chain's stack rather than by Ethereum consensus. On an OP Stack parent chain, `safe` means the block has been derived from batch data already posted to Ethereum, and `finalized` means that Ethereum data is itself finalized, so the chain of inheritance still terminates at Ethereum finality. Before relying on `finalized` when your parent chain runs a different stack, confirm two things: that its RPC actually serves both tags, and what each tag guarantees on that chain. A parent chain that returns its own head for `safe` and `finalized` gives your child chain no guarantee beyond `latest`, in which case you should derive your own finality signal as described in [Recommended confirmation levels for indexers](#recommended-confirmation-levels-for-indexers). ## AnyTrust and the DAC: what changes and what doesn't On an AnyTrust chain the batch poster asks the DAC to sign a DACert over the batch data. If enough committee members sign against the current keyset, the batch poster posts the compact DACert to the Sequencer Inbox instead of the full data. What this does and does not affect: * **Does not change the finality model.** Finality still tracks the parent chain. A DACert reaching parent-chain finality is what makes the block final; the DAC signature by itself is a data-availability guarantee. * **Does not change reorg behavior.** A DACert is posted to the Sequencer Inbox exactly like a full-data batch, so the reorg rules below apply unchanged. * **Affects liveness under DAC failure.** If the batch poster cannot collect enough signatures within a few minutes, it falls back to posting the full batch data directly to the parent chain as calldata, and the chain keeps producing and finalizing blocks. Fallback can be disabled (`disable-dap-fallback-store-data-on-chain: true`), in which case a DAC outage halts batch posting. See [DAC and DAS operations](/launch-arbitrum-chain/chain-config/data-availability/dac-das-operations.md) for the full failure-mode table. ## The reorg model Once a batch is posted to the Sequencer Inbox, the only thing that can reorganize an Arbitrum chain is a reorg of its underlying (parent) chain. Fraud proofs and dispute resolution never cause a reorg—a rejected assertion only prevents an invalid state from being confirmed for settlement; it never rewrites already-sequenced blocks. When a parent-chain reorg does change the Sequencer Inbox or delayed inbox ordering, the child chain reorgs as follows: 1. The node detects that the parent-chain-derived data has changed (the delayed-message accumulator or the sequencer-batch accumulator no longer matches what it stored). 2. It walks back to the most recent block that still matches the canonical parent-chain-derived history—the common ancestor. 3. It rolls the chain back to that block. **State reverts to the state root at the common ancestor, and every transaction after that point is discarded.** Block explorers and RPC no longer return the discarded transactions. 4. It re-derives the chain forward from the parent chain. Messages that were reorged out (within the re-sequencing window) may be re-sequenced, but block hashes, block numbers, and transaction ordering after the common ancestor can all differ from before. Never key an index on block number alone across a reorg After a reorg, the same block *number* can hold different transactions with a different block *hash*. Always store and compare block hashes and `parentHash` continuity, and rewind to the common ancestor rather than patching individual records. See [Reorg detection patterns](#reorg-detection-patterns). ## How deep can a reorg go? Child-chain reorg depth is bounded by parent-chain reorg depth, not by anything on the child chain and not by how often you post assertions. A common misconception is to reason from the assertion (state-root) cadence: "we post state roots every hour, which at 250 ms blocks is \~14,400 blocks, so a reorg could be 14,400 blocks deep." That conflates two independent things: * **Assertions / state roots** are periodic settlement checkpoints posted to the Rollup contract for the dispute protocol. They do **not** cause or bound reorgs, and their cadence is irrelevant to reorg depth. * **Batches (data)** are posted continuously by the batch poster—not on the assertion cadence. Reorgs are driven by parent-chain reorgs of these batches, and only for the batches that are not yet final on the parent chain. So the practical bound on a child-chain reorg is: **the child blocks whose batches were posted within the parent chain's unfinalized window.** For an indexer, this collapses to a simple rule: * If you index at `finalized`, you will never observe a reorg, because finalized parent-chain blocks do not revert. * If you index at `safe`, you are exposed only to a deep parent-chain reorg (rare on Ethereum-backed chains). * If you index at `latest` (soft), you are exposed to the full unfinalized window and must handle reorgs actively. A 14,400-block child reorg would require the parent chain to reorg its entire corresponding unfinalized range at once—which for a Base → Ethereum-backed chain does not happen under normal finality assumptions. The correct defense is not to guess a maximum depth but to gate crediting on `finalized` (or to rewind to the common ancestor for anything you display before finality). ## `ArbitrumInternalTx` behavior under a parent-chain reorg Every child-chain block begins with a system transaction of type `ArbitrumInternalTx` (`0x6A`), specifically an `InternalTxStartBlock`. It is guaranteed to be the first transaction in its block, and it records the parent-chain block number and parent-chain base fee that were in effect when the block's message was read. This is the mechanism that feeds parent-chain context (for example, the value returned by `block.number` and `BLOCKHASH`) into the EVM. See [Geth at the core](/how-arbitrum-works/reference/geth.md#arbitruminternaltx) for details. Because this transaction's contents are *derived from* the parent chain, a parent-chain reorg that changes a block's parent-chain context also changes that block's `InternalTxStartBlock`. There is no special handling that keeps it stable across a reorg—the affected blocks are reorged out and re-derived through the normal path above. Consequences for an indexer: * The `ArbitrumInternalTx` is ArbOS bookkeeping. It never moves user value and must never be credited or treated as a user transaction. * Do **not** assume the ordering, block number, or block hash of the surrounding transactions is preserved across a reorg. After the common ancestor, everything is recomputed: the internal tx stays first, but the transactions after it, their block assignment, and the block hashes can all change. * The only stable anchor across a reorg is the common ancestor's block hash. Rewind to it and re-scan. ## Recommended confirmation levels for indexers On Ethereum, indexers often wait a fixed number of confirmations (for example, 12 blocks). That model does not translate to Arbitrum: child blocks are produced far faster than the parent chain finalizes, and reorg exposure is a function of parent-chain finality, not of a child-block count. Wait on the **block tag**, not on a fixed depth. | Use case | Wait for | Rationale | | ------------------------------------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------- | | Crediting deposits, releasing funds, any irreversible action | `finalized` | Finalized parent-chain blocks do not revert, so a credited entry can never be reorged away. | | Low-value or reversible UX (pending balances, activity feed) | `safe`, or `latest` with active reorg handling | Faster, but you must be able to roll back if a reorg occurs. | | Real-time display only | `latest` | Sub-second, but explicitly provisional—label it as unconfirmed and expect it to change. | If your chain does not expose `safe`/`finalized` (see the note above), treat the Sequencer tip as soft and derive your own finality signal from the parent chain—for example, by tracking which batches have reached the parent chain's `finalized` block. ## Reorg detection patterns Whichever level you index at below `finalized`, detect reorgs by tracking hash continuity and rewinding: 1. Store each scanned block's hash, keyed by height. 2. On each poll, confirm that each new block's `parentHash` matches the hash you stored for the height below it. 3. On a mismatch, walk back to the most recent height whose stored hash still matches the canonical chain—the common ancestor. 4. Discard every record and stored hash above the common ancestor, move your scan cursor back to it, and re-scan forward. Because a reorg can both remove and introduce transactions, re-checking only the records you already have is not enough. 5. You never need to rewind below the `finalized` height, because finalized blocks cannot reorganize. The [Exchange integration checklist](/launch-arbitrum-chain/integrations/exchange-integration-checklist.md#step-1-stay-in-sync-with-the-chain-head) works through this algorithm in the context of a full deposit-detection pipeline. ## The `--node.bold.rpc-block-number` flag You may encounter the node flag `--node.bold.rpc-block-number`. It is a **validator-side** setting: it selects which parent-chain block tag the BoLD validator reads onchain Rollup and assertion state from. It accepts `latest`, `safe`, or `finalized`, and defaults to `finalized`. Not the tag your indexer reads `--node.bold.rpc-block-number` controls how the BoLD validator reads the *parent chain* during dispute-protocol operations. It does **not** configure the `latest`/`safe`/`finalized` tags your indexer requests from the child-chain RPC. Do not set it expecting to change indexer-facing finality; use the block tags on your RPC queries instead. The `latest` / `safe` / `finalized` names come from Ethereum's proof-of-stake finality: `finalized` is roughly two epochs (\~64 slots, \~13 minutes) behind the head and `safe` roughly one epoch (\~32 slots) behind. Nitro does not hard-code those distances — it reads the parent chain's own `safe`/`finalized` tags — but that is the origin of the "latest-64 / latest-32" shorthand you may have seen. ## Corner cases The scenarios below are the ones integrators most often ask about. They follow directly from the model above. ### A parent-chain reorg moves a block that held one of our state-root attestations No child-chain reorg results. State-root assertions are settlement checkpoints, not sequenced data; if the parent-chain block carrying an assertion is reorged, the node re-submits the assertion. A child-chain reorg happens only if the reorg changes the Sequencer Inbox or delayed-inbox ordering. ### A reorg on Base's parent (Ethereum) affects a block holding some of our data Handled the same way. If it changes the derived batch/delayed-message ordering, the affected *unfinalized* child blocks are re-derived; anything already `finalized` is unaffected. ### The DAC has an outage and cannot sign attestations With fallback enabled (the default), the batch poster stops waiting after a few minutes and posts the full batch data directly to the parent chain as calldata. The Sequencer keeps producing blocks and they keep finalizing—hard finality is not blocked by the DAC outage, though batches temporarily cost more to post. Finality is only delayed if you have disabled fallback. ### The DAC is completely down and the batch poster cannot reach it This halts batch posting **only if fallback is disabled** (`disable-dap-fallback-store-data-on-chain: true`). With fallback enabled, the chain continues via direct calldata posting. Decide this trade-off deliberately: fallback favors liveness, disabling it favors keeping all data off the parent chain. ### The Sequencer cannot post batches (or DACerts) to the parent chain The Sequencer keeps producing soft-confirmed blocks on the feed, and the batch poster retries until it can post. Until a batch reaches parent-chain finality, those blocks are soft only—treat them as reversible for indexing purposes, even though they appear immediately on the feed. ## Related resources * [Exchange integration checklist](/launch-arbitrum-chain/integrations/exchange-integration-checklist.md) — end-to-end deposit/withdrawal detection built on this model. * [Geth at the core](/how-arbitrum-works/reference/geth.md) — transaction types, `ArbitrumInternalTx`, and `ReorgToOldBlock`. * [The Sequencer](/how-arbitrum-works/deep-dives/sequencer.md) — soft vs hard finality and batch posting. * [AnyTrust protocol](/how-arbitrum-works/deep-dives/anytrust-protocol.md) — keysets, DACerts, and the fallback to Rollup mode. * [DAC and DAS operations](/launch-arbitrum-chain/chain-config/data-availability/dac-das-operations.md) — DAC failure modes and fallback behavior. * [Block numbers and time](/arbitrum-essentials/arbitrum-vs-ethereum/block-numbers-and-time.md) — parent-chain block numbers on the child chain. --- > For a complete page index, fetch # Geth at the core: modified Geth on Arbitrum Nitro [Arbitrum Nitro](/how-arbitrum-works/inside-arbitrum-nitro.md) makes minimal modifications to Geth to avoid violating its assumptions. This section will explore the relationship between Geth and [](/how-arbitrum-works/deep-dives/arbos.md)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 within a Geth `statedb` (state database). In doing so, ArbOS inherits the `statedb`'s statefulness and lifetime properties. For example, a transaction's direct state changes to ArbOS would get discarded upon a revert. `0xA4B05FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF` is a fictional account representing ArbOS. > **INFO** > > Any links on this page may point to older versions of Nitro or our fork of Geth. While we try to keep this up to date, most of this should be stable. Check the latest releases of [Nitro](https://github.com/OffchainLabs/nitro/releases) and [Geth](https://github.com/OffchainLabs/go-ethereum/releases) for the most recent changes. ## Hooks Arbitrum uses various hooks to modify Geth’s behavior during transaction processing. Each provides an opportunity for ArbOS to update its state and make decisions about the transaction during its lifetime. Transactions are applied using Geth's [`ApplyTransaction`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/state_processor.go#L152) function. Below is `ApplyTransaction`'s callgraph, with additional info on where the various Arbitrum-specific hooks are. Click any to jump to their section. By default, these hooks do nothing to leave Geth's default behavior unchanged, but for chains configured with [`EnableArbOS`](#enablearbos) set to true, [`ReadyEVMForL2`](#readyevmforl2) installs the alternative child chain hooks. * `core.ApplyTransaction` -> `core.applyTransaction` -> `core.ApplyMessage` * `core.NewStateTransition` * `ReadyEVMForL2` * `core.TransitionDb` * [`StartTxHook`](#starttxhook) * `core.transitionDbImpl` * if `IsArbitrum()` remove tip * [`GasChargingHook`](#gascharginghook) * `evm.Call` * `core.vm.EVMInterpreter.Run` * [`PushCaller`](#pushcaller) * `PopCaller` * `core.StateTransition.refundGas` * [`ForceRefundGas`](#forcerefundgas) * [`NonrefundableGas`](#nonrefundablegas) * [`EndTxHook`](#endtxhook) * added return parameter: `transactionResult` What follows is an overview of each hook in chronological order. ### [`ReadyEVMForL2`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbstate/geth-hook.go#L47) 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 the same results as vanilla Geth. A `TxProcessor` object carries these hooks and the associated Arbitrum-specific state during the transaction's lifetime. ### [`StartTxHook`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/tx_processor.go#L100) Geth calls the `StartTxHook` before a transaction executes, which allows ArbOS to handle two Arbitrum-specific transaction types. If the transaction is `ArbitrumDepositTx`, ArbOS adds balance to the destination account. This approach is safe because the parent chain [](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md)bridge submits such a transaction only after collecting the same amount of funds on the parent chain. If the transaction is an `ArbitrumSubmitRetryableTx`, ArbOS creates a [retryable](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#retryable-tickets) based on the transaction's fields. ArbOS schedules a retry of the new retryable if the transaction includes sufficient gas. The hook returns `true` for both transaction types, signifying that the state transition is complete. ### [`GasChargingHook`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/tx_processor.go#L354) This fallible hook ensures the user has enough funds to pay their poster’s parent chain calldata costs. If not, the transaction is reverted, and the EVM does not start. In the common case where the user can pay, the amount paid for calldata is set aside for later reimbursement to the poster. All other fees go to the network account, as they represent the transaction’s burden on validators and nodes more generally. Suppose the user attempts to purchase compute gas over ArbOS's per-block gas limit. In that case, the difference is [set aside](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/tx_processor.go#L407) and [refunded later](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/state_transition.go#L419) via `ForceRefundGas`, so 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](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/block_processor.go#L176) if ArbOS's larger gas pool falls below the [`MaxPerBlockGasLimit`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/l2pricing/l2pricing.go#L86) while processing the block's previous transactions. ### [`PushCaller`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/tx_processor.go#L76) These hooks track callers on the EVM call stack, pushing and popping as calls are made and completed. This hook provides [`ArbSys`](/arbitrum-essentials/precompiles/reference.md#arbsys) with info about the call stack, which is used to implement the methods `WasMyCallersAddressAliased` and `MyCallersAddressWithoutAliasing`. ### [`L1BlockHash`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/tx_processor.go#L617) In Arbitrum, the `BlockHash` and `Number` operations return data that relies on the underlying parent chain blocks rather than child chain blocks to accommodate the normal use case of these opcodes, which often assume Ethereum-like time passing between blocks. The `L1BlockHash` and `L1BlockNumber` hooks have the required data for these operations. ### [`ForceRefundGas`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/tx_processor.go#L425) This hook allows ArbOS to add additional refunds to the user's transaction. The only usage of this hook is to refund any compute gas purchased in excess of ArbOS's per-block gas limit during the `GasChargingHook`. ### [`NonRefundableGas`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/tx_processor.go#L418) Because poster costs are borne by the parent chain aggregators, not the network, payments toward parent chain calldata should not be refunded. This hook provides Geth access to the equivalent amount of child chain gas that the poster's cost equals, ensuring that this amount isn't reimbursed for network-incentivized behaviors like freeing storage slots. ### [`EndTxHook`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/tx_processor.go#L429) The `EndTxHook` is called after the `EVM` returns a transaction's result, providing one last opportunity for ArbOS to intervene before state transition finalization. Final gas amounts are known, enabling ArbOS to credit the network and poster share of the user's gas expenditures and adjust the pools. The hook returns from the [`TxProcessor`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/tx_processor.go#L38) for the final time, discarding its state as the system moves on to the next transaction, where its contents will be renewed. ### [`RevertedTxHook`](https://github.com/OffchainLabs/nitro/blob/a618155919315241665356fe60f3cd00d66d5e46/arbos/tx_processor.go#L960) The `RevertedTxHook` is called during Nitro's state transition. It checks whether the transaction has been previously reverted or is part of the transaction filter. If either is true, it will not execute the transaction and will use up a predetermined amount of gas. If it was a previously reverted transaction, the sender's nonce will be increased. ## Interfaces and components ### [`APIBackend`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/arbitrum/apibackend.go#L34) `APIBackend` implements the `ethapi.Backend` interface, which allows a simple integration of the Arbitrum chain to the existing Geth API. The `Backend` member answers most calls. ### [`Backend`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/arbitrum/backend.go#L15) This struct is an Arbitrum equivalent to the [`Ethereum`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/eth/backend.go#L68) struct. It is mostly glue logic, including a pointer to the `ArbInterface` interface. ### [`ArbInterface`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/arbitrum/arbos_interface.go#L10) This interface serves as the primary interface between the geth-standard APIs and the Arbitrum chain. Geth APIs either check the status by working on the `Blockchain` struct retrieved from the `Blockchain` call or send transactions to Arbitrum using the [`PublishTransactions`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/arbitrum/arbos_interface.go#L11) call. ### [`RecordingKV`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/arbitrum/recordingdb.go#L22) `RecordingKV` is a read-only key-value store that retrieves values from an internal trie database. All values accessed by a `RecordingKV` get recorded internally. This value records all preimages accessed during block creation, which will be needed to prove the execution of this particular block. A [`RecordingChainContext`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/arbitrum/recordingdb.go#L123) should also be used to record which block headers the block execution reads (another option is always to assume that the last 256 block headers have been accessed). The process is simplified using two functions: [`PrepareRecording`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/arbitrum/recordingdb.go#L152) creates a stateDB and chain context objects, running block creation process using these objects records the required preimages, and [`PreimagesFromRecording`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/arbitrum/recordingdb.go#L174) function extracts the preimages recorded. ## Transaction types Nitro Geth includes a few child chain-specific transaction types. Click any to jump to their section. | Transaction Type | Represents | Last Hook Reached | Source | | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------- | ----------- | | [`ArbitrumUnsignedTx`](#arbitrumunsignedtx) | A parent chain to child chain message | [`EndTxHook`](#endtxhook) | Bridge | | [`ArbitrumContractTx`](#arbitrumcontracttx) | A nonce-less parent chain to child chain message | [`EndTxHook`](#endtxhook) | Bridge | | [`ArbitrumDepositTx`](#arbitrumdeposittx) | A user deposit | [`StartTxHook`](#starttxhook) | Bridge | | [`ArbitrumSubmitRetryableTx`](#arbitrumsubmitretryabletx) | Creating a retryable | [`StartTxHook`](#starttxhook) | Bridge | | [`ArbitrumRetryTx`](#arbitrumretrytx) | A [](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#retryable-tickets)retryable redeem attempt | [`EndTxHook`](#endtxhook) | Child chain | | [`ArbitrumInternalTx`](#arbitruminternaltx) | ArbOS state update | [`StartTxHook`](#starttxhook) | ArbOS | The following reference documents each type. ### [`ArbitrumUnsignedTx`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/arb_types.go#L43) It provides a mechanism for a user on a parent chain to message a contract on a child chain. This mechanism uses the bridge for authentication rather than requiring the user's signature. Address remapping of the user's address will occur on the child chain to distinguish them from a normal child chain caller. ### [`ArbitrumContractTx`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/arb_types.go#L104) These are like [`ArbitrumUnsignedTx`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/arb_types.go#L43)'s but intended for smart contracts. These use the bridge's unique, sequential nonce rather than requiring the caller to specify their own. A parent chain contract may still use an `ArbitrumUnsignedTx`, but doing so may necessitate tracking the nonce in the parent chain state. ### [`ArbitrumDepositTx`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/arb_types.go#L338) It represents a user deposit from a parent chain to a child chain. This representation increases the user's balance by the amount deposited on the parent chain. ### [`ArbitrumSubmitRetryableTx`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/arb_types.go#L232) It represents a retryable submission and may schedule an [`ArbitrumRetryTx`](#arbitrumretrytx) if enough gas is available. For more info, see the [retryables documentation](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#retryable-tickets). ### [`ArbitrumRetryTx`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/arb_types.go#L161) These calls are scheduled using the `redeem` method of the [`ArbRetryableTx`](/arbitrum-essentials/precompiles/reference.md#arbretryabletx) precompile and via retryable auto-redemption. For more info, see the [retryables documentation](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#retryable-tickets). ### [`ArbitrumInternalTx`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/internal_tx.go) Because tracing support requires state changes to occur within a transaction, ArbOS may create a transaction of this type to update its state between user-generated transactions. Such a transaction has a [`Type`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/arb_types.go#L387) indicating 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 transaction types. ### [`InternalTxStartBlock`](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/internal_tx.go#L22) It updates the parent chain block number and the parent chain base fee. This transaction [is generated](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/block_processor.go#L181) whenever a new block gets created. They are [guaranteed to be the first](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/block_processor.go#L182) in their child block chain. ## Transaction run modes and underlying transactions A [Geth message](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/transaction.go#L634) may get processed for various purposes. For example, a message may estimate the gas of a contract call, whereas another may perform the corresponding state transition. Nitro Geth denotes the intent behind a message using [`TxRunMode`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/transaction.go#L701), [which it sets](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/internal/ethapi/api.go#L955) before processing the message. ArbOS uses this info to decide the transaction that the message ultimately constructs. A message [derived from a transaction](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/transaction.go#L676) will carry that transaction in a field accessible via its [`UnderlyingTransaction`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/transaction.go#L700) method. While this relates to how 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 Mode | Scope | Carries an Underlying Transaction? | | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- | ----------------------------------------------------------------------------------------------------- | | [`MessageCommitMode`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/transaction.go#L654) | state transition | Always | | [`MessageGasEstimationMode`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/transaction.go#L655) | gas estimation | When created via [`NodeInterface`](/arbitrum-essentials/nodeinterface/reference.md) or when scheduled | | [`MessageEthcallMode`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/transaction.go#L656) | eth\_calls | Never | ## Arbitrum chain parameters Nitro's Geth is configurable with the following [child chain-specific chain parameters](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/params/config_arbitrum.go#L25). These allow the rollup creator to customize their rollup at genesis. ### `EnableArbos` Introduces [ArbOS](/how-arbitrum-works/deep-dives/arbos.md), converting what would otherwise be a vanilla parent chain into a child chain Arbitrum rollup. ### `AllowDebugPrecompiles` Allows access to debug precompiles. Not enabled for Arbitrum One. When false, calls to debut precompiles will always revert. ### `DataAvailabilityCommittee` Currently, it does nothing besides indicate that the rollup will access a data availability service for preimage resolution in the future. On Arbitrum One, this indication isn't present, which is a strict state function of its parent chain inbox messages. ## Miscellaneous Geth changes ### ABI Gas Margin Vanilla Geth's ABI library submits transactions with the exact estimate the node returns, employing no padding. This process means a transaction may revert if another arrives just before it, even if it changes the transaction's code path by just a little. To account for this, we've added a `GasMargin` field to `bind.TransactOpts` that [pads estimates](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/accounts/abi/bind/base.go#L355) by the number of basis points set. ### Conservation of child chain **ETH** The total amount of the child chain 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](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/state/statedb.go#L42) each time a block is created, [alerting or panicking](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/block_processor.go#L424) if conservation is violated. ### `MixDigest` and `ExtraData` The root hash and leaf count of ArbOS's [send Merkle accumulator](https://github.com/OffchainLabs/nitro/blob/8e786ec6d1ac3862be85e0c9b5ac79cbd883791c/arbos/merkleAccumulator/merkleAccumulator.go#L13) are stored in each child chain block's `MixDigest` and `ExtraData` fields to aid with [](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md)outbox proof construction. 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/bonders, to store the send count. ### Retryable support ArbOS primarily implements retryables, while Geth requires some modifications to support them. * Added `ScheduledTxes` field to `ExecutionResult`. This process lists transactions scheduled during the execution. To enable this field, we also pass the `ExecutionResult` to callers of `ApplyTransaction`. * Added `gasEstimation` param to `DoCall`. When enabled, `DoCall` will also execute any retryables activated by the original call, allowing gas to be estimated for retryables. ### Added accessors We added [`UnderlyingTransaction`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/state_transition.go#L69) to the Message interface, and [`GetCurrentTxLogs`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/state/statedb_arbitrum.go) to StateDB. We created the `AdvancedPrecompile` interface, which executes and charges gas with the same function call. [Arbitrum precompiles](/arbitrum-essentials/precompiles/overview.md) use this interface, and it wraps Geth's standard precompiles. ### WASM build support The WASM executable for Arbitrum does not support file operations. We created [`fileutil.go`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/rawdb/fileutil.go) to wrap `fileutil` calls and stub them out during WASM builds. [`fake_leveldb.go`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/ethdb/leveldb/fake_leveldb.go) is a similar WASM mock for `leveldb`. The WASM block-replayer does not require these. ### Types Arbitrum introduces a new [`signer`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/arbitrum_signer.go) and multiple new [`transaction types`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/transaction.go). ### `ReorgToOldBlock` Geth natively only allows reorgs to a fork of the currently known network. In Nitro, sometimes reorgs can be detected before the forked block is computed. We added the [`ReorgToOldBlock`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/blockchain_arbitrum.go#L38) function to support re-orging to a block that's an ancestor of the current head. ### Genesis block creation The genesis block in Nitro is not necessarily block #0. Nitro supports importing blocks that take place before genesis. We split out [`WriteHeadBlock`](https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/genesis.go#L415) from genesis. Commit and use it to commit non-zero genesis blocks. --- > For a complete page index, fetch # Inputs to the State Transition Function Arbitrum nodes receive transaction inputs through two channels: * Nodes subscribe to the Sequencer feed and receive upcoming ordered transactions published in real time. To learn more, refer to the [real-time sequencer feed documentation](/how-arbitrum-works/deep-dives/sequencer.md#real-time-sequencer-feed). * Nodes also subscribe to the `SequencerBatchDelivered` event on the parent chain. This event occurs whenever a batch of transactions gets delivered to the parent chain via the batch poster. Upon receiving this event, nodes verify that the transactions recorded on the parent chain match those from the [Sequencer feed](/how-arbitrum-works/deep-dives/sequencer.md#sequencing-and-broadcasting). If discrepancies arise, nodes reorganize to adopt the transactions confirmed on the parent chain, treating it as the definitive source of truth. As discussed in the [considerations and limitations section](/how-arbitrum-works/deep-dives/sequencer.md#considerations-and-limitations), these methods suit different applications. These transactions serve as inputs for the [](/how-arbitrum-works/deep-dives/stf-gentle-intro.md)State Transition Function (STF). ## Message types Arbitrum supports multiple message types. These messages fall into two broad categories: * Messages submitted directly to the Sequencer as child chain messages * Messages submitted to the parent chain For other message types, see the [parent-to-child chain messaging documentation](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md). In the system, we refer to these messages as `L1IncomingMessage` (inspect the [source code reference](https://github.com/OffchainLabs/nitro/blob/4ac7e9268e9885a025e0060c9ec30f9612f9e651/arbos/incomingmessage.go#L54)). When submitted to the Sequencer feed, messages receive unique identifiers for proper routing. Below is the list of message types, their associated constant values, and descriptions: ```solidity uint8 constant L2_MSG = 3; uint8 constant L1MessageType_L2FundedByL1 = 7; uint8 constant L1MessageType_submitRetryableTx = 9; uint8 constant L1MessageType_ethDeposit = 12; uint8 constant L1MessageType_batchPostingReport = 13; uint8 constant L2MessageType_unsignedEOATx = 0; uint8 constant L2MessageType_unsignedContractTx = 1; uint8 constant ROLLUP_PROTOCOL_EVENT_TYPE = 8; uint8 constant INITIALIZATION_MSG_TYPE = 11; ``` | Message Type | Value | Description | | ---------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `L2MessageType_unsignedEOATx` | 0 | Unsigned child chain messages from EOAs submitted to the parent chain. | | `L2MessageType_unsignedContractTx` | 1 | Unsigned child chain messages submitted to the parent chain. | | `L2_MSG` | 3 | Child chain messages submitted directly to the Sequencer. | | `L1MessageType_L2FundedByL1` | 7 | Child chain messages that go to the parent chain's [](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#transacting-via-the-delayed-inbox)delayed inbox, with funding provided on the parent chain itself. | | `ROLLUP_PROTOCOL_EVENT_TYPE` | 8 | Arbitrum Classic used it for messages sent to bridge; Nitro does not use it. | | `L1MessageType_submitRetryableTx` | 9 | Submitting parent chain messages to the child chain via [retryable tickets](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#retryable-tickets). | | `INITIALIZATION_MSG_TYPE` | 11 | The first message added to a new Rollup inbox. Its presence indicates proper initialization of the Rollup. | | `L1MessageType_ethDeposit` | 12 | Child chain messages that handle deposits of native tokens (**ETH**) into the child chain. | | `L1MessageType_batchPostingReport` | 13 | Used by the Sequencer to update the pricing model based on payment(s) by the batch poster. | These identifiers enable nodes to route messages correctly. --- > For a complete page index, fetch # How Timeboost works Arbitrum Timeboost is a novel transaction ordering policy for Arbitrum chains that enables chain owners to capture the Maximal Extractable Value (MEV) on their chain, reduce spam, and preserve fast block times, all while protecting users from harmful types of MEV, such as sandwich attacks and front-running. Timeboost is the culmination of over a year of [research and development](https://arxiv.org/abs/2306.02179) by the team at Offchain Labs. It is currently live on Arbitrum One and Arbitrum Nova, and is available for Arbitrum chains, whose owners can adopt and customize it as they choose. ## Why do Arbitrum chains need Timeboost? In the past, Arbitrum chains ordered incoming transactions on a "First-Come, First-Serve (FCFS)" basis. This ordering policy is simple to understand and implement, enabling fast block times (starting at 250ms and down to 100ms if desired), and protecting users from harmful types of MEV, such as front-running & sandwich attacks. However, there are a few downsides to an FCFS ordering policy. Under FCFS, searchers have incentives to participate in and attempt to win latency races by investing in offchain hardware. This "race" means that for searchers on Arbitrum chains, generating a profit from arbitrage and liquidation opportunities involves a lot of spam, placing stress on the chain infrastructure and contributing to congestion. Additionally, all captured MEV on an Arbitrum chain under FCFS is allocated to searchers, returning none of the available MEV to the chain owner or the applications on the chain. ## What is Timeboost Timeboost retains most FCFS benefits while addressing FCFS limitations. Timeboost is a *transaction ordering policy*. It's a set of rules that the [sequencer](/how-arbitrum-works/deep-dives/sequencer.md) of an Arbitrum chain is trusted to follow when ordering transactions submitted by users. In the near future, multiple sequencers will be able to enforce those rules with decentralized Timeboost. For Arbitrum chains, the sequencer’s sole job is to take arriving, valid transactions from users, place them into an order dictated by the transaction ordering policy, and then publish the final sequence to a [real-time feed](/how-arbitrum-works/deep-dives/sequencer.md#real-time-sequencer-feed) and in compressed batches to the chain’s data availability layer. Before Timeboost, the transaction ordering policy was FCFS, and Timeboost is a modified FCFS ordering policy. #### Timeboost preserves the great UX that Arbitrum chains are known for * The default block time for Arbitrum chains continues to be industry-leading at 250ms, even with Timeboost. With Timeboost, some transactions not in the express lane may experience a delay to the next block. #### With Timeboost, Arbitrum chains will continue to protect users from harmful types of MEV * Timeboost only grants the auction winner a *temporary time advantage*—not the power to view or reorder incoming transactions or to be the first in every block. Furthermore, the transactions' mempool will continue to be private, which means users with Timeboost enabled will remain protected from harmful MEV-like front-running and sandwich attacks. #### Timeboost unlocks a new value accrual path for chain owners * Chain owners may use Timeboost to capture a portion of the available MEV on their chain that would have otherwise gone entirely to searchers. There are many flavors of this, too—including custom gas tokens and/or redistribution of these proceeds back to the applications and users on the chain. #### Timeboost may help reduce spam and congestion on a network * By introducing the ability to “purchase a time advantage” through the Timeboost auction, it is expected that rational, profit-seeking actors will spend on auctions *instead of* investing in hardware or infrastructure to win latency races. We expect that this diversion of resources will reduce FCFS MEV-driven spam on the network. ### How does it work? Timeboost uses three separate components that work together: * **A special “express lane”** which allows valid transactions to be sequenced as soon as the sequencer receives them for a given round. * **An offchain auction** to determine the controller of the express lane for a given round. This auction gets managed by an autonomous auctioneer. * **An auction contract** deployed on the target chain to serve as the canonical source of truth for the auction results and handling of auction proceeds. To start, the default duration of a round is 60 seconds. Transactions not in the express lane will be subject to a default 200-millisecond artificial delay in their arrival timestamp before being sequenced, which means that some non-express lane transactions may be delayed to the next block. It’s important to note that the default Arbitrum block time will remain at 250 milliseconds (which can be adjusted to 100 milliseconds if desired). Let’s dive into how each of these components works. ### The express lane ![using the express lane](/img/timeboost-centralized-timeboost-express-lane-workflow.jpg) The express lane implementation uses a special endpoint on the sequencer, formally titled `timeboost_sendExpressLaneTransaction`. This endpoint is special because transactions submitted to it will be sequenced immediately by the sequencer, hence the name "express lane." The sequencer will only accept valid transaction payloads to this endpoint if they are signed correctly by the current round’s express lane controller. Transactions submitted to the sequencer normally will be considered non-express lane transactions and will, therefore, have their arrival timestamp delayed by 200 milliseconds. It is important to note that transactions from both the express and non-express lanes will eventually get sequenced into a single, ordered stream of transactions for the sequencer to post to a data availability layer (and for node operations to construct the chain's state then). The express lane controller does *not*: * Have the right to reorder transactions. * Have a guarantee that their transactions will always be first at the “top-of-the-block.” * Guarantee a profit at all. The value of the express lane will be the sum of how much MEV the express lane controller predicts they can extract during the upcoming round (i.e., MEV opportunity estimates made before the auction closes) *plus* the amount of MEV extracted by the express lane controller while they are in control (that they otherwise did not predict). Understanding how the value of the express lane is determined can be useful for chain owners when adjusting to the artificial delay and the time before the auction closes. ### The Timeboost auction Determining control of the express lane in each round (default: 60 seconds) happens by a per-round auction, which is a sealed-bid, second-price auction. This auction occurs to determine the express lane controller for the next round. In other words, determining the express lane controller can happen at any point in time in the previous auction round. Bids for the auction can be made with any **ERC-20** token, in any amount, and can be collected by any address—at the full discretion of the chain owner. The auction for a round has a closing time that is `auctionClosingSeconds` (default: 15) seconds before the beginning of the round. This closing time means that, in the default parameters, parties have 45 seconds to submit bids before the auction will no longer accept bids. In the 15 seconds between when bidding is over and when the new round begins, the autonomous auctioneer will verify all bids, determine the winner, and make a call to the onchain auction contract to formally resolve the auction. > **INFO** — Bid behavior > > The autonomous auctioneer will consider only an address’s most recent bid, meaning that if you have placed a bid and wish to change it, you may resubmit a bid to “update it.” To cancel a bid, place a new bid that is significantly lower than your original bid or bid below the minimum reserve price. Remember that there is a maximum of five bids per round per address to mitigate DDoS risks. ### Auction contract Before placing a bid in the auction, a party must deposit funds into the `Auction` Contract. At any time, you can make a deposit or add additional funds to an existing deposit. These deposits are fully withdrawable, with a nominal delay (two rounds or two minutes by default), to prevent impacting the outcome of an existing round. There is no minimum deposit amount, but a starting minimum bid of 0.001 **WETH** (the default amount and token) is required, known as the "minimum reserve price". The chain owner sets the minimum reserve price, which can be updated at any time up to 30 seconds (default) before the start of the next round, ensuring that auction participants always know the reserve price at least 30 seconds before they must submit their bids. A reserve price can also be set by the chain owner (or by an address designated by the chain owner) as a way to raise the minimum bid, as the `Auction` Contract enforces that the reserve price is never less than the minimum reserve price. Once the autonomous auctioneer determines an auction winner, the `Auction` contract will deduct the second-highest bid amount from the account of the highest bidder and transfer those funds to a `beneficiary` account designated by the chain owner by default. The `expressLaneControllerAddress` specified in the highest bid will become the express lane controller for the round. > **INFO** — Additional FAQs > > For frequently asked questions refer to the [Timeboost FAQ](/how-arbitrum-works/timeboost/timeboost-faq.md). --- > For a complete page index, fetch # How to use Timeboost Timeboost is a transaction ordering policy for Arbitrum chains. With Timeboost, anyone can bid for the right to access an express lane on the **Sequencer** for faster transaction inclusion. In this how-to, you'll learn how to bid for the right to use the express lane and submit transactions through the express lane. To learn more about Timeboost and the key terms used on this page, refer to the [gentle introduction](/how-arbitrum-works/timeboost/gentle-introduction.md). This how-to assumes that you're familiar with [How Timeboost works](/how-arbitrum-works/timeboost/gentle-introduction.md) Note about transferring express lane rights A round's express lane controller, at their choice, can send transactions signed by others on a per-transaction basis, as explained later in this guide. ## How to submit bids for the right to be the express lane controller To use the express lane for faster transaction inclusion, you must win an auction for the right to be the express lane controller for a specific round. Note Remember that, by default, each round lasts 60 seconds, and the auction for a specific round closes 15 seconds before the start of the round. These default values can be configured on a chain using the `roundDurationSeconds` and `auctionClosingSeconds` parameters. An auction contract facilitates auctions, and bids get submitted to an autonomous auctioneer that interacts with the contract. Let's examine the process of submitting bids and determining the winner of an auction. ### Prerequisites: Gather the required information Before we begin, make sure you have: * Address of the auction contract * Endpoint of the autonomous auctioneer The following table shows this information for the Arbitrum DAO-owned chains: #### Timeboost reference URLs and addresses | Network | Auction contract | Autonomous auctioneer endpoint | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | Arbitrum Sepolia | [0x991DbEDf388CB5925318f06362D4fCa7b040527D](https://sepolia.arbiscan.io/address/0x991DbEDf388CB5925318f06362D4fCa7b040527D) | | | Arbitrum One | [0x5fcb496a31b7AE91e7c9078Ec662bd7A55cd3079](https://arbiscan.io/address/0x5fcb496a31b7AE91e7c9078Ec662bd7A55cd3079) | | | Arbitrum Nova | [0xa5aBADAF73DFcf5261C7f55420418736707Dc0db](https://nova.arbiscan.io/address/0xa5aBADAF73DFcf5261C7f55420418736707Dc0db) | | ### Step 1: Deposit funds into the auction contract Before bidding on an auction, we need to deposit funds in the auction contract. These funds are in the form of the **ERC-20** tokens used to bid, also known as the `bidding token`. We will be able to bid for an amount that is equal to or less than the tokens we have deposited in the auction contract. To see the amount of tokens we have deposited in the auction contract, we can call the function `balanceOf` in the `Auction` contract: ```tsx // Code example uses viem const depositedBalance = await publicClient.readContract({ address: auctionContractAddress, abi: auctionContractAbi, functionName: 'balanceOf', args: [userAddress], }); console.log(`Current balance of ${userAddress} in auction contract: ${depositedBalance}`); ``` If we want to deposit more funds to the `Auction` contract, we first need to know what the bidding token is. To obtain the address of the bidding token, we can call the function `biddingToken` in the `Auction` contract: ```tsx // Code example uses viem const biddingTokenContractAddress = await publicClient.readContract({ address: auctionContractAddress, abi: auctionContractAbi, functionName: 'biddingToken', }); console.log(`biddingToken: ${biddingTokenContractAddress}`); ``` Bidding token in Arbitrum chains On Arbitrum One and Arbitrum Nova, the default bidding token is **WETH**. Once we know what the bidding token is, we can deposit funds to the auction contract by calling the function `deposit` of the contract after having it approved as spender of the amount we want to deposit: ```tsx // Code example uses viem // Approving spending tokens const approveHash = await walletClient.writeContract({ account, address: biddingTokenContractAddress, abi: parseAbi(['function approve(address,uint256)']), functionName: 'approve', args: [auctionContract, amountToDeposit], }); console.log(`Approve transaction sent: ${approveHash}`); // Making the deposit const depositHash = await walletClient.writeContract({ account, address: auctionContractAddress, abi: auctionContractAbi, functionName: 'deposit', args: [amountToDeposit], }); console.log(`Deposit transaction sent: ${depositHash}`); ``` ### Step 2: Query the reserve pricer API Any auction participant submitting a valid bid must first know the minimum acceptable bid amount for the round being bid on. The Timeboost Reserve Pricer API exposes this data publicly over HTTP, returning the current reserve price and the round it applies to. Query this API before submitting any bid. The reserve price is updated at the 31st second of every minute. #### Endpoints The API exposes two endpoints over HTTPS: * `/api/latest` returns the newest reserve price and round. * `/api/recent` returns the reserve price and rounds for the last two hours. The base URL differs per network: | Network | Base URL | | ---------------- | ----------------------------------------------- | | Arbitrum Sepolia | `https://arbsepolia-reserve-pricer.arbitrum.io` | | Arbitrum One | `https://arb1-reserve-pricer.arbitrum.io` | For example, to fetch the latest reserve price and round: ```shell # Arbitrum One curl https://arb1-reserve-pricer.arbitrum.io/api/latest # Arbitrum Sepolia curl https://arbsepolia-reserve-pricer.arbitrum.io/api/latest ``` ### Step 3: submit bids Once we have deposited funds into the auction contract, we can submit bids for the current auction round. We can obtain the current round by calling the function `currentRound` in the `Auction` contract: ```tsx // Code example uses viem const currentRound = await publicClient.readContract({ address: auctionContractAddress, abi: auctionContractAbi, functionName: 'currentRound', }); console.log(`Current round: ${currentRound}`); ``` The above shows the current round that's running. At the same time, the auction for the next round might be open. For example, if the `currentRound` is 10, the auction for round 11 is currently happening. To check whether or not that auction is open, we can call the function `isAuctionRoundClosed` of the `Auction` contract: ```tsx // Code example uses viem let currentAuctionRoundIsClosed = await publicClient.readContract({ address: auctionContractAddress, abi: auctionContractAbi, functionName: 'isAuctionRoundClosed', }); ``` > **NOTE** > > Remember that, by default, auctions for a given round open 60 seconds before that round starts and close 15 seconds before the round starts, so there might be no auctions opened at certain times. Once we know the current round, we can bid for (`currentRound + 1`) and verify that the auction is still open (`!currentAuctionRoundIsClosed`), then we can submit a bid. > **TIP** — Fetching the minimum bid amount > > Before submitting a bid, query the reserve pricer API to retrieve the minimum acceptable bid amount for the round you're bidding on. When bids get submitted to the autonomous auctioneer endpoint, we need to send an `auctioneer_submitBid` request with the following information: * chain id * address of the express lane controller candidate (for example, our address if we want to be the express lane controller) * address of the auction contract * round we are bidding for (in our example, `currentRound + 1`) * the amount in `wei` of the deposit **ERC-20** token to bid * signature (explained below) Let's see an example of a call to this RPC method: ```tsx // Code example uses viem const currentAuctionRound = currentRound + 1; const hexChainId: `0x${string}` = `0x${Number(publicClient.chain.id).toString(16)}`; const res = await fetch(, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'submit-bid', method: 'auctioneer_submitBid', params: [ { chainId: hexChainId, expressLaneController: userAddress, auctionContractAddress: auctionContractAddress, round: `0x${currentAuctionRound.toString(16)}`, amount: `0x${Number(amountToBid).toString(16)}`, signature: signature, }, ], }), }); ``` The signature that needs to be sent is an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) signature over the following typed structure data: * Domain: `Bid(uint64 round,address expressLaneController,uint256 amount)` * `round`: auction round number * `expressLaneController`: address of the express lane controller candidate * `amount`: amount to bid Here's an example to produce that signature with viem: ```tsx // Code example uses viem const currentAuctionRound = currentRound + 1; const signatureData = hashTypedData({ domain: { name: 'ExpressLaneAuction', version: '1', chainId: Number(publicClient.chain.id), verifyingContract: auctionContractAddress, }, types: { Bid: [ { name: 'round', type: 'uint64' }, { name: 'expressLaneController', type: 'address' }, { name: 'amount', type: 'uint256' }, ], }, primaryType: 'Bid', message: { round: currentAuctionRound, expressLaneController: userAddress, amount: amountToBid, }, }); const signature = await account.sign({ hash: signatureData, }); ``` Note You can also call the function `getBidHash` in the auction contract to obtain the `signatureData`, specifying the `round`, `userAddress`, and `amountToBid`. When sending the request, the autonomous auctioneer will return an empty result with an HTTP status `200` if received correctly. If the result returned contains an error message, something went wrong. Following are some of the error messages that can help us understand what's happening: #### Errors relating to bid submission | Error | Description | | ----------------------- | ----------------------------------------------------------------------------------------------------------- | | `MALFORMED_DATA` | Wrong input data, failed to deserialize, missing certain fields, etc. | | `NOT_DEPOSITOR` | The address is not an active depositor in the auction contract | | `WRONG_CHAIN_ID` | Wrong chain id for the target chain | | `WRONG_SIGNATURE` | Signature failed to verify | | `BAD_ROUND_NUMBER` | Incorrect round, such as one from the past | | `RESERVE_PRICE_NOT_MET` | Bid amount does not meet the minimum required reserve price onchain | | `INSUFFICIENT_BALANCE` | The bid amount specified in the request is higher than the deposit balance of the depositor in the contract | ### Step 4: find out the winner of the auction After the auction closes and before the round starts, the autonomous auctioneer will call the auction contract with the two highest bids received, allowing the contract to declare the winner and deduct the second-highest bid from the winner's deposited funds. After this, the contract will emit an event with the new Express Lane Controller address. We can use this event to determine whether or not we've won the auction. The event signature is: ```solidity event SetExpressLaneController( uint64 round, address indexed previousExpressLaneController, address indexed newExpressLaneController, address indexed transferor, uint64 startTimestamp, uint64 endTimestamp ); ``` Here's an example to get the log from the auction contract to determine the new express lane controller: ```tsx // Code example uses viem const fromBlock = const logs = await publicClient.getLogs({ address: auctionContractAddress, event: auctionContractAbi.filter((abiEntry) => abiEntry.name === 'SetExpressLaneController')[0], fromBlock, }); const newExpressLaneController = logs[0].args.newExpressLaneController; console.log(`New express lane controller: ${newExpressLaneController}`); ``` If you won the auction, congratulations! You are the express lane controller for the next round, which, by default, will start 15 seconds after the auction closes. The following section explains how we can submit a transaction to the express lane. ## How to submit transactions to the express lane The sequencer immediately sequences transactions sent to the express lane, while regular transactions are delayed 200ms by default. However, only the express lane controller can send transactions to the express lane. The previous section explained how to participate in the auction as the express lane controller for a given round. For background on default sequencer ordering and the feed, see [Sequencing and broadcasting](/how-arbitrum-works/deep-dives/sequencer.md#sequencing-and-broadcasting). The sequencer handles the express lane. When sending transactions to the Sequencer endpoint, we need to send a `timeboost_sendExpressLaneTransaction` request with the following information: * chain id * current round (following the example above, `currentRound`) * address of the auction contract * sequence number: a per-round nonce of express lane submissions, which resets to 0 at the beginning of each round. You can also use the special "dontcare" sequence number (2^64 - 1) to indicate that you don't care about ordering relative to other `ExpressLaneSubmissions` (normal nonce ordering within transactions for an account is still respected) * RLP-encoded transaction payload * conditional options for Arbitrum transactions ([more information](https://github.com/OffchainLabs/go-ethereum/blob/48de2030c7a6fa8689bc0a0212ebca2a0c73e3ad/arbitrum_types/txoptions.go#L71)) * signature (explained below) > **INFO** — Timeboost-ing third party transactions > > Notice that while the express lane controller must sign the `timeboost_sendExpressLaneTransaction` request, any party can sign the transaction for execution. In other words, the express lane controller can receive transactions signed by other parties and sign them to apply the time advantage offered by the express lane to those transactions. > **INFO** — Support for `eth_sendRawTransactionConditional` > > Timeboost doesn't currently support the `eth_sendRawTransactionConditional` method. Let's see an example of a call to this RPC method: ```tsx // Code example uses viem const hexChainId: `0x${string}` = `0x${Number(publicClient.chain.id).toString(16)}`; const transaction = await walletClient.prepareTransactionRequest(...); const serializedTransaction = await walletClient.signTransaction(transaction); const res = await fetch(, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'express-lane-tx', method: 'timeboost_sendExpressLaneTransaction', params: [ { chainId: hexChainId, round: `0x${currentRound.toString(16)}`, auctionContractAddress: auctionContractAddress, sequenceNumber: `0x${sequenceNumber.toString(16)}`, transaction: serializedTransaction, options: {}, signature: signature, }, ], }), }); ``` The required signature is an Ethereum signature that needs to be sent with the following information: * Hash of `keccak256("TIMEBOOST_BID")` * Chain id in hexadecimal, padded to 32 bytes * Auction contract address * Round number in hexadecimal, padded to 8 bytes * Sequence number in hexadecimal, padded to 8 bytes * Serialized transaction Here's an example to produce that signature: ```tsx // Code example uses viem const hexChainId: `0x${string}` = `0x${Number(publicClient.chain.id).toString(16)}`; const transaction = await walletClient.prepareTransactionRequest(...); const serializedTransaction = await walletClient.signTransaction(transaction); const signatureData = concat([ keccak256(toHex('TIMEBOOST_BID')), pad(hexChainId), auctionContract, toHex(numberToBytes(currentRound, { size: 8 })), toHex(numberToBytes(sequenceNumber, { size: 8 })), serializedTransaction, ]); const signature = await account.signMessage({ message: { raw: signatureData }, }); ``` When sending the request, the sequencer will return an empty result with an HTTP status `200` if it received it correctly. If the result returned contains an error message, something went wrong. Following are some of the error messages that can help us understand what's happening: #### Errors relating to express lane transaction submission Note that if you get any of the errors below, then the sequence number used in your express lane transaction was *not* consumed. | Error | Description | | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MALFORMED_DATA` | wrong input data, failed to deserialize, missing certain fields, etc. | | `WRONG_CHAIN_ID` | wrong chain id for the target chain | | `WRONG_SIGNATURE` | signature failed to verify | | `BAD_ROUND_NUMBER` | incorrect round, such as one from the past | | `NOT_EXPRESS_LANE_CONTROLLER` | the sender is not the express lane controller | | `NO_ONCHAIN_CONTROLLER` | there is no defined, onchain express lane controller for the round | | `SEQUENCE_NUMBER_ALREADY_SEEN` | the sequence number used for the given transaction was already consumed, try resubmitting with a new sequence number | | `SEQUENCE_NUMBER_TOO_LOW` | the sequencer number used for the given transaction is numerically lower than the expeected sequence number, try resubmitting with the expected sequence number | | `sequence number has reached max allowed limit` | the limit on the number of buffered express lane transactions was reached. Read more about this on our [Troubleshoot Timeboost page](/how-arbitrum-works/timeboost/troubleshoot-timeboost.md#the-block-based-timeout-for-express-lane-transactions) | > **INFO** — What happens if you're not the express lane controller? > > If you are not the express lane controller and you try to submit a transaction to the express lane, the sequencer will respond with the error `NOT_EXPRESS_LANE_CONTROLLER` or `NO_ONCHAIN_CONTROLLER`. ## How to withdraw funds deposited in the auction contract Funds are deposited in the auction contract to have the right to bid in auctions. Withdrawing funds is possible through a two-step process: initiate the withdrawal, wait for two rounds, and then finalize the withdrawal. To initiate a withdrawal, we can call the function `initiateWithdrawal` in the `Auction` contract: ```tsx // Code example uses viem const initWithdrawalTransaction = await walletClient.writeContract({ account, address: auctionContractAddress, abi: auctionContractAbi, functionName: 'initiateWithdrawal', }); console.log(`Initiate withdrawal transaction sent: ${initWithdrawalTransaction}`); ``` This transaction will initiate a withdrawal of all funds deposited by the sender's account. When executing it, the contract will emit a `WithdrawalInitiated` event with the following structure: ```solidity event WithdrawalInitiated( address indexed account, uint256 withdrawalAmount, uint256 roundWithdrawable ); ``` In this event, the `account` is the address from which we will withdraw funds, `withdrawalAmount` specifies the amount we will take from the contract, and `roundWithdrawable` indicates the specific round during which we can finalize the withdrawal. After two rounds have passed, we can call the method `finalizeWithdrawal` in the `Auction` contract to finalize the withdrawal: ```tsx // Code example uses viem const finalizeWithdrawalTransaction = await walletClient.writeContract({ account, address: auctionContractAddress, abi: auctionContractAbi, functionName: 'finalizeWithdrawal', }); console.log(`Finalize withdrawal transaction sent: ${finalizeWithdrawalTransaction}`); ``` ## How to identify timeboosted transactions Transactions sent to the express lane by the express lane controller and that have been executed (regardless of whether they were successful or reverted) can be identified by examining their receipts or the message broadcast by the [Sequencer feed](/how-arbitrum-works/deep-dives/sequencer.md#real-time-sequencer-feed). Transaction receipts now include a new field, `timeboosted`, which will be `true` for timeboosted transactions and `false` for regular non-timeboosted transactions. For example: ```shell blockHash 0x56325449149b362d4ace3267681c3c90823f1e5c26ccc4df4386be023f563eb6 blockNumber 105169374 contractAddress cumulativeGasUsed 58213 effectiveGasPrice 100000000 from 0x193cA786e7C7CC67B6227391d739E41C43AF285f gasUsed 58213 logs [] logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 root status 1 (success) transactionHash 0x62ea458ad2bb408fab57d1a31aa282fe3324b2711e0d73f4777db6e34bc1bef5 transactionIndex 1 type 2 blobGasPrice blobGasUsed to 0x0000000000000000000000000000000000000001 gasUsedForL1 "0x85a5" l1BlockNumber "0x6e8b49" timeboosted true ``` In the sequencer feed, the `BroadcastFeedMessage` struct now contains a `blockMetadata` field that represents whether a particular transaction in the block was timeboosted or not. The field `block metadata` is an array of bytes, and it starts with a byte representing the version (`0`), followed by `ceil(N/8)` bytes, where `N` is the number of transactions in the block. If a particular transaction were time-boosted, the bit representing its position in the block would be set to `1`, while the rest would reset to `0`. For example, if the `blockmetadata` of a particular message, viewed as bits, is `00000000 01100000`, then the 2nd and 3rd transactions in that block were time boosted. ## How to view historical bid data In the current implementation, information about the winning bid for a resolved auction emits via the `AuctionResolved` event ([sample interface](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/express-lane-auction/IExpressLaneAuction.sol#L95-L103)). Historical bid information, including the round number and bid amounts, are published to a public S3 bucket at a regular cadence. The domain for the S3 bucket where historical bids get saved is: #### S3 URLs for historical bid data > **INFO** — URL updates for historical bid data > > On June 9 2025, at around 17:27 ET (UTC−05:00), the region in which the Amazon S3 bucket for historical bid data on Arbitrum One was *changed* from `s3://timeboost-auctioneer-arb1/uw2/validated-timeboost-bids/` to `s3://timeboost-auctioneer-arb1/ue2/validated-timeboost-bids/`. The below table has been updated for the new, correct URL to access bid data after June 9, 2025 17:27 ET, but if you require data from before June 9, 2025 17:27 ET, then please use `s3://timeboost-auctioneer-arb1/uw2/validated-timeboost-bids/`. | Chain | S3 bucket URL | | ---------------- | --------------------------------------------------------------- | | Arbitrum Sepolia | s3://timeboost-auctioneer-sepolia/ue2/validated-timeboost-bids/ | | Arbitrum One | s3://timeboost-auctioneer-arb1/ue2/validated-timeboost-bids/ | | Arbitrum Nova | s3://timeboost-auctioneer-nova/ue2/validated-timeboost-bids/ | > **NOTE** > > Make sure you use `--no-sign-request` with the [AWS S3 CLI](https://docs.aws.amazon.com/cli/latest/reference/s3/). Here is an example query on how to look up and download historical bid data: ```shell ➜ ~ aws s3 ls s3://timeboost-auctioneer-arb1/ue2/validated-timeboost-bids/2025/06/10/ --no-sign-request --recursive 2025-06-09 18:21:47 12553 ue2/validated-timeboost-bids/2025/06/09/0130304-0130343.csv.gzip 2025-06-09 18:36:46 4725 ue2/validated-timeboost-bids/2025/06/09/0130344-0130358.csv.gzip ... 2025-06-09 17:23:28 3407 uw2/validated-timeboost-bids/2025/06/09/0130264-0130284.csv.gzip 2025-06-09 17:27:32 1228 uw2/validated-timeboost-bids/2025/06/09/0130285-0130288.csv.gzip ➜ ~ aws s3 cp s3://timeboost-auctioneer-arb1/ue2/validated-timeboost-bids/2025/06/09/0130304-0130343.csv.gzip local.csv.gzip --no-sign-request download: s3://timeboost-auctioneer-arb1/ue2/validated-timeboost-bids/2025/06/09/0130304-0130343.csv.gzip to ./local.csv.gzip ``` ## Default parameters Below are a few of the default Timeboost parameters mentioned earlier. All these parameters and more are configurable by the chain owner. | Parameter name | Description | Recommended default value | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | `roundDurationSeconds` | Duration of time that the sequencer will honor the express lane privileges for transactions signed by the current round’s express lane controller. | 60 seconds | | `auctionClosingSeconds` | Time before the start of the next round. The autonomous auctioneer will not accept bids during this time interval. | 15 seconds | | `beneficiary` | Address where proceeds from the Timeboost auction are sent to when `flushBeneficiaryBalance()` gets called on the auction contract. | An address controlled by the chain's owner | | `_biddingToken` | Address of the token used to make bids in the Timeboost auction. It can be any **ERC-20** token (assuming the token chosen does not have fee-on-transfer, rebasing, transfer hooks, or otherwise non-standard **ERC-20** logic). | **WETH** | | `nonExpressDelayMsec` | The artificial delay applied to the arrival timestamp of non-express lane transactions *before* the non-express lane transactions are sequenced. | 0.2 seconds, or 200 milliseconds | | `reservePrice` | The minimum bid amount accepted by the auction contract for the current Timeboost auction, denominated in `_biddingToken`. | None | | `_minReservePrice` | A value that must be equal to or below the `reservePrice` to act as a "floor minimum" for Timeboost bids. Enforced by the auction contract. | 0.001 **WETH** | ## Troubleshooting and best practices Our guide on [Troubleshooting Timeboost](/how-arbitrum-works/timeboost/troubleshoot-timeboost.md) provides more information on how response times work and how express lane transactions are sequenced, along with common errors and best practices for using Timeboost. --- > For a complete page index, fetch # Frequently Asked Questions (FAQs) about Timeboost Below are some common and frequently asked questions about Timeboost. This list of questions is in no particular order and will be updated periodically as new questions arise. ## Who is Timeboost for, and how do I use it? Timeboost is an optional addition to an Arbitrum chain’s infrastructure, meaning that enabling Timeboost is at the discretion of the chain owner and that an Arbitrum chain can fully function normally without Timeboost. When enabled, Timeboost serves different groups of parties with varying degrees of impact and benefits. Let’s go through them below: #### For regular users: The only difference users should experience is a small delay when submitting their transactions. The default configuration for this delay is 200ms, and a chain's owner can adjust it. The delay intends to give the express lane controller an advantage, allowing them to include transactions slightly quicker than others. Importantly, user transactions will remain private until after they are sequenced, meaning that the express lane controller cannot frontrun or sandwich other users. #### For chain owners: Timeboost represents a unique way to accrue value for its token and generate revenue for the chain. Explicitly, chain owners can set up their Timeboost auction to collect bid proceeds in the same token used for gas on their network and then choose what to do with these proceeds afterward. #### For searchers/arbitrageurs: Timeboost adds a unique twist to your existing or prospective MEV strategies that may become more profitable than before. For instance, purchasing the time advantage offered by Timeboost’s auction may end up costing less than the costs of investing in hardware and winning latency races. Another example is the potential new business model of reselling express lane rights to other parties on a time slot or per-transaction basis. #### Special note on Timeboost for chain owners As with many new features and upgrades to [Arbitrum Nitro](/how-arbitrum-works/inside-arbitrum-nitro.md), Timeboost is an optional feature that chain owners may choose to deploy and customize however they see fit. Deploying and enabling/disabling Timeboost on a live Arbitrum chain will not halt or impact the chain but will instead influence the chain's transaction ordering policy. An Arbitrum chain will, by default, fall back to FCFS in scenarios where Timeboost is deployed but disabled, or if there is no express lane controller for a given round. We recommend that Arbitrum chains holistically assess the applicability and use cases of Timeboost for their chain before deploying and enabling Timeboost. This assessment is necessary because some Arbitrum chains may not have that much MEV (e.g., arbitrage) to begin with. Furthermore, we recommend that Arbitrum chains start with the default parameters recommended by Offchain Labs and closely monitor the results and impacts on their chain’s ecosystem over time before considering any adjustments to the parameters. ## Using Timeboost #### How can I participate in Timeboost directly? Interested parties can participate in the Timeboost auctions by depositing funds in the auction contract and sending bids to the autonomous auctioneer. Feel free to refer to [this guide](/how-arbitrum-works/timeboost/how-to-use-timeboost.md) for more information. The Timeboost auction is open to everyone; however, since auctions require a non-zero bid to win, only parties that can generate a return from capturing arbitrage opportunities, backrunning opportunities, or reselling the express lane rights will benefit from participating. The Timeboost protocol operates behind the scenes with minimal impact on normal users, generating revenue for the chain owners and opening up an additional revenue stream for sophisticated searchers. #### What is the goal of Timeboost? The goal of Timeboost is to provide chain owners with a way to capture available MEV on their chain and reduce spam from FCFS arbitrage while preserving a best-in-class user experience with both fast block times and protecting users from harmful MEV (e.g., frontrunning, sandwich attacks). #### Does it work with Arbitrum chains? Arbitrum chains can adopt Timeboost, and Arbitrum chain owners can also choose to use any **ERC-20** token for making bids. For example, a chain could decide to accept its (or any other) token for the auction. #### How do I change or cancel my bid after I have submitted it? The autonomous auctioneer will consider only an address’s most recent bid, meaning that if you have placed a bid and wish to change it, you may re-submit a bid to “update it.” To cancel a bid, place a new bid that is significantly lower than your original bid or bid below the minimum reserve price. Remember that there is a maximum of five bids per round per address to mitigate DDoS risks. ## Security questions #### Does Timeboost create new types of MEV extraction vectors? Timeboost does not create new types of Maximum Extractable Value (MEV). Instead, it introduces slight adjustments to when and how existing forms of MEV operate. Timeboost's design strikes a balance between capturing MEV value for the chain without introducing additional externalities. For example, Timeboost does not enable transaction reordering in a way that facilitates sandwich attacks. The protocol does allow users to attempt to process their transactions earlier by gaining control of the express lane. Still, it doesn't permit them to manipulate the order in which trades occur relative to others in the same block. This ordering means the fast lane controller \[at any given time] cannot be certain of how their transactions will get ordered relative to others' transactions. #### Does Timeboost give the auction winner an unfair advantage or power around transaction ordering? Winning a Timeboost auction gives you a time advantage — specifically, a proposed 200ms “head start” — but it does not ensure your transaction will always be the first in every block. The perceived value of the express lane is determined by its holder and the amount they choose to bid to win control of it; it’s a use-it-or-lose-it privilege. Let’s be clear on what Timeboost does not do: * It does not give anyone the right to reorder transactions. It does not allow you to view others’ transactions until they are sequenced (because the mempool remains private). * It does not ensure your transaction will always be the first in every block. * It does not mean your transaction will have absolutely zero total time delay. Winning the bid means you won’t experience the 200ms artificial delay others face, but natural delays — such as processing time or network distance — still apply. #### Is it expected for powerful, centralized entities to monopolize the Timeboost express lane? Could this lead to harmful outcomes? Timeboost's design is an auction-based system that encourages open competition. Although the idea of a monopoly can be intimidating, the auction process remains competitive. If one player dominates, they will be required to outbid other users, which prevents them from maintaining complete static control continuously. Additionally, the express lane only gives a 200ms time advantage. The system is designed to incentivize rational actors to participate when they believe there is an advantage to controlling the express lane and only bid up to the value they are willing to pay for that advantage (since it is a sealed-bid auction). Finally, Timeboost is entirely optional, meaning that Arbitrum chains can still function normally without it. Should Timeboost need to be disabled, the network would smoothly revert to FCFS transaction ordering, maintaining its current security and efficiency. Every chain can make its own decision about whether to enable Timeboost–your chain, your rules. ## Technical questions #### Does Timeboost mean an expectation for searchers to bid continuously in advance, expecting opportunities to happen one minute later, rather than “in real time” opportunities (I see something → I submit an arbitrage tx with priority)? Before answering this question, it is worth clarifying that the participant will likely attempt to predict the amount of MEV generated between 15 seconds and 1 minute 15 seconds in the future, not 1 minute later. This assumption is because the auction is closed and resolved within a maximum of 15 seconds before the start of the next round (as proposed in the current proposal). The expectation is willing participants will bid continuously for the right to use the express lane in advance so that they (the participant) can profit from both (1) MEV opportunities they predict between 15s and 1min 15s in the future **and** (2) MEV opportunities in real-time during the period that the participant is in control of the express lane that they didn’t otherwise predict in advance (proposed duration: 1 minute). Suppose the participant does not win control of the express lane. In that case, opportunities that they see in real-time are still exploitable, but with a 200ms delay, similar to all other transactions (since only the express lane controller’s transactions get sequenced with no delay). #### What are the different variations of Timeboost? Timeboost is implemented by modifying the sequencer to add an express lane and deploying an autonomous auctioneer service to facilitate the auction (sealed-bid, second-price) for temporary rights to control the express lane. Timeboost was designed and developed with decentralization in mind, including one that is compatible with decentralized sequencers (full specification [here](https://github.com/OffchainLabs/decentralized-timeboost-spec)). #### Will Timeboost work with future decentralized Arbitrum sequencers? Yes. Timeboost is compatible with both the current centralized sequencer and a future design that allows Arbitrum chains to benefit from a decentralized group of sequencers. The current approach allowed us to deliver Timeboost sooner rather than waiting until the decentralized sequencer design and implementation are complete. A full specification of Timeboost with decentralized sequencers can be found [here](https://github.com/OffchainLabs/decentralized-timeboost-spec)). #### Will there be plans for a clean user interface that allows users to understand the logic of how their transactions get sorted, as well as an optional setting to adjust the sensitivity to the time factor? For the first point about a more straightforward user interface, users can subscribe to the [sequencer feed](/how-arbitrum-works/deep-dives/sequencer.md#real-time-sequencer-feed) to view, in real time, the final order of transactions. Using the sequencer feed is a sufficient solution for helping users understand the logic behind how their transactions get sorted. Additional documentation and diagrams will be forthcoming to help illustrate this workflow. To the second point, chain owners can adjust the amount of time that non-express lane transactions get delayed. This parameter, defined as `NonExpressDelayMsec`, is denominated in milliseconds and is proposed to be 200ms initially. #### How will Timeboost affect block time finality on Arbitrum chains? Does this mean that an Arbitrum chain's new block time will be 450ms? Recall that Arbitrum chains have two types of finality: (1) a trusted or soft confirmation and (2) Ethereum-equivalent finality. A trusted or soft confirmation for a user’s transaction relies on the user trusting the sequencer and the near-instant transaction receipt issued by the sequencer, which takes approximately 250ms. For (2), the user can use the Ethereum-equivalent finality heuristic once their child chain transaction becomes finalized on the parent chain as part of a batch of transactions posted to Ethereum, which can take two epochs, or roughly 13 minutes, in today’s Proof-of-Stake Ethereum. Read more about these two types of finality in [Finality](/how-arbitrum-works/deep-dives/sequencer.md#finality). With Timeboost, both finality timelines for non-express lane transactions (250ms for soft finality and \~13 minutes for Ethereum-equivalent finality) will extend by the default 200ms delay proposed in Timeboost, which will be roughly \~450ms and \~13 minutes & 0.2 seconds for soft finality and Ethereum-equivalent finality, respectively. For express lane transactions, there will be no impact on transaction finality, meaning that finality will remain at 250ms and \~13 minutes for soft finality and Etheruem-equivalent finality, respectively. #### Is there a way to track the time (milliseconds, etc.) it takes for a transaction to be sent and received by the sequencer? Yes! Measure the time between when you send your transaction and when you see it in the sequencer feed. Here, we assume that “accepted” refers to the point at which the sequencer has seen your transaction and gets processed into a block. This number is not uniformly consistent because different teams will have access to different hardware and setups, which may affect how quickly they can send messages over the public internet to the sequencer and also how quickly they can read the sequencer’s feed for state updates and transaction receipts. #### Does gas have any effect on the transaction ordering in the sequencer? Yes, because if your transaction did not provide enough gas, it might get rejected outright. If you specify insufficient gas, your transaction may be excluded from an upcoming block because it does not meet the network's requirements for processing, which include child chain execution and parent chain data posting costs. For details on how those two cost components are calculated, see [Gas and fees](/how-arbitrum-works/deep-dives/gas-and-fees.md). #### Does Arbitrum support non-JSON formats for submitting transactions? No. #### How does the sequencer handle raw transaction requests that contain incorrect data, like an inconsistent nonce? For example, multiple transactions by same wallet with same nonce are submitted to the sequencer. Would there be any penalty for that wallet/IP address? You should expect to get a nonce error. This behavior will work today, but this is considered abuse and we provide no guarantees on how this behavior will be treated in the future. #### Does the sequencer have any rate limit? If it has, what is the limit, and is it per IP address or wallet? Yes, but these limits are not published, and we don’t expect anyone to reach them. The limits are per IP address. #### Is it recommended to send requests directly to the sequencer IP address(es) instead of the sequencer domain if we want the lowest latency? No, it is not recommended. #### Is there a more efficient way to track if our transaction has been accepted or rejected by the sequencer than listening to smart contract events or transaction counts? Yes. We recommend that teams monitor and verify transaction receipts to obtain formal confirmation that their transaction gets included in a block. #### For Timeboost, is there a limit on the number of transactions that can receive a boost from the winner in a round? There is no transaction limit, but there is a block-based limit: if your Timeboosted transactions do not get sequenced within five Arbitrum blocks (1250ms) from the time that the sequencer received them, then they will get dropped. The 200ms Timeboost time advantage only lasts for 1000ms anyway, so this is a safe limit that people will not hit. Note that the block gas limit may be a reason why your transaction(s) doesn't get included in an Arbitrum block. More details on this limit can be found in the [Timeboost troubleshooting guide](/how-arbitrum-works/timeboost/troubleshoot-timeboost.md). #### If a transaction is Timeboosted and we assume it arrived in the current block creation period, which includes other non-express lane transactions, would that Timeboosted transaction queue in front of those other non-express lane transactions that have already arrived but not included yet? It depends on the arrival timestamp of all the transactions. "Regular" transactions will receive a 200ms delay before being sequenced, while Timeboosted transactions will receive zero delay before being sequenced. In this scenario, if the "regular" transactions had arrived 200 milliseconds earlier than the Timeboosted transaction, they would get sequenced alongside the Timeboosted transaction based on their arrival timestamps. In this case, for that block, some "regular" transactions may indeed be ahead of Timeboosted transactions due to the timestamps. For additional information, read this [document that explains how Timeboost works](/how-arbitrum-works/timeboost/gentle-introduction.md). #### Apart from Timeboost, are there any other mechanisms or factors that can affect transaction priority or latency? There are many factors, including how your infrastructure is set up and built, as well as where you are sending transactions from. Latency is something that top teams will optimize for, so spend time focusing on this to ensure you can maximize the benefits of Timeboost. The 200ms time advantage you receive from using Timeboost is likely more than enough to exploit the arbitrage opportunities onchain (ahead of competitors who are not using Timeboost). #### Are there any recommendations for submitting transactions to Arbitrum with lower latency in addition to Timeboost? Yeah! We recommend: * Doing adequate testing of your infrastructure to optimize for the geographical latency considerations, A solid setup for bid submission if using Timeboost, * Running your transaction pre-checker, Arbitrum full node, and a client to subscribe to the sequencer feed for the fastest onchain updates, the ability to send transactions fast, robust nonce management, and * Build a good observability and monitoring stack to watch and take action on events from the auction and also to review and process transaction receipts quickly to confirm behavior --- > For a complete page index, fetch # Troubleshoot Timeboost This is a short guide on how response times work and how express lane transactions are sequenced, alongside common errors and best practices for using Timeboost. This guide assumes you have reviewed our guide on [How to use Timeboost](/how-arbitrum-works/timeboost/how-to-use-timeboost.md). ## How express lane transactions are ordered into blocks The express lane time advantage is currently set to 200ms, while the current block creation time is 250ms. Both express lane transactions and regular transactions are processed together in a single queue after taking into account the timeboost time advantage and artificial delay. This means that if an express lane transaction and a normal transaction both arrive at the [](/how-arbitrum-works/deep-dives/sequencer.md)sequencer at the same time, but *before 50ms have passed since the last block was produced*, then both transactions may appear in the same block, though the express lane transaction would be sequenced ahead of the normal transaction (assuming that the block's gas limit has not yet been reached). Express lane transactions are processed in the order of their `sequenceNumber`, which is a field in every express lane transaction. The `sequenceNumber` field is important because transactions with `sequenceNumber = n` can only be sequenced after all the transactions from `sequenceNumber = 0` to `sequenceNumber = n-1` have been sequenced. The first expected sequence number for a new round is zero and increments for each accepted transaction. There is a special "dontcare" sequence number (`2^64 - 1`) that can be used to indicate that you don't care about ordering of this express lane submission relative to others. Transactions with the "dontcare" sequence number will be sequenced right away without waiting for any other transactions. Note that normal nonce ordering within transactions for an account is still respected, so transactions from the same account will still be ordered by their nonce regardless of sequence number. The express lane controller can send `ExpressLaneSubmissions` with both "dontcare" and normal sequence numbers within the same round. ## How response times work The response for a transaction submission to the express lane is returned immediately once received by the sequencer. For example, if an express lane transaction is sent to the sequencer at `t=0ms` and it took 50ms to arrive at the sequencer (defined as `time_to_arrive`), then the expected response time is at `t=50ms`. Note that an accepted transaction is defined as an express lane transaction submission where the sequencer returns an empty result with an HTTP status of `200` and will always have their `sequenceNumber` consumed. You can read more about how to submit express lane transactions in: [How to submit transactions to the express lane](/how-arbitrum-works/timeboost/how-to-use-timeboost.md#how-to-submit-transactions-to-the-express-lane). ## Errors relating to the `sequenceNumber` When it comes to submitting express lane transactions, there are a few scenarios to consider. Note that if your use case doesn't require an ordering between `ExpressLaneSubmissions` beyond the usual per account nonce ordering, then you can use the "dontcare" sequence number described above. ### Scenario 1: You get an error response immediately In this scenario, an error response is immediately returned after you send an express lane transaction. In most cases the transaction's `sequenceNumber` will not be consumed if the error is Timeboost-related or if the transaction was invalid (e.g., nonce too low, malformed transaction). If the error contains "Error queuing expressLane transaction" you need to re-submit your transaction with the same sequence number after rectifying any errors, or submit a different transaction with that sequence number. See [Common Timeboost error responses](#common-error-responses) below for a full list of Timeboost-related error responses and how to interpret them. ### Scenario 2: Your transaction got an empty response with an HTTP status of `200` In this scenario, a `null` response is immediately returned after you send an express lane transaction. This means that your transaction's `sequenceNumber` was consumed and your transaction was accepted. However, this does not mean that your transaction was sequenced into a block due to a block-based timeout explained below. We recommend checking transaction receipts for confirmation on whether your transactions were sequenced into a block or not. #### The block-based timeout for express lane transactions If the express lane controller decides to send a burst of transactions to the express lane with ascending values for the `sequenceNumber`, then the sequencer will attempt to process them in the order defined by the `sequenceNumber` (as explained above). However, if the transactions arrive out-of-order at the sequencer, then the transactions that do not have the expected `sequenceNumber` will be buffered (up to a limit) to be processed until the sequencer receives the transaction with the expected `sequenceNumber`. Once the sequencer receives the transaction with the expected `sequenceNumber`, then the sequencer will begin processing the buffered transaction with the next `sequenceNumber`. In other words, a transaction will only be sequenced into a block once transactions with the other, missing sequence numbers arrive to fill in the “gap” between the expected `sequencerNumber` and a given transaction’s `sequenceNumber`. For background on the sequencer's default (non-express-lane) ordering and feed, see [Sequencing and broadcasting](/how-arbitrum-works/deep-dives/sequencer.md#sequencing-and-broadcasting). A block-based timeout is applied to all express lane transactions, even those in the buffer, such that any transactions accepted (meaning `sequenceNumber` is consumed) by the sequencer will be dropped if they are not sequenced into a block within five blocks. This timeout can occur if the cumulative gas usage of transactions (express lane or otherwise) fill up 5 blocks worth of transactions *before* all of the buffered express lane transactions are sequenced. No timeout error will be returned in this case and we recommend checking transaction receipts for confirmation on whether your transactions were sequenced into a block or not. Note that each Arbitrum block has a gas limit of 32 million gas and 1 Arbitrum block is produced every 250ms. This block-based timeout is likely to be reached before the limit on buffered transactions is hit in almost all cases. ## Common error responses The below two tables can also be found on our guide on [How to use Timeboost](/how-arbitrum-works/timeboost/how-to-use-timeboost.md). #### Table 1: Errors relating to bid submission | Error | Description | | ----------------------- | ----------------------------------------------------------------------------------------------------------- | | `MALFORMED_DATA` | wrong input data, failed to deserialize, missing certain fields, etc. | | `NOT_DEPOSITOR` | the address is not an active depositor in the auction contract | | `WRONG_CHAIN_ID` | wrong chain id for the target chain | | `WRONG_SIGNATURE` | signature failed to verify | | `BAD_ROUND_NUMBER` | incorrect round, such as one from the past | | `RESERVE_PRICE_NOT_MET` | bid amount does not meet the minimum required reserve price onchain | | `INSUFFICIENT_BALANCE` | the bid amount specified in the request is higher than the deposit balance of the depositor in the contract | #### Table 2: Errors relating to express lane transaction submission Note that if you get any of the errors below or errors related to invalidity of the transaction (e.g., nonce too low, malformed transaction), then the sequence number used in your express lane transaction was *not* consumed. | Error | Description | | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MALFORMED_DATA` | wrong input data, failed to deserialize, missing certain fields, etc. | | `WRONG_CHAIN_ID` | wrong chain id for the target chain | | `WRONG_SIGNATURE` | signature failed to verify | | `BAD_ROUND_NUMBER` | incorrect round, such as one from the past | | `NOT_EXPRESS_LANE_CONTROLLER` | the sender is not the express lane controller | | `NO_ONCHAIN_CONTROLLER` | there is no defined, onchain express lane controller for the round | | `SEQUENCE_NUMBER_ALREADY_SEEN` | the sequence number used for the given transaction was already consumed, try resubmitting with a new sequence number | | `SEQUENCE_NUMBER_TOO_LOW` | the sequencer number used for the given transaction is numerically lower than the expeected sequence number, try resubmitting with the expected sequence number | | `sequence number has reached max allowed limit` | the limit on the number of buffered express lane transactions was reached | ## Notes on Timeboost's implementation for Arbitrum One & Arbitrum Nova Although the auctioneer will function autonomously, please note that the ArbitrumDAO (formal owners of Arbitrum One and Arbitrum Nova) has granted the sequencer operator with: 1. The right to pause the acceptance and verification of bids. This is to allow the current sequencer operator to provide reliable, consistent UX and maximize infrastructure stability, and 2. The right to disable Timeboost entirely in the event of a security risk or otherwise malicious attempt to harm Arbitrum One and Arbitrum Nova node operators, existing deployed applications, and/or end users. The Arbitrum Foundation and Offchain Labs commits to sharing publicly post-mortems and analyses should this scenario arise. These rights, among a few others as described in the [original AIP](https://forum.arbitrum.foundation/t/constitutional-aip-proposal-to-adopt-timeboost-a-new-transaction-ordering-policy/25167), are expected to only be exercised in circumstances where doing so would enhance Timeboost’s long-term stability, preserve or improve the user experience for those using Timeboost-enabled Arbitrum chains, increase the security posture, resiliency, or stability of the chain, and/or otherwise help increase revenue for the ArbitrumDAO. It is important to emphasize that for Arbitrum One and Arbitrum Nova, the DAO-elected Arbitrum Security Council can, at any time, perform either Emergency Actions or Non-Emergency Actions to execute software upgrades, perform routine maintenance, and other parameter adjustments to Timeboost, in each case in accordance with its existing powers. These actions can include, but are not limited solely to, exercising the rights proposed above for the current sequencer operator. More information about the Arbitrum Security Council and their scope of powers can be found in the [ArbitrumDAO Constitution](https://docs.arbitrum.foundation/dao-constitution). --- > For a complete page index, fetch # Arbitrum glossary ### Activation The step that prepares a deployed [Stylus](/intro/glossary.md#stylus) contract to be called. Activation registers the program with [ArbOS](/intro/glossary.md#arbos), validates the [WASM](/intro/glossary.md#wasm) bytecode, and primes the per-node compiled-artifact cache so subsequent calls can execute the program at native speed. Activation costs gas proportional to program size, and re-activation is required after the cache entry expires (365 days by default) or after an ArbOS upgrade that changes the [WASM module root](/intro/glossary.md#wasm-module-root). ### Active Validator A bonded [Validator](/intro/glossary.md#validator) that makes disputable assertions to advance the state of an Arbitrum chain or to challenge the validity of others' assertions. (Not to be confused with the [Sequencer](/intro/glossary.md#sequencer).) ### Address Alias An address deterministically generated from a parent chain contract address used on child chain to safely identify the source of a parent-to-child chain message. ### Adjustment window Window during which the gas pricing algorithm measures the transaction backlog. For example, there can be a gas target of 10Mgas/s over ten minutes. That means the price will only increase if the average demand is over 10Mgas/s for that ten-minute window. ### Alt-DA An "alternative data-availability" mode for an [Arbitrum chain](/intro/glossary.md#arbitrum-chain) in which transaction data is posted to a third-party DA layer (for example, Avail or Celestia) rather than to Ethereum calldata/blobs (Rollup mode) or to a [DAC](/intro/glossary.md#data-availability-committee-dac) ([AnyTrust](/intro/glossary.md#arbitrum-anytrust-protocol) mode). Alt-DA exposes a different trust profile than either built-in mode and is configured at chain deployment. ### Decentralized application A decentralized application typically consists of smart contracts as well as a user-interface for interacting with them. **Note**: In our documentation, "apps" and "decentralized applications" are used interchangeably. ### Arb Token Bridge A series of contracts on an Arbitrum chain and its underlying chain that facilitate trustless movement of **ERC-20** tokens between the two layers. ### Arbified Token List A token list that conforms to [Uniswap's token list specification](https://github.com/Uniswap/token-lists); Arbified lists are generated by inputting an externally maintained list ([that is, CoinMarketCap's list](https://api.coinmarketcap.com/data-api/v3/uniswap/all.json)) and outputting a list that includes all of the instances of token contracts on the Arbitrum chain bridged via the canonical [Arbitrum Token Bridge](/intro/glossary.md#arb-token-bridge) from tokens on the input list. (See the [arbitrum-token-lists source code](https://github.com/OffchainLabs/arbitrum-token-lists).) ### Arbitrum Arbitrum is the finance-native blockchain platform providing infrastructure for applications, tokenization, and dedicated blockchains. The platform includes Arbitrum One, Arbitrum Nova, Arbitrum chains, Nitro, Stylus, and supporting protocols. ### Arbitrum AnyTrust Chain An [Arbitrum chain](/intro/glossary.md#arbitrum-chain) that implements the [Arbitrum AnyTrust Protocol](/intro/glossary.md#arbitrum-anytrust-protocol). ### Arbitrum AnyTrust Protocol An Arbitrum protocol that manages data availability with a permissioned set of parties known as the [Data Availability Committee (DAC)](/intro/glossary.md#data-availability-committee-dac). This protocol reduces transaction fees by introducing an additional trust assumption for data availability in lieu of Ethereum's [Trustless](/intro/glossary.md#trustless) data availability mechanism. [Arbitrum Nova](/intro/glossary.md#arbitrum-nova) is an example of an AnyTrust chain; [Arbitrum One](/intro/glossary.md#arbitrum-one) is an alternative chain that implements the purely trustless (and more L1-gas intensive) [Arbitrum Rollup Protocol](/intro/glossary.md#arbitrum-rollup-protocol). ### Arbitrum Bridge UI Web application built and maintained by [Offchain Labs](/intro/glossary.md#offchain-labs) for user interactions with the [Arbitrum Token Bridge](/intro/glossary.md#arb-token-bridge). Visit the [Arbitrum Bridge UI](https://bridge.arbitrum.io/). ### Arbitrum chain A blockchain that runs on the Arbitrum platform. Arbitrum chains are EVM compatible, and use an underlying EVM chain (for example, Ethereum) for settlement and for succinct fraud proofs (as needed). Arbitrum chains come in two forms: [Arbitrum Rollup chains](/intro/glossary.md#arbitrum-rollup-chain) and [Arbitrum AnyTrust chains](/intro/glossary.md#arbitrum-anytrust-chain). ### Arbitrum Chains An Arbitrum chain is any chain that is built using the Arbitrum stack. Anyone can deploy an Arbitrum chain permissionlessly. ### Arbitrum Classic [Old Arbitrum stack](https://github.com/OffchainLabs/arbitrum) that used custom virtual machine ("AVM"); no public Arbitrum chain uses the classic stack as of 8/31/2022 (they instead use [Arbitrum Nitro](/intro/glossary.md#arbitrum-nitro).) ### Arbitrum DAO The onchain governance body that owns and operates [Arbitrum One](/intro/glossary.md#arbitrum-one) and [Arbitrum Nova](/intro/glossary.md#arbitrum-nova). Holders of the **ARB** token vote on protocol upgrades, treasury actions, and [Arbitrum Expansion Program](/intro/glossary.md#arbitrum-expansion-program) revenue distributions. ### Arbitrum Expansion Program (AEP) The licensing framework under which third parties can deploy new [Arbitrum chains](/intro/glossary.md#arbitrum-chain) outside [Arbitrum One](/intro/glossary.md#arbitrum-one) and [Arbitrum Nova](/intro/glossary.md#arbitrum-nova). In exchange for permissionless deployment, the chain remits 10% of net revenue—8% to the [Arbitrum DAO](/intro/glossary.md#arbitrum-dao) and 2% to ecosystem programs. ### Arbitrum Full Node A party who keeps track of the state of an Arbitrum chain and receives remote procedure calls (RPCs) from clients. Analogous to a non-staking parent Ethereum node. ### Arbitrum Nitro Current Arbitrum tech stack; runs a fork of [Geth](/intro/glossary.md#geth) and uses WebAssembly as its underlying VM for fraud proofs. ### Arbitrum Nova The first [Arbitrum AnyTrust Chain](/intro/glossary.md#arbitrum-anytrust-chain) running on Ethereum mainnet. Introduces cheaper transactions; great for gaming and social use-cases. Implements the [Arbitrum AnyTrust Protocol](/intro/glossary.md#arbitrum-anytrust-protocol), not the [Arbitrum Rollup Protocol](/intro/glossary.md#arbitrum-rollup-protocol) protocol. Governed by the [Arbitrum DAO](https://docs.arbitrum.foundation/gentle-intro-dao-governance). ### Arbitrum One Arbitrum One is a public settlement layer built on Ethereum for applications that require strong security guarantees, deep liquidity, and predictable execution. ### Arbitrum Rollup Chain An [Arbitrum chain](/intro/glossary.md#arbitrum-chain) that implements the [Arbitrum Rollup Protocol](/intro/glossary.md#arbitrum-rollup-protocol). ### Arbitrum Rollup Protocol A trustless, permissionless Arbitrum protocol that uses its underlying base layer for data availability and inherits its security. This protocol is implemented by our [Arbitrum One](/intro/glossary.md#arbitrum-one) chain. ### ArbOS Arbitrum's "operating system" that trustlessly handles system-level operations; includes the ability to emulate the EVM. ### Assertion A bonded claim made by an Arbitrum [Validator](/intro/glossary.md#validator) representing a claim about an Arbitrum chain's state. An [Assertion](/intro/glossary.md#assertion) may, e.g., propose a new assertion, or may be a step in a [Challenge](/intro/glossary.md#challenge). Assertions can have different states: * **Proposed**: When a validator submits an assertion * **Challenged**: If another validator disputes the assertion, an interactive fraud-proof initiates. * **Confirmed**: An assertion becomes final if no one challenges it within the dispute window (6.4 days). ### Auction Contract A smart contract that handles the state, accounting of funds for bids, and various operations of the [Timeboost](/intro/glossary.md#timeboost) auction. The contract is deployed on the target chain for which Timeboost is enabled. ### Autonomous Auctioneer A protocol that receives bids from [Timeboost](/intro/glossary.md#timeboost) participants, processes and validates bids, and then posts the top valid bid (or top two valid bids in the case of a tie) to the [Auction Contract](/intro/glossary.md#auction-contract) to resolve the ongoing Timeboost auction. The autonomous auctioneer, for a given chain, is provisioned and deployed by an entity designated by the chain's owner. ### Batch A group of Arbitrum transactions posted in a single transaction on the [Underlying Chain](/intro/glossary.md#underlying-chain) into the [Sequencer Inbox](/intro/glossary.md#sequencer-inbox) by the [Sequencer](/intro/glossary.md#sequencer). ### Batch Poster The batch poster is an Externally Owned Account (EOA) controlled by the Sequencer. It is responsible for submitting the compressed transaction batches to the Sequencer Inbox contract on the parent chain. ### Bisection A move in the [BoLD](/intro/glossary.md#bold) [Challenge Protocol](/intro/glossary.md#challenge-protocol) in which an [edge](/intro/glossary.md#edge) is split in half, producing two child edges with shorter histories. Repeated bisection narrows a disagreement down to a single execution step, which can then be resolved with a [One-Step Proof](/intro/glossary.md#one-step-proof). BoLD's bisection generalises the older Arbitrum Classic [dissection](/intro/glossary.md#dissection) move. ### Blockchain A distributed digital ledger that is used to record transactions and store data in a secure, transparent, and tamper-resistant way, notably in cryptocurrency protocols. ### BLS Signature A cryptographic scheme that allows multiple signatures to be aggregated and compacted into one efficiently verifiable, constant-sized signature. Used in the [Arbitrum AnyTrust Protocol](/intro/glossary.md#arbitrum-anytrust-protocol) for the [Data Availability Committee (DAC)](/intro/glossary.md#data-availability-committee-dac)'s signatures. ### BoLD Short for "Bounded Liquidity Delay"; latest version of the Arbitrum [Challenge protocol](/intro/glossary.md#challenge-protocol) designed to eliminate [delay attack vectors](https://medium.com/offchainlabs/solutions-to-delay-attacks-on-rollups-434f9d05a07a) (see [here](https://medium.com/offchainlabs/bold-permissionless-validation-for-arbitrum-chains-9934eb5328cc) for more). ### Bonder A [Validator](/intro/glossary.md#validator) who deposits a bond (in Ether on [Arbitrum One](/intro/glossary.md#arbitrum-one) and [Arbitrum Nova](/intro/glossary.md#arbitrum-nova) ) to vouch for a particular [assertion](/intro/glossary.md#assertion) in an Arbitrum Chain. A validator who bonds on a false assertion can expect to lose their bond. An honest bonder can recover their bond once the assertion they are bonded on has been confirmed. *Also known as: staker* ### Bridge A set of smart contracts for sending [Cross-chain messages](/intro/glossary.md#crosschain-message) between blockchains. Every [Arbitrum chain](/intro/glossary.md#arbitrum-chain) includes a bridge to/from its [Parent chain](/intro/glossary.md#parent-chain). ### Chain Owner An entity (i.e., a smart contract) with affordance to carry out critical upgrades to an Arbitrum chain's core protocol; this includes upgrading protocol contracts, setting core system parameters, and adding and removing other chain owners. ### Chain state A particular point in the history of an [Arbitrum chain](/intro/glossary.md#arbitrum-chain). A chain's state is determined by applying Arbitrum state-transition function to sequence of transactions (i.e., the chain's history). ### Challenge When two [bonders](/intro/glossary.md#bonder) disagree about the correct verdict on an [assertion](/intro/glossary.md#assertion), those bonders can be put in a challenge. The challenge is refereed by the contracts on the underlying chain. Eventually one bonder wins the challenge. The protocol guarantees that an honest party will always win a challenge; the loser forfeits their bond. ### Challenge bond The per-level [bond](/intro/glossary.md#bonder) required to open or move within a sub-challenge under [BoLD](/intro/glossary.md#bold). Each level of the dispute tree has its own configured bond amount, set at chain deployment. Challenge bonds are distinct from the larger assertion bond that backs the proposed [Assertion](/intro/glossary.md#assertion) itself. ### Challenge Period Window of time (one week on Arbitrum One) over which an [Assertion](/intro/glossary.md#assertion) can be challenged, and after which the assertion can be confirmed. ### Challenge protocol The protocol by which assertions are submitted, disputed, and ultimately confirmed. The Challenge Protocol guarantees that only valid [assertions](/intro/glossary.md#assertion) will be confirmed provided that there is at least one honest [active validator](/intro/glossary.md#active-validator). ### Child chain An Arbitrum Chain that settles to a [Parent chain](/intro/glossary.md#parent-chain). For example, Arbitrum One and Arbitrum Nova are child chains of Ethereum. ### Client A program running on a user's machine, often in the user's browser, that interacts with contracts on an [Arbitrum chain](/intro/glossary.md#arbitrum-chain) and provides a user interface. ### Commitment In the context of Arbitrum, commitments represents a part of a chain's history. Commitments are used during the fraud proof dispute resolution process, where validators create a Merkle commitment to the history between two assertions. This allows them to efficiently narrow down disagreements about the chain state by using Merkle proofs to specific blocks or states within that range[(1)](https://docs.arbitrum.io/how-arbitrum-works/bold/bold-technical-deep-dive). ### Confirmation The decision by an [Arbitrum chain](/intro/glossary.md#arbitrum-chain) to finalize an assertion as part of the chain's history. Once an [assertion](/intro/glossary.md#assertion) is confirmed its [Child-to-parent chain Messages](/intro/glossary.md#l2-to-l1-message) (e.g., withdrawals) can be executed. ### Cross-chain message An action taken on some chain A which asynchronously initiates an additional action on chain B. ### Custom Arb-Token Any child chain token contract registered to the [Arbitrum Token Bridge](/intro/glossary.md#arb-token-bridge) that isn't a standard arb-token (i.e., a token that uses any gateway other than the [`StandardERC20` gateway](/intro/glossary.md#standarderc20-gateway) ). ### Custom gas token A non-**ETH** **ERC-20** token chosen at [Arbitrum chain](/intro/glossary.md#arbitrum-chain) deployment time to be used for paying transaction fees on that chain. Deposits work by escrowing the **ERC-20** in the chain's bridge on the [parent chain](/intro/glossary.md#parent-chain) and crediting the equivalent amount as the native asset on the child chain, where it replaces **ETH** everywhere **ETH** would otherwise be charged for gas. ### Custom gateway Any [Token Gateway](/intro/glossary.md#token-gateway) that isn't the [`StandardERC20` gateway](/intro/glossary.md#standarderc20-gateway). ### Data Availability Certificate Signed promise from a [Data Availability Committee (DAC)](/intro/glossary.md#data-availability-committee-dac) attesting to the availability of a batch of data for an [Arbitrum AnyTrust Chain](/intro/glossary.md#arbitrum-anytrust-chain). ### Data Availability Committee (DAC) A permissioned set of parties responsible for enforcing data availability on a chain using the [Arbitrum AnyTrust Protocol](/intro/glossary.md#arbitrum-anytrust-protocol). See [Introducing AnyTrust Chains: Cheaper, Faster L2 Chains with Minimal Trust Assumptions](https://medium.com/offchainlabs/introducing-anytrust-chains-cheaper-faster-l2-chains-with-minimal-trust-assumptions-31def59eb8d7) to learn more. ### Defensive Validator A [Validator](/intro/glossary.md#validator) that watches an Arbitrum chain and takes action (i.e., bonds and challenges) only when and if an invalid [Assertion](/intro/glossary.md#assertion) occurs. ### Delayed Inbox A contract that holds [Parent chain](/intro/glossary.md#parent-chain) initiated messages to be eventually included in the [Sequencer Inbox](/intro/glossary.md#sequencer). Inclusion of messages doesn't depend on the [Sequencer](/intro/glossary.md#sequencer). ### Deterministic proving If challenged, state transitions are replayable and verified onchain. To achieve this, Arbitrum compiles the State Transition Function (STF) into different formats: * **Execution mode**: Uses Go's native compiler for high-performance execution on validator nodes. * **Proving mode**: Compiles to WebAssembly (WASM), which transforms into WebAssembly for Arbitrum Virtual Machine (WAVM) for fraud-proof verification. ### Dev-Tools Dashboard Web application built and maintained by [Offchain Labs](/intro/glossary.md#offchain-labs) for developers and users to debug Arbitrum transactions; i.e., executing or checking the status of [Cross-chain messages](/intro/glossary.md#crosschain-message); visit it [here](https://retryable-dashboard.arbitrum.io/). ### Dissection A step in the [Challenge protocol](/intro/glossary.md#challenge-protocol) in which two challenging parties interactively narrow down their disagreement until they reach a [One Step Proof](/intro/glossary.md#one-step-proof). ### Edge A primitive in the [BoLD](/intro/glossary.md#bold) [Challenge Protocol](/intro/glossary.md#challenge-protocol). An edge is a claim about a portion of an Arbitrum chain's history, identified by a starting and ending [history commitment](/intro/glossary.md#history-commitment). Validators bisect edges to narrow disagreement down to a single execution step. ### Ethereum Wallet A software application used for transacting with the Ethereum [Blockchain](/intro/glossary.md#blockchain). ### Execution claim A cryptographic commitment to the computed state. ### Express Lane A component of [Timeboost](/intro/glossary.md#timeboost), the express lane is a special endpoint on the [Sequencer](/intro/glossary.md#sequencer) that immediately sequences incoming, valid transactions signed by the current express lane controller. ### Express Lane Controller An address, defined in the [Auction Contract](/intro/glossary.md#auction-contract), that is granted the privilege to use the [Express Lane](/intro/glossary.md#express-lane). These privileges are granted after verifying that the incoming transactions were properly signed by the express lane controller, among other checks. ### Externally Owned Accounts An externally owned account (EOA) is the account (public/private key pairs) that has a physical address location. Commonly referred to as a [wallet](/intro/glossary.md#wallet), however, we distinguish an EOA from the user client software wallet. ### Fast Exit / Liquidity Exit A means by which a user can bypass an Arbitrum chain's [Challenge Period](/intro/glossary.md#challenge-period) when withdrawing fungible assets (or more generally, executing some "fungible" child chain-to-parent chain operation); for trustless fast exits, a liquidity provider facilitates an atomic swap of the asset on a child chain directly to a parent chain. ### Fast withdrawals A protocol-level withdrawal path on an [Arbitrum chain](/intro/glossary.md#arbitrum-chain) in which a configured validator committee attests to the withdrawn amount, allowing child-to-parent exits to settle in minutes rather than waiting out the full [challenge period](/intro/glossary.md#challenge-period). Distinct from a [fast-exit liquidity exit](/intro/glossary.md#fast-exit--liquidity-exit), which is a third-party liquidity provider rather than a protocol primitive. ### First Come First Serve (FCFS) A type of [Transaction Ordering Policy](/intro/glossary.md#transaction-ordering-policy) used by the sequencer in Arbitrum chains whereby incoming transactions are sequenced into a block in the order that the transactions arrived. ### Force-Inclusion Censorship resistant path for including a message into an Arbitrum chain via the [Delayed Inbox](/intro/glossary.md#delayed-inbox) on its [Parent chain](/intro/glossary.md#parent-chain); bypasses any Sequencer involvement. ### Forwarder In Arbitrum, a forwarder is a component that forwards user transactions to the Sequencer. Full nodes use the forwarder to send transactions they receive via RPC to the sequencer for ordering and execution. ### Fraud proof The means by which an [Active Validator](/intro/glossary.md#active-validator) proves to its underlying chain that an invalid state transition has taken place. ### Gas Price Floor Protocol-enforced minimum gas price on an Arbitrum chain. These values can be found on the [chain info page](/for-devs/dev-tools-and-resources/chain-info.md#chain-parameters). ### Gas target The target consumption rate for an Arbitrum chain. [Arbitrum One](/intro/glossary.md#arbitrum-one) and [Arbitrum Nova](/intro/glossary.md#arbitrum-nova). When gas usage exceeds this limit, fees rise. Read more in the [gas and fees article](/how-arbitrum-works/deep-dives/gas-and-fees.md). ### Gateway Router Contracts in the [Arbitrum Token Bridge](/intro/glossary.md#arb-token-bridge) responsible for mapping tokens to their appropriate [Token Gateway](/intro/glossary.md#token-gateway). ### Generic-Custom Gateway A particular [Custom gateway](/intro/glossary.md#custom-gateway) via which a parent chain token contract can be registered to a token contract deployed to a child chain. A useful alternative to the [`StandardERC20` gateway](/intro/glossary.md#standarderc20-gateway) for projects that wish to control the address of their child chain token contract, maintain the child chain token contract upgradability, and for various other use-cases. ### Geth An execution-layer client that defines the Ethereum state transition function and handles network-layer logic like transaction memory pooling. [Arbitrum Nitro](/intro/glossary.md#arbitrum-nitro) utilizes a fork of Geth to implement Arbitrum's state transition function. ### Hard finality The state a transaction reaches once its [batch](/intro/glossary.md#batch) has been posted to the parent chain by the [Batch Poster](/intro/glossary.md#batch-poster) and that posting is itself final on the parent chain. Hard finality inherits the security of the parent chain and cannot be reverted without a parent-chain reorg. Contrast with [soft confirmation](/intro/glossary.md#soft-confirmation), which is immediate but trust-based on the honesty of the [Sequencer](/intro/glossary.md#sequencer). ### History commitment A Merkle-tree root committing to a sequence of execution states—either block hashes or instruction-level state hashes, depending on the level of the dispute. Each [BoLD](/intro/glossary.md#bold) [edge](/intro/glossary.md#edge) carries a start and end history commitment to represent a claim over a range of chain history. ### Host I/O The set of low-level imports that a [Stylus](/intro/glossary.md#stylus) [WASM](/intro/glossary.md#wasm) program uses to access chain state and runtime services—the equivalent of EVM opcodes for Solidity contracts. Host I/O calls cover storage access, account info, environment data, math primitives, and re-entrancy controls. Each call has a fixed [ink](/intro/glossary.md#ink) cost. ### Ink The equivalent of gas in the [Stylus](/intro/glossary.md#stylus) vm. Ink is introduced for finer granularity than gas offers since Stylus's operations are considerably cheaper than their EVM analogs. ### Keyset The onchain configuration object that defines a [Data Availability Committee (DAC)](/intro/glossary.md#data-availability-committee-dac)—the [BLS public keys](/intro/glossary.md#bls-signature) of every committee member and the signature threshold required to produce a valid [Data Availability Certificate](/intro/glossary.md#data-availability-certificate). A new keyset is published whenever committee membership or the threshold changes. ### L1 See [Layer 1](/intro/glossary.md#layer-1-l1) ### L2 See [Layer 2](/intro/glossary.md#layer-2-l2). ### L2 Block Data structure that represents a group of L2 transactions (analogous to L1 blocks). ### L2 to L1 Message A message initiated from within an Arbitrum chain to be eventually executed on [Layer 1 (parent chain)](/intro/glossary.md#layer-1-l1) (e.g., token or Ether withdrawals). On Rollup chains like [Arbitrum One](/intro/glossary.md#arbitrum-one), the [Challenge Period](/intro/glossary.md#challenge-period) must pass before a child-to-parent chain message is executed. ### Layer 1 (L1) L1, or Layer 1, refers to the underlying blockchain responsible for maintaining the integrity of the distributed ledger and executing smart contracts. It contains both Ethereum's execution layer and consensus layer. In the context of Arbitrum One and Nova, L1 is the main Ethereum blockchain (Mainnet), and Arbitrum is a Layer 2 platform built on top of this base (Ethereum) layer. However, any EVM-compatible blockchain can be an L1 to an Arbitrum chain. ### Layer 2 (L2) An L2, or Layer 2, is a trustless scaling solution built on top of Ethereum's Layer 1 (L1) base protocol. Arbitrum is a Layer 2 platform that increases throughput and reduces the cost of transactions on Ethereum's (L1) without introducing additional trust assumptions. It does that by executing some computation offchain and batch-posting transactions on the underlying Layer 1 chain (Ethereum for Arbitrum One/Nova). ### Layer 3 (L3) An Arbitrum chain whose core contract reside on an Arbitrum [Layer 2 (L2)](/intro/glossary.md#layer-2-l2) chain. ### Layer Leap A protocol that allows users to bridge **ETH** and **ERC-20** tokens from Ethereum directly to an [Arbitrum chain](/intro/glossary.md#arbitrum-chain) in a single transaction, without first depositing onto an intermediate [parent chain](/intro/glossary.md#parent-chain). ### MultiVM MultiVM refers to Arbitrum's ability to support multiple virtual machines (VMs). Specifically, Arbitrum Stylus introduces a WebAssembly (WASM)-based virtual machine alongside the traditional Ethereum Virtual Machine (EVM). This means developers can write smart contracts in languages like Rust, C, or C++ (compiled to WASM) or continue using Solidity for the EVM, and both types of contracts can interact on the same chain. This approach preserves EVM compatibility while enabling more efficient execution and access to a broader set of programming languages and libraries. Learn more about [Stylus](/stylus/gentle-introduction.md) and language support. ### Native Fee Token An **ERC-20** token used as the native currency for gas fees on an [Arbitrum chain](/intro/glossary.md#arbitrum-chain) (i.e., as opposed to using Ether). [Arbitrum chains](/intro/glossary.md#arbitrum-chains) introduced the option for chains to use native fee tokens. ### Native mint/burn gas token A [custom gas token](/intro/glossary.md#custom-gas-token) variant whose supply on the [Arbitrum chain](/intro/glossary.md#arbitrum-chain) is controlled by mint and burn calls to the `ArbNativeTokenManager` precompile (address `0x73`) rather than by bridging from the parent chain. Only accounts in the chain's `nativeTokenOwners` set—managed through the `ArbOwner` precompile—may mint or burn. Suited to closed-economy chains that need full control over gas-token issuance. ### Offchain Labs The initial builders Arbitrum; current contributors to the Arbitrum ecosystem and service providers to the [Arbitrum DAO](https://docs.arbitrum.foundation/gentle-intro-dao-governance). Offchain also runs and maintains the [Sequencers](/intro/glossary.md#sequencer) for [Arbitrum One](/intro/glossary.md#arbitrum-one) and [Arbitrum Nova](/intro/glossary.md#arbitrum-nova). ### One Step Proof Final step in a challenge; a single operation of the Arbitrum VM ([WASM](/intro/glossary.md#wasm)) is executed on the underlying chain, and the validity of its state transition is verified. ### Oracle Oracles are third-party services that provide smart contracts with external information. They act as a bridge between blockchains and the outside world, which expands their functionality by enabling smart contracts to access data beyond their native networks. ### Outbox A parent chain contract responsible for tracking [child-to-parent chain message](/intro/glossary.md#l2-to-l1-message)s, including withdrawals, which can be executed once they are confirmed. The outbox stores a Merkle root of all outgoing messages. ### Parent chain EVM compatible chain that acts as the settlement layer for one or more Arbitrum Chains (aka [Child chain](/intro/glossary.md#child-chain) ). For example, Ethereum is the parent chain of both Arbitrum One and Arbitrum Nova. Parent chain is synonymous with "underlying chain." ### Permissionless validation Anyone can become a validator by running an Arbitrum node, without permission. ### Portal A web application maintained by [Offchain Labs](/intro/glossary.md#offchain-labs) showcasing the Arbitrum ecosystem; visit it [here](https://portal.arbitrum.io/). ### Prechecker Node An [Arbitrum full node](/intro/glossary.md#arbitrum-full-node) configured to pre-validate transactions before forwarding `eth_sendRawTransaction` to the [Sequencer](/intro/glossary.md#sequencer), insulating it from transactions that would fail. At a configurable strictness level, a prechecker verifies the transaction type, signature, intrinsic gas (including L1 calldata gas), fee cap, nonce, sender balance, and any `eth_sendRawTransactionConditional` storage conditions. When [compliance filtering](/launch-arbitrum-chain/chain-config/sequencer/compliance-filtering.md) is enabled, it also rejects transactions that touch restricted addresses. ### Precompile A precompile is a predefined smart contract with a special address that provides specific functionality executed natively by the Arbitrum client, rather than at the EVM bytecode level. Precompiles are used to introduce functions that would be computationally expensive if run in EVM bytecode, or to facilitate interactions between the parent and child chains. Arbitrum supports all Ethereum precompiles and also provides additional precompiles specific to Arbitrum chains, which can be called from smart contracts like regular Solidity functions[(1)](https://docs.arbitrum.io/build-decentralized-apps/precompiles/overview). ### Predecessor Assertion The last confirmed valid state of the chain. ### Proposer A [Validator](/intro/glossary.md#validator) configured to actively submit new [Assertions](/intro/glossary.md#assertion) to the parent chain under [BoLD](/intro/glossary.md#bold). Proposers post the largest bonds in the staking hierarchy to deter delay attacks. Contrast with a [Watchtower Validator](/intro/glossary.md#watchtower-validator), which only observes, and a [Defensive Validator](/intro/glossary.md#defensive-validator), which acts only to counter an invalid claim. ### raas RaaS (Rollup as a Service) is a platform that provides the necessary infrastructure and tools to deploy and operate blockchain Rollups, eliminating the need for teams to build the underlying technical infrastructure themselves. It typically includes pre-built Rollup software, node hosting, data availability solutions, and monitoring tools, allowing developers to focus on their application logic rather than the complex technical implementation of Rollup technology. ### RBlock Refer to [Assertion](/intro/glossary.md#assertion) ### Reorg A situation in which transactions on a chain that were at some point considered accepted then get rejected. In the context of an Arbitrum chain, once transactions are posted in the chain's [Sequencer Inbox](/intro/glossary.md#sequencer-inbox), the only way the chain can experience a reorg is if its [Underlying Chain](/intro/glossary.md#underlying-chain) itself reorgs. Of note, [Fraud proofs](/intro/glossary.md#fraud-proof) do not cause reorgs. ### Retryable Autoredeem The "automatic" (i.e., requiring no additional user action) execution of a [Retryable Ticket](/intro/glossary.md#retryable-ticket) on an Arbitrum chain. ### Retryable Redeem The execution of a [Retryable Ticket](/intro/glossary.md#retryable-ticket) on a child chain; can be automatic (see [Retryable Autoredeem](/intro/glossary.md#retryable-autoredeem)) or manual via a user-initiated child chain transaction. ### Retryable Ticket A parent-to-child cross-chain message initiated by a parent chain transaction sent to an Arbitrum chain for execution (e.g., a token deposit). ### Reverse Token Gateway A [Token Gateway](/intro/glossary.md#token-gateway) in which the [Child chain](/intro/glossary.md#child-chain) gateway contract escrows and releases tokens, which the [Parent chain](/intro/glossary.md#parent-chain) Gateway contract mints and burns tokens. This in the inverse to how "typical" gateways work. ### Rival In the [BoLD](/intro/glossary.md#bold) [Challenge Protocol](/intro/glossary.md#challenge-protocol), two [edges](/intro/glossary.md#edge) are *rivals* when they share a starting point but make incompatible claims about the same range of chain history. Rivaling an edge stops its unrivaled timer and unlocks [bisection](/intro/glossary.md#bisection) moves to resolve the disagreement. ### Rollup Event Inbox A parent chain contract (one per [Arbitrum chain](/intro/glossary.md#arbitrum-chain)) that serves as an authorized [delayed inbox](/intro/glossary.md#delayed-inbox) used exclusively by the Rollup contract to communicate chain-level protocol events to the child chain. It is separate from the regular `Inbox`, `Bridge`, and `SequencerInbox` contracts that handle user transactions. Its main role is to seed the child chain with its bootstrap configuration. During initialization, the Rollup admin contract calls `rollupInitialized(chainId, chainConfig, l1BaseFeeEstimate)` exactly once; this enqueues a delayed message of type `INITIALIZATION_MSG_TYPE` carrying the chain's `chainId`, full chain config JSON, and an initial parent-chain base-fee estimate. That delayed message is then read by the child chain as its very first L2 message, which initializes the L2 state. Two implementations share the `IRollupEventInbox` interface and `AbsRollupEventInbox` base: `RollupEventInbox` for **ETH**-based chains, and `ERC20RollupEventInbox` for chains using a custom (**ERC-20**) gas token. The contract is deployed by `BridgeCreator` and registered as an allowed delayed inbox on the `Bridge`. In some support contexts the contract is informally referred to as "rollupEventBox"; the canonical name in the codebase is `RollupEventInbox`. ### Rollup Improvement Proposal A Rollup Improvement Proposal (RIP) is a process for proposing, discussing, and recording changes or additions to Ethereum’s rollup ecosystem. ### Sequencer An entity (currently a single-party on Arbitrum One) given rights to order transactions in the [Sequencer Inbox](/intro/glossary.md#sequencer-inbox) over a fixed window of time, who can thus give clients sub-blocktime [Soft Confirmations](/intro/glossary.md#soft-confirmation). (Not to be confused with a [Validator](/intro/glossary.md#validator)). ### Sequencer Feed Offchain data feed published by the [Sequencer](/intro/glossary.md#sequencer) which clients can subscribe to for [Soft Confirmations](/intro/glossary.md#soft-confirmation) of transactions before they are posted in [batches](/intro/glossary.md#batch). ### Sequencer Inbox Contract that holds a sequence of messages sent by clients to an Arbitrum Chain; a message can be put into the Sequencer Inbox directly by the [Sequencer](/intro/glossary.md#sequencer) or indirectly through the [Delayed Inbox](/intro/glossary.md#delayed-inbox). ### Shared Sequencing A protocol design space in which multiple rollups use the same entity as their [Sequencer](/intro/glossary.md#sequencer); potential benefits include enhanced interoperability and credible neutrality. ### Smart Contract A computer program whose operations are defined and executed within a blockchain consensus protocol. ### Soft Confirmation A semi-trusted promise from the [Sequencer](/intro/glossary.md#sequencer) to post a user's transaction in the near future; soft-confirmations happen prior to posting on the [Parent chain](/intro/glossary.md#parent-chain), and thus can be given near-instantaneously (i.e., faster than the parent chain's block times) ### Standard Arb-Token An token contract on an Arbitrum chain deployed via the [`StandardERC20` gateway](/intro/glossary.md#standarderc20-gateway); offers basic **ERC-20** functionality in addition to deposit/withdrawal affordances. ### StandardERC20 gateway [Token Gateway](/intro/glossary.md#token-gateway) via which any underlying chain's **ERC-20** token can permissionlessly bridge; the `StandardERC20` gateway contracts deploy a [Standard Arb-Token](/intro/glossary.md#standard-arbtoken) on the [Child chain](/intro/glossary.md#child-chain) for each bridged token. ### State Transition Function The STF (State Transition Function) defines how new blocks are produced from input messages (i.e., transactions) on an Arbitrum chain. The State Transition Function's output is the result of applying those input messages (transactions). ### Stylus Upgrade to the [Arbitrum Nitro](/intro/glossary.md#arbitrum-nitro) virtual machine that allows smart contract support for languages like Rust and C++ by taking advantage of Nitro's use of WASM. Currently on testnet ([read more](https://docs.arbitrum.io/stylus/stylus-gentle-introduction)). ### Timeboost A transaction ordering policy in which entities can bid for the right to access an express lane on the [Sequencer](/intro/glossary.md#sequencer) for faster transaction inclusion. See the [research specification](https://github.com/OffchainLabs/timeboost-design/tree/main) to learn more. ### Token Gateway A pair of contracts in the token bridge—one on the [Parent chain](/intro/glossary.md#parent-chain) , one on the [Child chain](/intro/glossary.md#child-chain)—that provide a particular mechanism for handling the transfer of tokens between layers. Token gateways currently active in the bridge include the [`StandardERC20` gateway](/intro/glossary.md#standarderc20-gateway) , the [Generic-Custom Gateway](/intro/glossary.md#genericcustom-gateway) , and the [**WETH** Gateway](/intro/glossary.md#weth-gateway). ### Transaction A user-initiated interaction with a Blockchain. Transactions are typically signed by users via wallets and are paid for via transaction fees. ### Transaction Ordering Policy The rules and logic employed by a chain to order incoming transactions into a block. ### Trustless In the context of Ethereum, trustless refers to the ability of a system to operate without reliance on a central authority or intermediary. Instead, users place their trust in math and protocols. This is achieved through the use of cryptographic techniques and decentralized consensus mechanisms that let users verify the integrity of network transactions using open-source software. Trustless systems are considered to be more secure and resistant to fraud or tampering because they don't rely on a single point of failure that can be exploited by attackers. ### Trustless bonding pool A smart contract that allows multiple participants to pool funds and post an [Assertion](/intro/glossary.md#assertion) or [challenge bond](/intro/glossary.md#challenge-bond) under [BoLD](/intro/glossary.md#bold) without trusting one another. Refunds and rewards are distributed proportionally on resolution, so any honest party can contribute to defending the chain without staking the full bond alone. ### Trustless verification Validators confirm assertions, ensuring transactions adhere to the protocol rules. ### Underlying Chain Synonymous with [Parent chain](/intro/glossary.md#parent-chain). ### Upgrade Executor The single privileged contract—deployed once per [Arbitrum chain](/intro/glossary.md#arbitrum-chain) by the rollup creator—that authorizes protocol upgrades and admin actions. The Upgrade Executor owns both the chain's rollup contracts and its `ProxyAdmin`, so every privileged call to the core contracts is routed through it. Its admin role is typically held by the [chain owner](/intro/glossary.md#chain-owner). ### Validator An [Arbitrum Full Node](/intro/glossary.md#arbitrum-full-node) that tracks the status of the chains' [Assertion](/intro/glossary.md#assertion)s. A validator may be a [Watchtower Validator](/intro/glossary.md#watchtower-validator), a [Defensive Validator](/intro/glossary.md#defensive-validator), or an [Active Validator](/intro/glossary.md#active-validator). ### Wallet When referring to a wallet, we mean the user client software enabling user actions. Often, client software is a browser extension, mobile, or desktop app. Also refer to [Externally Owned Accounts](/intro/glossary.md#externally-owned-accounts). ### WASM Widely supported binary code format for executable programs. Used by [Arbitrum Nitro](/intro/glossary.md#arbitrum-nitro) for [Fraud proofs](/intro/glossary.md#fraud-proof) , and more broadly used by [Stylus](/intro/glossary.md#stylus) to support performant smart contracts in a wide variety of languages. ### WASM module root A 32-byte cryptographic hash that uniquely identifies a specific version of Arbitrum's [State Transition Function](/intro/glossary.md#state-transition-function) (STF), compiled to [WASM](/intro/glossary.md#wasm) for use in [fraud proofs](/intro/glossary.md#fraud-proof). It is computed as the Merkle root over the Keccak256 hashes of every module in the compiled prover image—the main STF replay binary plus runtime libraries such as `soft-float`, `host_io`, `user_host`, and `program_exec`. The same WASM module root appears in two places: * **Onchain**, as the `wasmModuleRoot` field on the Rollup contract. It is set at deployment and updated by the chain owner via `setWasmModuleRoot`; every [Assertion](/intro/glossary.md#assertion) records the root used to compute it. * **Offchain**, by every [Validator](/intro/glossary.md#validator), as a compiled prover image under `target/machines//`. To validate or challenge an assertion, a validator must have the prover image matching the root recorded on **that** assertion—which may be the current onchain root, or an older one if the assertion predates an [ArbOS](/intro/glossary.md#arbos) upgrade. Every ArbOS upgrade changes the STF and therefore produces a new WASM module root. Validators must retain the prover images for every older root they may still need to validate or dispute historical assertions against. ### WASMer A popular WebAssembly runtime for executing [WASM](/intro/glossary.md#wasm) binaries. [A fork of WASMer](https://github.com/OffchainLabs/wasmer) is used for executing [Stylus](/intro/glossary.md#stylus) programs. WASMer executes considerably faster than Geth executes EVM code, contributing to Stylus's lower fees. ### Watchtower Validator A [Validator](/intro/glossary.md#validator) that never bonds / never takes on chain action, who raises the alarm (by whatever offchain means it chooses) if it witnesses an invalid assertion. ### WAVM Arbitrum's variant of [WASM](/intro/glossary.md#wasm), used inside the [State Transition Function](/intro/glossary.md#state-transition-function) for [fraud-proof](/intro/glossary.md#fraud-proof) execution. WAVM constrains standard WASM so that execution is deterministic and provable onchain—non-deterministic instructions are removed and floating-point operations are replaced with software implementations. The specific WAVM prover image in use is identified by the [WASM module root](/intro/glossary.md#wasm-module-root). ### WETH Gateway [Token Gateway](/intro/glossary.md#token-gateway) for handing the bridging of wrapped Ether (**WETH**). **WETH** is unwrapped on the parent chain and rewrapped on the parent chain upon depositing (and vice-versa upon withdrawing), ensuring **WETH** on the child chain always remains collateralized. --- > For a complete page index, fetch # Additional configuration parameters: Arbitrum chains The following configuration parameters can be used when deploying or managing your Arbitrum chain: | Parameter | Description | How to set | | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Extra challenge period blocks** | Amount of time to wait before a challenge period expires. Like the challenge period parameter, this is measured in blocks on the underlying L1 chain, not the base (L2) chain. The default for this parameter is 200 blocks, or roughly 40 minutes. | Either in the `extraChallengeTimeBlocks` field in the `RollupCreator` config, or by calling `Rollup.setExtraChallengeTimeBlocks()`. | | **Loser stake escrow** | The address where funds bonded by a validator that has lost a challenge are sent to be escrowed. It is recommended that this be set to an address that is controlled by the chain owners or to the burn address if you want escrowed funds to be lost. | Either in the `loserStakeEscrow` field in the `RollupCreator` config, or by calling `Rollup.setLoserStakeEscrow()`. | | **WASM module root** | Hash of the WASM module root to be used when validating. The WASM module root is a 32 byte hash usually expressed in hexadecimal which is a merkelization of the replay binary, which is too large to be posted onchain. This hash is set in the L1 Rollup contract to determine the correct replay binary during fraud proofs. Unless the STF has been customized, the default WASM module root in the latest consensus release should be used. | Either in the `wasmModuleRoot` field in the `RollupCreator` config, or by calling `Rollup.setWasmModuleRoot()`. | | **Gas target** | Target gas usage per second, over which the congestion mechanism activates. For the values set on Arbitrum One and Nova, [refer to child chain gas fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#child-chain-gas-fees). Alterations to this should be considered carefully, as setting it too high may result in state bloat that impacts the performance of the chain. | Call `ArbOwner.setSpeedLimit()` passing in the maximum number of gas units to be executed per second for single gas target. Call `ArbOwner.setGasPricingConstraints()` with an array of gas targets for [setting up dynamic pricing](/launch-arbitrum-chain/chain-config/costs/dynamic-pricing.md). | | **Block gas limit** | Maximum amount of gas that can be consumed by all of the transactions within a block. On Arbitrum One this is set to 30 million. It can comfortably be set higher, but may harm UX as the processing time of a block will increase correspondingly. | Call `ArbOwner.setMaxTxGasLimit()` passing in the maximum number of gas units to be executed per block and transaction. | | **Gas price floor** | Minimum gas price and is defaulted to 0.1 `gwei`. This can be set lower or higher as needed, and will impact the willingness of users to transact on the network. | Either in the `minL2BaseFee` field in the Arbitrum chain setup script config or by calling `ArbOwner.setMinimumL2BaseFee()` passing in the minimum base fee in `wei`. | | **Network fee account** | Account that will receive the L2 surplus fees. It is recommended this is set to an address controlled by the chain owners, or the burn address if fees are intended to be burned. If set to zero, this defaults to the owner address. | Either in the `networkFeeReceiver` field in the Arbitrum chain setup script config or by calling `ArbOwner.setNetworkFeeAccount()`. | | **Infrastructure fee account** | Account that will receive the L2 base fees. It is recommended this is set to an address controlled by the chain owners, or the burn address if fees are intended to be burned. If set to zero, this defaults to the owner address. | Either in the `infrastructureFeeCollector` field in the Arbitrum chain setup script config or by calling `ArbOwner.setInfraFeeAccount()`. | | **L1 pricing reward recipient** | Address that will receive the rewards from the L1 fees. It is recommended this is set to an address controlled by the chain owners, or the burn address if fees are intended to be burned. By default, this is set to the owner address. | Call `ArbOwner.setL1PricingRewardRecipient()`. | | **L1 pricing reward per unit (rate)** | Amount of rewards per unit to send to the L1 pricing reward recipient (multiplied by the unitsAllocated). The default for this parameter is 15 `wei`. | Call `ArbOwner.setL1PricingRewardRate()` passing in the amount of `wei` per unit to reward. | | **Sequencer inbox maximum time variation** | Boundaries of the sequencer to manipulate blocks and timestamps. The default values are as follows, and are set as such on Arbitrum One: - `delayBlocks`: 5760
- `futureBlocks`: 12
- `delaySeconds`: 86400
- `futureSeconds`: 3600 | Either in the `sequencerInboxMaxTimeVariation` field in the `RollupCreator` config or by calling `SequencerInbox.setMaxTimeVariation` on the parent chain. | | **Force-include period** | Length of the period after which a delayed message can be included into the inbox without any action from the sequencer, measured in L1 block time. | Corresponds to `delayBlocks` and `delaySeconds` in the sequencer inbox maximum time variation above. | | **Batch posting minimum frequency** | Maximum time to wait after a transaction is sent to post a batch containing it. Note that if no transactions are sent, no batches will be posted, regardless of this setting. The default setting is one hour, and can be set lower but may reduce efficiency in the case of low activity on the Arbitrum chain. | `--node.batch-poster.max-delay` in the batch poster config. | | **Validator node (branch) creation frequency** | Minimum time to wait since the last assertion to post a new assertion, if configured to post new assertions (`MakeNodes`). This is bypassed if there is an incorrect assertion and a dispute needs to be made by making a new assertion. Note that if no new batches are posted (and no force inclusion happens), no new assertions will be posted, regardless of this setting. The default setting is 1 hour and is alterable but should always be greater than the rollup contract's `minimumAssertionPeriod`, which is measured in L1 blocks and is defaulted to 75 blocks, or roughly 15 minutes. | `--node.staker.make-assertion-interval` in the validator config. | --- > For a complete page index, fetch # Batch Poster Batch posting configurations allow the [sequencer](/how-arbitrum-works/deep-dives/sequencer.md) to group, compress, and post transaction batches from the Arbitrum chain (L3) to its parent chain (typically an L2 such as Arbitrum One or Nova). Key parameters include the maximum delay before posting a batch and the maximum size of a batch to be posted, both of which control the frequency and timeliness of postings. These help prevent transaction reordering by ensuring that the sequencer commits batches in the order transactions are received, posting them atomically to the parent chain's inbox contract. This batch posting creates an immutable record on L1 (via the L2), reducing the risk of malicious reordering. For chain reorganizations, timely batch posting minimizes the window for L1 reorgs to affect unposted data, as posted batches become part of the canonical chain history. [Assertion](/how-arbitrum-works/deep-dives/assertions.md) posting configurations enable validators to periodically create and post assertions (state roots) of the Arbitrum chain's execution to the parent chain. Parameters control the interval between assertions and staking requirements. These prevent reordering and reorgs by allowing any honest validator to challenge incorrect assertions during a dispute window (typically seven days on Arbitrum chains). Regular assertions ensure state commitments are frequent enough to detect discrepancies early, triggering onchain resolution via bisection games, which enforce the correct chain state and deter reorganizations. ## Batch posting minimum frequency ### Maximum wait time to post a batch The batch posting minimum frequency is primarily controlled by the `--node.batch-poster.max-delay` parameter in the [Nitro node's configuration](https://github.com/OffchainLabs/nitro/blob/master/arbnode/batch_poster.go) (set via the JSON config file or command-line flags when deploying an Arbitrum chain). This parameter defines the maximum time the batch poster will wait after receiving a transaction before posting a batch that includes it. The default value is one hour (3600 seconds). * **Configuration options**: Set this in the `node.batch-poster` section of the config, e.g., `"max-delay": "30m"` for a 30-minute maximum wait. Lower values increase posting frequency but may result in smaller, less efficient batches during periods of low activity, thereby increasing gas costs on the parent chain. If no transactions are received, no batches will post regardless of this setting. * **Prevention of issues**: A shorter max delay reduces the opportunity for transaction reordering by minimizing the time transactions sit uncommitted in the sequencer's mempool. It also limits exposure to chain reorgs, as batches post sooner, anchoring them to the parent chain before potential L1 fluctuations can invalidate sequencing. However, extremely low settings (e.g., seconds) could spam the parent chain with tiny batches, resulting in increased costs without any benefits. * **Recommended settings**: For high-throughput chains, set to 5-15 minutes to balance latency and efficiency. For low-activity chains, stick to the default one hour to avoid unnecessary postings. ### Maximum batch size to post a batch The maximum size of a batch controls when the batch poster posts based on data volume. If the total queued but unposted transaction size reaches this number, the batch poster will post the batch. It is controlled by the `--node.batch-poster.max-calldata-batch-size` parameter in the [Nitro node's configuration](https://github.com/OffchainLabs/nitro/blob/master/arbnode/batch_poster.go) (set via the JSON config file or command-line flags when deploying an Arbitrum chain). This parameter replaces the deprecated `--node.batch-poster.max-size`; on AnyTrust chains, the size of batches sent to the DAC is controlled separately by `--node.da.anytrust.max-batch-size` (default 1,000,000 bytes). * **Configuration options**: Set this in the `node.batch-poster` section of the config, e.g., `"max-calldata-batch-size": "100000"` for 100,000 bytes. Lower values increase posting frequency but may result in smaller, less efficient batches during periods of low activity, thereby increasing gas costs on the parent chain. If no transactions are received, no batches will post regardless of this setting. * **Prevention of issues**: A smaller max size reduces the opportunity for transaction reordering by minimizing the time transactions sit uncommitted in the sequencer's mempool. It also limits exposure to chain reorgs, as batches post sooner, anchoring them to the parent chain before potential L1 fluctuations can invalidate sequencing. However, extremely low settings (e.g., a few hundred bytes) could spam the parent chain with tiny batches, resulting in increased costs without any benefits. * **Recommended settings**: For high-throughput chains, consider a lower max size to ensure frequent posting. For low-activity chains, the default value is generally sufficient to avoid unnecessary postings. --- > For a complete page index, fetch # Enabling blob transactions for Arbitrum batch poster This guide explains how to configure your Arbitrum node to post [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) blob transactions to the parent chain, which can significantly reduce data availability costs. ## Prerequisites Before enabling blob transactions, verify that your setup meets these requirements: ### 1. Chain configuration * Your Arbitrum chain must be running in **Rollup mode** ### 2. Parent chain compatibility Your parent chain (typically Ethereum mainnet or a testnet) must support [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844). You can verify this by checking that recent block headers contain: * `ExcessBlobGas` field * `BlobGasUsed` field ### 3. ArbOS version Your [ArbOS](/how-arbitrum-works/deep-dives/arbos.md) version must be **20 or higher**. To check your current version: #### Method 1: Smart contract call Call the `arbOSVersion()` function on the ArbSys [precompile](/arbitrum-essentials/precompiles/reference.md) contract: * Contract address: `0x0000000000000000000000000000000000000064` * Function: `arbOSVersion()` returns `uint256` * You can call this using any Ethereum client or block explorer on your Arbitrum chain #### Method 2: Using `cast` (if you have Foundry installed) ```shell cast call 0x0000000000000000000000000000000000000064 "arbOSVersion()" --rpc-url YOUR_ARBITRUM_RPC_URL ``` If your version is below 20, upgrade by following the [ArbOS upgrade guide](/launch-arbitrum-chain/operate/arbos-upgrade.md). ## Configuration To enable blob transaction posting, add the following configuration to your [Batch Poster](/how-arbitrum-works/deep-dives/assertions.md) node: ```json { "node": { "batch-poster": { "post-4844-blobs": true } }, "parent-chain": { "blob-client": { "beacon-url": "YOUR_BEACON_URL" } } } ``` After updating your configuration: 1. Save the configuration file 2. Restart your Arbitrum node 3. Monitor the logs to confirm blob posting is active ## Verification Once restarted, you can verify that blob transactions are being posted successfully by monitoring your node logs. ### Log messages to look for When a blob transaction is successfully posted, you'll see a log entry similar to: ```shell INFO [05-23|00:49:16.160] BatchPoster: batch sent sequenceNumber=6 from=24 to=28 prevDelayed=13 currentDelayed=14 totalSegments=9 numBlobs=1 ``` **Key indicator**: The `numBlobs` field shows the number of blobs included in the transaction: * `numBlobs=0`: Traditional calldata transaction was posted * `numBlobs>0`: Blob transaction was successfully posted (in the example above, 1 blob was sent) ## Troubleshooting ### Why is my node still posting calldata instead of blobs? Your node may continue using calldata in these scenarios: 1. **Cost optimization**: When blob gas prices are high, calldata posting may be more economical, but you can set the `--node.batch-poster.ignore-blob-price` flag to `true` to force the batch poster to use blobs. 2. **Batch Type Switching Protection**: After a non-blob transaction is posted, the next 16 transactions will also use calldata to prevent frequent switching Check your node logs for blob-related error messages and verify that your parent chain is accessible and fully synced. ## Optional parameters You can also set the following optional parameters to control blob posting behavior: | Flag | Description | | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--node.batch-poster.ignore-blob-price` | Boolean. Default: `false`. If the parent chain supports `EIP-4844` blobs and `ignore-blob-price` is set to `true`, the batch poster will use `EIP-4844` blobs even if using calldata is cheaper. Can be `true` or `false`. | | `--parent-chain.blob-client.authorization` | String. Default: `""`. Value to send with the HTTP Authorization: header for Beacon REST requests, must include both scheme and scheme parameters | | `--parent-chain.blob-client.secondary-beacon-url` | String. Default: `""`. Value to send with the HTTP Authorization: header for Beacon REST requests, must include both scheme and scheme parameters | | `--node.batch-poster.data-poster.blob-tx-replacement-times` | durationSlice. Default: `[5m0s,10m0s,30m0s,1h0m0s,4h0m0s,8h0m0s,16h0m0s,22h0m0s]`. comma-separated list of durations since first posting a blob transaction to attempt a replace-by-fee | | `--node.batch-poster.data-poster.max-blob-tx-tip-cap-gwei` | float. Default: `1`. the maximum tip cap to post `EIP-4844` blob-carrying transactions at | | `--node.batch-poster.data-poster.min-blob-tx-tip-cap-gwei` | float. Default: `1`. the minimum tip cap to post `EIP-4844` blob-carrying transactions at | --- > For a complete page index, fetch # Batch poster fee tuning The [Batch Poster](/how-arbitrum-works/deep-dives/assertions.md) relies on an internal component, the data poster, to manage gas pricing for transactions submitted to the parent chain. When posting batches—whether as traditional calldata or [`EIP-4844`](https://eips.ethereum.org/EIPS/eip-4844) blob transactions—the data poster estimates fees, sets gas price caps, and handles Replace-by-Fee (RBF) escalation when transactions aren't included promptly. This guide covers how to tune fee-related parameters to balance cost efficiency against batch posting reliability. For initial setup of blob posting, see [Enabling blob transactions](/launch-arbitrum-chain/chain-config/batch-poster/enable-4844-blobs.md). For general batch poster configuration, see [Run a batch poster](/launch-arbitrum-chain/run-a-node/batch-poster.md). ## Blob transaction fee configuration [`EIP-4844`](https://eips.ethereum.org/EIPS/eip-4844) introduces a separate fee market for blob transactions. Blob transactions have two fee components: * **Blob base fee**: Set by the protocol based on blob demand. This isn't directly configurable—it's determined by the parent chain's blob gas pricing mechanism. * **Priority fee (tip cap)**: The amount the batch poster is willing to pay per gas as a tip to incentivize block producers to include the blob transaction. The data poster controls blob transaction tips through two parameters: | Flag | Default | Description | | ---------------------------------------------------------- | ------- | -------------------------------------------------------------- | | `--node.batch-poster.data-poster.max-blob-tx-tip-cap-gwei` | `1` | The maximum tip cap (in gwei) for `EIP-4844` blob transactions | | `--node.batch-poster.data-poster.min-blob-tx-tip-cap-gwei` | `1` | The minimum tip cap (in gwei) for `EIP-4844` blob transactions | By default, both parameters are set to 1 gwei, using a fixed tip for blob transactions. ### When to adjust blob tip caps * **Blob transactions not being included**: If your blob transactions are consistently pending in the mempool, the tip cap may be too low relative to other blob submitters. Increase `max-blob-tx-tip-cap-gwei` to give the data poster room to offer higher tips during congestion. * If network activity is low and blobs are included quickly, defaults suffice. The base fee auto-adjusts; tip changes matter mainly during high demand. * **Spreading the RBF range**: Setting `min-blob-tx-tip-cap-gwei` below max allows the data poster to start low and escalate tips via RBF—helpful for variable congestion. ### Configuration example To allow the data poster to start with a `1 gwei` tip and escalate up to `5 gwei` through RBF: ```json { "node": { "batch-poster": { "data-poster": { "min-blob-tx-tip-cap-gwei": 1, "max-blob-tx-tip-cap-gwei": 5 } } } } ``` ## Gas price spike behavior When parent chain gas prices rise sharply, the data poster's behavior is governed by fee cap parameters that control how high it bids to get batch transactions included. ### Fee cap parameters | Flag | Description | | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--node.batch-poster.data-poster.target-price-gwei` | The maximum target price the data poster is willing to pay when there's no backlog (default: 60). The formula for this is: [Default formula](https://github.com/OffchainLabs/nitro/blob/02a590530cffa9a1f79ab8f692d66582b2ec4371/arbnode/dataposter/data_poster.go#L1451): `((BacklogOfBatches _ UrgencyGWei) ** 2) + ((ElapsedTime/ElapsedTimeBase) ** 2) _ ElapsedTimeImportance + TargetPriceGWei` | * **Trade-off**: Setting this value too low risks halting batch posting during gas spikes, causing a backlog of unposted batches. Setting it too high means the batch poster may post at very expensive gas prices. Choose a value that reflects your cost tolerance while ensuring batches continue to post during moderate price increases. ### Replace-by-Fee escalation When a batch transaction is pending in the parent chain mempool, the data poster uses RBF to gradually increase the gas price to improve inclusion chances. For blob transactions, the following parameter controls this escalation schedule: | Flag | Default | Description | | ----------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ | | `--node.batch-poster.data-poster.blob-tx-replacement-times` | `5m0s, 10m0s, 30m0s, 1h0m0s, 4h0m0s, 8h0m0s, 16h0m0s, 22h0m0s` | Durations since first posting a blob transaction at which to attempt RBF | Each duration triggers a replacement attempt with a higher fee. The schedule starts with an aggressive posture (five minutes, then ten minutes) and becomes more conservative over time. This prevents overspending while ensuring that the parent chain eventually includes the transactions. ### What happens during a gas spike 1. The data poster estimates the current gas price from the parent chain. 2. If the estimated price is within the configured fee cap, the batch is posted. 3. If the parent chain doesn't include the transaction, the RBF schedule triggers replacement attempts with incrementally higher fees. 4. If gas prices exceed [`node.batch-poster.data-poster.target-price-gwei` formula](https://github.com/OffchainLabs/nitro/blob/02a590530cffa9a1f79ab8f692d66582b2ec4371/arbnode/dataposter/data_poster.go#L1451), the data poster pauses new batch submissions until prices decrease. 5. During this pause, batches queue up. Once prices fall, the data poster resumes and works through the backlog. > **CAUTION** > > If the data poster pauses due to gas prices exceeding the fee cap for an extended period, unposted batches accumulate. Monitor your batch posting logs for signs of a growing backlog. See [Batch poster troubleshooting](/launch-arbitrum-chain/operate/batch-poster-troubleshooting.md) for guidance on diagnosing and resolving backlogs. ## Tuning recommendations The right fee configuration depends on your chain’s activity level and cost sensitivity. The following table provides general guidance: | Scenario | `max-blob-tx-tip-cap-gwei` | `min-blob-tx-tip-cap-gwei` | `node.batch-poster.data-poster.target-price-gwei` | `blob-tx-replacement-times` | Notes | | ------------------------ | -------------------------- | -------------------------- | ------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------- | | Low-activity chain | `1` (default) | `1` (default) | Conservative (low) | Default schedule | Batch posting is infrequent; gas spikes are unlikely to cause backlogs | | High-throughput chain | `5-10` | `1` | Moderate to high | Consider shorter initial intervals (e.g., `2m0s, 5m0s, ...`) | Timely batch posting is critical; willing to pay more for reliability | | Volatile parent chain | `10+` | `1` | High | Default or shorter intervals | Gas spikes are common; higher caps prevent frequent posting pauses | | Cost-sensitive operation | `1-2` | `1` | Low to moderate | Default schedule | Willing to tolerate occasional delays for lower costs | > **INFO** — Determining appropriate values > > The specific gwei values in the table above are illustrative. Appropriate values depend on your parent chain’s gas price history, your chain’s throughput requirements, and your operational budget. Monitor your batch posting costs and adjust based on observed behavior. Tools like parent chain gas trackers and your node’s batch posting logs provide the data needed to calibrate these settings. --- > For a complete page index, fetch # chainConfig reference The full `chainConfig` contains the standard Ethereum genesis/fork fields, but the [Chain SDK](https://github.com/OffchainLabs/arbitrum-chain-sdk)'s [`prepareChainConfig()`](https://github.com/OffchainLabs/arbitrum-chain-sdk/blob/main/src/prepareChainConfig.ts) only exposes a small subset for customization. Everything else is filled from hardcoded `defaults` and should not be changed. When you call `prepareChainConfig`, you pass a `chainId` (top-level) and an `arbitrum` object where `InitialChainOwner` is required and the fields below are optional overrides. **Example**: ```javascript import { prepareChainConfig } from '@arbitrum/chain-sdk'; const chainConfig = prepareChainConfig({ chainId: 123_456, arbitrum: { InitialChainOwner: '0xYourChainOwnerAddress', // required // Optional overrides (defaults shown): InitialArbOSVersion: 51, DataAvailabilityCommittee: false, MaxCodeSize: 24576, MaxInitCodeSize: 49152, }, }); ``` ## Customizable fields (via `prepareChainConfig`) | Field | Type | Description | | --------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `chainId` | `number` | **Required**. The unique chain ID for your chain. | | `InitialChainOwner` | `string` | **Required**. Address that owns the chain and holds upgrade/admin privileges. | | `InitialArbOSVersion` | `51` | The ArbOS version the chain launches with. | | `DataAvailabilityCommittee` | `false` | `false` = Rollup (L1 data posting); `true` = AnyTrust ([DAC](/launch-arbitrum-chain/chain-config/data-availability/dac-get-started.md)) | | `MaxCodeSize` | `24576` | Max deployed contract bytecode size in bytes (default matches Ethereum's EIP-170 limit). | | `MaxInitCodeSize` | `49152` | Max init/constructor code size in bytes (default matches EIP-3860). | ## Intentionally not customizable `prepareChainConfig` deliberately excludes Arbitrum-specific fields so they keep their defaults: | Field | Type | Description | | ----------------------- | ------- | -------------------------------------- | | `EnableArbOS` | `true` | Must stay enabled. | | `GenesisBlockNum` | `0` | Genesis block number. | | `AllowDebugPrecompiles` | `false` | Debug precompiles; must stay disabled. | ## Standard fields > **WARNING** — For informational purposes only > > The following fields are populated automatically from the SDK's hardcoded `defaults`. They are standard Ethereum genesis/fork-activation parameters and are **not** exposed for customization by `prepareChainConfig`. > > **Changing these fields can produce an invalid or non-functional chain configuration**. > > Leave them at their default values. | Field | Default | Description | | --------------------- | ------------- | ------------------------------------------------------ | | `homesteadBlock` | `0` | Block at which the Homestead fork activates. | | `daoForkBlock` | `null` | DAO fork block (disabled). | | `daoForkSupport` | `true` | Whether the chain supports the DAO fork rules. | | `eip150Block` | `0` | Activation block for EIP-150 (gas cost changes). | | `eip150Hash` | `0x0000…0000` | Hash associated with the EIP-150 fork. | | `eip155Block` | `0` | Activation block for EIP-155 (replay protection). | | `eip158Block` | `0` | Activation block for EIP-158 (state-clearing changes). | | `byzantiumBlock` | `0` | Activation block for the Byzantium fork. | | `constantinopleBlock` | `0` | Activation block for the Constantinople fork. | | `petersburgBlock` | `0` | Activation block for the Petersburg fork. | | `istanbulBlock` | `0` | Activation block for the Istanbul fork. | | `muirGlacierBlock` | `0` | Activation block for the Muir Glacier fork. | | `berlinBlock` | `0` | Activation block for the Berlin fork. | | `londonBlock` | `0` | Activation block for the London fork. | | `clique.period` | `0` | Clique PoA block period (seconds). | | `clique.epoch` | `0` | Clique PoA epoch length. | --- > For a complete page index, fetch # The AEP fee router: introduction ## What is the Arbitrum expansion program? The [Arbitrum Expansion Program](https://forum.arbitrum.foundation/t/the-arbitrum-expansion-program-and-developer-guild/20722) (AEP) allows Arbitrum chains to deploy on *any chain* permissionlessly. As part of the [AEP license](https://docs.arbitrum.foundation/aep/ArbitrumExpansionProgramTerms.pdf), Arbitrum Chains deployed outside of Arbitrum One and Arbitrum Nova must pay 10% of their **Protocol Net Revenue** to the Arbitrum Foundation. The Arbitrum Expansion Program and Developer Guild are initiatives launched in collaboration with Offchain Labs to promote the development of customized Arbitrum chains using the Arbitrum chain framework. The Expansion Program simplifies the process for teams to create Layer 2 (L2) and Layer 3 (L3) chains, offering self-service tools and customization options. Projects benefit from features like: * Dedicated block space * Custom gas tokens * Flexible governance. These chains can settle to any chain relying on the Ethereum security model. The Developer Guild incentivizes developers contributing to the Arbitrum codebase, with 2% of revenue from new chains going to a fund dedicated to this purpose. The Expansion Program is designed to align with Ethereum, encourage innovation, and enable projects to tailor the Arbitrum stack to their specific needs. The program also aims to streamline chain deployment, making it easier for developers to adopt Arbitrum's technology while contributing back to the community. As an Arbitrum Chain Owner, you may have the following questions: ## How do I send my AEP fees from my Arbitrum chain to the Arbitrum DAO? Arbitrum provides Arbitrum chains with easily deployable smart contracts that can streamline the transfer of AEP Fees to the Arbitrum Foundation that routes them to the Arbitrum DAO treasury. These contracts are known as **AEP Fee Routers**. ## What is protocol net revenue? Protocol Net Revenue is equivalent to an Arbitrum chain's profit (revenue minus costs). ## How can I ensure I'm complying with the AEP license? The Arbitrum Foundation will track compliance based on fees received through the **AEP Fee Router**. ## How can I set up an AEP fee router on my Arbitrum chain? You can learn how to set up your AEP fee router in the [implementation guide](/launch-arbitrum-chain/chain-config/costs/aep-router-contracts.md). --- > For a complete page index, fetch # How to set up an AEP fee router ## Quick start You can adopt the AEP Fee Router by using the [AEP Router deployment scripts](https://github.com/OffchainLabs/arbitrum-chain-sdk/tree/main/examples/setup-aep-fee-router) provided in the [Arbitrum chain SDK](https://github.com/OffchainLabs/arbitrum-chain-sdk/tree/main) ### Canonical contracts | Network | Contract | Address | Configured for | | ------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------- | | Ethereum | `Arbitrum Foundation's multisig wallet` | [0x1Afa41C006dA1605846271E7bdae942F2787f941](https://etherscan.io/address/0x1Afa41C006dA1605846271E7bdae942F2787f941) | **ETH**, **ERC-20** | | Arbitrum Nova | `Child2ParentRouter` | [0xd27cb0fe2a696ebaa80d606ce0edf55aabaeab84](https://nova.arbiscan.io/address/0xd27cb0fe2a696ebaa80d606ce0edf55aabaeab84) | **ETH** | | Base | `Child2ParentRouter` | [0xd9a2e0e5d7509f0bf1b2d33884f8c1b4d4490879](https://basescan.org/address/0xd9a2e0e5d7509f0bf1b2d33884f8c1b4d4490879) | **ETH** | ## The AEP fee router contract system This section describes the different fee distribution and router contracts that are available. You can find their source code in the [fund distribution contracts repository](https://github.com/OffchainLabs/fund-distribution-contracts/tree/main/src/FeeRouter). ### RewardDistributor The **AEP fee router** system relies on configuring an escrow contract as the intended reward address for protocol fee components. This intermediary contract is known as the `RewardDistributor.` The `RewardDistributor` is configured to separate the AEP portion of the fees from fees intended for the chain owner. A `rewardDistributor` will be deployed in three instances to collect from `L2Surplus`, `L2BaseFee`, `L1surplus` addresses. The `RewardDistributor` can be *permissionlessly* called to perform a withdrawal, which simultaneously transfers 90% of accrued fees to the chain’s fee collector and 10% of accrued fees to a target address on the parent chain. From here, the chain owner has complete control over their earned fees, and the routing contracts can direct AEP fees to a collecting address for the Arbitrum DAO. ### ChildToParentRouter AEP fees from the `RewardDistributor` must first be sent to Ethereum before they can be deposited to the DAO-controlled address on Arbitrum One. To facilitate this transfer to Ethereum, AEP fees are sent through a series of contracts known as `ChildToParentRouters.` The `ChildToParentRouter` is configured to withdraw a single token (immutable and specified at deployment) from the child chain to a specific target address on the parent chain: either another `ChildToParentRouter` or an address controlled by the Arbitrum Foundation on Ethereum. ## Deploying your AEP fee router contracts An Arbitrum chain is responsible for deploying all `ChildToParentRouters` necessary for their AEP funds to arrive at the address controlled by the Arbitrum Foundation on Ethereum. This includes: * Deploying a `ChildToParentRouter` on their Arbitrum chain configured for their gas token and configured to send funds to either: * An address controlled by the Arbitrum Foundation on Ethereum (assuming the network is a Layer-2) * Another `ChildToParentRouter` configured to the same gas token and configured to send funds to a successive parent chain (this is the case for a Layer-3 network or higher) * Deploying a `RewardDistributor` contract configured to forward 10% of fees to the `ChildToParentRouter` and 90% to the chain owner’s preferred reward-receiving address. In the event that a `ChildToParentRouter` does not connect to the address controlled by the Arbitrum Foundation on Ethereum, an Arbitrum chain must deploy successive `ChildToParentRouter` contracts until a connection to such address is established. Additional `ChildToParentRouter` contracts configured to route **ETH** have been deployed in certain networks and can be leveraged by chains created on top of these networks. You can [see which networks have a router deployed](#canonical-contracts). Here are a few flows to help visualize the deployment: ![AEP scenario 1](/img/arb-chain-aep-scenario-1.svg) ![AEP scenario 2](/img/arb-chain-aep-scenario-2.svg) Please reach out if: 1. Your chain is on another L2 for which we do not have a `ChildToParent` router deployed 2. Your custom gas token is only available on the L2 and non-EVM L1 ## Deployment scripts The Arbitrum Chain SDK provides a [configurable script](https://github.com/OffchainLabs/arbitrum-chain-sdk/tree/main/examples/setup-aep-fee-router) that allows a chain operator to deploy quickly and set up the AEP fee router contracts. Note for L3 chains with custom gas tokens The standard script deploys and sets up the AEP fee router contracts to route funds to the parent chain. L2 chains are expected to route funds to the [multisig wallet owned by the Arbitrum Foundation](#canonical-contracts) on Ethereum. L3 chains (or further layers) might need to specify a different target address on the parent chain depending on the gas token of the chain. If the chain uses **ETH** as the gas token, and a [ChildToParentRouter](#canonical-contracts) contract is deployed in the parent chain, they can route their funds to that contract. If the chain uses a different gas token, please contact the Arbitrum Foundation to confirm the target address to withdraw the AEP fees to. The script performs the following operations: 1. Obtain the rollup and inbox contract of the chain. These are needed to execute the next steps. 2. Obtain the current fee collectors of the chain: Arbitrum chain base fee collector, Arbitrum chain surplus fee collector, and Parent chain surplus fee collector. 3. Deploy the `ChildToParentRouter` contract, configured to send the amounts received to the appropriate target address on the parent chain. 4. Deploy a `RewardDistributor` contract for each different fee collector account, configured to distribute 90% of the amounts received to the current fee collector, and 10% to the ChildToParentRouter contract deployed in the previous step. 5. Set each of the fee collectors to the `RewardDistributor` contracts. > **INFO** > > If the same address collects all three fee types, only one `RewardDistributor` contract will be deployed, which will collect all those fees. To configure the script, you need to specify the following [environment variables](https://github.com/OffchainLabs/arbitrum-chain-sdk/blob/main/examples/setup-aep-fee-router/.env.example): * `ROLLUP_ADDRESS`: address of the rollup contract * `CHAIN_OWNER_PRIVATE_KEY`: private key of the account with executor privileges in the `UpgradeExecutor` admin contract for the chain * `ORBIT_CHAIN_ID`: chain id of the Arbitrum chain * `ORBIT_CHAIN_RPC`: RPC of the Arbitrum chain * `PARENT_CHAIN_ID`: chain id of the parent chain, which shouldn't be an Arbitrum chain * `PARENT_CHAIN_TARGET_ADDRESS`: address on the parent chain where 10% of the revenue will be sent to. You can find the potential target addresses in this document's [canonical contracts](#canonical-contracts) section. If the parent chain is not on that list, or if your chain uses a gas token different than the one the router is configured for, contact the Arbitrum Foundation to obtain a specific target address for your chain. Finally, follow these steps to execute the script (from the `examples/setup-aep-fee-router` folder): 1. Install dependencies ```shell yarn install ``` 2. Create `.env` file and add the env vars ```shell cp .env.example .env ``` 3. Run the script ```shell yarn dev ``` ## Deploying a `ChildToParent` router for a custom **ERC-20** on Base Some RaaS providers and Arbitrum chains may need to route AEP fees using a non-**ETH** **ERC-20** token on Base, or other L2s. To support this, the Arbitrum Chain SDK now includes a helper for deploying a `ChildToParentRouter` on Base for any specified **ERC-20**. Starting with `arbitrum-chain-sdk` v0.24.2, you can deploy your own **ERC-20** router on Base programmatically. ### When you need this You should deploy your own **ERC-20** router if: * Your chain uses a custom gas token * Your chain is an L3 that settles to Base (or another OP chain L2) * Your chain's custom gas token is represented as an **ERC-20** on Base or the chain that your chain settles to ### How it works * The SDK exposes a function that handles deployment of a `ChildToParentRouter` contract configured for: * A specific **ERC-20** token address on Base * A target address on the parent chain (often Ethereum or a parent L2) The standard AEP withdrawal logic: ```javascript import { feeRouterDeployOpChildToParentRewardRouter } from 'arbitrum/chain-sdk'; async function main() { const tx = await feeRouterDeployOpChildToParentRewardRouter({ childChainWalletClient, parentChainTargetAddress, minDistributionInvervalSeconds, parentChainTokenAddress, childChainTokenAddress, }); console.log('ChildToParentRouter deployed:', tx.childToParentRouter); } main(); ``` ## Triggering movement of funds Any `ChildToParentRouter` contract deployed by an Arbitrum chain must be periodically called to move funds. The following commands should be set up to execute at the same interval as the router's minimum distribution interval (`minDistributionIntervalSeconds`). The [router contracts repository](https://github.com/OffchainLabs/fund-distribution-contracts) contains scripts for calling the routers. 1. Periodically trigger withdrawals to the parent chain > **INFO** > > If the `ChildToParentRouter` is routing its chain's native token, this step should be skipped. ```shell cast send "routeToken()" --rpc-url --private_key ``` 2. Periodically redeem withdrawals on the parent chain ```shell git clone https://github.com/OffchainLabs/fund-distribution-contracts cd fund-distribution-contracts && yarn # set PARENT_CHAIN_PK in .env cp .env.sample .env # add --opStack if the router is on an OP stack chain yarn redeem-child-to-parent \ --parentRPCUrl \ --childRPCUrl \ --childToParentRewardRouterAddr \ --oneOff ``` --- > For a complete page index, fetch # Use native interop token with mint/burn as your Arbitrum chain gas token > **CAUTION** — Bridge counterparty risk > > While this feature allows an Arbitrum chain to use native interop tokens as gas tokens via third-party protocols, it introduces new trust assumptions. In a standard Arbitrum chain, the canonical bridge is the sole authority for minting the chain's gas token, inheriting the security of the parent chain. > > However, by using a third-party bridge for the chain's gas token, your chain's gas token—and thus its liveness—becomes directly dependent on the security and integrity of the third-party bridge provider. > **INFO** > > While Native Mint/Burn was introduced in [ArbOS](/how-arbitrum-works/deep-dives/arbos.md) 41, we strongly recommend that teams upgrade to [ArbOS 51 Dia](/run-arbitrum-node/arbos-releases/arbos51.md) to enable this feature. ## Recommended configuration ### Prerequisites Before starting, ensure that the chain has: | Item | Minimum version | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `nitro-contracts` | [v3.1.0](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v3.1.0) or higher | | `nitro-node` | [v3.9.3](https://github.com/OffchainLabs/nitro/releases/tag/v3.9.3) or higher (includes the fix for `ArbNativeTokenManager` precompile inclusion) | | `go-ethereum` fork | Included in [ArbOS 51 Dia](/run-arbitrum-node/arbos-releases/arbos51.md) (please use the latest WASM module root) or later | We suggest the following baseline or values for production chains: * [ArbOS version 51](/run-arbitrum-node/arbos-releases/arbos51.md) or later. * Call [`ArbOwner.setNativeTokenManagementFrom`](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/f49a4889b486fd804a7901203f5f663cfd1581c8/ArbOwner.sol#L38-L40) to activate with a future timestamp ≥ 7 days ahead. This is recommended for chains which are already live. * Native-token owners list: only the bridge adapter contract(s) to minimize the attack surface. * Outbox sweep bot: run once per hour to prevent stray collateral build-up. * Adapter contract must implement replay‑protection and supply accounting as these requirements are out-of-scope for ArbOS. ## How to configure the feature for your chain > **INFO** > > The native token mint/burn feature is currently not available for implementation at chain genesis. The ability to activate it during chain initialization will be part of a future release. ### Enable the feature post-genesis Below steps show the flow a chain owners can use to allowlist a bridge adapter. The example below uses a Foundry call, but you can also use other tools to interact with the precompiles directly. 1. Update your chain to [ArbOS 51 Dia](/run-arbitrum-node/arbos-releases/arbos51.md) binaries and deploy the consensus-v51 WASM root. 2. Call [`setNativeTokenManagementFrom`](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/f49a4889b486fd804a7901203f5f663cfd1581c8/ArbOwner.sol#L38-L40) with the chain owner and a UNIX timestamp that is 7+ days in the future to enable the feature, otherwise the call will fail. > **IMPORTANT** > > The feature becomes live once the chain timestamp passes `NativeTokenManagementFromTime`. Until then, the `add/removeNativeTokenOwner` call will revert. 3. Deploy your bridge adapter (e.g., LayerZero OFT) and note the contract address. 4. Add the deployed bridge adapter as a native-token owner: ```shell cast send 0x0000000000000000000000000000000000000070 \ "addNativeTokenOwner(address)" \ {0xTheNewOwnerToBeAdded} \ --private-key $PRIVATE_KEY \ --rpc-url $RPC_URL ``` 5. *(Optional)* Deploy [`ERC20MigrationOutbox`](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/bridge/extra/ERC20MigrationOutbox.sol) on L1 and run a bot to call `sweep()` on a desired cadence. 6. Chain owners should monitor the following events: * `OwnerActs(0xaeb3a464, address indexed owner, bytes data)` to see adding native owner * `OwnerActs(0x96a3751d, address indexed owner, bytes data)` to see removing native owner * `NativeTokenMinted` and `NativeTokenBurned` to see token mint and burn events ### Temporarily disabling the mint/burn feature There may be situations where you want to disable the feature temporarily, e.g., to investigate a bridge bug or supply imbalance. All calls below are made from an [**ArbOwner**](https://github.com/OffchainLabs/nitro-precompile-interfaces/) wallet. These commands are for chain owners, bridge teams have their own operating procedures. 1. Inspect the current list of owners. ```shell cast call 0x000000000000000000000000000000000000006b \ "getAllNativeTokenOwners()(address[])" \ --rpc-url $RPC_URL ``` 2. Remove each owner. ```shell cast send 0x0000000000000000000000000000000000000070 \ "removeNativeTokenOwner(address)" \ {0xTheOwnerToBeRemoved} \ --private-key $PRIVATE_KEY \ --rpc-url $RPC_URL ``` `mintNativeToken` and `burnNativeToken` now revert because no authorized senders remain. Canonical bridge exits are now open, warn liquidity partners. 3. Verify the pause. ```shell cast call 0x000000000000000000000000000000000000006b \ "getAllNativeTokenOwners()(address[])" \ --rpc-url $RPC_URL ``` The list should be empty 4. *(Optional but **recommended**)* Notify downstream infrastructure: Bridges, indexers, and other relevant teams may need to be informed that canonical exits are open again and the adapter is offline. **To re-enable the feature later** * Add the adapter back (no delay if `NativeTokenManagementFromTime` is a past timestamp). ```bash cast send 0x0000000000000000000000000000000000000070 \ "addNativeTokenOwner(address)" \ {0xTheNewOwnerToBeAdded} \ --private-key $PRIVATE_KEY \ --rpc-url $RPC_URL ``` * You can call `GetNativeTokenManagementFrom` to verify `NativeTokenManagementFromTime` value: ```jsx cast send 0x000000000000000000000000000000000000006b \ "getNativeTokenManagementFrom()(uint64)" \ {0xTheNewOwnerToBeAdded} \ --private-key $PRIVATE_KEY \ --rpc-url $RPC_URL ``` ### Disabling the native mint/burn feature permanently 1. Remove all native-token owners via `removeNativeTokenOwner`. 2. The `ArbOwner` calls the function `SetNativeTokenManagementFrom(0)` to freeze the action to add or remove the list of trusted adapters (owners). Setting this parameter to `0` disables future modifications to `NativeTokenOwners` ; do this after the list is empty. In other words, it will lock the token-owner list forever: no new owners can be added, and nobody can re-enable Native Mint/Burn. 3. To verify behavior: `getAllNativeTokenOwners()` should return `[]` (empty list) 4. Canonical-bridge withdrawals resume automatically once the list of native-token owners is empty. ## Security checklist | Risk | Mitigation | | ------------------------------------ | --------------------------------------------------------------------------------------------------- | | Admin key compromise enables minting | Seven-day timelock, set up alerting on `NativeTokenManagementFromTime` updates | | Buggy adapter over-mints tokens | Remove owner would block mint/burn; users can burn & exit via canonical bridge after owners removed | | Collateral split across two exits | Canonical bridge auto-blocks withdrawals when owners exist; run Outbox sweep | ## FAQs ### Who should read this page? * Bridge/infra engineers building an adapter that mints native gas. * Chain owners who need to allowlist such an adapter and run the sweeper bot. ### Can I run the feature on an older ArbOS version? While native mint/burn was technically introduced in ArbOS 41, we strongly recommend upgrading to [ArbOS 51 Dia](/run-arbitrum-node/arbos-releases/arbos51.md) for those enabling this feature. ### What happens to value-bearing withdrawals once I disable the feature? When the last native-token owner is removed, Nitro automatically re-opens canonical-bridge withdrawals. No additional action is needed. ### How can developers get the timestamp value of `NativeTokenManagementFromTime`? Filter event `OwnerActs(bytes4 indexed method, address indexed owner, bytes data)` with first index `0xbdb8f707` to get all results, and decode the latest result's data field to get the last valid timestamp. That timestamp would be the point after which `ArbOwner`'s can take action to `nativeTokenOwner` list. > **CAUTION** — Testing Responsibility > > While Offchain Labs has conducted comprehensive [system tests](https://github.com/OffchainLabs/nitro/blob/master/system_tests/arbos_upgrade_test.go#L292) to verify the core ArbOS logic for native mint/burn functionality, this does not cover your specific implementation. > > **Individual teams are responsible to:** > > * Thoroughly test their own adapter contracts and app-layer solutions. > * Verify the security and collateralization of their specific third-party token providers (e.g., LayerZero, xERC20). > * Ensure their custom configurations are compatible with their chain's unique environment. --- > For a complete page index, fetch # How to configure a custom gas token for your AnyTrust Arbitrum chain When deploying your [AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) Arbitrum chain you have the option of using a custom gas token, different than **ETH**, that is natively used for gas payments on the network. When choosing this option, there are certain requirements that the token needs to comply with, as well as certain chain configuration that needs to be adjusted. This guide covers this information. ## Requirements of the custom gas token The main requirements for a custom gas token is that it must be an **ERC-20** token, and there *must be some representation* on the parent chain. During chain deployment, the gas token is "natively bridged" and then properly configured as the native gas token on the Arbitrum chain. There are other important considerations to keep in mind when deciding to use a custom gas token. Restrictions on the **ERC-20** token include: * The token can't be rebasing or have a transfer fee. * The token must not revert on transfers of 0 value (as per standard implementations). * The token must only be transferrable via a call to the token address itself. * The token must only be able to set allowance via a call to the token address itself. * The token must not have a callback on transfer, and more generally a user must not be able to make a transfer to themselves revert. > **INFO** > > While certain restrictions are in place today (see above), some of them may be removed in upcoming releases of the custom gas token feature. Please reach out to the Offchain Labs team with any questions or requests for additions to the custom gas token feature. ## Configuration of the Arbitrum chain when using a custom gas token There are several parameter changes required to ensure proper functioning of an Arbitrum chain configured to use a custom gas token. After deploying the chain, you must reset the base fees of the parent chain by calling the following functions in the [`ArbOwner`](/arbitrum-essentials/precompiles/reference.md#arbowner) precompile: * `SetL1PricePerUnit`, setting `pricePerUnit` to `0` * `SetL1PricingRewardRate`, setting `perUnitReward` to `0` > **NOTE** > > These methods use L1 to refer to the parent chain of the Arbitrum chain. These parameter changes are strongly recommended to avoid charging users for a non-existent parent chain's base fee. The impact of not doing this is that Nitro will apply a parent chain's fee to all transactions. As Nitro assumes the native asset is **ETH**, all fees are expected to be denominated in **ETH**. This has the consequence of overcharging users for parent chain fees in native tokens that are more expensive than **ETH** (for example, if you use wrapped **BTC** as the custom gas token). In the case for tokens with prices much lower than **ETH**, the impact is far less pronounced. In either case we strongly recommend taking these steps to avoid any issues with chain economics. You can read more about how Nitro manages fees in [Gas and fees](/how-arbitrum-works/deep-dives/gas-and-fees.md). > **INFO** — Native Mint/Burn for Custom Gas Tokens > > Starting with **ArbOS 51 Dia**, Arbitrum chains have the option to use a **native interop token with mint/burn** as their custom gas tokens via third-party interoperability providers (such as LayerZero OFTs, xERC20s, or native USDC) instead of using the canonical "lock and mint" bridge. > > To enable this feature for your chain, see our guide on [enabling native mint/burn for gas tokens](/launch-arbitrum-chain/chain-config/costs/configure-native-mint-burn.md). > **NOTE** > > Arbitrum provides the infrastructure for this feature, but does not provide the bridge adapter itself. To use this feature, you must directly coordinate with your chosen bridge provider to build, audit, and deploy a custom adapter contract. --- > For a complete page index, fetch # How to configure a custom gas token for your Rollup Arbitrum chain When deploying an Arbitrum chain in [Rollup mode](/run-arbitrum-node/data-availability.md#rollup-mode), you can use a custom **ERC-20** token as the native gas token. This token is usable for Transaction fees on that specific Arbitrum chain and reimbursing the [Batch Poster](/how-arbitrum-works/deep-dives/assertions.md) for data posted to Ethereum. An example would be an L2 Rollup that uses **USDC** as its custom gas token but pays **ETH** to post data to L1 Ethereum. Enabling a custom fee token for a Rollup chain requires additional configuration compared to an [AnyTrust chain](/launch-arbitrum-chain/chain-config/costs/custom-gas-token-anytrust.md); it also introduces important exchange-rate considerations. This guide outlines specific implementation steps and operational considerations for chain owners or operators to consider when enabling a custom fee token for Rollups. ## Requirements of the custom gas token A key requirement for a custom gas token is that it has a representation on the parent chain. During chain deployment, the gas token is "natively bridged" and then properly configured as the native gas token on the Arbitrum chain. Additional requirements include: * Must be a standard **ERC-20** token * Transfers and approvals must occur directly via the token contract, not via proxies or hooks * Must not be rebasing or include transfer fees and * Must not use transfer callbacks or any onchain behavior that reverts if the sender and recipient are the same ## Understanding the fee token pricer In Rollup mode, data posting costs to the Parent chain are paid in the parent chain’s native token but get reimbursed in the Child chain’s fee token. To facilitate this, you must deploy and register a fee token pricer contract, providing an exchange rate between the two tokens (child fee token and parent fee token). We use this exchange rate to calculate the custom fee tokens required to reimburse the Batch Poster for each batch they post to the parent chain. Below are three example implementation options that chain owners may consider. Chain owners are free to develop their custom fee token pricers. > **WARNING** > > The implementations included in the examples below have not undergone comprehensive testing or auditing. The intent is to illustrate different options for chain owners to consider. ### 1. Manual exchange rate The Chain Owner manually sets and updates the exchange rate; this is the simplest option but requires manual updates to track price changes or dependence on an owner-defined oracle. This approach generally makes sense if: * You, as the chain owner, want full control over the exchange rate * The chain owner and the batch poster are the same entity * You are operating in a tightly controlled or experimental environment An (unaudited) example to reference is: [`OwnerAdjustableExchangeRatePricer.sol`](https://github.com/OffchainLabs/nitro-contracts/blob/main/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol) ### 2. External oracle An external oracle fetches the exchange rate; this reduces operational requirements for the chain owner but introduces a dependency on an external service to provide accurate, reliable pricing data. This approach generally makes sense if: * Reliable, robust oracles are available for the gas token pair * Reducing trust in the chain owner is important for your implementation * You, as the chain owner, want to avoid manual exchange rate updates See an (unaudited) example here using a TWAP oracle: [`UniswapV2TwapPricer.sol`](https://github.com/OffchainLabs/nitro-contracts/blob/main/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol) ### 3. Exchange rate tracking The batch poster records the rate when converting the child gas token to the parent gas token. This approach can ensure accurate reimbursement but also requires trusted reporting and more complex accounting. This approach generally makes sense if: * Your batch poster is trusted and willing to record exchange rates when purchasing parent chain gas * You want reimbursement to be precise and not over or under-charging users * You want to minimize reliance on the chain owner or external oracles Consider this (unaudited) example: [`TradeTracker.sol`](https://github.com/OffchainLabs/nitro-contracts/blob/main/test/foundry/fee-token-pricers/trade-tracker/TradeTracker.sol) An important risk for chain owners to consider is exchange rate stability. If the fee token pricer returns stale or manipulated prices, the batch poster may be under- or over-reimbursed. In the case of under-reimbursement, the batch poster would continue operating but at a loss. In the case of over-reimbursement, end users would end up overpaying transaction fees. While this risk may be acceptable for experimental or early-stage chains, you should carefully consider the financial consequences for the batch poster or end users on your Arbitrum chain. The “External oracle” and the “Exchange rate tracking” approaches seek to mitigate this risk. Ultimately, the chain owner should assess their unique situation when determining which fee-pricing strategy makes the most sense for them (if any). ## Configuration of the Rollup when using a custom gas token Here are the steps you should take to deploy your Arbitrum chain with a custom gas token: 1. Deploy your **ERC-20** token on the parent chain (if not already deployed) 2. Deploy your fee token pricer * Choose a pricer approach that best meets your needs; unaudited examples are: * [OwnerAdjustableExchangeRatePricer.sol](https://github.com/OffchainLabs/nitro-contracts/blob/main/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol) * [UniswapV2TwapPricer.sol](https://github.com/OffchainLabs/nitro-contracts/blob/main/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol) * [TradeTracker.sol](https://github.com/OffchainLabs/nitro-contracts/blob/main/test/foundry/fee-token-pricers/trade-tracker/TradeTracker.sol) * Make sure the pricer is deployed successfully on the parent chain. 3. Configure the fee token and pricer when creating the rollup * Use the [createERC20Rollup.ts](https://github.com/OffchainLabs/nitro-contracts/blob/main/scripts/createERC20Rollup.ts) script, which accepts environment variables for: * `FEE_TOKEN_ADDRESS` = your **ERC-20** gas token * `FEE_TOKEN_PRICER_ADDRESS` = your deployed pricer * `ROLLUP_CREATOR_ADDRESS` = the Rollup creator contract on L1 * `STAKE_TOKEN_ADDRESS` = token used for bonding (see [BoLD docs](/launch-arbitrum-chain/chain-config/validation/bold.md#use-of-your-projects-native-token-as-the-bonding-asset-to-secure-the-chain) for more details) * The script will deploy and initialize the Rollup accordingly. You can refer to the source files for more details on: * [`fee-token-pricers`](https://github.com/OffchainLabs/nitro-contracts/tree/main/test/foundry/fee-token-pricers) * [`createERC20Rollup.ts`](https://github.com/OffchainLabs/nitro-contracts/blob/main/scripts/createERC20Rollup.ts) You can also find more info about how Nitro manages [gas and fees](/how-arbitrum-works/deep-dives/gas-and-fees.md) here. If you’re unsure how to configure the Rollup correctly or have questions about fee pricer implementations, please get in touch with Offchain Labs or your chain’s deployment provider. > **INFO** — Native Mint/Burn for Custom Gas Tokens > > Starting with **ArbOS 51 Dia**, Arbitrum chains have the option to use a **native interop token with mint/burn** as their custom gas tokens via third-party interoperability providers (such as LayerZero OFTs, xERC20s, or native USDC) instead of using the canonical "lock and mint" bridge. > > To enable this feature for your chain, see our guide on [enabling native mint/burn for gas tokens](/launch-arbitrum-chain/chain-config/costs/configure-native-mint-burn.md). > **NOTE** > > Arbitrum provides the infrastructure for this feature, but does not provide the bridge adapter itself. To use this feature, you must directly coordinate with your chosen bridge provider to build, audit, and deploy a custom adapter contract. --- > For a complete page index, fetch # Dynamic Pricing for Arbitrum chains ## Dynamic pricing: a new pricing algorithm introduced in ArbOS 51 Dia Arbitrum chains support a new gas pricing algorithm that uses multiple-gas targets, as part of the [ArbOS 51 upgrade](/run-arbitrum-node/arbos-releases/arbos51.md), to reduce the frequency and severity of child-chain gas price spikes. This is the first optimization of many towards Dynamic Pricing. * This change is **backward compatible**. Setting new gas targets is **not mandatory**; if no new configuration is set, the legacy gas pricing algorithm with a single gas target remains active. * **Additional details:** For those interested in how these values are derived, refer to the [Gas and fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#the-gas-target) page as it provides the underlying parameters used to calculate the new gas targets and adjustment windows. > **NOTE** > > The upgrade to ArbOS 51 doesn't require activation of these new gas targets. The new targets are completely opt-in, so you can choose when to enable them. ## Purpose This improvement aims to reduce the severity, frequency, and duration of high L2 gas prices during periods when demand exceeds the gas target. Detailed information on the purpose of this upgrade can be found in the [AIP for implementing improvements to the pricing algorithm](https://forum.arbitrum.foundation/t/aip-raise-the-gas-target-min-l2-base-fee-implement-improvements-to-the-pricing-algorithm/30182). **What does this improvement NOT help with?** * L1 gas fee spikes on Ethereum * Data availability pricing > **INFO** > > For more information about Effective block gas limit, refer to the [Gas and fees page](/how-arbitrum-works/deep-dives/gas-and-fees.md#effective-block-gas-limit). ## How to set multiple gas targets You can call `SetGasPricingConstraints` on the [ArbOwner precompile](/for-devs/dev-tools-and-resources/partials/precompile-tables/_ArbOwner) with an array of gas targets (the code refers to these targets as "constraints"), where each constraint contains `[gasTargetPerSecond, adjustmentWindowSeconds, startingBacklogValue]`. ```ts // Example: Set short-term and long-term constraints // Define constraints: [gasTargetPerSecond, adjustmentWindowSeconds, startingBacklogValue] const shortTerm = [30_000_000, 1, 800_000]; // Reacts to immediate spikes [cite: 10, 18] const longTerm = [15_000_000, 600, 1_600_000]; // Ensures price stability [cite: 13, 18] // Call ArbOwner precompile to set the constraints await arbOwner.setGasPricingConstraints([shortTerm, longTerm]); ``` Refer to the definitions below to understand what each parameter means: * `gasTargetPerSecond` is the target gas processing rate for your Arbitrum chain. It's the amount of gas the system aims to process each second under normal conditions. Any demand above this target will trigger a gas price increase to economically disincentivize usage until it falls back to the target. * `adjustmentWindowSeconds` is the time period within which the system will measure average gas demand/usage to determine whether or not the gas price needs to be adjusted, relative to the gas target. A larger window results in more stable, predictable pricing, while a smaller window reacts aggressively to traffic spikes. * `startingBacklogValue` is the initial amount of gas the system uses to set prices the **exact moment** the `gasTargetPerSecond` and `adjustmentWindowSeconds` get changed. * Ideally, the backlog value should be set to the existing backlog of the chain when the function call to `setGasPricingConstraints` is made. Given this is not always easy, using `0` is a common practice (the same will be done on Arbitrum One). * However, using `0` means that the system assumes that the demand is `0`, causing the gas price to adjust to the minimum. L2 base fee (0.02 gwei/gas). After which, the system will resume tracking the backlog based on demand to price gas. ## How to calculate the values for your chain Offchain Labs has developed a [script to help calculate pricing constraints](https://colab.research.google.com/drive/1D28YghOmCjvvrLWS5FsTp5xNKz9JHAA3?usp=sharing), which takes in certain parameters to find the most optimized gas targets to use for your chain. The script takes in the following params: * `long_term_gas_target` is the desired long-term average gas throughput for the chain. A higher gas target means that your chain can handle more TPS before the gas begins to spike. However, a higher gas target also leads to faster state growth and requires more powerful hardware. * [State size limits](/launch-arbitrum-chain/chain-config/costs/gas-target.md): Guidance for setting your chain's gas target * [State growth guidance](/launch-arbitrum-chain/operate/state-growth.md): Impact of high gas targets on state growth and its consequences * `node_throughput` is the sustainable maximum throughput that the nodes can maintain without any issues. It is highly dependent on your specific hardware and the workload of your chain. Offchain Labs will soon publish testing frameworks and tools to benchmark the throughput your nodes can handle across various workloads. * `gas_limit` is the theoretical max block size limit. With the introduction of the `MaxTxGasLimit` in ArbOS 51, the effective block gas limit is twice the `MaxBlockGasLimit`. We recommend using an in-between value of `MaxBlockGasLimit` and Effective Block Gas Limit `(MaxTxGasLimit + MaxBlockGasLimit)` for this parameter. * `max_rate_of_increase_percent_per_second` is the maximum allowed rate of increase in the fee per second when the chain is working at the maximum capacity. * `long_term_adjustment_window_seconds` is the maximum timeframe over which gas prices adjust. This window specifically applies to the **lowest gas target** to ensure price stability during low-demand periods. The values listed below are used for calculating the gas targets for Arbitrum One. These values have been calculated based on various UX and research considerations. | Parameter | Arbitrum One Config | | ----------------------------------------- | ------------------- | | `long_term_gas_target` | 10 Mgas/s | | `node_throughput` | 80 Mgas/s | | `gas_limit` | 128 Mgas/s | | `max_rate_of_increase_percent_per_second` | 20% | | `long_term_adjustment_window_seconds` | 86400 sec | ## Recommendation for Arbitrum chains Arbitrum chain teams can use the following recommended values for the script parameters. The script will generate the optimized gas targets and adjustment windows for your chain, which you can then use to enable the new pricing system. | Parameter | Arbitrum One Config | Recommendation | | ----------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `long_term_gas_target` | 10 Mgas/s | Configure based on resource management considerations or the same as Arbitrum One. | | `node_throughput` | 80 Mgas/s | Same as Arbitrum One | | `gas_limit` | 128 Mgas/s | Based on the block limit for the chain, which by default is 32 million gas and assumes a 250ms block time. Can be set to (MaxBlockGasLimit + MaxEffectiveGasLimit) / 2 | | `max_rate_of_increase_percent_per_second` | 20 | Same as Arbitrum One | | `long_term_adjustment_window_seconds` | 86400 sec | Same as Arbitrum One | --- > For a complete page index, fetch # How to manage the fee parameters of your Arbitrum chain Different fees get collected for every transaction as part of an Arbitrum chain activity. These fees are collected as a single amount (the transaction fees) but split internally into different components depending on their purpose. Each component is transferrable to a different fee collector address that is configurable on your chain. This guide describes the different collected fees and explains how to specify the fee collector address on your chain for each fee type. This guide describes the different fees collected on your chain, how to configure them, and how to specify the fee collector address for each type. ## What fees are collected on an Arbitrum chain? There are four fee types that are collected on every transaction of an Arbitrum chain: * **Arbitrum chain base fee**: fees paid for executing the transaction on the chain based on the minimum base price configured. * **Arbitrum chain surplus fee**: if the chain is congested (i.e., the base price paid for the transaction is higher than the minimum base price), these fees account for executing the transaction on the chain based on any gas price paid above the minimum base price configured. * **Parent chain base fee**: relative fees paid for posting the transaction on the parent chain. This amount is calculated based on the transaction's estimated size and the current view of the parent chain's base fee. * **Parent chain surplus fee**: if configured, these are extra fees rewarded to the [Batch Poster](/how-arbitrum-works/deep-dives/assertions.md). You can find more detailed information about these fee types in these pages: * [L2 fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#child-chain-gas-pricing) for the Arbitrum chain base fee and surplus fee * [L1 fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#parent-chain-gas-pricing) for the Parent chain base fee and surplus fee ## How to configure the fees collected? Let's see in what ways we can configure each fee type: ### Arbitrum chain base fee (minimum) Your chain is configured with a minimum base fee for execution. This value can be obtained by calling the method `getMinimumGasPrice()(uint256)` of the [`ArbGasInfo`](/arbitrum-essentials/precompiles/reference.md#arbgasinfo) precompile. ```shell cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006C "getMinimumGasPrice() (uint256)" ``` Alternatively, you can use the Chain SDK to retrieve the minimum Arbitrum chain base fee configured: ```typescript const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbGasInfoPublicActions); const orbitMinimumBaseFee = await orbitChainClient.arbGasInfoReadContract({ functionName: 'getMinimumGasPrice', }); ``` > **NOTE** > > This minimum base fee defines the minimum value that the chain's base fee can have. However, in periods of congestion, the actual base fee might be higher than this minimum. Check the next section "Arbitrum chain surplus fee" for more information. To set a new minimum base fee, use the method `setMinimumL2BaseFee(uint256)` of the [`ArbOwner`](/arbitrum-essentials/precompiles/reference.md#arbowner) precompile, and pass the new minimum base fee in `wei`. For example: ```shell cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setMinimumL2BaseFee(uint256) ()" $NEW_MINIMUM_BASE_FEE_IN_WEI ``` Or using the Chain SDK: ```typescript const owner = privateKeyToAccount(); const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbOwnerPublicActions); const transactionRequest = await orbitChainClient.arbOwnerPrepareTransactionRequest({ functionName: 'setMinimumL2BaseFee', args: [], upgradeExecutor: false, account: owner.address, }); await orbitChainClient.sendRawTransaction({ serializedTransaction: await owner.signTransaction(transactionRequest), }); ``` ### Arbitrum chain surplus fee In periods of congestion, the actual base fee of your Arbitrum chain might be higher than the configured minimum. You can see the current base fee of your chain by calling the method `getPricesInWei()(uint256,uint256,uint256,uint256,uint256,uint256)` of the [`ArbGasInfo`](/arbitrum-essentials/precompiles/reference.md#arbgasinfo) precompile, and check the last result of the returned tuple. ```shell cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006C "getPricesInWei() (uint256,uint256,uint256,uint256,uint256,uint256)" ``` You can then calculate the current Arbitrum chain surplus fees as `currentBaseFee - minimumBaseFee`. > **NOTE** > > `getPricesInWei()` also returns the correspondent fees due to congestion in the second-to-last result of the returned tuple. > **INFO** — Arbitrum chain surplus fees are automatically adjusted > > Arbitrum chains automatically adjust the Arbitrum chain surplus fee based on the traffic of the chain. If the gas consumed goes over the gas target, the chain's base fee will start increasing. Likewise, the base fee will gradually go down if demand of gas returns to below the configured gas target, until it reaches the minimum base fee configured. ### Parent chain base fee To obtain the current parent chain base fee of your chain, you can call the method `getL1BaseFeeEstimate()(uint256)` of the [`ArbGasInfo`](/arbitrum-essentials/precompiles/reference.md#arbgasinfo) precompile. ```shell cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006C "getL1BaseFeeEstimate() (uint256)" ``` Alternatively, you can use the Chain SDK to retrieve the current parent chain base fee: ```typescript const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbGasInfoPublicActions); const parentChainBaseFee = await orbitChainClient.arbGasInfoReadContract({ functionName: 'getL1BaseFeeEstimate', }); ``` You can modify the current estimate of the parent chain base fee by calling the method `setL1PricePerUnit(uint256)` of the [`ArbOwner`](/arbitrum-essentials/precompiles/reference.md#arbowner) precompile. For example: ```shell cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setL1PricePerUnit(uint256) ()" $NEW_PARENT_CHAIN_BASE_FEE ``` Or using the Chain SDK: ```typescript const owner = privateKeyToAccount(); const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbOwnerPublicActions); const transactionRequest = await orbitChainClient.arbOwnerPrepareTransactionRequest({ functionName: 'setL1PricePerUnit', args: [], upgradeExecutor: false, account: owner.address, }); await orbitChainClient.sendRawTransaction({ serializedTransaction: await owner.signTransaction(transactionRequest), }); ``` > **INFO** — Parent chain base fees are automatically adjusted > > Arbitrum chains are configured to automatically adjust the current parent chain base fee estimation based on the batch poster reports sent from the parent chain. That means that even though you can set a new parent chain base fee, the chain will automatically adjust it based on the reports received afterwards. > **INFO** — Parent chain base fee configuration for chains using a custom gas token > > Arbitrum chains that use a custom gas token should have their parent chain base fees disabled (set to 0), to avoid charging users for a non-existent parent chain's base fee, as explained in [How to use a custom gas token](/launch-arbitrum-chain/chain-config/costs/custom-gas-token-rollup.md#configuration-of-the-rollup-when-using-a-custom-gas-token). ### Parent chain surplus fee The parent chain surplus fee collected is based on a reward rate configured in the chain. To obtain this parameter, you can call the method `getL1RewardRate()(uint64)` of the [`ArbGasInfo`](/arbitrum-essentials/precompiles/reference.md#arbgasinfo) precompile. This function will return the amount of `wei` per gas unit paid to the appropriate fee collector. For example: ```shell cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006C "getL1RewardRate() (uint64)" ``` Alternatively, you can obtain this information using the Chain SDK: ```typescript const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbGasInfoPublicActions); const parentChainRewardRate = await orbitChainClient.arbGasInfoReadContract({ functionName: 'getL1RewardRate', }); ``` To change the reward rate, you can use the method `setL1PricingRewardRate(uint64)` of the [`ArbOwner`](/arbitrum-essentials/precompiles/reference.md#arbowner) precompile and pass the amount of `wei` per gas unit to reward. For example: ```shell cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setL1PricingRewardRate(uint64) ()" $NEW_REWARD_RATE ``` Or using the Chain SDK: ```typescript const owner = privateKeyToAccount(); const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbOwnerPublicActions); const transactionRequest = await orbitChainClient.arbOwnerPrepareTransactionRequest({ functionName: 'setL1PricingRewardRate', args: [], upgradeExecutor: false, account: owner.address, }); await orbitChainClient.sendRawTransaction({ serializedTransaction: await owner.signTransaction(transactionRequest), }); ``` ## How to configure the fee collector addresses? Let's now look at how to configure the collector addresses for each fee type. ### Arbitrum chain base fee Arbitrum chain base fees are paid to the `infraFeeAccount` configured in your chain. You can retrieve the current configured address by calling the method `getInfraFeeAccount()(address)` of the [`ArbOwnerPublic`](/arbitrum-essentials/precompiles/reference.md#arbownerpublic) precompile. For example: ```shell cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006B "getInfraFeeAccount() (address)" ``` > **NOTE** > > The [`ArbOwner`](/arbitrum-essentials/precompiles/reference.md#arbowner) precompile also has a `getInfraFeeAccount()(address)` method that can be used, but only by the owner of the chain. Alternatively, you can use the Chain SDK to retrieve the current address configured as `infraFeeAccount`: ```typescript const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbOwnerPublicActions); const infraFeeAccount = await orbitChainClient.arbOwnerReadContract({ functionName: 'getInfraFeeAccount', }); ``` To set a new `infraFeeAccount`, use the method `setInfraFeeAccount(address)` of the [`ArbOwner`](/arbitrum-essentials/precompiles/reference.md#arbowner) precompile. For example: ```shell cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setInfraFeeAccount(address) ()" $NEW_INFRAFEEACCOUNT_ADDRESS ``` Or using the Chain SDK: ```typescript const owner = privateKeyToAccount(); const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbOwnerPublicActions); const transactionRequest = await orbitChainClient.arbOwnerPrepareTransactionRequest({ functionName: 'setInfraFeeAccount', args: [], upgradeExecutor: false, account: owner.address, }); await orbitChainClient.sendRawTransaction({ serializedTransaction: await owner.signTransaction(transactionRequest), }); ``` ### Arbitrum chain surplus fee Arbitrum chain surplus fees are paid to the `networkFeeAccount` configured in your chain. You can retrieve the current configured address by calling the method `getNetworkFeeAccount()(address)` of the [`ArbOwnerPublic`](/arbitrum-essentials/precompiles/reference.md#arbownerpublic) precompile. For example: ```shell cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006B "getNetworkFeeAccount() (address)" ``` > **NOTE** > > The [`ArbOwner`](/arbitrum-essentials/precompiles/reference.md#arbowner) precompile also has a `getNetworkFeeAccount()(address)` method that can be used, but only by the owner of the chain. Alternatively, you can use the Chain SDK to retrieve the current address configured as `networkFeeAccount`: ```typescript const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbOwnerPublicActions); const networkFeeAccount = await orbitChainClient.arbOwnerReadContract({ functionName: 'getNetworkFeeAccount', }); ``` To set a new `networkFeeAccount`, use the method `setNetworkFeeAccount(address)` of the [`ArbOwner`](/arbitrum-essentials/precompiles/reference.md#arbowner) precompile. For example: ```shell cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setNetworkFeeAccount(address) ()" $NEW_NETWORKFEEACCOUNT_ADDRESS ``` Or using the Chain SDK: ```typescript const owner = privateKeyToAccount(); const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbOwnerPublicActions); const transactionRequest = await orbitChainClient.arbOwnerPrepareTransactionRequest({ functionName: 'setNetworkFeeAccount', args: [], upgradeExecutor: false, account: owner.address, }); await orbitChainClient.sendRawTransaction({ serializedTransaction: await owner.signTransaction(transactionRequest), }); ``` ### Parent chain base fee Parent chain base fees are paid to the fee collector of the active batch poster configured in your chain. #### Getting batch posters The recommended way to get the current configured batch posters is to use the Chain SDK to query the `SequencerInbox` contract on the parent chain. This method reads from events emitted when configuring a new batch poster: ```typescript const parentChainClient = createPublicClient({ chain: , transport: http(), }); const batchPosters = await getBatchPosters(parentChainClient, { rollup: rollupAddress, sequencerInbox: sequencerInboxAddress, }); ``` Alternatively, you can verify a specific address directly by checking the `isBatchPoster` mapping in the `SequencerInbox` contract on the parent chain: ```shell cast call --rpc-url $PARENT_CHAIN_RPC $SEQUENCER_INBOX_ADDRESS "isBatchPoster(address) (bool)" $BATCH_POSTER_ADDRESS ``` **Alternative method using ArbAggregator:** You can also query batch posters by calling the `getBatchPosters()(address[])` method of the [`ArbAggregator`](/arbitrum-essentials/precompiles/reference.md#arbaggregator) precompile on your Arbitrum chain: ```shell cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006D "getBatchPosters() (address[])" ``` > **CAUTION** > > The list returned by `ArbAggregator.getBatchPosters()` may be incomplete. It only includes: > > * Addresses that have been manually added via `addBatchPoster()` > * Addresses that have successfully posted at least one batch > > This method is primarily used for fee management purposes, not for authoritative permission verification. Always verify batch poster permissions against the `SequencerInbox` contract on the parent chain. #### Managing fee collectors Once you have the batch poster address, you can obtain the fee collector address configured for that batch poster by calling the method `getFeeCollector(address)(address)` of the [`ArbAggregator`](/arbitrum-essentials/precompiles/reference.md#arbaggregator) precompile: ```shell cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006D "getFeeCollector(address) (address)" $BATCH_POSTER_ADDRESS ``` You can also use the Chain SDK: ```typescript const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbAggregatorActions); const networkFeeAccount = await orbitChainClient.arbAggregatorReadContract({ functionName: 'getFeeCollector', args: [], }); ``` #### Setting a fee collector > **IMPORTANT** > > Before calling `ArbAggregator.setFeeCollector()`, you must ensure the batch poster address is already registered in the `BatchPostersTable`. This is done either by: > > * Manually calling `ArbAggregator.addBatchPoster()` for the address, or > * The address having successfully posted at least one batch To set a new fee collector for a specific batch poster, use the method `setFeeCollector(address, address)` of the [`ArbAggregator`](/arbitrum-essentials/precompiles/reference.md#arbaggregator) precompile: ```shell cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x000000000000000000000000000000000000006D "setFeeCollector(address,address) ()" $BATCH_POSTER_ADDRESS $NEW_FEECOLLECTOR_ADDRESS ``` Or using the Chain SDK: ```typescript const owner = privateKeyToAccount(); const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbAggregatorActions); const transactionRequest = await orbitChainClient.arbAggregatorPrepareTransactionRequest({ functionName: 'setFeeCollector', args: [, ], upgradeExecutor: false, account: owner.address, }); await orbitChainClient.sendRawTransaction({ serializedTransaction: await owner.signTransaction(transactionRequest), }); ``` #### Adding a new batch poster To add a new batch poster, call the `setIsBatchPoster(address,bool)` method of the `SequencerInbox` contract on the parent chain: ```shell cast send --rpc-url $PARENT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY $SEQUENCER_INBOX_ADDRESS "setIsBatchPoster(address,bool) ()" $NEW_BATCH_POSTER_ADDRESS true ``` **Optional: Pre-register for fee management** If you want to configure a fee collector before the batch poster sends its first batch, you can optionally pre-register the address on the Arbitrum chain by calling `addBatchPoster(address)` of the [`ArbAggregator`](/arbitrum-essentials/precompiles/reference.md#arbaggregator) precompile: ```shell cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x000000000000000000000000000000000000006D "addBatchPoster(address) ()" $NEW_BATCH_POSTER_ADDRESS ``` > **NOTE** > > When setting a new batch poster, its fee collector will be configured to the same address by default. ### Parent chain surplus fee Parent chain surplus fees are paid to a specific `L1RewardRecipient` address that is configured individually per chain. The current fee collector address can be obtained by calling the method `getL1RewardRecipient()(address)` of the [`ArbGasInfo`](/arbitrum-essentials/precompiles/reference.md#arbgasinfo) precompile. For example: ```shell cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006C "getL1RewardRecipient() (address)" ``` Alternatively, you can obtain this information using the Chain SDK: ```typescript const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbGasInfoPublicActions); const parentChainRewardRecipient = await orbitChainClient.arbGasInfoReadContract({ functionName: 'getL1RewardRecipient', }); ``` To set a new `L1RewardRecipient` address, you can call the method `setL1PricingRewardRecipient(address)` of the [`ArbOwner`](/arbitrum-essentials/precompiles/reference.md#arbowner) precompile, and pass the address of the new reward recipient. For example: ```shell cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setL1PricingRewardRecipient(address) ()" $NEW_L1REWARDRECIPIENT_ADDRESS ``` Alternatively, you can use the Chain SDK to set the new address: ```typescript const owner = privateKeyToAccount(); const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbOwnerPublicActions); const transactionRequest = await orbitChainClient.arbOwnerPrepareTransactionRequest({ functionName: 'setL1PricingRewardRecipient', args: [], upgradeExecutor: false, account: owner.address, }); await orbitChainClient.sendRawTransaction({ serializedTransaction: await owner.signTransaction(transactionRequest), }); ``` ## How to use the fee distribution contracts? In the previous section we described how to set the individual collector addresses for each fee type. Some chains may require multiple addresses to receive the collected fees of any of the available types. In those cases, there's the possibility of using a distributor contract that can gather all fees of a specific type and distribute those among multiple addresses. This section shows how to configure a distributor contract to manage the fees of a specific type. > **INFO** — Example scripts available in the Chain SDK > > This section will explain the process of deploying and configuring a distribution contract manually, but the Chain SDK includes an [example to perform this process through a script](https://github.com/OffchainLabs/arbitrum-chain-sdk/tree/main/examples/setup-fee-distributor-contract). ### Step 1. Deploy the distributor contract An example implementation of a distributor contract can be found [in the fund-distribution-contracts repo](https://github.com/OffchainLabs/fund-distribution-contracts/blob/main/src/RewardDistributor.sol). You'll have to deploy this contract on your Arbitrum chain. ### Step 2. Set the contract address as the desired fee type collector address Use the instructions provided in the previous section to set the address of the deployed distributor contract as the collector of the desired fee type. For example, if you want the distributor contract to manage the Arbitrum chain surplus fees, set the `networkFeeAccount` to the address of the deployed contract. ### Step 3. Configure the recipients of fees in the contract Now you can set the different addresses that will be receiving fees from that distributor contract. To do that, you can call the method `setRecipients(address[], uint256[])` of the distributor contract, and specify the list of addresses that will be receiving fees, and the proportion of fees for each address. For example, if you want to set two addresses as receivers, with the first one receiving 80% of the fees and the second one receiving 20% of the fees, you'll use the following parameters: ```shell cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY $DISTRIBUTOR_CONTRACT_ADDRESS "setRecipients(address[],uint256[]) ()" "[$RECEIVER_1, $RECEIVER_2]" "[8000, 2000]" ``` ### Step 4. Trigger the distribution of fees With the recipients configured in the distributor contract, and with the contract having collected some fees, you can now trigger the distribution of fees to the recipients by using the method `distributeRewards(address[], uint256[])` of the distributor contract, and specifying the list of addresses that are configured, and the proportion of fees for each address. The parameters passed must match the information that is set in the contract (i.e., you can't specify different addresses or proportions than what's been configured beforehand). For example, if you want to distribute the fees to the two addresses specified before, you'll use the following parameters: ```shell cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY $DISTRIBUTOR_CONTRACT_ADDRESS "distributeRewards(address[],uint256[]) ()" "[$RECEIVER_1, $RECEIVER_2]" "[8000, 2000]" ``` --- > For a complete page index, fetch # Configure and optimize gas To configure gas behavior on an Arbitrum chain using the [Chain SDK](https://github.com/OffchainLabs/arbitrum-chain-sdk), you primarily use the `ArbOwner` precompile (at address `0x0000000000000000000000000000000000000070`), which allows the chain owner to manage key parameters via function calls. These are called with post-deployment using tools like `cast` (from Foundry) or a custom script with a library like `ethers.js` or `viem`. The defaults inherit from the parent chain or standard Nitro settings, but it is possible to customize your Arbitrum chain as they are permissioned by design. Below is a breakdown of each specified parameter, including its role, typical defaults (based on Arbitrum One), and how to configure it. Note that changes may require careful consideration to avoid impacting chain performance, user experience, or security. For example, setting limits too high could lead to state bloat or slower block processing. > **WARNING** — Caution > > Always test changes on a devnet or testnet Arbitrum chain, as misconfiguration can lead to unexpected fee behavior or chain instability. Refer to the [`ArbOwnerPublic` precompile](/arbitrum-essentials/precompiles/reference.md#arbowner) for reading current settings without modifications. If using a custom gas token, ensure configurations are compatible. ## Gas target This is the target gas consumption rate per second that the chain can sustainably handle. It influences the congestion mechanism: if gas usage exceeds this limit, the base fee rises to throttle demand; if below the limit, the cost decreases (down to the floor). [ArbOS](/how-arbitrum-works/deep-dives/arbos.md) uses this to update gas pools and enforce dynamic per-block gas limits. Arbitrum Chains support two configuration modes - * **\[RECOMMENDED] Multiple Gas Targets (supported starting [ArbOS 51 Dia](/run-arbitrum-node/arbos-releases/arbos51.md))** * **Description**: Provides smoother price transitions. It uses multiple gas targets to reduce the frequency and severity of child chain gas price spikes. * **Configuration**: Call the `setGasPricingConstraints(uint64[3][] calldata newConstraints)` method on the [`ArbOwner` precompile](/arbitrum-essentials/precompiles/reference.md#arbowner), where `newConstraints` is an array of `[gasTargetPerSecond, adjustmentWindowSeconds, startingBacklogValue]` tuples.
Detailed information on how to set these parameters can be found in [how to configure dynamic pricing for your chain](/launch-arbitrum-chain/chain-config/costs/dynamic-pricing.md).
Example using cast: ```shell cast send --rpc-url $ORBIT_RPC --private-key $OWNER_KEY 0x0000000000000000000000000000000000000070 "setGasPricingConstraints((uint64,uint64,uint64)[])" $NEW_CONSTRAINTS ``` * **Single Gas Target** * **Description**: Uses a single gas target as the reference for price adjustments. If gas usage exceeds this limit, the base fee rises to throttle demand; if usage is below, the price moves towards the floor. * **Configuration**: Call the `setSpeedLimit(uint256 newSpeedLimit)` method on the [`ArbOwner` precompile](/arbitrum-essentials/precompiles/reference.md#arbowner), where `newSpeedLimit` is the desired gas per second.
Example using cast: ```shell cast send --rpc-url $ORBIT_RPC --private-key $OWNER_KEY 0x0000000000000000000000000000000000000070 "setSpeedLimit(uint256)" $NEW_SPEED_LIMIT ``` This setting is adjustable post-deployment. Any modifications should align with your node's capabilities to prevent issues with the congestion mechanism activating. Refer to the [Chain parameters](/arbitrum-essentials/reference/chain-params.md) page, or the [Additional configuration parameters](/launch-arbitrum-chain/chain-config/additional-configuration-parameters.md) for additional information. ## Block gas limit * **Description**: This limits the maximum amount consumable by all transactions in a single L2 block—for execution, not for the parent chain data availability (DA) fee charge. It helps control block size and processing time. In practice, ArbOS enforces a dynamic limit based on the gas target and gas pools, but this sets a hard cap. * **Default**: 32,000,000 gas per block. * **Configuration**: Call the [`setMaxTxGasLimit(uint256 newLimit)`](/arbitrum-essentials/precompiles/reference.md#arbowner) method on the [`ArbOwner` precompile](/arbitrum-essentials/precompiles/reference.md#arbowner), where `newLimit` is the desired max gas. This method is described as setting the block limit, although technically it may focus on per-transaction limits; in practice, it caps block usage. Example using `cast`: ```shell cast send --rpc-url $ORBIT_RPC --private-key $OWNER_KEY 0x0000000000000000000000000000000000000070 "setMaxTxGasLimit(uint256)" $NEW_LIMIT ``` This setting is adjustable post-deployment. Setting it too high can harm user experience due to longer block times. Refer to the [Chain parameters](/arbitrum-essentials/reference/chain-params.md) page, or the [Additional configuration parameters](/launch-arbitrum-chain/chain-config/additional-configuration-parameters.md) for additional information. ## Gas price floor * **Description**: This is the minimum L2 base fee (in `wei` per gas unit), preventing fees from dropping too low during periods of low activity. The actual base fee fluctuates based on demand, but it will not fall below this set floor. It will affect user transaction costs. * **Default**: 0.1 `gwei` (100,000,000 `wei`). * **Configuration**: * **During deployment**: Set the `minL2BaseFee` field in the Arbitrum chain setup script's config JSON. * **Post-deployment**: Call the `setMinimumL2BaseFee(uint256 newMinBaseFee)` method on the [`ArbOwner` precompile](/arbitrum-essentials/precompiles/reference.md#arbowner), where `newMinBaseFee` is the value in `wei`. Example using `cast`: ```shell cast send --rpc-url $ORBIT_RPC --private-key $OWNER_KEY 0x0000000000000000000000000000000000000070 "setMinimumL2BaseFee(uint256)" $NEW_FLOOR_IN_WEI ``` You can query the current floor via the `ArbGasInfo` precompile's `getMinimumGasPrice()`. Refer to the [Chain parameters](/arbitrum-essentials/reference/chain-params.md), [Additional configuration parameters](/launch-arbitrum-chain/chain-config/additional-configuration-parameters.md), or the [How to manage the fee parameters](/launch-arbitrum-chain/chain-config/costs/fee-management.md) pages for additional information. ## Per batch gas cost * **Description**: This refers to the fixed L1 base charge applied for the overhead of posting each batch to the parent chain. It is used in transaction pricing to amortize the parent chain's fixed costs (like calldata posting) over the batch's transactions. It can be adjusted to reflect actual posting costs or incentivize batching. The surplus (if any) gets rewarded to the Batch Poster. * **Default**: Varies based on parent chain fees, but typically around 210,000 gas (adjustable to match). * **Configuration**: Call the `setPerBatchGasCharge(int64 newCharge)` on the [`ArbOwner` precompile](/arbitrum-essentials/precompiles/reference.md#arbowner), where `newCharge` is the fixed gas units per batch. This configuration adjusts the allocation of L2 gas for batch overhead in pricing. Additionally, you can configure related surplus fees (extra reward to batch poster) via `setL1PricingRewardRate(uint64 rate)`. The parent chain base fee estimates influence the actual cost, which is dynamic; therefore, monitor them via `ArbGasInfo`'s `getPricesInWei()` method. Example using `cast` (for per batch charge): ```shell cast send --rpc-url $ORBIT_RPC --private-key $OWNER_KEY 0x000000000000000000000000000000000000000070 "setPerBatchGasCharge(int64)" $NEW_BATCH_COST ``` This configuration is adjustable post-deployment. When working with layers or parent chains, you may need to further configure the data availability setup. Refer to the [How to manage the fee parameters](/launch-arbitrum-chain/chain-config/costs/fee-management.md) page for more information. ## Gas floor per token > **INFO** > > This setting is only available starting from [ArbOS 51 Dia](/run-arbitrum-node/arbos-releases/arbos51.md). * **Description**: Gas floor per token refers to the minimum gas price floor applied for batch posting to [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623)- enabled parent chains. It ensures that transactions with large calldata pay enough fees to cover data posting costs. * **Default**: Default value of this setting is based on the parent chain - * If your L2 chain posts calldata to Ethereum, the value should be set to **10**, as specified in the [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623).
**Note:** While this value does not affect posting EIP-4844 blobs, we recommend keeping the default value of 10 as a safeguard. If your chain falls back to calldata posting - due to higher fees for blob posting or configuration changes - this setting ensures you continue to collect sufficient fees to cover L1 data posting costs. * If your L3 chain posts data to Arbitrum One / Nova, this parameter doesn't need to be set as these chains do not support EIP-7623. * If your L2 or L3 chain posts data to another parent chain, the parameter will have to be set based on the parent chain. * **Configuration**: Call the `setParentGasFloorPerToken(uint64 newFloor)` on the [`ArbOwner` precompile](/arbitrum-essentials/precompiles/reference.md#arbowner) to set the value for `gasFloorPerToken`. This value should be adjusted based on the parent chain's EIP-7623 settings to ensure the data-posting costs are fully covered. > **WARNING** > > Changing this value from the recommended default is not advised and should be left untouched in most circumstances, unless required by your parent chain. Example using `cast` (for per batch charge): ```shell cast send --rpc-url $ORBIT_RPC --private-key $OWNER_KEY 0x000000000000000000000000000000000000000070 "setParentGasFloorPerToken(uint64)" $NEW_FLOOR_VALUE ``` This setting is adjustable post-deployment. You can query the current floor via the `ArbOwnerPublic` precompile's `getParentGasFloorPerToken()`. --- > For a complete page index, fetch # Guidance for Arbitrum chains gas target ## What is the gas target on an Arbitrum chain? The parameter that governs an Arbitrum chain's throughput limit is known as the `gas target`. The gas target is measured in *gas per second* and is used as a threshold for increasing gas prices. For example, when cumulative usage on Arbitrum One and Arbitrum Nova exceed a certain amount of gas per second, the `L2 base fee` rises to increase the amount of `gwei` charged per unit of gas. This happens using a similar approach to [Ethereum's EIP-1559 pricing algorithm](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md). [You can read more about how gas fees are calculated on Arbitrum in this explainer](/how-arbitrum-works/deep-dives/gas-and-fees.md) ## Why do we have throughput limits on blockchains? The effect of raising gas prices at the gas target is to curb user demand when the chain is congested. Doing so protects the chain's underlying infrastructure from being overloaded. This is because blockchain nodes have computation constraints that should not be exceeded. Charging more during congested periods ensures that high-priority transactions can still be processed while deterring users and apps from submitting low-priority transactions until a lower activity period. The gas target, therefore, is fundamentally a protective mechanism. If the chain load exceeds what a Nitro validator node can process, then a chain risks halting due to validator downtime. It's important to note here that the security and liveness of an Arbitrum chain are always maintained through its parent chain contracts, but undoubtedly, the best user experience requires the validators and sequencer to be online. ## What are the risks of increasing my gas target? An increase in the speed target allows users and apps to perform more onchain actions without incurring additional costs. This makes it possible for a chain's nodes to experience higher and unexpected loads. When faced with high, sustained demand, the additional load could eventually lead to undesirable increases in infrastructure costs, cause nodes to lag behind the chain, and risk halting if the demand exceeds the resources of validator nodes. Refer to the [State Growth](/launch-arbitrum-chain/operate/state-growth.md) article for more information. ## Is Offchain Labs working on software improvements to allow Arbitrum chain owners to *safely* raise their chain's speed target? Yes. Offchain Labs is currently working on several key initiatives to improve the core Nitro node software that would result in a safe and formally endorsed increase in the speed targets for Arbitrum chains. These initiatives include migrations to PathDB and PebbleDB (alongside their respective optimizations for Arbitrum chains) and alternative execution layer client implementations for Nitro (e.g., Reth). We will share updates and news on these initiatives when we have them––stay tuned! --- > For a complete page index, fetch # Reporting on Fees Info As a RaaS or Chain Owner, if you require internal daily reporting on fees, we have an [RPC script you can leverage for reporting purposes](https://github.com/yahgwai/aep-fee-tracker/). ## Fee collection system overview ### 1. Fee collector address types (L1 focus) There are two main L1 fee collector addresses: 1. **L1 Base Fee**: Refunds [Batch Poster](/how-arbitrum-works/deep-dives/assertions.md) for parent chain posting costs 2. **L1 Surplus**: Collects profit on Batch posting fees #### Important distinction: * L1 Base Fee is for cost recovery only (NOT for profit) * L1 Surplus is for profit collection * Using L1 Base Fee for profit indicates misconfiguration ### 2. How the system works (same gas token scenario) #### Transaction flow: 1. Users send transactions on the child chain 2. Transactions are packaged into batches 3. Batch poster sends batches to parent chain (paying parent chain gas) 4. Batch posting reports are generated containing: * Batch size * Gas price used #### Fee recovery mechanism: 1. [ArbOS](/how-arbitrum-works/deep-dives/arbos.md) consumes batch posting reports 2. Creates gas price view of parent chain on child chain 3. Uses this view for pricing future transactions 4. Implements balancing mechanism to ensure: * Amount collected in L1 base fee ≈ Amount spent on batch posting * Uses estimates that average out over time ### 3. Custom gas token scenario (different tokens on parent/child chains) #### Transaction flow: 1. Users send transactions on the child chain 2. Transactions are packaged into batches 3. Batch poster sends batches to parent chain (paying parent chain gas) 4. **Fee token price setter mechanism scales the gas price by the exchange rate** 5. Batch posting reports are generated containing: * Batch size * Gas price used **(scaled by the exchange rate)** #### Fee recovery mechanism: Same as the same gas token scenario, but uses the gas price from the batch posting report that was already scaled by the exchange rate. #### The Exchange rate problem: * Batch poster spends in parent chain gas units * Gets refunded in child chain gas units * Creates exchange rate risk * Should NOT set artificially high exchange rate for profit #### Revenue calculation methods: #### Method 1: Exchange rate analysis (parent chain only): * **Revenue Calculation**: * Simulated revenue = gas used × gas price (from batch posting report) * Note: This gas price has already been adjusted by the fee token pricer mechanism * This represents the child chain tokens that will be collected from users * **Data Source**: Batch posting reports only * **Process**: 1. Extract gas used and scaled gas price from batch posting report 2. Calculate simulated revenue in child chain tokens 3. Compare against actual costs in parent chain tokens (using ideal exchange rate) * **Advantage**: Lightweight - only needs parent chain data #### Method 2: Direct balance tracking (child chain): * **Revenue**: Actual amount collected in L1 base fee collector address * **Data Source**: Child chain L1 base fee address balance * **Process**: Track actual collected amounts * **Advantage**: More accurate - uses real collection data > **NOTE** > > Method 1 and Method 2 should be roughly equivalent over the long term. #### Analysis criteria (both must be met): 1. **Volume Threshold** * Check the total value in batch posting reports * Must be above threshold (filters out small chains with low volume) 2. **Cost vs Revenue Deviation** * Calculate the deviation between revenue and estimated costs * Use either Method 1 or Method 2 for revenue * Must exceed the deviation threshold > **WARNING** > > If both criteria are met → indicates misconfiguration #### Exchange rate sources: * Use third-party sources for "ideal" exchange rates (e.g., CoinGecko, CoinMarketCap, etc.) #### Output: * The tool generates a simple report showing exchange rate deviations. * If both criteria are met, contact the chain owner to inform them that they need to use a better pricing mechanism Info As a RaaS, if you want to dispute the fees, you can contact the Arbitrum Foundation to review your fees. --- > For a complete page index, fetch # Understand network revenue routing on your Arbitrum chain Every transaction on an Arbitrum chain pays a single fee, but under the hood that fee is split into components that travel very different paths before reaching their final destination. Some components are credited to a collector address in the same transaction that paid them, while others are routed through a system pool and paid out after batches are posted. This page traces the complete life of a transaction fee: which address collects each component, when funds actually move, why block explorers estimate, and how and when to retrieve funds. To change the fee parameters and collector addresses described here, follow the step by step instructions in [How to manage the fee parameters of your Arbitrum chain](/launch-arbitrum-chain/chain-config/costs/fee-management.md). ## Main fee components Transaction fees on an Arbitrum chain split into four components, each with its own collector: | Fee component | What it covers | Collected by | Paid out | | ------------------------------ | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | ------------------------------- | | **Arbitrum chain base fee** | Execution, up to the configured minimum base fee | `infraFeeAccount` | Immediately, per transaction | | **Arbitrum chain surplus fee** | Execution paid above the minimum base fee (congestion), plus any priority fees | `networkFeeAccount` | Immediately, per transaction | | **Parent chain base fee** | Estimated cost of posting the transaction's data to the parent chain | `L1PricerFundsPoolAddress` (system pool), then the batch poster's **fee collector** | After each batch posting report | | **Parent chain surplus fee** | Optional per data unit reward configured by the chain owner | `L1RewardRecipient` | After each batch posting report | Fallback behavior If `infraFeeAccount` is not set (the zero address), the entire execution fee (base and surplus) goes to `networkFeeAccount`. At chain deployment, `networkFeeAccount` is initialized to the initial chain owner and `infraFeeAccount` starts unset, so a freshly deployed chain sends all execution fees to the chain owner's address. The first two components are straightforward: ArbOS credits them to their collector addresses during transaction execution in the same block. The rest of this page focuses on the parent chain base fee and the parent chain surplus fee. ## Fee lifecycle ![A user pays one gas fee on the child chain. ArbOS splits that fee into two paths. ArbOS credits the execution fee in the same transaction: the Arbitrum chain base fee to infraFeeAccount, and the surplus fee plus priority fees to networkFeeAccount. ArbOS credits the parent chain fee to the L1PricerFundsPoolAddress system pool (0xA4B0…00f6), where it accumulates. Separately, the batch poster posts a batch to SequencerInbox on the parent chain and pays that gas out of pocket. SequencerInbox then sends a batch posting report into the delayed inbox. When ArbOS processes that report, it pays out from the pool: first the parent chain surplus fee to L1RewardRecipient, then the amount due to the batch poster's fee collector.](/img/arb-chain-fee-lifecycle.svg) 1. **A user pays the transaction fee.** ArbOS estimates the transaction's parent chain footprint by compressing it and multiplying the size by the current price per data unit (its running estimate of parent chain costs). This amount is charged as part of the transaction's gas. 2. **The fee is split at the end of execution.** The execution components go directly to `infraFeeAccount` and `networkFeeAccount`. The parent chain component is credited to `L1PricerFundsPoolAddress` (`0xA4B00000000000000000000000000000000000f6`), a system owned pool with no private key. The pool exists to reimburse whoever ends up paying for batch posting. 3. **The batch poster posts a batch.** The batch poster submits your chain's transaction data to the `SequencerInbox` contract on the parent chain, paying parent chain gas **from its own parent chain balance**. This is the cost the pool will cover later on. 4. **The parent chain reports the spending.** In the same transaction that records the batch, the `SequencerInbox` contract sends a *batch posting report* message into your chain's delayed inbox, containing who posted the batch and the parent chain base fee at the time. 5. **ArbOS processes the report and pays out.** When the report arrives on the child chain, ArbOS computes what the batch actually cost (reported base fee × the batch's data gas, plus a fixed per batch overhead) and records it as funds due to that batch poster. It then pays, from the pool's balance: * First, any **parent chain surplus fee** owed to the configured reward recipient (reward rate × data units processed). * Then, the accumulated amount due to the batch poster, sent to that poster's **fee collector** address. If the pool doesn't hold enough to cover everything owed, the remainder stays on the books as funds due and is paid from future collections. 6. **The price adjusts.** ArbOS compares the pool's balance against the total funds due and nudges the price per data unit up or down, so that over time, collections converge on actual costs. The full algorithm is described in [Gas and fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#parent-chain-gas-pricing). ## When do funds move? Payouts to the fee collector and reward recipient are triggered **by batch posting reports**. In practice this means: * Funds move shortly after each batch is posted once the report has transited the delayed inbox and been processed on the child chain. * Batch posting frequency depends on your chain's traffic and batch poster configuration (size, frequency), so payout frequency varies with it. Information about the batch poster configuration is described in [Batch Poster](/launch-arbitrum-chain/chain-config/batch-poster/config-batch-poster.md). * Every report will cover as much of the funds as it can, ## Estimated versus actual fees Block explorers such as Blockscout display a per transaction "Gas used for L1" value (and a corresponding L1 fee). A common question is whether this is the **actual** cost of posting that transaction to the parent chain. **It is an estimate**, and it is never retroactively corrected. Here is why: * When a transaction executes, its batch doesn't exist yet, meaning the transaction has yet to be posted to the parent chain. Therefore ArbOS estimates the parent chain costs by using the price per data unit from the previously posted batch as an estimate. * The charged amount is recorded in the transaction receipt as `gasUsedForL1`: the parent chain fee re-expressed in child chain gas units at the transaction's gas price. This receipt field is what explorers display. * The transaction's *actual* share of batch posting costs is only knowable later, when the batch containing it is posted and reported. That actual cost is never attributed back to individual transactions. Instead, the difference between what was collected and what batches actually cost accumulates as a surplus or deficit in `L1PricerFundsPoolAddress`, and the adaptive pricing algorithm adjusts future charges to drive that difference toward zero. Gas estimation pads the fee `eth_estimateGas` responses include an additional \~10% padding on the parent chain component to protect users against parent chain gas price increases between estimation and execution. The amount actually charged at execution time uses the unpadded current price, so estimates typically exceed final charges. You can read the parent chain fee charged to the current transaction from within a contract, or inspect a live chain's pricing state, using the [`ArbGasInfo`](/arbitrum-essentials/precompiles/reference.md#arbgasinfo) precompile (`getCurrentTxL1GasFees()`, `getL1BaseFeeEstimate()`). ## Funds are stagnant unless moved All four collector addresses are addresses **on your chain**, and every payout described above is a native token transfer on your chain. The protocol never bridges revenue to the parent chain on your behalf. Two balances that operators sometimes conflate are entirely separate: * **The batch poster's parent chain balance**: the wallet that pays for posting batches. It spends the parent chain's gas token and depletes over time. You must keep it funded. If you hold that key in a KMS or a remote signer rather than locally, see [Batch poster: External signing (KMS)](/launch-arbitrum-chain/integrations/bp-kms-signing-services.md). * **The fee collector's child chain balance**: where reimbursement and revenue accumulate, denominated in your chain's gas token. The protocol's pricing loop makes the second balance grow in proportion to what the first one spends, but moving value between them is the chain operator's job. There are two options to move revenue from the child chain to the parent chain: 1. **Withdraw manually through the bridge.** If a collector is an EOA or multisig you control, initiate a standard child to parent withdrawal and execute it on the parent chain after the challenge period. 2. **Use the AEP fee router contracts.** Set your collector addresses to `ChildToParentRouter` contracts, which route received funds to a configurable parent chain address and can be triggered permissionlessly. This is the recommended setup for chains with [Arbitrum Expansion Program](/launch-arbitrum-chain/chain-config/costs/aep-overview.md) obligations (see [AEP fee router contracts](/launch-arbitrum-chain/chain-config/costs/aep-router-contracts.md)). Custom gas token chains On chains with a custom gas token, fee collection is denominated in your gas token while the batch poster spends the parent chain's native token, so the reimbursement stream and the actual cost are in different assets. Such chains typically disable the parent chain base fee (set the price per unit to zero) and handle batch posting costs off-protocol. See [How to use a custom gas token](/launch-arbitrum-chain/chain-config/costs/custom-gas-token-rollup.md) and the custom gas token scenario in [AEP fee reporting](/launch-arbitrum-chain/chain-config/costs/reporting-on-fees.md). ## Monitoring the fee pool A few useful read calls for reporting on the parent chain fee stream (all against your chain's RPC): ```shell # Balance sitting in the fee pool, waiting to be paid out cast balance 0xA4B00000000000000000000000000000000000f6 --rpc-url $CHAIN_RPC # Pool funds recognized by the pricer as available for payouts cast call --rpc-url $CHAIN_RPC 0x000000000000000000000000000000000000006C "getL1FeesAvailable() (uint256)" # Surplus (positive) or deficit (negative): available funds minus everything owed cast call --rpc-url $CHAIN_RPC 0x000000000000000000000000000000000000006C "getL1PricingSurplus() (int256)" # Amount accrued to the reward recipient but not yet paid cast call --rpc-url $CHAIN_RPC 0x000000000000000000000000000000000000006C "getL1PricingFundsDueForRewards() (uint256)" # Batch posters known to the chain, and a poster's fee collector cast call --rpc-url $CHAIN_RPC 0x000000000000000000000000000000000000006D "getBatchPosters() (address[])" cast call --rpc-url $CHAIN_RPC 0x000000000000000000000000000000000000006D "getFeeCollector(address) (address)" $BATCH_POSTER_ADDRESS ``` A negative surplus is normal after periods of high parent chain gas prices since recent batches cost more than what was collected. Therefore the adaptive pricing algorithm is raising the per unit price to recover. A persistently large positive surplus means users are being overcharged relative to costs; a chain owner can correct the price directly (`ArbOwner.setL1PricePerUnit`) or leave the algorithm to converge. Funds sent directly to the pool address The pricer only pays out funds it has accounted for. If tokens are transferred directly to `L1PricerFundsPoolAddress` outside of fee collection, a chain owner can make them available for payouts using `ArbOwner.releaseL1PricerSurplusFunds(uint256)`. For structured daily reporting across all fee streams, see [AEP fee reporting](/launch-arbitrum-chain/chain-config/costs/reporting-on-fees.md), which includes a ready made [RPC reporting script](https://github.com/yahgwai/aep-fee-tracker/). ## See also * [How to manage the fee parameters of your Arbitrum chain](/launch-arbitrum-chain/chain-config/costs/fee-management.md) — configuring every parameter and collector mentioned here * [Gas and fees](/how-arbitrum-works/deep-dives/gas-and-fees.md#parent-chain-gas-pricing) — the parent chain pricing mechanism and adaptive algorithm in depth * [AEP fee router contracts](/launch-arbitrum-chain/chain-config/costs/aep-router-contracts.md) — routing revenue to the parent chain automatically * [Configure your chain's batch poster](/launch-arbitrum-chain/chain-config/batch-poster/config-batch-poster.md) — the operational side of the component this revenue reimburses * [Batch poster: External signing (KMS)](/launch-arbitrum-chain/integrations/bp-kms-signing-services.md) — securing the parent chain wallet that this revenue stream reimburses --- > For a complete page index, fetch # Configure data availability During Arbitrum chain configuration, you'll choose the data availability (DA) option that best suits your needs. This choice affects how transaction data is stored and accessed for validation—impacting security, costs, and performance. Using the [Chain SDK](https://github.com/OffchainLabs/arbitrum-chain-sdk) lets you select from multiple DA options, tailoring your decision to meet your specific requirements. ## Available data availability options ### 1. Rollup mode (Ethereum/parent chain DA) This is the default option for chains that prioritize security. Transaction data is posted to the parent chain as calldata or blobs (after [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844)), using Ethereum's DA layer. * Set `DataAvailabilityCommittee` to `false` in your chain config using the Chain SDK. No additional steps are required. ### 2. AnyTrust mode This mode uses an external Data Availability Committee (DAC), which is a group of permissioned nodes that you choose and run, or outsource to a Rollup-as-a-Service (RaaS). Instead of posting all the data to the parent chain, the batch poster sends a lightweight Data Availability Certificate (DACert) signed by the required number of DAC members (configurable). The data is stored offchain and made available when needed. * Set `DataAvailabilityCommittee` to `true` in your chain config. * Deploy a Data Availability Server (DAS) for each DAC member using Nitro's DAS software. * Generate a keyset (BLS public keys and quorum threshold) from DAC members. * Register the generated keyset on the `SequencerInbox` contract after deployment. * Configure nodes (sequencer, batch poster, validators) to connect to the DAS endpoints (via REST/RPC aggregators). For a complete, detailed set of instructions on setting up DAC, DAS, and generating keysets, refer to [Get started](/launch-arbitrum-chain/chain-config/data-availability/dac-get-started.md). ### 3. Alternative data availability (Alt-DA) In this mode, transaction data is posted to an external modular DA network (such as Celestia) using blobs and data availability sampling. You integrate this by running a sidecar server alongside your node, which handles communication with the external DA network. Onchain commitments, such as [Blobstream](https://docs.celestia.org/learn/blobstream/) for verification, ensure that transaction data is available and verifiable. * Use a modified Nitro build with [Celestia integration](https://docs.celestia.org/build/stacks/nitro-das-server/), such as the celestia-server sidecar, to enable alternative data availability. Set the DA provider in your node flags and specify your preferences, for instance, selecting Celestia as the main provider with a fallback to AnyTrust or Ethereum. You can integrate other DA options, like [EigenDA](https://github.com/Layr-Labs/nitro) or [Avail](https://docs.availproject.org/da/build-with-avail/deploy-rollup-on-avail/Optimium/arbitrum-nitro/overview), in a similar way. --- > For a complete page index, fetch # How to configure the Data Availability Committee (DAC) in your chain AnyTrust chains rely on an external Data Availability Committee (DAC) to store data and provide it on-demand instead of using its parent chain as the Data Availability (DA) layer. The members of the DAC run a Data Availability Server (DAS) to handle these operations. Once the DA servers are running, the chain needs to be configured with their information to effectively store and retrieve data from them. In this how-to, you'll learn how to configure the DAC in your chain. Refer to the [Introduction](/launch-arbitrum-chain/chain-config/data-availability/dac-get-started.md) for the full process of running DA servers and configuring the chain. This how-to assumes that you're familiar with: * The DAC's role in the AnyTrust protocol. Refer to [Inside AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) for a refresher. * [Kubernetes](https://kubernetes.io/). The examples in this guide use Kubernetes to containerize your DAS. * [How to deploy a Data Availability Server (DAS)](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md). This is needed to understand where the data we'll be handling in this guide comes from. * The [Foundry toolkit](https://github.com/foundry-rs/foundry) ## Step 0: Prerequisites Before starting to generate the keyset and configuring the nodes and chain, you'll need to gather the following information from all the DA servers run by the DAC members: * Public BLS Key * URL of the RPC endpoint * URL(s) of the REST endpoint(s) You should also make sure that at least one DAS is running as an [archive DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md#archive-da-servers), otherwise the information will not be available after the expiry time. ## Step 1: Generate the keyset and keyset hash with all the information from the servers ### What is a keyset? The AnyTrust protocol assumes that for the `n` members of the DAC, a minimum of `h` members maintain integrity. So `h` is then the minimum number of trusted committee members on an AnyTrust chain. In scenarios where `k = (n + 1) - h` members of the DAC pledge to grant access to a specific piece of information, these `k` members must sign and attest they have stored the data to be considered successful. To perform this signing operation, each DAC member must generate their own set of BLS public and private keys. They should do this independently and ensure these keys are random and only used by them. You can find more information about how to generate a BLS pair of keys in [Generating BLS Keys](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md#step-1-generate-the-bls-keypair). An AnyTrust chain needs to know all DAC members' public keys to validate the integrity of the data being batched and posted. A *keyset* is a list of all DAC members' RPC endpoint and BLS public key. Additionally, it also contains information about how many signatures are needed to approve a Data Availability Certificate (DACert), via a special `assumed-honest` parameter (i.e., the `h` parameter we mentioned above). This design lets the chain Owner modify the DAC membership over time, and DAC members change their keys if needed. See [Inside AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) for more information. We use this keyset, and its hash to configure the `SequencerInbox` contract with the valid keyset, and also the Batch Poster (to request storing information) and full nodes (to request information already stored). ### How to generate a keyset and a keyset hash Nitro comes with a special tool to generate both the keyset and the keyset hash. To use it, you need to first structure the keyset information in a JSON object with the following structure: ```json { "keyset": { "assumed-honest": h, "backends": [ { "url": "https://rpc-endpoint-of-member-1/", "pubkey":"PUBLIC_KEY_OF_MEMBER_1" }, { "url": "https://rpc-endpoint-of-member-2/", "pubkey":"PUBLIC_KEY_OF_MEMBER_2" }, ... { "url": "https://rpc-endpoint-of-member-n/", "pubkey":"PUBLIC_KEY_OF_MEMBER_N" } ] } } ``` The JSON fields represent the following: * `assumed_honest` is the amount of members that we assume are honest from the `n` members of the DAC. This is the `h` variable we mentioned in the previous section. * `backends` contain information about each member of the DAC: * `url` contains the RPC endpoint of the DAS run by that member * `pubkey` contains the base64-encoded BLS public key used in the DAS run by that member Once you have the JSON structure, save it into a file, for example, `keyset-info.json`. Finally, we'll use Nitro's `anytrusttool dumpkeyset` utility inside Docker to generate the keyset and keyset hash. ```shell docker run -v $(pwd):/data/keyset --entrypoint anytrusttool offchainlabs/nitro-node:v3.11.3-beb2108 dumpkeyset --conf.file /data/keyset/keyset-info.json ``` This command will output two results: `Keyset` and `KeysetHash`. Save them to use in the next steps. ### Example with mocked-up data Here's an example that uses mocked-up data: The JSON file is: ```json { "keyset": { "assumed-honest": 2, "backends": [ { "url": "http://example", "pubkey": "YAbcteVnZLty5qRebeswHKhdjEMVwdou+imSfyrI+yVXHOMdLWA3Nf4DGW9tVry/mhmZqJp01TaYIsREXWdsFe1S5QCNqnddyag5yZ/5Y6GZRqx0BXmHTaxPY5kHrhvGnwxmlJVbUk1xjKRFgxxTdTk3c0AfM3JaeWYTed3avV//KGGdwHC+/Z7XPWmeXCNsGhY75YuoEAK2EwcJvAZK9de6lHEwtyBWvxcmOADxo6siacalEO+OdBL9VtHvG5FqEwbjsdnILAmTcb2YYVgqyq2joW6d/uXQ685hCWWYqC8RLQqTXoyrXEjYLjEEsMe6eRV9rRoBmj5/atB3uOYwixFv7A9YI5YiRjw2MfoB4rQnJAkhW4AJQiwWcV2+3lkJBg==" }, { "url": "http://example", "pubkey": "YAg1+ZXyR48kiS0FDaoon4trnBsYW80oUy+I1hDCZCotxvNQl0AjbTPD4tkTaqsX+BnIxnEpO7ondxd2Lo0cH3usnhfdKNKTmpWbs45QD5wRw4zrvEJuLeqXxAF1plXRdACubHX/SeiEx5RpJJ5wlTJYhUtk+oRFxYWtRdxtxpdVAcavfP9wdCAsaH+Ke/GjrBkmiXVfIyJ1tMhCGxpWaem5BMKaKSzflht4OnwLTOc2kA3k2MY8X4WmXLRK80vvhArO+Eq3X0TEyRN2ELaBB6/zu9zBkRnHqSfBFbe5v7J9hcUA7nfRPsWpejrmv1HTtwpVAuhBbee1646f7uN2QRyjXIp/P1l8dgZXjPlqRxXOWjXPSOOcCh+qLe4i105oGQ==" } ] } } ``` And when running the command we obtain: ```shell $ docker run -v $(pwd):/data/keyset --entrypoint anytrusttool offchainlabs/nitro-node:v3.11.3-beb2108 dumpkeyset --conf.file /data/keyset/keyset-info.json Keyset: 0x0000000000000002000000000000000201216006dcb5e56764bb72e6a45e6deb301ca85d8c4315c1da2efa29927f2ac8fb25571ce31d2d603735fe03196f6d56bcbf9a1999a89a74d5369822c4445d676c15ed52e5008daa775dc9a839c99ff963a19946ac740579874dac4f639907ae1bc69f0c6694955b524d718ca445831c5375393773401f33725a79661379dddabd5fff28619dc070befd9ed73d699e5c236c1a163be58ba81002b6130709bc064af5d7ba947130b72056bf17263800f1a3ab2269c6a510ef8e7412fd56d1ef1b916a1306e3b1d9c82c099371bd9861582acaada3a16e9dfee5d0ebce61096598a82f112d0a935e8cab5c48d82e3104b0c7ba79157dad1a019a3e7f6ad077b8e6308b116fec0f58239622463c3631fa01e2b4272409215b8009422c16715dbede5909060121600835f995f2478f24892d050daa289f8b6b9c1b185bcd28532f88d610c2642a2dc6f3509740236d33c3e2d9136aab17f819c8c671293bba277717762e8d1c1f7bac9e17dd28d2939a959bb38e500f9c11c38cebbc426e2dea97c40175a655d17400ae6c75ff49e884c79469249e70953258854b64fa8445c585ad45dc6dc6975501c6af7cff7074202c687f8a7bf1a3ac192689755f232275b4c8421b1a5669e9b904c29a292cdf961b783a7c0b4ce736900de4d8c63c5f85a65cb44af34bef840acef84ab75f44c4c9137610b68107aff3bbdcc19119c7a927c115b7b9bfb27d85c500ee77d13ec5a97a3ae6bf51d3b70a5502e8416de7b5eb8e9feee376411ca35c8a7f3f597c7606578cf96a4715ce5a35cf48e39c0a1faa2dee22d74e6819 KeysetHash: 0xfdca3e4e2de25f0a56d0ced68fd1cc64f91b20cde67c964c55105477c02f49be ``` ## Step 2: Update the `SequencerInbox` contract Once we have the keyset and its hash, we can configure the `SequencerInbox` contract so it accepts DACerts signed by the DAC members. The `SequencerInbox` can be configured with the new keyset by invoking the [setValidKeyset](https://github.com/OffchainLabs/nitro-contracts/blob/dcc51066b26b84cb157cbeba2f9f492ab33f9093/src/bridge/SequencerInbox.sol#L743) method. Note that only the chain owner can call this method. Here's an example of how to use Foundry to configure the `SequencerInbox` with the keyset generated in the previous step: ```shell cast send --rpc-url $PARENT_CHAIN_RPC --private-key $CHAIN_OWNER_PRIVATE_KEY $SEQUENCERINBOX_ADDRESS "setValidKeyset(bytes)" 0x0000000000000002000000000000000201216006dcb5e56764bb72e6a45e6deb301ca85d8c4315c1da2efa29927f2ac8fb25571ce31d2d603735fe03196f6d56bcbf9a1999a89a74d5369822c4445d676c15ed52e5008daa775dc9a839c99ff963a19946ac740579874dac4f639907ae1bc69f0c6694955b524d718ca445831c5375393773401f33725a79661379dddabd5fff28619dc070befd9ed73d699e5c236c1a163be58ba81002b6130709bc064af5d7ba947130b72056bf17263800f1a3ab2269c6a510ef8e7412fd56d1ef1b916a1306e3b1d9c82c099371bd9861582acaada3a16e9dfee5d0ebce61096598a82f112d0a935e8cab5c48d82e3104b0c7ba79157dad1a019a3e7f6ad077b8e6308b116fec0f58239622463c3631fa01e2b4272409215b8009422c16715dbede5909060121600835f995f2478f24892d050daa289f8b6b9c1b185bcd28532f88d610c2642a2dc6f3509740236d33c3e2d9136aab17f819c8c671293bba277717762e8d1c1f7bac9e17dd28d2939a959bb38e500f9c11c38cebbc426e2dea97c40175a655d17400ae6c75ff49e884c79469249e70953258854b64fa8445c585ad45dc6dc6975501c6af7cff7074202c687f8a7bf1a3ac192689755f232275b4c8421b1a5669e9b904c29a292cdf961b783a7c0b4ce736900de4d8c63c5f85a65cb44af34bef840acef84ab75f44c4c9137610b68107aff3bbdcc19119c7a927c115b7b9bfb27d85c500ee77d13ec5a97a3ae6bf51d3b70a5502e8416de7b5eb8e9feee376411ca35c8a7f3f597c7606578cf96a4715ce5a35cf48e39c0a1faa2dee22d74e6819 ``` ## Step 3: Craft the new configuration for the batch poster To configure the batch poster, we'll use the JSON structure we created in Step 1. This will allow the batch poster to send RPC requests to all the DA servers (to store the information of the transactions being included in the next batch), craft the DACert, and store it in the `SequencerInbox`. The configuration to enable the DAC in the batch poster looks like this: ```json { ... "node": { ... "da": { "anytrust": { "enable": true, "rpc-aggregator": { "enable": true, "assumed-honest": h, "backends": [ { "url": "https://rpc-endpoint-of-member-1/", "pubkey":"PUBLIC_KEY_OF_MEMBER_1" }, { "url": "https://rpc-endpoint-of-member-2/", "pubkey":"PUBLIC_KEY_OF_MEMBER_2" }, ... { "url": "https://rpc-endpoint-of-member-n/", "pubkey":"PUBLIC_KEY_OF_MEMBER_N" } ] } } } }, ... } ``` The following parameters are used (prior to Nitro v3.10.0, they lived under the now-deprecated `node.data-availability` section instead of `node.da.anytrust`): * `node.da.anytrust.enable`: tells the batch poster to handle information stored in a DAC * `node.da.anytrust.rpc-aggregator`: includes information on the RPC endpoints of all the DA servers run by DAC members. * `enable`: tells the batch poster that the RPC aggregator will be used * `assumed-honest` and `backends`: include information from the DA servers (following the same format as specified in Step 1) Once the configuration is in place, you can restart your batch poster so it begins communicating with the DA servers to store transaction data, while storing the DACert in the `SequencerInbox`. ## Step 4: Craft the new configuration for your chain's nodes Finally, we also need to configure all other nodes so they can communicate with the DAC. To do that, we'll also use the JSON structure we created in Step 1. The configuration to enable the DAC in a full node looks like this: ```json { ... "node": { ... "da": { "anytrust": { "enable": true, "rest-aggregator": { "enable": true, "urls": [ "https://rest-endpoint-of-member-1/", "https://rest-endpoint-of-member-2/", ... "https://rest-endpoint-of-member-n/", ], "online-url-list": "https://url-of-list-of-rest-endpoints" } } } }, ... } ``` The following parameters are used (prior to Nitro v3.10.0, they lived under the now-deprecated `node.data-availability` section instead of `node.da.anytrust`): * `node.da.anytrust.enable`: tells the node to query information from the DAC * `node.da.anytrust.rest-aggregator`: includes information on the REST endpoints of all the DA servers run by DAC members. * `enable`: tells the node that the REST aggregator will be used * `urls` or `online-url-list`: usually only one of these is used, although both parameters can be used and the information will be aggregated together. `urls` is a list of all REST endpoints of the DA servers, and `online-url-list` is a URL to a list of URLs of the REST endpoints of the DA servers. Once the configuration is in place, you can restart your node so it begins communicating with the DA servers to retrieve transaction data. --- > For a complete page index, fetch # DAC configuration defaults Default settings for a Data Availability Committee (AnyTrust) chain as defined in the Arbitrum Chain SDK. * `src/prepareChainConfig.ts` * `src/prepareNodeConfig.ts` * `src/types/NodeConfig.generated.ts` Nitro v3.10.0 renamed these config keys As of Nitro v3.10.0, the node-side AnyTrust keys moved: * `node.data-availability.*` is deprecated in favor of `node.da.anytrust.*` * `das-rpc-client` was renamed to `rpc-client` (with the server URL under its `rpc.url` key) * `node.da-provider` moved to `node.da.external-provider` * `node.batch-poster.das-retention-period` was renamed to `node.batch-poster.anytrust-retention-period` * `node.batch-poster.max-size` was deprecated in favor of `node.batch-poster.max-calldata-batch-size` (calldata batches) and `node.da.anytrust.max-batch-size` (AnyTrust batches). * The node no longer accepts `sequencer-inbox-address` or `parent-chain-node-url` in its DA section — those settings now exist only as `--parent-chain.*` flags of the standalone `anytrustserver`, and a Nitro v3.10.0+ node fails to start when they are present. The reference tables below use the new paths; configs generated by the Arbitrum Chain SDK (shown in the next section) may still use the legacy names until the SDK types are regenerated. ## Chain config defaults Set at rollup creation time. ```json { "arbitrum": { "EnableArbOS": true, "AllowDebugPrecompiles": false, "DataAvailabilityCommittee": false, "InitialArbOSVersion": 51, "GenesisBlockNum": 0, "MaxCodeSize": 24576, "MaxInitCodeSize": 49152 } } ``` To enable DAC, override `DataAvailabilityCommittee` to `true` at deployment: ```typescript prepareChainConfig({ chainId: 12345, arbitrum: { InitialChainOwner: '0x...', DataAvailabilityCommittee: true, }, }); ``` ## Node config defaults (when DAC is enabled) Generated by `prepareNodeConfig()` when `chainConfig.arbitrum.DataAvailabilityCommittee` is `true`. ```json { "node": { "data-availability": { "enable": true, "sequencer-inbox-address": "", "parent-chain-node-url": "", "rest-aggregator": { "enable": true, "urls": ["http://localhost:9877"] }, "rpc-aggregator": { "enable": true, "assumed-honest": 1, "backends": "[{\"url\":\"http://localhost:9876\",\"pubkey\":\"YAAA...AAA==\"}]" } }, "batch-poster": { "enable": true, "max-size": 90000, "parent-chain-wallet": { "private-key": "" } } } } ``` ## Full default values reference Every DAC-related setting with its documented default, organized by config section. ### `node.da.anytrust` | Key | Default | Description | | ----------------- | ---------------- | ------------------------------------------------ | | `enable` | `false` | Enable AnyTrust Data Availability mode | | `max-batch-size` | `1000000` (1 MB) | Maximum compressed batch size for AnyTrust DA | | `panic-on-error` | `false` | Fail immediately on DAS errors (not recommended) | | `request-timeout` | `5s` | Timeout for Store requests | The parent chain connection settings (`parent-chain.node-url`, `parent-chain.connection-attempts` with default `15`, and `parent-chain.sequencer-inbox-address`) are flags of the standalone `anytrustserver` only — refer to [How to deploy a DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md). ### `node.da.anytrust.rest-aggregator` | Key | Default | Description | | -------------------------------- | ------------------------ | ----------------------------------------------------- | | `enable` | `false` | Enable REST retrieval of batch data | | `max-per-endpoint-stats` | `20` | Latency/success-rate stats entries per endpoint | | `online-url-list` | — | URL to a remote list of REST DAS endpoint URLs | | `online-url-list-fetch-interval` | `1h0m0s` | How often to re-fetch the online URL list | | `strategy` | `simple-explore-exploit` | Endpoint selection strategy | | `strategy-update-interval` | `10s` | How often to refresh strategy with latency/error data | | `urls` | `[]` | Static list of REST DAS endpoint URLs | | `wait-before-try-next` | `2s` | Time before trying the next set of REST endpoints | ### `node.da.anytrust.rest-aggregator.simple-explore-exploit-strategy` | Key | Default | Description | | -------------------- | ------- | -------------------------------------------------------------------------------------- | | `exploit-iterations` | `1000` | Consecutive `GetByHash` calls using best-latency endpoints before switching to explore | | `explore-iterations` | `20` | Consecutive `GetByHash` calls using random endpoints before switching to exploit | ### `node.da.anytrust.rest-aggregator.sync-to-storage` | Key | Default | Description | | ------------------------------ | ---------- | --------------------------------------------------------- | | `delay-on-error` | `1s` | Wait time before retrying after sync error | | `eager` | `false` | Eagerly sync batch data using L1 as index (vs lazy) | | `eager-lower-bound-block` | — | Starting L1 block for eager sync (only if no prior state) | | `ignore-write-errors` | `true` | Log-only on write failures during sync | | `parent-chain-blocks-per-read` | `100` | Max L1 blocks to read per eager sync poll | | `retention-period` | `360h0m0s` | How long to retain synced data | | `state-dir` | — | Directory to persist sync progress | | `sync-expired-data` | `true` | Sync expired data (needed for mirror configs) | ### `node.da.anytrust.rpc-aggregator` | Key | Default | Description | | ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `assumed-honest` | — | `H` value; required signatures `K = N + 1 - H` | | `backends` | `null` | JSON array of `{url, pubkey}` objects. The aggregator assigns each backend a `signersMask` of `1 << i` based on its index in the array, so member order must match the position of each public key in the onchain keyset. | | `enable` | `false` | Enable RPC storage of batch data (Batch Poster only) | ### `node.da.anytrust.rpc-aggregator.rpc-client` | Key | Default | Description | | ---------------------- | ----------- | -------------------------------------------------------------------- | | `enable-chunked-store` | `true` | Send data to DAS in chunks instead of all at once | | `rpc.url` | `self-auth` | DAS server URL; overridden at runtime by each `backends[].url` entry | ### `node.da.anytrust.rpc-aggregator.rpc-client.data-stream` | Key | Default | Description | | --------------------------- | ---------------- | -------------------------------- | | `max-store-chunk-body-size` | `5242880` (5 MB) | Maximum HTTP body size per chunk | ### `node.da.anytrust.rpc-aggregator.rpc-client.data-stream.rpc-methods` | Key | Default | Description | | ----------------- | ------------------------ | ---------------------------------------------- | | `finalize-stream` | `das_commitChunkedStore` | RPC method to finalize a chunked store session | | `start-stream` | `das_startChunkedStore` | RPC method to initiate a chunked store session | | `stream-chunk` | `das_sendChunk` | RPC method to send a data chunk | ### `node.batch-poster` (DAC-relevant settings) | Key | Default | Description | | ------------------------------------------ | ---------- | -------------------------------------------------------------------------------------------- | | `anytrust-retention-period` | `360h0m0s` | Period DASes retain stored batches in AnyTrust mode | | `disable-dap-fallback-store-data-on-chain` | `false` | If `true`, disables fallback to onchain batch posting when DA fails | | `enable` | `false` | Enable batch posting | | `max-calldata-batch-size` | `100000` | Maximum estimated compressed calldata batch size (bytes); replaces the deprecated `max-size` | | `error-delay` | `10s` | Delay after batch posting error before retry | | `extra-batch-gas` | `50000` | Extra gas added to batch posting estimate | | `max-delay` | `1h0m0s` | Maximum batch posting delay | | `max-empty-batch-delay` | `72h0m0s` | Maximum delay before posting an empty batch | | `poll-interval` | `10s` | Interval to check for ready batches | | `check-batch-correctness` | `true` | Verify batch against inbox multiplexer | | `compression-level` | `11` | Batch compression level | | `l1-block-bound-bypass` | `1h0m0s` | Post even if outside L1 bounds, within this margin | | `delay-buffer-always-updatable` | `true` | Always treat delay buffer as updatable | | `delay-buffer-threshold-margin` | `25` | Blocks before delay buffer threshold to post batch | ### `node.da.external-provider` (newer DAProvider interface) | Key | Default | Description | | -------------------- | ------------------ | ------------------------------------------------- | | `enable` | `false` | Enable DAProvider client | | `store-rpc-method` | `daprovider_store` | Single-shot store RPC method name | | `use-data-streaming` | `false` | Use chunked streaming protocol for large payloads | | `with-writer` | `false` | DAProvider server supports writer interface | ### `node.da.external-provider.data-stream` | Key | Default | Description | | --------------------------- | ---------------- | -------------------------------- | | `max-store-chunk-body-size` | `5242880` (5 MB) | Maximum HTTP body size per chunk | ### `node.da.external-provider.data-stream.rpc-methods` | Key | Default | Description | | ----------------- | ------------------------------- | ---------------------------------------------- | | `finalize-stream` | `daprovider_commitChunkedStore` | RPC method to finalize a chunked store session | | `start-stream` | `daprovider_startChunkedStore` | RPC method to initiate a chunked store session | | `stream-chunk` | `daprovider_sendChunk` | RPC method to send a data chunk | ### `node.da.external-provider.rpc` | Key | Default | Description | | ------------------------------ | -------------------------------------------------------------------------------------------------- | -------------------------------------- | | `arg-log-limit` | `2048` | Limit size of arguments in log entries | | `retries` | `3` | Retries on failure (0 = one attempt) | | `retry-errors` | `websocket: close.*\|dial tcp .*\|.*i/o timeout\|.*connection reset by peer\|.*connection refused` | Regex for auto-retried errors | | `websocket-message-size-limit` | `268435456` (256 MB) | WebSocket message size limit | ## SDK-applied defaults(`prepareNodeConfig`) When `prepareNodeConfig()` detects `DataAvailabilityCommittee: true`, it applies these concrete values. Source: `src/prepareNodeConfig.ts:158-182` | Setting | Value Applied | Notes | | ------------------------------------------- | ------------------------------ | --------------------------------------------- | | `data-availability.enable` | `true` | Overrides the `false` default | | `data-availability.sequencer-inbox-address` | `coreContracts.sequencerInbox` | From deployment output | | `data-availability.parent-chain-node-url` | `parentChainRpcUrl` | From params | | `rest-aggregator.enable` | `true` | Overrides the `false` default | | `rest-aggregator.urls` | `[":9877"]` | Falls back to `http://localhost:9877` | | `rpc-aggregator.enable` | `true` | Overrides the `false` default | | `rpc-aggregator.assumed-honest` | `1` | Single honest member (dev default) | | `rpc-aggregator.backends` | JSON with 1 backend | URL at `:9876` | | `batch-poster.enable` | `true` | — | | `batch-poster.max-size` | `90000` | Lower than the NodeConfig default of `100000` | --- > For a complete page index, fetch # DAC/DAS Operations Guide ## DAC Quorum mechanics A Data Availability Committee has two critical parameters encoded in its **keyset**: | Parameter | Symbol | Meaning | | ------------------- | ------ | ---------------------------------------------------------- | | `numberOfMembers` | `N` | Total committee members (derived from `publicKeys.length`) | | `assumedHonest` | `H` | Number of members assumed to be honest | | Required signatures | `K` | Computed as `K = N + 1 - H` | The formula `K = N + 1 - H` means: if `H` members are honest, you need responses from at least `K` members to guarantee at least one honest signature. ### Example configurations **Single member (test/dev)** * `N=1, H=1 → K = 1+1-1 = 1 signature required` **8-member production committee** * `N=8, H=2 → K = 8+1-2 = 7 signatures required` * This means you tolerate 1 unavailable member but assume at least 2 are honest **5-member committee with strong honesty assumption** * N=5, H=3 → K = 5+1-3 = 3 signatures required * Tolerates 2 unavailable members, assumes 3 are honest ### How to configure Set the quorum when preparing the keyset: ```typescript import { prepareKeyset } from './prepareKeyset'; const publicKeys = [blsKey1, blsKey2, blsKey3 /* ... */]; // N = publicKeys.length const assumedHonest = 2; // H const keyset = prepareKeyset(publicKeys, assumedHonest); ``` The node config must match the `assumed-honest` number: ```typescript 'rpc-aggregator': { 'enable': true, 'assumed-honest': 2, // H - must match keyset's assumedHonest parameter 'backends': '...', } ``` ## Keyset binary encoding format The keyset is a compact, onchain-registered binary blob: ```shell Offset Size Field Encoding ────── ──── ───── ──────── 0 8 bytes assumedHonest (H) uint64, big-endian 8 8 bytes numberOfMembers (N) uint64, big-endian For each member i (0..N-1): +0 2 bytes keyLength[i] uint16, big-endian +2 variable publicKey[i] bytes raw BLS12-381 key (decoded from base64) ``` **Concrete example:** single member, zero key ```shell 0x 0000000000000001 ← assumedHonest = 1 0000000000000001 ← numberOfMembers = 1 0121 ← keyLength = 0x0121 = 289 bytes 60000000... ← 289 bytes of BLS public key ``` ### Keyset hash computation Keysets are identified onchain by their hash: ```shell hash = keccak256(0xfe || keccak256(keysetBytes)) keysetHash = hash XOR (1 << 255) // set the high bit ``` The high bit distinguishes keyset hashes from other hashes in the `SequencerInbox` storage. ## DAS Committee member management ### Adding/rotating members (set a new valid keyset) You cannot add individual memebers. You must register an entirely new keyset. Use `buildSetValidKeyset` to do so: ```typescript import { prepareKeyset } from './prepareKeyset'; import { buildSetValidKeyset } from './actions/buildSetValidKeyset'; // 1. Prepare keyset bytes with the new member list const newKeyset = prepareKeyset( [exisitingKey1, exisitingKey2, newMemberKey], // all BLS pubkeys (base64) 2, // assumedHonest ); // 2. Register onchain via UpgradeExecutor -> SequencerInbox.setValidKeyset const tx = await buildSetValidKeyset(publicClient, { account: ownerAddress, upgradeExecutor: '0x...', sequencerInbox: '0x...', params: { keyset: newKeyset }, }); ``` This emits a `SetValidKeyset(bytes32 indexed keysetHash, bytes keysetBytes)` event. ### Removing members (invalidate old keyset, set new one) Use `buildInvalidateKeysetHash`: ```typescript import { prepareKeysetHash } from './prepareKeysetHash'; import { buildInvalidateKeysetHash } from './actions/buildInvalidateKeysetHash'; // 1. Compute hash of the keyset to remove const oldKeysetHash = prepareKeysetHash(oldKeysetBytes); // 2. Invalidate it onchain await buildInvalidateKeysetHash(publicClient, { account: ownerAddress, upgradeExecutor: '0x...', sequencerInbox: '0x...', params: { keysetHash: oldKeysetHash }, }); // 3. Set the new keyset (without the removed member) const newKeyset = prepareKeyset(remainingKeys, assumedHonest); await buildSetValidKeyset(publicClient, { /* ... */ params: { keyset: newKeyset } }); ``` This emits an `InvalidateKeyset(bytes32 indexed keysetHash)` event. ### Querying current valid keysets Use `getKeysets`, which replays `SetValidKeyset` and `InvalidateKeyset` events to reconstruct the current state: ```typescript import { getKeysets } from './getKeysets'; const { keysets } = await getKeysets(publicClient, { sequencerInbox: '0x...', }); // keysets = { [keysetHash]: keysetBytes, ... } - only currently valid ones ``` ### Checking if a specific keyset is valid ```typescript import { isValidKeysetHash } from './actions/isValidKeysetHash'; const isValid = await isValidKeysetHash(publicClient, { sequencerInbox: '0x...', params: { keysetHash: '0x...' }, }); ``` ## DAC failure modes and fallback to batch posting ### Failure modes | Failure | Impact | Recovery | | -------------------------- | -------------------------------------------- | --------------------------------------------------- | | < `K` members respond | Cannot form valid DACert | Batch Poster falls back to onchain posting | | Wrong BLS key | Signature verification fails for that member | Other members still counted; if `K` met, cert valid | | All DAS backends down | No DACert possible | Immediate fallback to batch posting | | Keyset invalidated onchain | DACerts referencing that keyset rejected | Must set a new valid keyset | | Network partition | Subset of members unreachable | Falls back if remaining < `K` | ### The fallback mechanism When `DataAvailabilityCommittee: true`, the batch poster first attempts to store data via the DAS RPC aggregator. If it cannot obtain a valid DACert (due to insufficient signatures), the batch poster falls back to posting the full batch data directly to the Parent chain—the same way a standard rollup operates. The fallback is **enabled by default**: ```typescript /** If unable to batch to DA provider, disable fallback storing data on chain */ 'disable-dap-fallback-store-data-on-chain'?: boolean; ``` > **DANGER** — Dangerous in production > > Setting this to `true` **disables the fallback**, meaning the batch poster will keep retrying the DAS and not post data onchain. Doing so in production is dangerous, a DAC outage would halt the chain. ### How batch posters enable fallback Batch posters are authorized addresses on the `SequencerInbox` that can post transaction data. They are configured at rollup creation and managed via `setIsBatchPoster`. During normal [AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) operation, the batch poster posts compact DACerts. During fallback, it posts the full calldata. The batch poster config: ```typescript 'batch-poster': { 'max-size': 90000, // max batch size in bytes 'enable': true, 'parent-chain-wallet': { 'private-key': '', }, } ``` #### Normal vs fallback data flow ```typescript Normal (AnyTrust): Sequencer → DAS Store (RPC :9876) → K signatures → DACert posted to L1 Fallback (Rollup-equivalent): Sequencer → DAS Store fails → full batch data posted to L1 (calldata/blob) Retrieval: Node → REST aggregator (:9877) → DAS backends → batch data Node → (if REST fails) → reads L1 calldata directly ``` ## Enabling AnyTrust at chain creation Set `DataAvailabilityCommittee: true` in the chain config at deployment time: ```typescript chainConfig: prepareChainConfig({ chainId, arbitrum: { InitialChainOwner: deployer.address, DataAvailabilityCommittee: true, // Enable AnyTrust/DAC mode }, }); ``` To detect whether an exisitng chain uses AnyTrust, use `isAnyTrust`: ```typescript import { isAnyTrust } from './isAnyTrust'; const isDac = await isAnyTrust({ rollup: '0x...', publicClient, }); ``` ## DAS committee member troubleshooting ### Checklist for a non-responding DAS member 1. **Verify BLS public key matches**: The base64-encoded pubkey in the node config backend must correspond to the same key encoded in the onchain keyset at the member's index position. 2. **Check DAS server endpoints**: Two ports: * RPC endpoint (default: `:9876`): used by the batch poster to store data * REST endpoint (default: `:9877`): used by nodes to retrieve data 3. **Check the `assumed-honest` value**: Must match the onchain keyset's `assumedHonest`. If they diverge, the aggregator may reject valid certificates or accept insufficient ones. 4. **Review timeout settings**: `data-availability.request-timeout` defaults to `5s`. Slow DAS backends may need a longer timeout. 5. **Panic on-error**: `data-availability.panic-on-error` should be `false` (default) in production. Setting it to `true` causes the node to crash on DAS errors rather than falling back. 6. **REST aggregatory strategy**: The `simple-explore-exploit` strategy tracks per-endpoint latency and success rates. A member with sustained failures will be deprioritized. Check `max-per-endpoint-stats` (default 20) to control how quickly the strategy adapts. --- > For a complete page index, fetch # How to configure a Data Availability Committee: Introduction AnyTrust chains rely on an external Data Availability Committee (DAC) to store data and provide it on-demand instead of using its parent chain as the Data Availability (DA) layer. The members of the DAC run a Data Availability Server (DAS) to handle these operations. This section offers information and a series of how-to guides to help you along the process of setting up a Data Availability Committee. These guides target two audiences: Committee members who wish to deploy a Data Availability Server, and chain owners who wish to configure their chain with the information of the Committee. Before following the guides in this section, you should be familiar with how the AnyTrust protocol works and the role of the DAC in the protocol. Refer to the [AnyTrust Protocol](/how-arbitrum-works/deep-dives/anytrust-protocol.md) documentation to learn more. ## If you are a DAC member Committee members will need to run a DAS. To do that, they will first need to generate a pair of keys and deploy a DAS. They may also choose to deploy an additional mirror DAS. Find more information in [How to deploy a DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md) and [How to deploy a mirror DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-mirror-das.md). Here's a basic checklist of actions to complete for DAC members: * [Deploy a DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md). Send the following information to the chain owner: * Public BLS key * The https URL for the RPC endpoint which includes some random string (e.g., das.your-chain.io/rpc/randomstring123), communicated through a secure channel * The https URL for the REST endpoint (e.g., das.your-chain.io/rest) * [Deploy a mirror DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-mirror-das.md) if you want to complement your setup with a mirror DAS. Send the following information to the chain owner: * The https URL for the REST endpoint (e.g., das.your-chain.io/rest) ## If you are a chain owner Chain owners will need to gather the information from the committee members to craft the necessary data to update their chain and the batch poster (more information in [How to configure the DAC in your chain](/launch-arbitrum-chain/chain-config/data-availability/configure-dac.md)). They might also want to test each DAS individually, by following the testing guides available in [How to deploy a DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md#testing-the-das) and [How to deploy a mirror DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-mirror-das.md#testing-the-das). Here's a basic checklist of actions to complete for chain owners: * Gather the following information from every member of the committee: * Public BLS Key * URL of the RPC endpoint * URL(s) of the REST endpoint(s) * Ensure that at least one DAS is running as an [archive DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md#archive-da-servers) * [Generate the keyset and keyset hash](/launch-arbitrum-chain/chain-config/data-availability/configure-dac.md#step-1-generate-the-keyset-and-keyset-hash-with-all-the-information-from-the-servers) with all the information from the servers * [Update the `SequencerInbox` contract](/launch-arbitrum-chain/chain-config/data-availability/configure-dac.md#step-2-update-the-sequencerinbox-contract) * [Craft the new configuration for the batch poster](/launch-arbitrum-chain/chain-config/data-availability/configure-dac.md#step-3-craft-the-new-configuration-for-the-batch-poster) * [Craft the new configuration for your chain's nodes](/launch-arbitrum-chain/chain-config/data-availability/configure-dac.md#step-4-craft-the-new-configuration-for-your-chains-nodes) ## Ask for help Configuring a DAC might be a complex process. If you need help setting it up, don't hesitate to ask us on [Discord](https://discord.gg/arbitrum). --- > For a complete page index, fetch # How to deploy a Data Availability Server (DAS) with Docker AnyTrust chains rely on an external Data Availability Committee (DAC) to store data and provide it on-demand instead of using its parent chain as the Data Availability (DA) layer. The members of the DAC run a Data Availability Server (DAS) to handle these operations. In this how-to, you'll learn how to deploy a DAS using Docker and [Docker Compose](https://docs.docker.com/compose/), without a Kubernetes cluster. Every step runs inside the official Nitro Docker image, including key generation, so you never need to build Nitro from source. [How to deploy a DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md) covers the same deployment using Kubernetes, and explains in depth how a DAS works and which configuration options are available. This page doesn't repeat that conceptual content; when in doubt about an option, refer to that guide. **When to choose Docker instead of Kubernetes:** * You're a committee member running a single DAS on one machine or VM. * You're testing a DAC setup locally before moving to production infrastructure. * Your team doesn't operate a Kubernetes cluster and doesn't want to adopt one for a single service. For production setups that need self-healing, rolling updates, or multiple replicas behind a load balancer, prefer the [Kubernetes-based guide](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md) or the [Helm chart](https://artifacthub.io/packages/helm/offchainlabshelm/das). This how-to assumes that you're familiar with: * The DAC's role in the AnyTrust protocol. Refer to [Inside AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) for a refresher. * [Docker](https://docs.docker.com/) and Docker Compose. Binary names changed in Nitro v3.10.0 As of Nitro v3.10.0, the `daserver` and `datool` binaries have been renamed to `anytrustserver` and `anytrusttool`, and the Docker image only ships the new names. The parent chain flags also moved: `--data-availability.parent-chain-node-url` is now `--parent-chain.node-url`, and `--data-availability.sequencer-inbox-address` is now `--parent-chain.sequencer-inbox-address`. If you're following older guides or scripts that use the legacy names with a recent image, the commands will fail. This guide uses the new names throughout. ## How to deploy the DAS ### Step 0: Prerequisites Gather the following information and tooling: * A recent version of [Docker Engine](https://docs.docker.com/engine/install/) with the Docker Compose plugin (so that `docker compose` works). * The latest Nitro docker image: `offchainlabs/nitro-node:v3.11.3-beb2108` * An RPC endpoint for the parent chain. It is recommended to use a [third-party provider RPC](/arbitrum-essentials/reference/node-providers.md#third-party-rpc-providers) or [run your own node](/run-arbitrum-node/run-full-node.md) to prevent being rate limited. * The `SequencerInbox` contract address in the parent chain. * If you wish to configure a REST aggregator for your DAS, the URL where the list of REST endpoints is kept. Refer to [State synchronization](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md#state-synchronization) for more information. The hardware requirements are the same as for a Kubernetes deployment: a single CPU and 1 GiB of RAM handle normal DAS duties comfortably. See [Hardware requirements](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md#hardware-requirements) for details, including guidance for mirrors and CDNs. All files in this guide live in a single working directory. Create it, along with the two directories that will be mounted into the container: ```shell mkdir -p das-server/bls_keys das-server/das-data cd das-server ``` Create mounted directories before starting the container The DAS container runs as a non-root user with UID and GID `1000` (the first regular user on the image's Debian base). If a bind-mounted directory doesn't exist when the container starts, Docker creates it owned by `root`, and the server will fail with permission errors when writing to it. Always create the directories on the host first. On Linux, if your user's UID isn't `1000`, also make them writable for the container user: `sudo chown -R 1000:1000 bls_keys das-data`. Docker Desktop on macOS and Windows maps file ownership automatically, so this step isn't needed there. ### Step 1: Generate the BLS keypair We'll generate a BLS keypair. The private key will be used to sign the Data Availability Certificates (DACert) when receiving requests to store data, and the public key will be used to prove that the DACert was signed by the DAS. The BLS private key is sensitive and care must be taken to ensure it is generated and stored in a safe environment. The keypair is generated with the `anytrusttool keygen` utility, which ships in the same Docker image as the DAS, so you can run it with `docker run` and a volume mount to have the keys land on your host: ```shell docker run --rm -v $(pwd)/bls_keys:/home/user/bls_keys --entrypoint anytrusttool \ offchainlabs/nitro-node:v3.11.3-beb2108 keygen --dir /home/user/bls_keys ``` This creates two files in `bls_keys/`, both base64-encoded and readable only by their owner (mode `600`): * `das_bls`: the private key. Never share it, and back it up securely. * `das_bls.pub`: the public key. You'll send this one to the chain owner so it can be included in the keyset. To print the public key: ```shell cat bls_keys/das_bls.pub ``` ### Step 2: Create the Docker Compose file Save the following as `docker-compose.yml` in your working directory, replacing the placeholders with your parent chain RPC endpoint and `SequencerInbox` address. The image tag is pinned to a specific release: don't use `latest`, so that upgrades are always an explicit, reviewable change. ```yaml services: das-server: image: offchainlabs/nitro-node:v3.11.3-beb2108 entrypoint: ['/usr/local/bin/anytrustserver'] command: - --parent-chain.node-url= - --parent-chain.sequencer-inbox-address=
- --enable-rpc - --rpc-addr=0.0.0.0 - --enable-rest - --rest-addr=0.0.0.0 - --log-level=INFO - --data-availability.key.key-dir=/home/user/bls_keys - --data-availability.local-file-storage.enable - --data-availability.local-file-storage.data-dir=/home/user/das-data - --data-availability.local-cache.enable ports: - '9876:9876' - '9877:9877' volumes: - ./bls_keys:/home/user/bls_keys:ro - ./das-data:/home/user/das-data restart: unless-stopped healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:9877/health'] interval: 30s timeout: 5s retries: 3 ``` The configuration is intentionally minimal: it enables the RPC interface (used by the sequencer to store data, port `9876`) and the REST interface (used to retrieve data, port `9877`), reads the BLS keypair from the mounted `bls_keys` directory, and stores batch data as local files in the mounted `das-data` directory. The `healthcheck` uses the `curl` binary included in the image against the REST interface's `/health` endpoint, so `docker ps` reports the service as `healthy` only when the underlying storage is working. All the configuration options available for a Kubernetes deployment apply here too, as command-line flags in the `command` list: caching, S3 storage, archive mode (`discard-after-timeout`), and the REST aggregator. Refer to [Configuration options](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md#configuration-options) for the full tables. For example, to let your DAS repair gaps in its data by syncing from other DA servers, add: ```yaml - --data-availability.rest-aggregator.enable - --data-availability.rest-aggregator.online-url-list= ``` Running a mirror DAS with Docker A [mirror DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-mirror-das.md) uses the same image and a subset of this configuration: remove `--enable-rpc`, `--rpc-addr`, and the `9876` port mapping (mirrors don't store data for the sequencer, so they need no RPC interface and no BLS key), and enable the REST aggregator with the `--data-availability.rest-aggregator.sync-to-storage.*` options so that it syncs data from your main DAS and other public mirrors. ### Step 3: Start the DAS ```shell docker compose up -d ``` Follow the logs and check that both servers start: ```shell docker compose logs -f das-server ``` You should see the hex-encoded BLS public key logged at startup (in the line `AnyTrust public key used for signing`), followed by lines similar to: ```shell INFO [...] Starting HTTP-RPC server addr=0.0.0.0 port=9876 revision=v3.11.1-8512b8c INFO [...] Starting REST server addr=0.0.0.0 port=9877 revision=v3.11.1-8512b8c ``` ## Volumes and persistence Only the two mounted directories hold state that must survive the container: * `bls_keys` **must persist and be backed up**. If the private key is lost, the chain owner has to register a new keyset that replaces your public key, following [committee member management](/launch-arbitrum-chain/chain-config/data-availability/dac-das-operations.md#das-committee-member-management). Keep an offline backup in secure storage. The directory is mounted read-only because the server never needs to write to it. * `das-data` **must persist** for at least the data retention period; if you run an [archive DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md#archive-da-servers), it must persist indefinitely and its size grows with chain history. Losing it is recoverable — a DAS can re-sync missing batches from other DA servers through the [REST aggregator](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md#state-synchronization) — but avoid relying on that in production. * If you enable eager syncing (typical for mirrors), also persist the directory you configure in `--data-availability.rest-aggregator.sync-to-storage.state-dir`, so the sync doesn't restart from scratch after a container restart. The container itself is disposable: recreating it (for example, to upgrade the image tag) loses nothing as long as the mounts stay in place. The in-memory cache is rebuilt on demand. This guide uses bind mounts rather than named volumes because they make key provisioning (Step 1) and backups straightforward: the files sit in plain directories on the host. Named volumes work too, but you'd need an extra copy step to place the generated keys inside the volume. ## Networking with the batch poster For your DAS to do its job, two parties need to reach it: * The batch poster sends `das_store` RPC requests to the **RPC interface** (port `9876`). This endpoint should not be publicly discoverable: share its URL only with the chain owner through a private channel, and include a random string in the path (e.g., `das.your-chain.io/rpc/randomstring123`). * Your chain's nodes retrieve data from the **REST interface** (port `9877`), which is safe to expose publicly — ideally behind a CDN, or served by a [mirror DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-mirror-das.md) instead of your main DAS. The server itself has no TLS support, so don't expose ports `9876` and `9877` directly to the internet. Run a reverse proxy (e.g., [nginx](https://www.nginx.com/), [Caddy](https://caddyserver.com/), or [Traefik](https://traefik.io/traefik)) on the host — or as another Compose service — to terminate HTTPS and forward to the DAS ports. When using nginx, raise `client_max_body_size` (to at least `50M`, default is `1M`); otherwise, the batch poster receives `413 Request Entity Too Large` errors for larger batches and the DAS can't sign certificates for them. Your DAS only becomes part of the committee once the chain owner registers a keyset containing your BLS public key and RPC URL in the `SequencerInbox` contract, and configures the batch poster with the same information. That process is covered in [How to configure the DAC in your chain](/launch-arbitrum-chain/chain-config/data-availability/configure-dac.md). ## Verify the deployment The verification methods are the same as in the Kubernetes guide; here they are in their Docker form. ### Test 1: REST health check The REST interface has a health check on the path `/health`, which returns `200` if the underlying storage is working, otherwise `503`: ```shell curl -I http://localhost:9877/health ``` ### Test 2: RPC health check The RPC interface has a health check for the underlying storage, invoked with the `das_healthCheck` RPC method: ```shell curl -X POST \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":0,"method":"das_healthCheck","params":[]}' \ http://localhost:9876 ``` A healthy DAS answers `{"jsonrpc":"2.0","id":0,"result":null}`. ### Test 3: Store and retrieve data By default, the DAS only accepts store requests signed by the sequencer's key (identified via the `SequencerInbox` contract). To send a test store request yourself, configure the DAS to also accept requests signed with an ECDSA key of your choosing. First, generate an ECDSA keypair (again, no local Nitro build needed): ```shell mkdir -p ecdsa_keys docker run --rm -v $(pwd)/ecdsa_keys:/home/user/ecdsa_keys --entrypoint anytrusttool \ offchainlabs/nitro-node:v3.11.3-beb2108 keygen --dir /home/user/ecdsa_keys --ecdsa ``` Then add the extra signer to your `docker-compose.yml` — a new flag in `command` and a new entry in `volumes` — and apply the change with `docker compose up -d`: ```yaml command: # ...existing flags... - --data-availability.extra-signature-checking-public-key=/home/user/ecdsa_keys/ecdsa.pub volumes: # ...existing mounts... - ./ecdsa_keys:/home/user/ecdsa_keys:ro ``` Now store a message through the RPC interface, running `anytrusttool` inside the container so it can reach the server on `localhost`: ```shell docker compose exec das-server anytrusttool client rpc store \ --rpc-client.rpc.url http://localhost:9876 \ --message "Hello world" \ --signing-key /home/user/ecdsa_keys/ecdsa ``` The command prints the hex-encoded DACert and data hash. Note that the DACert comes back in the response to this single request: certificates are signed synchronously, per store request. Use the data hash to retrieve the message back through the REST interface: ```shell docker compose exec das-server anytrusttool client rest getbyhash \ --url http://localhost:9877 \ --data-hash 0x052cca0e379137c975c966bcc69ac8237ac38dc1fcf21ac9a6524c87a2aab423 ``` If you stored `"Hello world"`, the data hash matches the one above and the output is `Message: Hello world`. Remove the extra signer configuration when you're done testing, or keep it (with the private key stored safely) to run periodic canary checks against your production DAS. ## Troubleshooting * **The container exits immediately or logs `permission denied` on `/home/user/das-data`**: the bind-mounted directory isn't writable by the container user (UID `1000`). This typically happens on Linux when Docker auto-created the directory as `root` or your host user has a different UID. Create the directories before starting the container and run `sudo chown -R 1000:1000 bls_keys das-data`. * **The batch poster can't gather enough signatures, and batches fall back to the parent chain**: a keyset with `N` members and `assumed-honest` `H` needs `K = N + 1 - H` live, signing committee members for every batch. If any DAS in the keyset is unreachable — for example, a keyset registered with two members while only one server is actually running — the batch poster can't produce a DACert and, by default, silently falls back to posting the full batch data to the parent chain, like a regular Rollup. The chain keeps working, but you lose the cost savings of AnyTrust. Make sure every member listed in the keyset is deployed, reachable at its registered RPC URL, and configured with the matching BLS key, and that `assumed-honest` is consistent between the keyset and the batch poster configuration. Refer to [DAC failure modes and fallback to batch posting](/launch-arbitrum-chain/chain-config/data-availability/dac-das-operations.md#dac-failure-modes-and-fallback-to-batch-posting) for the full failure mode table. * **Store requests fail after the host sleeps or on a laptop running Docker Desktop**: store requests are time-limited, so a significant clock drift between the batch poster and the DAS can cause them to be rejected. Keep the host clock NTP-synced; on Docker Desktop, the VM clock can drift after the host sleeps, and restarting Docker Desktop resyncs it. * **`docker compose up` fails with `port is already allocated`**: another process is using `9876` or `9877` on the host. Change the host side of the port mapping (e.g., `'19876:9876'`) and update the URLs you communicate to the chain owner and your reverse proxy accordingly. * **The DAS logs connection errors to the parent chain RPC**: remember that `localhost` inside the container is the container itself, not the host. If your parent chain node runs on the same host, use `host.docker.internal` (Docker Desktop) or the host's IP address (Linux) in `--parent-chain.node-url`, and check firewalls and DNS from inside the container with `docker compose exec das-server curl -I `. ## Costs and operational notes A DAS signs and returns a DACert immediately for each store request it receives from the batch poster; there is no waiting period on the DAS side. How often those certificates (or, in fallback, full batches) are posted to the parent chain is governed by the batch poster's own limits — its maximum batch size and maximum delay parameters (`--node.batch-poster.max-delay`, and `--node.da.anytrust.max-batch-size` for AnyTrust batches) — so DAS costs are dominated by steady infrastructure, not per-batch fees. Approximate costs (as of July 2026) Typical operational cost for running an AnyTrust DAS node is roughly $200/month (a small VM, storage, and egress). For comparison, posting data to Celestia costs roughly $0.08/MB. These are rough estimates that change over time; validate them against current provider pricing before making decisions. ## What to do next? Once the DAS is deployed and tested, you'll have to communicate the following information to the chain owner, so they can update the chain parameters and configure the sequencer: * Public key (the contents of `bls_keys/das_bls.pub`) * The https URL for the RPC endpoint which includes some random string (e.g., `das.your-chain.io/rpc/randomstring123`), communicated through a secure channel * The https URL for the REST endpoint (e.g., `das.your-chain.io/rest`) The chain owner then follows [How to configure the DAC in your chain](/launch-arbitrum-chain/chain-config/data-availability/configure-dac.md) to generate and register the keyset. ## Optional parameters Besides the parameters described in this guide, there are some more options that can be useful when running the DAS. For a comprehensive list of configuration parameters, you can run `anytrustserver --help`. | Parameter | Description | | ----------- | -------------------------------------------------------------------------------------------------------------------- | | --conf.dump | Prints out the current configuration | | --conf.file | Absolute path to the configuration file inside the volume to use instead of specifying all parameters in the command | ## Metrics The DAS comes with the option of producing Prometheus metrics. This option can be activated by using the following parameters: | Parameter | Description | | -------------------------------- | -------------------------------------------- | | --metrics | Enables the metrics server | | --metrics-server.addr | Metrics server address (default "127.0.0.1") | | --metrics-server.port | Metrics server port (default 6070) | | --metrics-server.update-interval | Metrics server update interval (default 3s) | When metrics are enabled, several useful metrics are available at the configured port, at path `debug/metrics` or `debug/metrics/prometheus`. ### RPC metrics | Metric | Description | | ---------------------------------------------------------------- | ------------------------------------ | | arb\_das\_rpc\_store\_requests | Count of RPC Store calls | | arb\_das\_rpc\_store\_success | Successful RPC Store calls | | arb\_das\_rpc\_store\_failure | Failed RPC Store calls | | arb\_das\_rpc\_store\_bytes | Bytes retrieved with RPC Store calls | | arb\_das\_rpc\_store\_duration (p50, p75, p95, p99, p999, p9999) | Duration of RPC Store calls (ns) | ### REST metrics | Metric | Description | | --------------------------------------------------------------------- | ----------------------------------------- | | arb\_das\_rest\_getbyhash\_requests | Count of REST GetByHash calls | | arb\_das\_rest\_getbyhash\_success | Successful REST GetByHash calls | | arb\_das\_rest\_getbyhash\_failure | Failed REST GetByHash calls | | arb\_das\_rest\_getbyhash\_bytes | Bytes retrieved with REST GetByHash calls | | arb\_das\_rest\_getbyhash\_duration (p50, p75, p95, p99, p999, p9999) | Duration of REST GetByHash calls (ns) | --- > For a complete page index, fetch # DAS RPC method reference This document covers the JSON-RPC methods exposed by Arbitrum Data Availability Servers (DAS) and the newer DAProvider interface, as defined in the [Arbitrum Chain SDK](https://github.com/OffchainLabs/arbitrum-chain-sdk/) node configuration types [`NodeConfig.generated.ts`](https://github.com/OffchainLabs/arbitrum-chain-sdk/blob/main/src/types/NodeConfig.generated.ts) Config paths renamed in Nitro v3.10.0 As of Nitro v3.10.0, the node-side AnyTrust configuration moved from `node.data-availability.*` (now deprecated) to `node.da.anytrust.*`, the `das-rpc-client` group was renamed to `rpc-client` (with the server URL now set under its `rpc.url` key), `node.da-provider` moved to `node.da.external-provider`, and `node.batch-poster.das-retention-period` was renamed to `node.batch-poster.anytrust-retention-period`. This page uses the new paths; types generated from older Nitro versions (such as `NodeConfig.generated.ts` in the Arbitrum Chain SDK) may still show the legacy names. ## Overview DAS servers exposed two interfaces for data storage and retrieval: | Interface | Port (typical) | Used by | Purpose | | --------- | -------------- | ------------ | -------------------------------- | | RPC | `:9876` | Batch Poster | Store batch data, obtain DACerts | | REST | `:9877` | Nitro nodes | Retrieve batch data by hash | The RPC interface supports two storage modes: 1. **Single-shot store**: entire payload sent in one call (legacy, used when `enable-chunked-store: false`). 2. **Chunked (streaming) store**: payload sent in chunks across multiple calls (default, enable by `enable-chunked-store: true`). ## DAS RPC methods (legacy DAS interface) These methods are used by the **RPC aggregator** (`node.da.anytrust.rpc-aggregator.rpc-client`) when communicating with standalone DAS servers. ### `das_startChunkedStore` Initiates a chunked data stream for storing a large batch payload on the DAS server. | Field | Value | | ------------------- | --------------------------------------------------------------------------------- | | Default method name | `das_startChunkedStore` | | Config path | `node.da.anytrust.rpc-aggregator.rpc-client.data-stream.rpc-methods.start-stream` | | Config key | `start-stream` | | Direction | Client (batch poster) -> DAS server | **Behavior:** * Called once at the beginning of a chunked store operation * Returns a stream/session identifier used by subsequent `das_sendChunk` calls * The server allocations resources to receive the incoming data stream **When invoked:** * The batch poster calls this method on each DAS backend in the RPC aggregator's backend list * Only used when `enable-chunked-store` is `true` (default) ### `das_sendChunk` Sends a single chunk of batch data within an active chunked store session. | Field | Value | | ------------------- | --------------------------------------------------------------------------------- | | Default method name | `das_sendChunk` | | Config path | `node.da.anytrust.rpc-aggregator.rpc-client.data-stream.rpc-methods.stream-chunk` | | Config key | `stream-chunk` | | Direction | Client (batch poster) -> DAS server | **Behavior:** * Called one or more times after `das_startChunkedStore` to transmit the payload in pieces * Each chunk must fit within `max-store-chunk-body-size` (default: 5,242,880 bytes / 5 MB) * Chunks are sent sequentially within a stream session **Related config:** ```text 'data-stream': { 'max-store-chunk-body-size': 5242880, // 5 MB per chunk } ``` ### `das_commitChunkedStore` Finalizes a chunked store session, signaling that all chunks have been transmitted. | Field | Value | | ------------------- | ------------------------------------------------------------------------------------ | | Default method name | `das_commitChunkedStore` | | Config path | `node.da.anytrust.rpc-aggregator.rpc-client.data-stream.rpc-methods.finalize-stream` | | Config key | `finalize-stream` | | Direction | Client (batch poster) -> DAS server | **Behavior:** * Called once after all chunks have been sent via `das_sendChunk` * The DAS server assembles the chunks, stores the complete payload, and returns a signed DACert component (the server's BLS Signature over the data hash) * On success, the server commits the data to its configured storage backend ## DAProvider RPC methods (newer interface) These methods are used by the DA provider client (`node.da.external-provider`)—the newer generalized data availability interface that can work with DAS or other DA backends. ### `daprovider_startChunkedStore` Initiates a chunked data stream on the DA provider server. | Field | Value | | ------------------- | ---------------------------------------------------------------- | | Default method name | `daprovider_startChunkedStore` | | Config path | `node.da.external-provider.data-stream.rpc-methods.start-stream` | | Config key | `start-stream` | Functionally equivalent to `das_startChunkedStore` but routed through the DAProvider abstraction. ### `daprovider_sendChunk` Sends a chunk of data to the DA provider server. | Field | Value | | ------------------- | ---------------------------------------------------------------- | | Default method name | `daprovider_sendChunk` | | Config path | `node.da.external-provider.data-stream.rpc-methods.stream-chunk` | | Config key | `stream-chunk` | Functionally equivalent to `das_sendChunk`. Max chunk body size is governed by `node.da.external-provider.data-stream.max-store-chunk-body-size` (default: 5,242,880 bytes / 5 MB). ### `daprovider_commitChunkedStore` Finalizes a chunked store session on the DA provider server. | Field | Value | | ------------------- | ------------------------------------------------------------------- | | Default method name | `daprovider_commitChunkedStore` | | Config path | `node.da.external-provider.data-stream.rpc-methods.finalize-stream` | | Config key | `finalize-stream` | Functionally equivalent to `das_commitChunkedStore`. ### `daprovider_store` Single-shot store method—sends the entire payload in one RPC call. Used when data streaming is disabled. (`use-data-streaming: false`). | Field | Value | | ------------------- | -------------------------------------------- | | Default method name | `daprovider_store` | | Config path | `node.da.external-provider.store-rpc-method` | | Config key | `store-rpc-method` | **Behavior:** * Sends the complete batch data in a single request * Simpler than a chunked store but limted by HTTP body size constraints * Enabled by default. Set `use-data-streaming: true` to switch to the chunked protocol. ## REST retrieval interface DAS nodes also expose a REST interface for **reading** stored data. This is not a JSON-RPC interface—it used HTTP GET requests. ### `GetByHash` Retrieves batch data by its hash from REST DAS endpoints. | Field | Value | | -------------- | ----------------------------------- | | Protocol | HTTP REST (GET) | | Port (typical) | `:9877` | | Used by | Nitro nodes (via `rest-aggregator`) | **Configuration:** ```typescript 'rest-aggregator': { 'enable': true, 'urls': ['http://das-server:9877'], 'strategy': 'simple-explore-exploit', // endpoint selection strategy 'wait-before-try-next': '2s', // timeout before trying next endpoint 'max-per-endpoint-stats': 20, // latency/success stats window 'simple-explore-exploit-strategy': { 'exploit-iterations': 1000, // calls using best-performing endpoint 'explore-iterations': 20, // calls trying random endpoints }, } ``` **Endpoint selection strategy:** The `simple-explore-exploit` strategy alternates between: * **Exploit mode** (default 1000 iterations): Selects endpoints based on best latency and success rate. * **Explore mode** (default 20 iterations): Randomly selects endpoints to discover better options. ## Configuration quick reference ### AnyTrust RPC client (`rpc-client`, formerly `das-rpc-client`) Full config path: `node.da.anytrust.rpc-aggregator.rpc-client` ```typescript 'rpc-client': { 'enable-chunked-store': true, // default: true 'rpc': { 'url': 'http://...', // DAS server URL }, 'data-stream': { 'max-store-chunk-body-size': 5242880, // 5 MB 'rpc-methods': { 'start-stream': 'das_startChunkedStore', // customizable 'stream-chunk': 'das_sendChunk', // customizable 'finalize-stream': 'das_commitChunkedStore', // customizable }, }, } ``` ### DAProvider client (newer—`external-provider`) Full config path: `node.da.external-provider` ```typescript 'external-provider': { 'enable': true, 'store-rpc-method': 'daprovider_store', // single-shot store 'use-data-streaming': true, // use chunked protocol 'with-writer': true, // server supports writes 'data-stream': { 'max-store-chunk-body-size': 5242880, // 5 MB 'rpc-methods': { 'start-stream': 'daprovider_startChunkedStore', 'stream-chunk': 'daprovider_sendChunk', 'finalize-stream': 'daprovider_commitChunkedStore', }, }, 'rpc': { 'url': 'http://...', 'retries': 3, 'retry-delay': '...', 'timeout': '...', 'connection-wait': '...', 'jwtsecret': '...', // JWT auth (optional) 'arg-log-limit': 2048, 'websocket-message-size-limit': 268435456, }, } ``` ### RPC aggregator backend entry Full config path: `node.da.anytrust.rpc-aggregator.backends` (JSON string) ```typescript // Type: NodeConfigDataAvailabilityRpcAggregatorBackendsJson // Source: src/types/NodeConfig.ts:23-29 [ { url: 'http://das-member-0:9876', // DAS RPC endpoint pubkey: '', // member's BLS key }, // Additional members go here. Each backend's signer bit is auto-derived from // its index in this array (index i -> mask 1 << i) and must line up with // the member's position in the onchain keyset. ]; ``` ## Method Summary Table | Method | Interface | Default Name | Purpose | Config Key | | -------------------- | ---------- | ------------------------------- | ---------------------------- | ------------------ | | Start chunked store | DAS | `das_startChunkedStore` | Begin streaming session | `start-stream` | | Send chunk | DAS | `das_sendChunk` | Transmit data chunk | `stream-chunk` | | Commit chunked store | DAS | `das_commitChunkedStore` | Finalize and store | `finalize-stream` | | Start chunked store | DAProvider | `daprovider_startChunkedStore` | Begin streaming session | `start-stream` | | Send chunk | DAProvider | `daprovider_sendChunk` | Transmit data chunk | `stream-chunk` | | Commit chunked store | DAProvider | `daprovider_commitChunkedStore` | Finalize and store | `finalize-stream` | | Single-shot store | DAProvider | `daprovider_store` | Store entire payload at once | `store-rpc-method` | | Get by hash | REST | N/A (HTTP GET) | Retrieve data by hash | N/A | ### Related configuration | Config Key | Default | Description | | ---------------------------------------- | ------------------------ | ------------------------------------------------------------------ | | `da.anytrust.enable` | `false` | Enable AnyTrust DA mode | | `da.anytrust.request-timeout` | `5s` | Store request timeout | | `da.anytrust.panic-on-error` | `false` | Crash on DAS errors (not recommended) | | `parent-chain.sequencer-inbox-address` | — | SequencerInbox contract address (standalone `anytrustserver` only) | | `parent-chain.node-url` | — | Parent chain RPC URL (standalone `anytrustserver` only) | | `batch-poster.anytrust-retention-period` | `360h` | How long DAS retains stored batches | | `rpc-aggregator.assumed-honest` | — | H value for quorum (K=N+1-H) | | `rpc-client.enable-chunked-store` | `true` | Use streaming protocol | | `rest-aggregator.strategy` | `simple-explore-exploit` | Endpoint selection strategy | | `rest-aggregator.wait-before-try-next` | `2s` | Timeout before next endpoint | | `sync-to-storage.retention-period` | `360h` | Synced data retention period | --- > For a complete page index, fetch # How to deploy a Data Availability Server (DAS) AnyTrust chains rely on an external Data Availability Committee (DAC) to store data and provide it on-demand instead of using its parent chain as the Data Availability (DA) layer. The members of the DAC run a Data Availability Server (DAS) to handle these operations. In this how-to, you'll learn how to deploy a DAS that exposes: 1. **An RPC interface** that the [Sequencer](/how-arbitrum-works/deep-dives/sequencer.md) uses to store batches of data on the DAS. 2. **An HTTP REST interface** that lets the DAS respond to requests for those batches of data. For more information related to configuring a DAC, refer to the [Introduction](/launch-arbitrum-chain/chain-config/data-availability/dac-get-started.md). This how-to assumes that you're familiar with: * The DAC's role in the AnyTrust protocol. Refer to [Inside AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) for a refresher. * [Kubernetes](https://kubernetes.io/). The examples in this guide use Kubernetes to containerize your DAS. ## How does a DAS work? A Data Availability Server (DAS) allows storage and retrieval of transaction data batches for an AnyTrust chain. It's the software that the members of the DAC run in order to provide the Data Availability service. DA servers accept time-limited requests to store data batches from the sequencer of an AnyTrust chain, and return a signed certificate promising to store that data during the established time. They also respond to requests to retrieve the data batches. ## Configuration options When setting up a DAS, there are certain options you can configure to suit your infrastructure needs: ### Interfaces available in a DAS There are two main interfaces that can be enabled in a DAS: an **RPC interface** to store data in the DAS, intended to be used only by the AnyTrust Sequencer; and a **REST interface** that supports only `GET` operations and is intended for public use. DA servers listen on two primary interfaces: 1. Its **RPC interface** listens for `das_store` RPC messages coming from the sequencer. Messages are signed by the sequencer, and the DAS checks this signature upon receipt. 2. Its **REST interface** respond to HTTP `GET` requests pointed at `/get-by-hash/`. This uses the hash of the data batch as a unique identifier, and will always return the same data for a given hash. ### Storage options A DAS can be configured to use one or more of three storage backends: * [AWS S3](https://aws.amazon.com/s3/) bucket * Files on local disk * (**EXPERIMENTAL**) [Google Cloud Storage](https://cloud.google.com/storage) bucket > **WARNING** — Google Cloud Storage is experimental > > The Google Cloud Storage option (set with `google-cloud-storage`) is experimental and hasn't been tested thoroughly. It is recommended to not rely solely on this storage option and to use it alongside other storage options. > **WARNING** — Local Badger database removed > > The local Badger DB storage option (previously set with `local-db-storage`) was deprecated and removed in Nitro v3.8.0, together with its `--data-availability.migrate-local-db-to-file-storage` migration parameter. If your DAS still stores data in a local Badger database, run a Nitro version prior to v3.8.0 with that parameter to migrate the data to the local files storage option (`local-file-storage`) before upgrading. If more than one option is selected, store requests must succeed to all of them for it to be considered successful, while retrieve requests only require one of them to succeed. If there are other storage backends you'd like us to support, send us a message on [Discord](https://discord.gg/arbitrum), or contribute directly to the [Nitro repository](https://github.com/OffchainLabs/nitro/). ### Caching An in-memory cache can be enabled to avoid needing to access underlying storage for retrieve requests. Requests sent to the REST interface (to retrieve data from the DAS) always return the same data for a given hash, so the result is cacheable. It also contains a `cache-control` header specifying that the object is immutable and to cache it for up to 28 days. ### State synchronization DA servers also have an optional REST aggregator which, when a data batch is not found in cache or storage, requests that batch to other REST servers defined in a list and stores that batch upon receiving it. This is how a DAS that misses storing a batch (the AnyTrust protocol doesn't require all of them to report success in order to post the batch's certificate to the parent chain) can automatically repair gaps in the data it stores, and also how a [mirror DAS](#running-a-mirror-das) can sync its data. A public list of REST endpoints is published online, which the DAS can be configured to download and use, and additional endpoints can be specified in the configuration. ## How to deploy the DAS Binary and flag names changed in Nitro v3.10.0 As of Nitro v3.10.0, the `daserver` and `datool` binaries have been renamed to `anytrustserver` and `anytrusttool`, and the parent chain flags moved: `--data-availability.parent-chain-node-url` is now `--parent-chain.node-url`, and `--data-availability.sequencer-inbox-address` is now `--parent-chain.sequencer-inbox-address`. Current Docker images (v3.11.2 and later) only ship the new binaries, so scripts using the old names fail. This guide uses the new names throughout. ### Step 0: Prerequisites Gather the following information: * The latest Nitro docker image: `offchainlabs/nitro-node:v3.11.3-beb2108` * An RPC endpoint for the parent chain. It is recommended to use a [third-party provider RPC](/arbitrum-essentials/reference/node-providers.md#third-party-rpc-providers) or [run your own node](/run-arbitrum-node/run-full-node.md) to prevent being rate limited. * The `SequencerInbox` contract address in the parent chain. * If you wish to configure a [REST aggregator for your DAS](#state-synchronization), you'll need the URL where the list of REST endpoints is kept. #### Hardware requirements * **Data Availability Server (DAS)** – A single CPU and 1 GiB of RAM can comfortably handle normal DAS duties. CPU spikes are rare and memory usage stays well below 1 GiB—even with an in-memory cache enabled. * **Mirror DAS** – Mirrors do even less work. When a CDN (e.g., Cloudflare, Fastly, CloudFront) sits in front, most requests never reach the node. **Note** that CDN is mandatory for any publicly reachable REST endpoint (mirror or main DAS). Without a CDN absorbing traffic, you need beefier hardware and you leave the server open to DoS attacks. > **INFO** — Heads-up > > If you crank up the in-memory cache or co-host other services, consider bumping the memory to 2 GiB for safety. Disk (or S3) requirements scale with your retention policy: archive nodes need space for the full history, while non-archive nodes can offload older data via lifecycle rules. ### Step 1: Generate the BLS keypair Next, we'll generate a BLS keypair. The private key will be used to sign the Data Availability Certificates (DACert) when receiving requests to store data, and the public key will be used to prove that the DACert was signed by the DAS. The BLS private key is sensitive and care must be taken to ensure it is generated and stored in a safe environment. The BLS keypair must be generated using the `anytrusttool keygen` utility. Later, it will be passed to the DAS by file or command line. When running the key generator, we'll specify the `--dir` parameter with the absolute path to the directory inside the volume to store the keys in. Here's an example of how to use the `anytrusttool keygen` utility inside Docker and store the key that will be used by the DAS in the next step. ```shell docker run -v $(pwd)/bls_keys:/data/keys --entrypoint anytrusttool \ offchainlabs/nitro-node:v3.11.3-beb2108 keygen --dir /data/keys ``` ### Step 2: Deploy the DAS To run the DAS, we'll use the `anytrustserver` tool and we'll configure the following parameters: | Parameter | Description | | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | --parent-chain.node-url | RPC endpoint of a parent chain node | | --parent-chain.sequencer-inbox-address | Address of the `SequencerInbox` in the parent chain | | --data-availability.key.key-dir | The absolute path to the directory inside the volume to read the BLS keypair ('das\_bls.pub' and 'das\_bls') from | | --enable-rpc | Enables the HTTP-RPC server listening on --rpc-addr and --rpc-port | | --rpc-addr | HTTP-RPC server listening interface (default "localhost") | | --rpc-port | (Optional) HTTP-RPC server listening port (default 9876) | | --enable-rest | Enables the REST server listening on --rest-addr and --rest-port | | --rest-addr | REST server listening interface (default "localhost") | | --rest-port | (Optional) REST server listening port (default 9877) | | --log-level | Log level: CRIT, ERROR, WARN, INFO, DEBUG, or TRACE (default INFO) | To enable caching, you can use the following parameters: | Parameter | Description | | ---------------------------------------- | ---------------------------------------------------------------------------------- | | --data-availability.local-cache.enable | Enables local in-memory caching of sequencer batch data | | --data-availability.local-cache.capacity | Maximum number of entries (up to 64KB each) to store in the cache. (default 20000) | To enable the REST aggregator, use the following parameters: | Parameter | Description | | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | --data-availability.rest-aggregator.enable | Enables retrieval of sequencer batch data from a list of remote REST endpoints | | --data-availability.rest-aggregator.online-url-list | A URL to a list of URLs of REST DAS endpoints that is checked at startup. This option is additive with the urls option | | --data-availability.rest-aggregator.urls | List of URLs including 'http\://' or 'https\://' prefixes and port numbers to REST DAS endpoints. This option is additive with the online-url-list option | | --data-availability.rest-aggregator.sync-to-storage.check-already-exists | When using a REST aggregator, checks if the data already exists in this DAS's storage. Must be disabled for fast sync with an IPFS backend (default true) | | --data-availability.rest-aggregator.sync-to-storage.eager | When using a REST aggregator, eagerly syncs batch data to this DAS's storage from the REST endpoints, using the parent chain as the index of batch data hashes; otherwise only syncs lazily | | --data-availability.rest-aggregator.sync-to-storage.eager-lower-bound-block | When using a REST aggregator that's eagerly syncing, starts indexing forward from this block from the parent chain. Only used if there is no sync state. | | --data-availability.rest-aggregator.sync-to-storage.retention-period | When using a REST aggregator, period to retain the synced data (defaults to forever) | | --data-availability.rest-aggregator.sync-to-storage.state-dir | When using a REST aggregator, directory to store the sync state in, i.e., the block number currently synced up to, so that it doesn't sync from scratch each time | Finally, for the storage backends you wish to configure, use the following parameters. Toggle between the different options to see all available parameters.
AWS S3 bucket | Parameter | Description | | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | --data-availability.s3-storage.enable | Enables storage/retrieval of sequencer batch data from an AWS S3 bucket | | --data-availability.s3-storage.access-key | S3 access key | | --data-availability.s3-storage.bucket | S3 bucket | | --data-availability.s3-storage.region | S3 region | | --data-availability.s3-storage.secret-key | S3 secret key | | --data-availability.s3-storage.object-prefix | Prefix to add to S3 objects | | --data-availability.s3-storage.discard-after-timeout | (**Deprecated**) Expiration should be directly set as a rule in Lifecycle Configuration of S3 bucket |
Local files > **WARNING** — Create the local directory before launching your DAS > > Make sure you create the directory specified in `local-file-storage.data-dir` before launching your DAS to avoid potential permission issues with Docker or Kubernetes. | Parameter | Description | | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | --data-availability.local-file-storage.enable | Enables storage/retrieval of sequencer batch data from a directory of files, one per batch | | --data-availability.local-file-storage.data-dir | Absolute path of the directory inside the volume in which to store the data (it must exist) | | --data-availability.local-file-storage.enable-expiry | Enables expiry of batches | | --data-availability.local-file-storage.max-retention | Store requests with expiry times farther in the future than max-retention will be rejected (default: 504h0m0s) |
(Experimental) Google Cloud Storage | Parameter | Description | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------ | | --data-availability.google-cloud-storage.enable | Enables storage/retrieval of sequencer batch data from a Google Cloud Storage bucket | | --data-availability.google-cloud-storage.access-token | Google Cloud Storage access token | | --data-availability.google-cloud-storage.bucket | Google Cloud Storage bucket | | --data-availability.google-cloud-storage.object-prefix | Prefix to add to Google Cloud Storage objects | | --data-availability.google-cloud-storage.enable-expiry | Enable expiry of batches (setting it to false, activates the "archive" mode) | | --data-availability.google-cloud-storage.max-retention | Store requests with expiry times farther in the future than max-retention will be rejected |
Here's an example `anytrustserver` command for a DAS that: * Enables both interfaces: RPC and REST * Enables local cache * Enables a [REST aggregator](#state-synchronization) * Enables AWS S3 bucket storage * Enables local files storage ```shell anytrustserver --parent-chain.node-url "" --parent-chain.sequencer-inbox-address "
" --data-availability.key.key-dir /home/user/data/keys --enable-rpc --rpc-addr '0.0.0.0' --log-level INFO --enable-rest --rest-addr '0.0.0.0' --data-availability.local-cache.enable --data-availability.rest-aggregator.enable --data-availability.rest-aggregator.online-url-list "" --data-availability.s3-storage.enable --data-availability.s3-storage.access-key "" --data-availability.s3-storage.bucket "" --data-availability.s3-storage.region "" --data-availability.s3-storage.secret-key "" --data-availability.s3-storage.object-prefix "/" --data-availability.local-file-storage.enable --data-availability.local-file-storage.data-dir /home/user/data/das-data ``` And here's an example of how to use a k8s deployment to run that command: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: das-server spec: replicas: 1 selector: matchLabels: app: das-server strategy: rollingUpdate: maxSurge: 0 maxUnavailable: 50% type: RollingUpdate template: metadata: labels: app: das-server spec: containers: - command: - bash - -c - | /usr/local/bin/anytrustserver --parent-chain.node-url "" --parent-chain.sequencer-inbox-address "
" --data-availability.key.key-dir /home/user/data/keys --enable-rpc --rpc-addr '0.0.0.0' --log-level INFO --enable-rest --rest-addr '0.0.0.0' --data-availability.local-cache.enable --data-availability.rest-aggregator.enable --data-availability.rest-aggregator.online-url-list "" --data-availability.s3-storage.enable --data-availability.s3-storage.access-key "" --data-availability.s3-storage.bucket "" --data-availability.s3-storage.region "" --data-availability.s3-storage.secret-key "" --data-availability.s3-storage.object-prefix "/" --data-availability.local-file-storage.enable --data-availability.local-file-storage.data-dir /home/user/data/das-data image: offchainlabs/nitro-node:v3.11.3-beb2108 imagePullPolicy: Always resources: limits: cpu: "4" memory: 10Gi requests: cpu: "4" memory: 10Gi ports: - containerPort: 9876 hostPort: 9876 protocol: TCP - containerPort: 9877 hostPort: 9877 protocol: TCP volumeMounts: - mountPath: /home/user/data/ name: data readinessProbe: failureThreshold: 3 httpGet: path: /health/ port: 9877 scheme: HTTP initialDelaySeconds: 5 periodSeconds: 5 successThreshold: 1 timeoutSeconds: 1 volumes: - name: data persistentVolumeClaim: claimName: das-server ``` ## Archive DA servers Archive DA servers are servers that don't discard any data after expiring. Each DAC should have at the very least one archive DAS to ensure all historical data is available. To activate the "archive mode" in your DAS, configure your storage backend to never discard data: `local-file-storage` retains all data by default (as long as `enable-expiry` is not set), and `s3-storage` relies on the bucket's lifecycle configuration, as explained in the notes below. For the experimental `google-cloud-storage` backend, set the parameter `discard-after-timeout` to `false`: ```shell --data-availability.google-cloud-storage.discard-after-timeout=false ``` > **NOTE** > > The `s3-storage` **doesn't support** expiration of data, instead that can be enabled from user side by setting `Expiration` as a rule in Lifecycle Configuration of the corresponding S3 bucket. More information available at [AWS Expiring objects](https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-expire-general-considerations.html). > **NOTE** > > The `local-file-storage` doesn't discard data after expiring by default, but expiration can be enabled with `enable-expiry`. Archive servers should make use of the `--data-availability.rest-aggregator.sync-to-storage` options described above to pull in any data that they don't have. ## Helm charts A helm chart is available at [ArtifactHUB](https://artifacthub.io/packages/helm/offchainlabshelm/das). It supports running a DAS by providing the BLS key and the parameters for your server. Find more information in the [OCL community Helm charts repository](https://github.com/OffchainLabs/community-helm-charts/tree/main/charts/das). ## Testing the DAS Once the DAS is running, we can test if everything is working correctly using the following methods. ### Test 1: RPC health check The RPC interface enabled in the DAS has a health check for the underlying storage that can be invoked by using the RPC method `das_healthCheck` that returns a status `200` if the DAS is active. **Example**: ```shell curl -X POST \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":0,"method":"das_healthCheck","params":[]}' \ ``` ### Test 2: Store and retrieve data The RPC interface of the DAS validates that requests to store data are signed by the sequencer's ECDSA key, identified via a call to the `SequencerInbox` contract on the parent chain. It can also be configured to accept store requests signed with another ECDSA key of your choosing. This could be useful for running load tests, canaries, or troubleshooting your own infrastructure. Using this facility, a load test could be constructed by writing a script to store arbitrary amounts of data at an arbitrary rate; a canary could be constructed to store and retrieve data on some interval. We show here a short guide on how to do that. #### Step 1: Generate an ECDSA keypair First we'll generate an ECDSA keypair with `anytrusttool keygen`. Create a folder inside `/some/local/dir` to store the ECDSA keypair, for example `/some/local/dir/keys`. Then run `anytrusttool keygen`: ```shell anytrusttool keygen --dir /some/local/dir/keys --ecdsa ``` You can also use the `docker run` command as follows: ```shell docker run --rm -it -v /some/local/dir:/home/user/data --entrypoint anytrusttool offchainlabs/nitro-node:v3.11.3-beb2108 keygen --dir /home/user/data/keys --ecdsa ``` #### Step 2: Change the DAS configuration and restart the server Add the following configuration parameter to `anytrustserver`: ```shell --data-availability.extra-signature-checking-public-key /some/local/dir/keys/ecdsa.pub ``` OR ```shell --data-availability.extra-signature-checking-public-key "0x" ``` And then restart it. #### Step 3: Store data signed with the ECDSA private key Now you can use the `anytrusttool` utility to send store requests signed with the ECDSA private key: ```shell anytrusttool client rpc store --rpc-client.rpc.url http://localhost:9876 --message "Hello world" --signing-key /some/local/dir/keys/ecdsa ``` OR ```shell anytrusttool client rpc store --rpc-client.rpc.url http://localhost:9876 --message "Hello world" --signing-key "0x" ``` You can also use the `docker run` command: ```shell docker run --rm -it -v /some/local/dir:/home/user/data --network="host" --entrypoint anytrusttool offchainlabs/nitro-node:v3.11.3-beb2108 client rpc store --rpc-client.rpc.url http://localhost:9876 --message "Hello world" --signing-key /home/user/data/keys/ecdsa ``` The above command will output the `Hex Encoded Data Hash` which can then be used to retrieve the data in the next step. #### Step 4: Retrieve the stored data Use again the `anytrusttool` to retrieve the stored data. Notice that to perform this step you must have the REST interface enabled in the DAS: ```shell anytrusttool client rest getbyhash --url http://localhost:9877 --data-hash 0xDataHash ``` You can also use the `docker run` command: ```shell docker run --rm -it --network="host" --entrypoint anytrusttool offchainlabs/nitro-node:v3.11.3-beb2108 client rest getbyhash --url http://localhost:9877 --data-hash 0xDataHash ``` If we set `0xDataHash` to `0x052cca0e379137c975c966bcc69ac8237ac38dc1fcf21ac9a6524c87a2aab423` (from the previous step), then the result should be: `Message: Hello world` The retention period defaults to 24 hours, but can be configured when calling `anytrusttool client rpc store` with the parameter `--anytrust-retention-period` and a duration for the retention period (e.g., `48h`). ### Test 3: REST health check The REST interface has a health check on the path `/health` which will return a status `200` if the underlying storage is working, otherwise `503`. Example: ```shell curl -I /health ``` ### Test 4: Retrieve data from a batch poster transaction You can also do a test to retrieve the transaction data posted by a Batch Poster transaction. The transaction will contain both keyset and data hash information in its `data` field in method `addSequencerL2BatchFromOrigin(uint256 sequenceNumber, bytes data,uint256 afterDelayedMessagesRead, address gasRefunder,uint256 prevMessageCount,uint256 newMessageCount)`. After you decode a batch poster transaction and get its `data` within the function data, you can continue to decode the `data` as follows: The first part (1 byte) is the `header flag`, which is used to specify which type of batch it is. Here we need to check if it has bit `0x80` (For example, `0x88` and `0x80` are both valid, but `0x55` is wrong). The second part (32 bytes) is the keyset hash. You can learn more about what keyset is [here](/how-arbitrum-works/deep-dives/anytrust-protocol.md#keysets). The third part (32 bytes) is the data hash, and this is what we need to retrieve data. When you get this hash, you can retrieve data directly by following what we demonstrate in Step 4. ## Running a mirror DAS To avoid exposing the REST interface of your main DAS to the public in order to prevent spamming attacks (as explained in [Security considerations](#security-considerations)), you can choose to run a mirror DAS to complement your setup. The mirror DAS will handle all public REST requests, while reading information from the main DAS via its (now private) REST interface. In general, mirror DA servers serve two main purposes: 1. Prevent the main DAS from having to serve requests for data, allowing it to focus only on storing the data received. 2. Provide resiliency to the network in the case of a DAS going down. Find information about how to set up a mirror DAS in [How to deploy a mirror DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-mirror-das.md). ## Security considerations Keep in mind the following information when running the DAS. A DAS should strive not to miss any batch of information sent by the sequencer. Although it can use a REST aggregator to fetch missing information from other DA servers, it should aim to synchronize all received information directly. To facilitate this, avoid placing any load balancing layer before the DAS, enabling it to handle all incoming traffic. Taking that into account, there's a risk of Denial of Service attacks on those servers if the endpoint for the RPC interface is publicly known. To mitigate this risk, ensure the RPC endpoint's URL is not easily discoverable. It should be known only to the sequencer. Share this information with the chain owner through a private channel to maintain security. Finally, as explained in the previous section, if you're also running a mirror DAS, there's no need to publicly expose the REST interface of your main DAS. Your mirrors can synchronize over your private network using the REST interface from your main DAS and other public mirrors. ## External signer support By default the batch poster uses the same ECDSA key to sign `das_store` requests as it uses to sign the batch transactions sent to the Sequencer Inbox contract. Many installations use an external signer for securing the batch poster's key. While using an external signer is suported for signing batch transactions, it is not currently supported for signing the requests sent to the DA Committee. Currently, if you want to use an external signer for the batch transactions together with AnyTrust, you must generate a separate key for signing the requests sent to the DA Committee. If a wallet file is used the account must be named "l1-batch-poster". The batch poster would need to have the configuration for the external signer ```text --node.batch-poster.data-poster.external-signer... ``` and the configuration for the key which is only used for signing DA Committee requests. ```text --node.batch-poster.parent-chain-wallet... ``` The Committee servers would need to additionally specify the public key to accept signed messages from. ```text --data-availability.extra-signature-checking-public-key ``` ## Other considerations * When using [nginx](https://www.nginx.com/) in the networking stack, a DAS might fail receiving batches that are over a certain size. If this happens, the DAS won't be able to sign any more certificates and the batch poster will receive an error `413 Request Entity Too Large`. To prevent this behavior, the parameter `client_max_body_size` from nginx configuration should be configured with a higher value than the default 1M. It's recommended to set it to at least 50M. ## What to do next? Once the DAS is deployed and tested, you'll have to communicate the following information to the chain owner, so they can update the chain parameters and configure the sequencer: * Public key * The https URL for the RPC endpoint which includes some random string (e.g., das.your-chain.io/rpc/randomstring123), communicated through a secure channel * The https URL for the REST endpoint (e.g., das.your-chain.io/rest) ## Optional parameters Besides the parameters described in this guide, there are some more options that can be useful when running the DAS. For a comprehensive list of configuration parameters, you can run `anytrustserver --help`. | Parameter | Description | | ----------- | -------------------------------------------------------------------------------------------------------------------- | | --conf.dump | Prints out the current configuration | | --conf.file | Absolute path to the configuration file inside the volume to use instead of specifying all parameters in the command | ## Metrics The DAS comes with the option of producing Prometheus metrics. This option can be activated by using the following parameters: | Parameter | Description | | -------------------------------- | -------------------------------------------- | | --metrics | Enables the metrics server | | --metrics-server.addr | Metrics server address (default "127.0.0.1") | | --metrics-server.port | Metrics server port (default 6070) | | --metrics-server.update-interval | Metrics server update interval (default 3s) | When metrics are enabled, several useful metrics are available at the configured port, at path `debug/metrics` or `debug/metrics/prometheus`. ### RPC metrics | Metric | Description | | ---------------------------------------------------------------- | ------------------------------------ | | arb\_das\_rpc\_store\_requests | Count of RPC Store calls | | arb\_das\_rpc\_store\_success | Successful RPC Store calls | | arb\_das\_rpc\_store\_failure | Failed RPC Store calls | | arb\_das\_rpc\_store\_bytes | Bytes retrieved with RPC Store calls | | arb\_das\_rpc\_store\_duration (p50, p75, p95, p99, p999, p9999) | Duration of RPC Store calls (ns) | ### REST metrics | Metric | Description | | --------------------------------------------------------------------- | ----------------------------------------- | | arb\_das\_rest\_getbyhash\_requests | Count of REST GetByHash calls | | arb\_das\_rest\_getbyhash\_success | Successful REST GetByHash calls | | arb\_das\_rest\_getbyhash\_failure | Failed REST GetByHash calls | | arb\_das\_rest\_getbyhash\_bytes | Bytes retrieved with REST GetByHash calls | | arb\_das\_rest\_getbyhash\_duration (p50, p75, p95, p99, p999, p9999) | Duration of REST GetByHash calls (ns) | --- > For a complete page index, fetch # How to deploy a mirror Data Availability Server (DAS) > **CAUTION** — Running a regular DAS vs running a mirror DAS > > The main use-case for running a mirror DAS is to complement your setup as a Data Availability Committee (DAC) member. That means that you should run your main DAS first, and then configure the mirror DAS. Refer to [How to deploy a DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md) if needed. [AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) chains rely on an external Data Availability Committee (DAC) to store data and provide it on-demand instead of using its parent chain as the Data Availability (DA) layer. The members of the DAC run a Data Availability Server (DAS) to handle these operations. In this how-to, you'll learn how to configure a mirror DAS that serves `GET` requests for stored batches of information through a REST HTTP interface. For a refresher on DACs, refer to the [Introduction](/launch-arbitrum-chain/chain-config/data-availability/dac-get-started.md). This how-to assumes that you're familiar with: * How a regular DAS works and what configuration options are available. Refer to [How to deploy a DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md) for a refresher. * [Kubernetes](https://kubernetes.io/). The examples in this guide use Kubernetes to containerize your DAS. ## What is a mirror DAS? To avoid exposing the REST interface of your main DAS to the public in order to prevent spamming attacks (as explained in [How to deploy a DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md#security-considerations)), you can choose to run a mirror DAS to complement your setup. The mirror DAS will handle all public REST requests, while reading information from the main DAS via its (now private) REST interface. In general, mirror DA servers serve two main purposes: 1. Prevent the main DAS from having to serve requests for data, allowing it to focus only on storing the data received. 2. Provide resiliency to the network in the case of a DAS going down. ## Configuration options A mirror DAS will use the same tool and, thus, the same configuration options as your main DAS. You can find an explanation of those options in [How to deploy a DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md#configuration-options). ## How to deploy a mirror DAS ### Step 0: Prerequisites Gather the following information: * The latest Nitro docker image: `offchainlabs/nitro-node:v3.11.3-beb2108` * An RPC endpoint for the parent chain. It is recommended to use a [third-party provider RPC](/arbitrum-essentials/reference/node-providers.md#third-party-rpc-providers) or [run your own node](/run-arbitrum-node/run-full-node.md) to prevent being rate limited. * The SequencerInbox contract address in the parent chain. * URL of the list of REST endpoints of other DA servers to configure the REST aggregator. ### Step 1: Set up a persistent volume First, we'll set up a volume to store the DAS database. In k8s, we can use a configuration like this: ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: das-mirror spec: accessModes: - ReadWriteOnce resources: requests: storage: 200Gi storageClassName: gp2 ``` ### Step 2: Deploy the mirror DAS To run the mirror DAS, we'll use the `anytrustserver` tool and we'll configure the following parameters: | Parameter | Description | | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | --parent-chain.node-url | RPC endpoint of a parent chain node | | --parent-chain.sequencer-inbox-address | Address of the SequencerInbox in the parent chain | | --enable-rest | Enables the REST server listening on --rest-addr and --rest-port | | --rest-addr | REST server listening interface (default "localhost") | | --rest-port | (Optional) REST server listening port (default 9877) | | --log-level | Log level: CRIT, ERROR, WARN, INFO, DEBUG, or TRACE (default INFO) | | --data-availability.rest-aggregator.enable | Enables retrieval of Sequencer Batch data from a list of remote REST endpoints | | --data-availability.rest-aggregator.online-url-list | A URL to a list of URLs of REST DAS endpoints that is checked at startup. This option is additive with the URLs option | | --data-availability.rest-aggregator.urls | List of URLs including 'http\://' or 'https\://' prefixes and port numbers to REST DAS endpoints. This option is additive with the online-url-list option | | --data-availability.rest-aggregator.sync-to-storage.check-already-exists | When using a REST aggregator, checks if the data already exists in this DAS's storage. Must be disabled for fast sync with an IPFS backend (default true) | | --data-availability.rest-aggregator.sync-to-storage.eager | When using a REST aggregator, eagerly syncs batch data to this DAS's storage from the REST endpoints, using the parent chain as the index of batch data hashes; otherwise only syncs lazily | | --data-availability.rest-aggregator.sync-to-storage.eager-lower-bound-block | When using a REST aggregator that's eagerly syncing, starts indexing forward from this block from the parent chain. Only used if there is no sync state. | | --data-availability.rest-aggregator.sync-to-storage.retention-period | When using a REST aggregator, period to retain the synced data (defaults to forever) | | --data-availability.rest-aggregator.sync-to-storage.state-dir | When using a REST aggregator, directory to store the sync state in, i.e., the block number currently synced up to, so that it doesn't sync from scratch each time | To enable caching, you can use the following parameters: | Parameter | Description | | ---------------------------------------- | ---------------------------------------------------------------------------------- | | --data-availability.local-cache.enable | Enables local in-memory caching of sequencer batch data | | --data-availability.local-cache.capacity | Maximum number of entries (up to 64KB each) to store in the cache. (default 20000) | Finally, for the storage backends you wish to configure, use the following parameters. Toggle between the different options to see all available parameters. > **WARNING** — Local Badger database removed > > The local Badger DB storage option (previously set with `local-db-storage`) was deprecated and removed in Nitro v3.8.0, together with its `--data-availability.migrate-local-db-to-file-storage` migration parameter. If your DAS still stores data in a local Badger database, run a Nitro version prior to v3.8.0 with that parameter to migrate the data to the local files storage option (`local-file-storage`) before upgrading.
AWS S3 bucket | Parameter | Description | | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | --data-availability.s3-storage.enable | Enables storage/retrieval of sequencer batch data from an AWS S3 bucket | | --data-availability.s3-storage.access-key | S3 access key | | --data-availability.s3-storage.bucket | S3 bucket | | --data-availability.s3-storage.region | S3 region | | --data-availability.s3-storage.secret-key | S3 secret key | | --data-availability.s3-storage.object-prefix | Prefix to add to S3 objects | | --data-availability.s3-storage.discard-after-timeout | (**Deprecated**) Expiration should be directly set as a rule in Lifecycle Configuration of S3 bucket |
Local files > **WARNING** — Create the local directory before launching your DAS > > Make sure you create the directory specified in `local-file-storage.data-dir` before launching your DAS to avoid potential permission issues with Docker or Kubernetes. | Parameter | Description | | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | --data-availability.local-file-storage.enable | Enables storage/retrieval of sequencer batch data from a directory of files, one per batch | | --data-availability.local-file-storage.data-dir | Absolute path of the directory inside the volume in which to store the data (it must exist) | | --data-availability.local-file-storage.enable-expiry | Enables expiry of batches | | --data-availability.local-file-storage.max-retention | Store requests with expiry times farther in the future than max-retention will be rejected (default: 504h0m0s) |
(Experimental) Google Cloud Storage | Parameter | Description | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------ | | --data-availability.google-cloud-storage.enable | Enables storage/retrieval of sequencer batch data from a Google Cloud Storage bucket | | --data-availability.google-cloud-storage.access-token | Google Cloud Storage access token | | --data-availability.google-cloud-storage.bucket | Google Cloud Storage bucket | | --data-availability.google-cloud-storage.object-prefix | Prefix to add to Google Cloud Storage objects | | --data-availability.google-cloud-storage.enable-expiry | Enable expiry of batches (setting it to false, activates the "archive" mode) | | --data-availability.google-cloud-storage.max-retention | Store requests with expiry times farther in the future than max-retention will be rejected |
Here's an example `anytrustserver` command for a mirror DAS that: * Enables local cache * Enables AWS S3 bucket storage that doesn't discard data after expiring ([archive](#archive-da-servers)) * Enables local file storage that, by default, doesn't discard data after expiring ([archive](#archive-da-servers)) * Uses a local main DAS as part of the REST aggregator ```shell anytrustserver --parent-chain.node-url "" --parent-chain.sequencer-inbox-address "
" --enable-rest --rest-addr '0.0.0.0' --log-level INFO --data-availability.local-cache.enable --data-availability.rest-aggregator.enable --data-availability.rest-aggregator.urls "http://your-main-das.svc.cluster.local:9877" --data-availability.rest-aggregator.online-url-list "" --data-availability.rest-aggregator.sync-to-storage.eager --data-availability.rest-aggregator.sync-to-storage.eager-lower-bound-block "BLOCK NUMBER" --data-availability.rest-aggregator.sync-to-storage.state-dir /home/user/data/syncState --data-availability.s3-storage.enable --data-availability.s3-storage.access-key "" --data-availability.s3-storage.bucket "" --data-availability.s3-storage.region "" --data-availability.s3-storage.secret-key "" --data-availability.s3-storage.object-prefix "/" --data-availability.local-file-storage.enable --data-availability.local-file-storage.data-dir /home/user/data/das-data ``` And here's an example of how to use a k8s deployment to run that command: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: das-mirror spec: replicas: 1 selector: matchLabels: app: das-mirror strategy: rollingUpdate: maxSurge: 0 maxUnavailable: 50% type: RollingUpdate template: metadata: labels: app: das-mirror spec: containers: - command: - bash - -c - | mkdir -p /home/user/data/syncState /usr/local/bin/anytrustserver --parent-chain.node-url "" --parent-chain.sequencer-inbox-address "
" --enable-rest --rest-addr '0.0.0.0' --log-level INFO --data-availability.local-cache.enable --data-availability.rest-aggregator.enable --data-availability.rest-aggregator.urls "http://your-main-das.svc.cluster.local:9877" --data-availability.rest-aggregator.online-url-list "" --data-availability.rest-aggregator.sync-to-storage.eager --data-availability.rest-aggregator.sync-to-storage.eager-lower-bound-block "BLOCK NUMBER" --data-availability.rest-aggregator.sync-to-storage.state-dir /home/user/data/syncState --data-availability.s3-storage.enable --data-availability.s3-storage.access-key "" --data-availability.s3-storage.bucket "" --data-availability.s3-storage.region "" --data-availability.s3-storage.secret-key "" --data-availability.s3-storage.object-prefix "/" --data-availability.local-file-storage.enable --data-availability.local-file-storage.data-dir /home/user/data/das-data image: offchainlabs/nitro-node:v3.11.3-beb2108 imagePullPolicy: Always resources: limits: cpu: "4" memory: 10Gi requests: cpu: "4" memory: 10Gi ports: - containerPort: 9877 hostPort: 9877 protocol: TCP volumeMounts: - mountPath: /home/user/data/ name: data readinessProbe: failureThreshold: 3 httpGet: path: /health/ port: 9877 scheme: HTTP initialDelaySeconds: 5 periodSeconds: 5 successThreshold: 1 timeoutSeconds: 1 volumes: - name: data persistentVolumeClaim: claimName: das-mirror ``` ## Archive DA servers Archive DA servers are servers that don't discard any data after expiring. Each DAC should have at the very least one archive DAS to ensure all historical data is available. To activate the "archive mode" in your DAS, configure your storage backend to never discard data: `local-file-storage` retains all data by default (as long as `enable-expiry` is not set), and `s3-storage` relies on the bucket's lifecycle configuration, as explained in the notes below. For the experimental `google-cloud-storage` backend, set the parameter `discard-after-timeout` to `false`: ```shell --data-availability.google-cloud-storage.discard-after-timeout=false ``` > **NOTE** > > The `s3-storage` **doesn't support** expiration of data, instead that can be enabled from user side by setting `Expiration` as a rule in Lifecycle Configuration of the corresponding S3 bucket. More information available at [AWS Expiring objects](https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-expire-general-considerations.html). > **NOTE** > > The `local-file-storage` doesn't discard data after expiring by default, but expiration can be enabled with `enable-expiry`. Archive servers should make use of the `--data-availability.rest-aggregator.sync-to-storage` options described above to pull in any data that they don't have. ## Helm charts A helm chart is available at [ArtifactHUB](https://artifacthub.io/packages/helm/offchainlabshelm/das). It supports running a mirror DAS by providing the parameters for your server. Find more information in the [OCL community Helm charts repository](https://github.com/OffchainLabs/community-helm-charts/tree/main/charts/das). ## Testing the DAS Once the DAS is running, we can test if everything is working correctly using the following methods. ### Test 1: REST health check The REST interface enabled in the mirror DAS has a health check on the path `/health` which will return `200` if the underlying storage is working, otherwise `503`. Example: ```shell curl -I /health ``` ## Security considerations Keep in mind the following information when running the mirror DAS. For a mirror DAS, using a load balancer is recommended to manage incoming traffic effectively. Additionally, as the REST interface is cacheable, consider deploying a Content Delivery Network (CDN) or caching proxy in front of your REST endpoint. The URL for the REST interface will be publicly known; ensure that it is sufficiently distinct from the RPC endpoint to prevent the latter from being easily discovered. ## What to do next? Once the DAS is deployed and tested, you'll have to communicate the following information to the chain owner, so they can update the chain parameters and configure the sequencer: * The https URL for the REST endpoint (e.g., `das.your-chain.io/rest`) ## Optional parameters Besides the parameters described in this guide, there are some more options that can be useful when running the DAS. For a comprehensive list of configuration parameters, you can run `anytrustserver --help`. | Parameter | Description | | ----------- | -------------------------------------------------------------------------------------------------------------------- | | --conf.dump | Prints out the current configuration | | --conf.file | Absolute path to the configuration file inside the volume to use instead of specifying all parameters in the command | ## Metrics The DAS comes with the option of producing Prometheus metrics. This option can be activated by using the following parameters: | Parameter | Description | | -------------------------------- | -------------------------------------------- | | --metrics | Enables the metrics server | | --metrics-server.addr | Metrics server address (default "127.0.0.1") | | --metrics-server.port | Metrics server port (default 6070) | | --metrics-server.update-interval | Metrics server update interval (default 3s) | When metrics are enabled, several useful metrics are available at the configured port, at path `debug/metrics` or `debug/metrics/prometheus`. ### RPC metrics | Metric | Description | | ---------------------------------------------------------------- | ------------------------------------ | | arb\_das\_rpc\_store\_requests | Count of RPC Store calls | | arb\_das\_rpc\_store\_success | Successful RPC Store calls | | arb\_das\_rpc\_store\_failure | Failed RPC Store calls | | arb\_das\_rpc\_store\_bytes | Bytes retrieved with RPC Store calls | | arb\_das\_rpc\_store\_duration (p50, p75, p95, p99, p999, p9999) | Duration of RPC Store calls (ns) | ### REST metrics | Metric | Description | | --------------------------------------------------------------------- | ----------------------------------------- | | arb\_das\_rest\_getbyhash\_requests | Count of REST GetByHash calls | | arb\_das\_rest\_getbyhash\_success | Successful REST GetByHash calls | | arb\_das\_rest\_getbyhash\_failure | Failed REST GetByHash calls | | arb\_das\_rest\_getbyhash\_bytes | Bytes retrieved with REST GetByHash calls | | arb\_das\_rest\_getbyhash\_duration (p50, p75, p95, p99, p999, p9999) | Duration of REST GetByHash calls (ns) | --- > For a complete page index, fetch # Configure smart contract size limit The smart contract size limit must be set during the initial configuration and deployment of a custom chain (Layer 2 or Layer 3 Rollup). It is not possible to modify it post-deployment due to the lack of a versioning mechanism for such parameters. The limit applies to both deployed contract code (`MaxCodeSize`) and initialization code during deployment (`MaxInitCodeSize`). `MaxCodeSize` is configurable up to 96KB (98,304 bytes) and `MaxInitCodeSize` is configurable up to 192KB (196,608 bytes), which is higher than the default 24.5KB (24,576 bytes) inherited from Ethereum's [EIP-170](https://eips.ethereum.org/EIPS/eip-170) for compatibility. ## Key specifics * **Default values**: 24KB (24,576 bytes) for `MaxCodeSize` and `MaxInitCodeSize` is `2 * DefaultMaxCodeSize`. * **Maximum values**: 96KB (98,304 bytes) for `MaxCodeSize`. 192KB (196,608 bytes) for `MaxInitCodeSize`. Setting higher is not supported and may cause deployment failures or compatibility issues. * **Where applicable**: Only in custom Arbitrum chains. Public chains like Arbitrum One or Nova remain fixed at 24KB without a network-wide upgrade. * **Requirements**: You'll need the Arbitrum Chain SDK (installed via `npm install @arbitrum/chain-sdk` or similar), a funded wallet on the parent chain (e.g., Ethereum or Arbitrum One), and access to deployment tools like Hardhat or Foundry for integration. > **INFO** > > The `chainId` and `InitialChainOwner` parameters must be equal to the `chainId` and `owner` defined in the `Config` struct. #### `MaxCodeSize` limitations * Entering a contract requires reading its code from the database, which means larger code requires more work. The EVM doesn't charge differently based on code size, but the computation cost could vary. However, that doesn't seem to be a strict limit compared to the readability aspect. * Reading code occurs via a preimage fetch (`code hash -> code`). The limit of preimage size is that a one-step proof (OSP) of a preimage must include the full preimage as part of the transaction. #### `MaxInitCodeSize` limitations **For L2s building on top of Ethereum (parent chain):** The effective limit on calldata seems to be 128KB: * Some clients (Geth, AFAU) use 128KB as the limit, even though the protocol doesn't enforce it * 128KB of calldata, with 40 gas per byte (according to [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623)), would put the gas at 6M, which is within the limit for a transaction The conclusion is that using a 96KB code size limit is conservative and safe. **For L3s building on top of Arbitrum One:** * The sequencer has a default limit on `TxMaxDataSize` of `95000`, so L3s should use a lower preimage limit than that, roughly 85KB. **Other chains:** * Should deduct limits from their prospective parent chain **Safe values** | Configuration | Max contract size limit | Init code size | | ------------- | ----------------------- | -------------- | | Config A | 24kb | 48kb | | Config B | 48kb | 96kb | ::: ### Process overview 1. Prepare the chain configuration using the Chain SDK's `prepareChainConfig` function, where you specify the parameters. 2. Use this config in `prepareNodeConfig` to generate the full node configuration (e.g., for nitro-node.toml). 3. Deploy the chain contracts to the parent chain. 4. Run the node with the generated config. > **CAUTION** > > * This change is immutable after deployment—plan carefully. > * Higher limits increase risks like state bloat, slower node performance, and potential DoS vulnerabilities. > * Test on a devnet or testnet first to ensure stability. > * Ensure values are in bytes (not KB) when setting them in code. ## Code examples The example below assumes you've set up a project with the SDK installed and have environment variables for your private key and RPC URLs. ### 1. Basic chain configuration (setting the parameters) * The snippet below demonstrates how to prepare the chain config JSON, including the size limit parameters. Place this in a file like `prepareChainConfigExample.ts`. ```typescript import { prepareChainConfig } from '@offchainlabs/chain-sdk'; // Import the function from the SDK // Define the parameters for your custom Arbitrum chain const orbitChainParams = { chainId: 123456, // Your unique chain ID (must not conflict with existing chains) homesteadBlock: 0, // Typically 0 for new chains eip155Block: 0, // Typically 0 // ... other standard Ethereum chain config params as needed arbitrum: { // Arbitrum-specific extensions MaxCodeSize: 98304, // Set to 96KB (98,304 bytes) for deployed contract code size limit MaxInitCodeSize: 196608, // Set to 192KB (196,608 bytes) for init code size during contract deployment // Note: Init code can often be set higher (e.g., 2x MaxCodeSize) if needed for complex constructors, // but stick to <= 96KB to avoid issues. Default is 24576 bytes (24KB) if omitted. // Warning: Higher values may increase state size and node resource demands. // Other Arbitrum params like DataAvailabilityCommittee: true/false can go here }, }; // Prepare the chain config JSON using the SDK function const chainConfig = prepareChainConfig(orbitChainParams); // Output the config (e.g., for use in deployment or node setup) console.log(JSON.stringify(chainConfig, null, 2)); // You can now use this chainConfig in deployment scripts or pass it to prepareNodeConfig. ``` * Run this with `ts-node prepareChainConfigExample.ts` to generate the config JSON. ### 2. Full node configuration and deployment example * This extends Step 1 in preparation of the full node config (e.g., for running a [sequencer](/how-arbitrum-works/deep-dives/sequencer.md) or validator node). Place this in a file like `deployOrbitChain.ts`. It assumes you’re deploying to a parent chain, such as Sepolia (testnet). ```typescript import { prepareChainConfig, prepareNodeConfig } from '@offchainlabs/chain-sdk'; // Import SDK functions import { ethers } from 'ethers'; // For wallet and provider (install via npm) import { writeFileSync } from 'fs'; // For saving config files // Load environment variables (e.g., from .env file) const parentChainRpcUrl = process.env.PARENT_RPC_URL; // e.g., https://sepolia.infura.io/v3/YOUR_KEY const privateKey = process.env.PRIVATE_KEY; // Your wallet private key (funded with ETH) // Step 1: Prepare chain config with increased size limits const orbitChainParams = { chainId: 123456, arbitrum: { MaxCodeSize: 98304, // Increase to max 96KB for larger contracts MaxInitCodeSize: 196608, // Matching increase for init code // Additional params: e.g., InitialChainOwner: '0xYourAddress' for governance }, }; const chainConfig = prepareChainConfig(orbitChainParams); // Step 2: Set up provider and wallet for deployment const parentProvider = new ethers.JsonRpcProvider(parentChainRpcUrl); const wallet = new ethers.Wallet(privateKey, parentProvider); // Step 3: Prepare node config (generates nitro-node config like chain.toml) const nodeConfig = prepareNodeConfig({ chainName: 'my-arbitrum-chain', // Name for your chain chainConfig, // Pass the prepared chain config here parentChainId: 11155111, // e.g., Sepolia chain ID // Other options: batchPosterPrivateKey, validatorPrivateKey, etc. // Note: This generates a config object or file with settings for the node, // including the immutable chain params like size limits. }); // Save the node config to a file (e.g., for running the node) writeFileSync('my-arbitrum-chain.toml', nodeConfig); // Adjust format as needed (TOML or JSON) // Step 4: Deploy the core contracts to the parent chain // Use the SDK's deployment functions (simplified; refer to full docs for complete script) const deploymentResult = await deployOrbitChain({ wallet, chainConfig, // ... other deployment params like data availability settings }); console.log('Chain deployed! Contracts:', deploymentResult.contractAddresses); console.log('Use the generated my-arbitrum-chain.toml to run your node.'); // Post-deployment: Run the nitro node with Docker or binary, pointing to the config file. // Example command: docker run --rm -v $(pwd)/my-arbitrum-chain.toml:/config.toml offchainlabs/nitro-node --conf.file /config.toml ``` * Run this with `ts-node deployOrbitChain.ts`. This script deploys the chain and generates the node config with your custom limits embedded. For exact SDK function signatures and more options, refer to the official [Arbitrum Chain SDK repository on GitHub](https://github.com/OffchainLabs/arbitrum-chain-sdk). Always test in a development environment, as incorrect configs can lead to failed deployments or insecure chains. If your contracts still exceed the limit, consider optimizations such as code splitting or using [Stylus](/stylus/gentle-introduction.md) to create more efficient WASM-based contracts. --- > For a complete page index, fetch # How to configure Delayed Inbox finality ## Child chain transactions Generally, transactions executed through the Sequencer on Arbitrum chains [achieve finality](/how-arbitrum-works/deep-dives/transaction-lifecycle.md) equivalent to their parent chain once the relevant transaction data has been [posted in a batch](/how-arbitrum-works/inside-arbitrum-nitro.md). This means that transactions on Arbitrum Chains are considered final in minutes. ## Parent chain → child chain transactions Messages being sent through the Delayed Inbox of a parent chain as [retryable tickets](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#retryable-tickets), including deposits through token bridges, are released by the [sequencer](/how-arbitrum-works/deep-dives/sequencer.md) once it has reasonable confidence of finality on the parent chain. For example, on an L2 chain settling to Ethereum, the sequencer will release delayed messages to the inbox after 40 blocks. Following this, the transaction must complete another finality period for the Ethereum transaction that prompted it to achieve finality. Arbitrum chain L3s may configure the finality of transactions executed through the Delayed Inbox to depend on different layers of finality. By default, Arbitrum chains will rely on the number of L1 block confirmations, effectively finalizing an L3 deposit as soon as L1 finalizes the batch posted by Arbitrum One or when a DACert is posted by Arbitrum Nova. This would be on the order of tens of minutes. However, in the instance of an L3 settling to Arbitrum One or Nova an L3 may also choose to rely only on L2 finality by configuring their sequencer as follows: ```text --node.delayed-sequencer.use-merge-finality=false ``` Additionally, the delay in L3 finalization can be decreased to achieve extremely fast (one minute) deposits by configuring the sequencer to wait for fewer L2 block confirmations: ```text --node.delayed-sequencer.finalize-distance=1 ``` Note, however, that if you choose to enable fast bridging, a re-org of un-finalized blocks on the L3 may occur if Arbitrum One/Nova (or the settlement chain of choice) experiences a re-org. ## Child chain → parent chain transactions Normally, [outgoing transactions](/how-arbitrum-works/deep-dives/l2-to-l1-messaging.md) must wait until the [assertion](/how-arbitrum-works/deep-dives/assertions.md) that includes their L2 message is confirmed (\~one week) before a client can execute the message on L1. However, in the near future [AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) chains will be able to leverage their DAC to enable fast confirmations of withdrawals through the native token bridge. By immediately confirming assertions that have been signed by the DAC, finality can be reduced to \~15 minutes. --- > For a complete page index, fetch # Compliance filtering Note This feature is available through the ArbOS 61 upgrade, however off by default. We recommend adopting this feature at least 30 days after the release of ArbOS 61 on Arbitrum One. New, optional components can be added to the Sequencer and State Transition Function (STF) to enable protocol-level transaction filtering for regulatory or compliance purposes, at the discretion of the chain owner. The chain owner may configure an external compliance service (e.g., TRM or Chainalysis) to define address-level policies. Any transaction that originates from, targets, or otherwise involves a restricted address is filtered prior to execution, including transactions submitted via the Delayed Inbox on the parent chain. ## Rationale The Arbitrum platform must evolve to meet the needs of a new wave of teams looking to come onchain—specifically teams who have an obligation to comply with regulatory frameworks and rules in their respective jurisdictions. This feature represents the first of many to ensure that the Arbitrum platform can serve the needs of enterprises and institutions looking to tap into internet-scale, blockchain technology to increase margins, reduce risk, and improve transparency across their businesses. While this feature is not intended or proposed to be used by Arbitrum One, its inclusion in ArbOS 61 Elara is intended to simplify the codebase and canonicalize this feature in the node software. This feature is instead intended to be used by Arbitrum chains who have compliance and regulatory obligations to restrict onchain activity from sanctioned entities or actors. ## How it works Compliance Filtering operates across two layers: the Sequencer and the State Transition Function (STF). Together, these layers ensure that restricted activity is blocked regardless of how a transaction enters the system. ### Restricted address list The chain owner configures an external compliance provider (e.g., TRM Labs, Chainalysis) to define which addresses are restricted. This produces a restricted address list, which is: 1. Stored in-memory on any node performing compliance screening 2. Encoded as salted hashes in the form `sha256(salt || address)` to protect address privacy 3. Refreshed periodically via a synchronization pipeline When evaluating a transaction, the compliance engine hashes all relevant addresses and checks them against this list. ### Sequencer-level filtering The Compliance Rules Engine runs inside the Sequencer and simulates each transaction before it is sequenced. If a transaction violates any compliance rule, it is rejected at the Sequencer level and never included in a block. This is the primary enforcement layer and handles the vast majority of cases. ### Compliance rules Specific rules can be defined by the chain owner for any address found on the restricted list. These rules apply to direct transfers as well as delegated approval mechanisms. A few examples: * Block any transaction originating from or to a restricted address * Block execution of **ERC-20**, **ERC-721**, and **ERC-1155** transfers from or to a restricted address * Block execution of the following opcodes targeting a restricted address: `CALL`, `CREATE`, `CREATE2`, `SELFDESTRUCT` ### Delayed Inbox filtering (STF level) Transactions submitted via the Delayed Inbox cannot be filtered at the Sequencer layer alone, since they are eligible for force inclusion after a timeout window. To handle this, Compliance Filtering introduces two additional components: #### Transaction Guardian Precompile A new precompile allows authorized entities (e.g., the Sequencer) to register the hash of a restricted transaction onchain before it is force-included. When the STF processes a transaction from the delayed inbox, it checks whether that transaction's hash has been registered in the guardian. If so, the transaction is forcibly failed on inclusion, consuming any gas attached to prevent spam. #### Delayed Inbox Sentinel The sentinel is responsible for monitoring the delayed inbox for restricted transactions. When a restricted transaction is detected, the sentinel: 1. Submits the transaction's hash to the Transaction Guardian Precompile 2. Ensures this occurs before the force inclusion window closes 3. Allows the Sequencer to then process the transaction, which will fail at the STF level as expected This ensures that even transactions submitted through the parent chain → child chain messaging path are subject to compliance enforcement. ### Exception rules (burn allowance) Certain DeFi protocols (e.g., lending protocols like Aave, Morpho) require the ability to liquidate or burn token balances associated with restricted accounts in order to maintain solvency and prevent exploits. To support this, a narrow exception is available: **ERC-20**, **ERC-721**, and **ERC-1155** transfers where the `to` field of the Transfer event is the zero address (`0x0`) (a burn operation) are permitted even if the `from` address is restricted. Important constraints: * If a "burn" transaction includes any other compliance violation, the entire transaction will fail. * A restricted address cannot initiate a burn themselves—their `tx.origin` is blocked at the Sequencer level regardless. This design allows third-party liquidators and protocol contracts (e.g., Paxos, Aave) to burn non-compliant balances while preventing the restricted address from taking any action on the chain. ### Synchronization pipeline The restricted address list is distributed to Sequencer and prechecker nodes via a cloud-based pipeline: * The compliance provider periodically publishes a new hash list to a designated S3 bucket. * An SNS notification triggers ingestion into the chain operator's own S3 bucket. * Nodes subscribe to updates and load the new list into memory, fully replacing the prior list once the new one is validated and complete. Nodes are expected to always have a valid list in memory. On startup or restart, a node will fetch the most recent list before beginning to sequence or precheck transactions. ## Deployment considerations | Component | Availability | Notes | | ------------------------------- | -------------- | ----------------------------------------------- | | Sequencer-level filtering | ArbOS 61 Elara | Opcode and event filtering | | Transaction Guardian Precompile | ArbOS 61 Elara | Required for Delayed Inbox enforcement | | Delayed Inbox Sentinel (STF) | ArbOS 61 Elara | Requires audit; must be activated at deployment | | Burn exception rules | ArbOS 61 Elara | Covers Aave, Paxos, and similar protocols | Compliance Filtering is disabled by default. Chain owners must explicitly configure and enable each component. Activation after chain deployment may require an ArbOS upgrade. ## Security considerations * **Force inclusion protection:** Without STF-level enforcement via the Transaction Guardian, a restricted user could bypass Sequencer filtering by waiting for the force inclusion window. The sentinel + precompile combination closes this gap. * **Parent chain bridge deposits:** If a restricted address deposits **ETH** via `depositEth()` on the parent chain, the **ETH** enters the bridge contract on the parent chain before any filtering occurs on the child chain. If the corresponding credit is dropped on the child chain, the **ETH** may become locked in the bridge with no corresponding child chain asset. Chain owners should account for this in their compliance design and user communication strategy. * **Address privacy:** Restricted addresses are never stored in plaintext. Salted hashing ensures that the list cannot be trivially reversed to enumerate sanctioned addresses. ## Configuration To set up transaction filters, Arbitrum Nitro nodes provide a few configuration options for both address level filtering and event filtering. Event filters are optional, but address filters must both be enabled and configured properly before transactions will be properly filtered. ### Address filter To define the addresses that are to be restricted from executing or receiving onchain transactions, an S3-compatible file store must be used. When address filtering is enabled, Arbitrum Nitro will poll and read the latest file from the S3 bucket. The addresses are defined as hashes where `hashed_address = SHA256(salt16Byte || address20Byte)`. * `--execution.transaction-filtering.address-filter.enable`: true/false value that enables address filtering * `--execution.transaction-filtering.address-filter.poll-interval`: how often to resync from S3 (e.g., `60m`) * `--execution.transaction-filtering.address-filter.s3.bucket`: S3 bucket holding the hashed list * `--execution.transaction-filtering.address-filter.s3.object-key`: S3 object key for the hash list * `--execution.transaction-filtering.address-filter.s3.region`: AWS region where S3 bucket is hosted (e.g., `us-east-1`) * `--execution.address-filter.s3.access-key`: AWS access key * `--execution.address-filter.s3.secret-key`: AWS secret key * `--execution.address-filter.s3.endpoint`: S3-compatible endpoint where file is hosted (if not using AWS S3) The hash list of restricted addresses should be provided as a JSON file with the following format: ```json { salt: string, # UUID hashes: [string], # list of 32-byte hashes as hex encoded strings issued_at: string, # timestamp hashing_scheme: string # sha256-stringinput or sha256-rawbytesinput. } ``` Note: `sha256-stringinput` assumes that the string-based representation of the UUID salt is concatenated with the string-based version of the hex-encoded address (e.g. `sha256("d35e8ac5-9775-44af-878e-bad86409e80c" || "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")`). Raw bytes uses the raw 16 bytes of the UUID and raw 20 bytes of the address concatenated as input to the sha256 function to produce the 32-byte hash. ### Event filters Event filters allow for chain owners to filter out transactions based on an arbitrary list of emitted events. For example, a chain owner might want to block all **ERC-20** transfers involving a restricted address. A chain owner can setup their nodes to listen for `Transfer(address,address,uint256)` events and block any transactions that target a restricted address in either the `from` or `to` field. To setup event filters, two CLI flags are provided: * `--execution.transaction-filtering.event-filter.rules`: optional inline list of event rules (passed as a JSON string) * `--execution.transaction-filtering.event-filter.path`: optional path to a JSON file containing event filter rules. If both `path` and `rules` are provided, the file rules are loaded first and then inline rules are appended after them. Generally, path-based JSON rules should be preferred. Each entry in the rules is an `EventRule` with the following shape: * `event`: human-readable event signature (e.g., `Transfer(address,address,uint256)` * `selector`: hex string of the first 4 bytes of `keccak256(event)`; the canonical signature of the event type * `topicAddresses`: list of 1-indexed topic positions (1-3) whose values should be treated as addresses for filtering * `bypass`: defines an optional exception condition that allows the transaction to execute if a specific value is true (example would be transfers to `0x0` burn address). It is a nested object with: * `topicIndex`: 1-indexed topic position (1-3) * `equals`: conditional value for the exception at the topic position defined An example JSON file to provide to `--execution.transaction-filtering.event-filter.path`: ```json { "rules": [ { "event": "Transfer(address,address,uint256)", "selector": "0xddf252ad", "topicAddresses": [1, 2], "bypass": { "topicIndex": 2, "equals": "0x0000000000000000000000000000000000000000" } }, { "event": "Approval(address,address,uint256)", "selector": "0x8c5be1e5", "topicAddresses": [1, 2] } ] } ``` --- > For a complete page index, fetch # Configure Sequencer timing adjustments When launching an Arbitrum chain, the Sequencer plays a central role in ordering transactions and producing blocks. Several timing-related parameters can be adjusted in the node configuration (via the Nitro node's JSON config or command-line flags) to optimize performance, user experience, security, and cost for your specific use case. To configure [Sequencer](/how-arbitrum-works/deep-dives/sequencer.md) timing parameters when launching an Arbitrum chain, you'll primarily adjust settings in two places: 1. **Chain-level parameters**: Set during deployment via the Chain SDK; these affect Sequencer behavior boundaries. 2. **Node-level parameters**: Set when running your Nitro Sequencer node—these control runtime behavior, such as block production speed and batch posting. Most timing tweaks happen at the node level for the Sequencer. Use the official Arbitrum Chain SDK to generate a base node config JSON, then override specific fields, or pass flags directly when running the `nitro-node` Docker image. ## Key Sequencer timing parameters to adjust | Parameter | Location | Default | How to configure | Why adjust | | ----------------------------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | Delayed sequencer finalize distance | Node (`node.delayed-sequencer`) | Higher (e.g., waits for full parent chain finality) | CLI: `--node.delayed-sequencer.finalize-distance=1` and enable: `--node.delayed-sequencer.enable=true` and disable: `node.delayed-sequencer.use-merge-finality=false` | Lower value for near instant deposits (\~seconds instead of minutes). Trade-off: ⚠️ **DANGER** ⚠️ Risk of reorg on parent chain. | | Delayed sequencer rescan interval | Node (`node.delayed-sequencer`) | `1s` | CLI: `--node.delayed-sequencer.rescan-interval=1s` | How often to rescan for new delayed messages. The parent chain reader's poll-interval is usually the more impactful setting. | | Delayed sequencer filtered-tx retry interval | Node (`node.delayed-sequencer`) | `30s` | CLI: `--node.delayed-sequencer.filtered-tx-full-retry-interval=30s` | How often to do a full re-execution when halted on a filtered delayed message. | | Sequencer Inbox max time variation | Chain deployment (`sequencerInboxMaxTimeVariation`) | `delayBlocks`: 5760, `futureBlocks`: 12, `delaySeconds`: 86400, `futureSeconds`:3600 | In Chain SDK config struct or `chainConfig` JSON during deployment. | Allows sequencer minor timestamp adjustments to avoid re-orgs if batch posting lags. Rarely needs change from defaults. | | [Timeboost](/how-arbitrum-works/timeboost/gentle-introduction.md) non-express delay | Node (`execution.sequencer.timeboost`) | 200ms | In node config: `"execution": { "sequencer": { "timeboost": { "non-express-delay-msec": 300 } } }` | Larger delay for more advantage/revenue for Express Lane winner. Increases latency for regular transactions. | ## Step-by-step configuration process 1. **Deploy your chain first**: using the Chain SDK, this sets immutable params like `sequencerInboxMaxTimeVariation`. 2. **Generate base node config**: Use the Chain SDK: ```typescript import { prepareNodeConfig } from '@arbitrum/orbit-sdk'; const nodeConfig = await prepareNodeConfig({ // Your deployment tx receipt or params // Override defaults here, e.g.: }); // Write nodeConfig to file: nodeConfig.json ``` 3. **Run the Sequencer node**: * Use Docker (recommended): ```shell docker run -v /path/to/nodeConfig.json:/config/nodeConfig.json \ offchainlabs/nitro-node: \ --conf.file=/config/nodeConfig.json \ --node.sequencer=true \ --execution.sequencer.enable=true \ # Add other required flags (parent-chain URL, chain info-json, keys, etc.) ``` Latest version of Nitro You can find the latest recommended version of Nitro on the [Run a Node](/run-arbitrum-node/start-here.md#recommended-nitro-version) page. * or override via CLI flags for testing 4. **Test changes** on a devnet or testnet first—monitor TPS, state growth, posting costs, and deposit latency. For a full list of flags, run `nitro-node --help`. Refer to Arbitrum Docs sections on [Running a Sequencer Node](/run-arbitrum-node/sequencer/run-sequencer-node.md) and [How to configure your Arbitrum chain's node using the Chain SDK](/launch-arbitrum-chain/deploy/configure-node.md) for your exact version. If using Timeboost or advanced features, additional setup (e.g., Auction Contract) is needed. ## Understanding `maxTimeVariation` `maxTimeVariation` is a four-field setting on the Sequencer Inbox contract on the parent chain. It bounds how far a sequenced message's claimed parent-chain block number and timestamp may differ from the parent-chain block in which the batch carrying that message is actually posted. ```solidity struct MaxTimeVariation { uint256 delayBlocks; // max parent-chain blocks in the past a message may be received uint256 futureBlocks; // max parent-chain blocks in the future a message may be received uint256 delaySeconds; // max parent-chain seconds in the past a message may be received uint256 futureSeconds; // max parent-chain seconds in the future a message may be received } ``` The four fields define a **two-sided window** around the current parent-chain block and time: * `delayBlocks` and `delaySeconds` bound the **past** edge—how old a message's claimed block or timestamp may be. * `futureBlocks` and `futureSeconds` bound the **future** edge—how far ahead a message's claimed block or timestamp may be. Blocks and seconds are tracked independently: `delayBlocks` and `futureBlocks` are measured in parent-chain block numbers (`block.number`), while `delaySeconds` and `futureSeconds` are measured in parent-chain seconds (`block.timestamp`). ## How the time window is enforced Understanding what this setting guards requires knowing *where* it is enforced, which is not where most people expect. When a batch is posted, the Sequencer Inbox computes the window from the **current** parent-chain block and time at the moment the batch lands: * Timestamp window: `[block.timestamp - delaySeconds, block.timestamp + futureSeconds]` * Block window: `[block.number - delayBlocks, block.number + futureBlocks]` The contract records these bounds (they are emitted in the `SequencerBatchDelivered` event and folded into the batch's data hash), but it **does not reject** a batch whose messages fall outside them. Enforcement happens later, off the parent chain, when the batch is replayed by the node's state-transition function: each message whose claimed block or timestamp is outside the recorded window is **clamped** to the nearest bound—pulled up to the minimum if it is too far in the past, or pulled down to the maximum if it is too far in the future. Out-of-window messages are clamped, not rejected Because out-of-window messages are silently clamped rather than rejected, a message can be assigned a different parent-chain block or timestamp than the Sequencer originally computed locally. When the locally executed chain and the chain derived from onchain data disagree, the result is a child-chain reorg. `maxTimeVariation` therefore does not guard by blocking bad batches; it defines the window inside which the Sequencer and batch poster must keep every message to avoid such reorgs. ## What each field guards, with examples **Future edge (`futureBlocks` / `futureSeconds`).** These cap how far ahead of the parent chain a message may claim to be. For example, suppose `futureSeconds` is `3600` (one hour) and the batch lands in a parent-chain block whose timestamp is `T`. Any message in that batch claiming a timestamp later than `T + 3600` is clamped down to `T + 3600`. If `futureBlocks` is small—say `48` on a parent chain with two-second blocks, roughly 96 seconds of headroom—then a batch that is delayed, or that contains messages sequenced slightly ahead of the parent chain, can easily reach the future edge and have its messages clamped down. Raising `futureBlocks` widens this headroom. **Past edge (`delayBlocks` / `delaySeconds`).** These cap how old a message may be. A message claiming a timestamp earlier than `block.timestamp - delaySeconds` is clamped up to that minimum. For example, with `delaySeconds` set to `345600` (four days), a message may be up to four days older than the parent-chain block that carries it before it is clamped forward. **`delayBlocks` also sets the force-inclusion wait.** The same `delayBlocks` value determines how long a message submitted through the Delayed Inbox must wait before it can be force-included, bypassing the Sequencer. `forceInclusion` reverts with `ForceIncludeBlockTooSoon` until `delayBlocks` parent-chain blocks have elapsed since the message was submitted. Raising `delayBlocks` therefore lengthens the Sequencer's exclusive window and delays when users can force their transactions in. The optional delay-buffer feature can shorten this window under sustained delay, but never lengthen it. For more on force inclusion and the Delayed Inbox, see the [Sequencer deep dive](/how-arbitrum-works/deep-dives/sequencer.md). ## Default values The Arbitrum Chain SDK does not use fixed numbers for these fields. It derives them from the parent chain's block time, holding the *time* windows constant and converting them to block counts: * `delaySeconds` is `345600` (four days) and `futureSeconds` is `3600` (one hour), constant for all parent chains. * `delayBlocks` is `delaySeconds / parentBlockTime` and `futureBlocks` is `futureSeconds / parentBlockTime`. `parentBlockTime` is two seconds when the parent chain is Base or Base Sepolia (whose `block.number` advances every two seconds), and 12 seconds for every other parent chain, including Ethereum and Arbitrum. The SDK selects the two-second value only for those two chain IDs, so a different OP-stack parent that the SDK does not recognize falls back to the 12-second default. This yields different block defaults depending on where your chain settles: | Parent chain | Parent block time | `delayBlocks` | `futureBlocks` | `delaySeconds` | `futureSeconds` | | ------------------- | ----------------- | ------------- | -------------- | --------------- | --------------- | | Base / Base Sepolia | 2s | 172800 | 1800 | 345600 (4 days) | 3600 (1 hour) | | All other parents | 12s | 28800 | 300 | 345600 (4 days) | 3600 (1 hour) | Why the default futureBlocks can be 300 or 1800 A default `futureBlocks` can appear to be either 300 or 1800: 1800 is the default for a Base or Base Sepolia parent (`3600 / 2`), while 300 is the default for every other parent, including Ethereum and Arbitrum (`3600 / 12`). A much smaller value such as 48 is not a current SDK default—it is a manual override or a value from an older deployment. Restoring `futureBlocks` to the parent-appropriate default widens the future headroom (on a two-second parent, from roughly 96 seconds to one hour), reducing how often messages near the future edge are clamped. ## Relationship with the batch poster: `reorg-resistance-margin` `maxTimeVariation` defines the window; the batch poster is responsible for keeping every message it posts safely inside it. Two node-level settings do this self-policing, one per edge: * **Future (max) edge** — the batch poster stops adding messages that would exceed `block.number + futureBlocks` or `block.timestamp + futureSeconds`. Which parent-chain header it measures against is selected by `--node.batch-poster.l1-block-bound`, which must not be set to `ignore` for the past-edge check below to run. * **Past (min) edge** — governed by `--node.batch-poster.reorg-resistance-margin`. `--node.batch-poster.reorg-resistance-margin` (a duration, default `10m0s`) tells the batch poster: do not post a batch if its oldest message is within this margin of the window's past edge (`block.timestamp - delaySeconds` or `block.number - delayBlocks`). If the oldest non-delayed message is closer to the minimum bound than the margin allows, the poster halts rather than posting. ### Why the margin exists Message timestamps and block numbers are assigned by the Sequencer at sequencing time, but the onchain minimum bounds are computed from the parent-chain block and time at the **later** moment the batch actually lands. Between those two moments the parent chain advances, and it can also reorg. If a batch's oldest message is sitting right at the past edge, either a delayed landing or a parent-chain reorg can move the minimum bound *past* that message. The state-transition function then clamps the message up to the new minimum, silently changing the child-chain block or timestamp the Sequencer's own node had already produced, which is a child-chain reorg. The default 10-minute margin keeps messages far enough from the past edge that ordinary parent-chain reorgs cannot push them out of the window. ### The `reorg-resistance-margin=0` bypass and its risk profile Setting `--node.batch-poster.reorg-resistance-margin=0` disables the past-edge check entirely. The batch poster will then post batches whose oldest messages sit arbitrarily close to the window's past edge. reorg-resistance-margin=0 removes reorg protection With `reorg-resistance-margin=0`, a parent-chain reorg — or simply a batch landing later than expected — can move the minimum bound past messages that were near the edge. Those messages are then clamped forward by the state-transition function, diverging the onchain-derived chain from the chain the Sequencer executed locally: a child-chain reorg. Use `0` only in controlled situations, such as draining a backlog where you accept the reorg risk; it is not a safe steady-state setting. Note the interaction with `maxTimeVariation`: a wider past window (`delayBlocks` and `delaySeconds`) gives the batch poster more room before the margin is threatened, while a narrower window makes the margin bite sooner. See the [CLI flags reference](/run-arbitrum-node/nitro/cli-flags-reference.md) for the full batch-poster flag set. ## Changing `maxTimeVariation` `maxTimeVariation` is set at deployment through the Chain SDK, and can be changed afterward by the chain owner by calling `setMaxTimeVariation` on the Sequencer Inbox contract on the parent chain. Keep two effects in mind when changing it on a live chain: * Raising `delayBlocks` lengthens the Sequencer's exclusive window and delays force inclusion. Very large values weaken the chain's censorship-resistance guarantee, because users must wait longer to force transactions in. * Raising `futureBlocks` and `futureSeconds` widens the future headroom, reducing clamping of messages sequenced ahead of the parent chain; it does not affect force inclusion. Test any change on a testnet first, and confirm the batch poster's `l1-block-bound` and `reorg-resistance-margin` settings remain consistent with the new window. --- > For a complete page index, fetch # Timeboost for Arbitrum chains > **INFO** — PUBLIC PREVIEW DOCUMENT > > This document is currently in **public preview** and may change significantly as feedback is captured from readers like you. Click the **Request an update** button at the top of this document or [join the Arbitrum Discord](https://discord.gg/arbitrum) to share your feedback. Info AEP fees will apply here in the future. ## Launch status and key dates * **Status:** Alpha—not formally supported yet for deployments on Arbitrum chains * **Arbitrum Sepolia**: Feb 12, 2025 * **Arbitrum One**: April 17, 2025 * **Arbitrum Nova**: April 17, 2025 ## tldr; Arbitrum Timeboost is a novel transaction ordering policy that can be optionally deployed and enabled for any Arbitrum chain. Timeboost allows a chain owner to capture some of the available Maximum Extractable Value (MEV) on their blockchain and reduces latency-related spamming, in exchange for a small impact on user response times (despite block times not changing). It is therefore recommended that chains only consider deploying and enabling Timeboost if there is substantial DeFi and related MEV activity (e.g., liquidations, arbitrage, backrunning) on the chain. Please read the [gentle introduction to Timeboost](/how-arbitrum-works/timeboost/gentle-introduction.md) to learn more about how Timeboost works. As with all features on the Arbitrum stack, Arbitrum chains can adopt Timeboost at their own discretion and on their own timeline. To deploy and enable Arbitrum Timeboost on your chain, please refer to this guide on how to [deploy and configure Timeboost](#enabling-timeboost-for-your-arbitrum-chain). ## Recommended adoption path It is recommended that most Arbitrum chains **do not** deploy Arbitrum Timeboost, as the benefits do not outweigh the trade-offs in most cases. The primary reason behind this recommendation is based on cost and user experience considerations. The only instances in which Timeboost might make sense are for chains with significant DeFi and related MEV activity, as the potential revenue from bids may outweigh the costs and possible impact on user experience. Again, Arbitrum chains can adopt Timeboost at their discretion and on their timeline, so this is only a recommendation. ## Benefits of adopting Timeboost Timeboost enables a chain owner to capture a portion of the available MEV on their blockchain, reducing latency-related spam while preserving the built-in protections and UX benefits that Arbitrum users have come to know and enjoy. A more in-depth overview of the benefits of Timeboost is explored in the [gentle introduction to Timeboost](/how-arbitrum-works/timeboost/gentle-introduction.md), and also a paper on how Timeboost is more profitable for arbitrageurs compared to other forms of MEV capture, like Priority Gas Auctions (PGA) [here](https://arxiv.org/abs/2410.10797). ### Fair(er) MEV capture Timeboost provides sophisticated actors (e.g., searchers) with the ability to purchase a fixed time advantage over a specified number of blocks to perform various MEV activities, such as backrunning, liquidations, or arbitrage. This design preserves the use of a private mempool that all Arbitrum chains have by default, and it does not impact block times. This approach protects users from harmful types of MEV (e.g., frontrunning) while maintaining the same block times. ### Potential revenue capture for chain owners The auction, run at a fixed cadence, is held offchain. Bids can be made in any asset the chain owner designates, including custom **ERC-20** tokens. Furthermore, the chain owner has full discretion over how to use the bid proceeds. For example, chain owners may decide to burn the bid proceeds or use the bid proceeds to support the chain in other ways. It stands to reason that sophisticated actors (e.g., searchers) will bid up to the amount they believe they can profit or realize from the time advantage. Therefore, at equilibrium, one could reasonably expect that the bid proceeds will approach or equal the amount of available MEV on a Timeboost-enabled chain. ### Potential reduction in latency-based spam As explained earlier, searchers will be incentivized to bid onchain for the time advantage, rather than spending money on offchain hardware to win latency races. Therefore, at equilibrium, one could reasonably expect that the amount of latency-based spam should reduce on a Timeboost-enabled chain. To learn more about this phenomenon, please check out this analysis on how the Timeboost ordering policy impacts backrunning strategies: [TimeBoost and Backrunning: Probabilistic Strategies](https://research.arbitrum.io/t/timeboost-and-backrunning-probabilistic-strategies/9727). ## Trade offs with adopting Timeboost As mentioned earlier, chain owners should consider several trade-offs when deciding whether to adopt Timeboost. We will cover two of them below. ### Cost As explained in the guide on [how to deploy and configure Timeboost](#enabling-timeboost-for-your-arbitrum-chain), there are are three core components to Timeboost: * An offchain auctioneer (responsible for receiving and validating bids, and resolving Express Lane auctions), * An onchain smart contract (to manage the Express Lane auction), and * A new configuration on the [Sequencer](/how-arbitrum-works/deep-dives/sequencer.md) is implemented to take advantage of the Express Lane time. Often times, the cost required to set up, configure, and maintain the infrastructure above (especially the auctioneer) will exceed that of the potential revenue that Timeboost brings in. Most importantly, the revenue that Timeboost can generate for the chain is *not guaranteed* either. ### User experience As explained in the [gentle introduction to Timeboost](/how-arbitrum-works/timeboost/gentle-introduction.md), the Timeboost Express Lane time advantage is implemented by imposing an artificial delay (default: `200ms`) on all non-express lane transactions whenever there is an Express Lane Controller for a round (default: `1 minute`). While Timeboost does not change the default Arbitrum blocktimes (default: `250ms`), this artificial delay *does* mean that the average response time for a user in the non-express lane is the sum of the artificial delay and the block time of the chain. Note that this artificial delay is only applied when there is an express lane controller for a round, meaning there is no change in user experience if nobody is using Timeboost, even though it is enabled (for the duration of that round). ## Enabling Timeboost for your Arbitrum chain This guide walks you through the process of enabling Timeboost for your Arbitrum chain. For a conceptual introduction to Timeboost, see the [Timeboost Introduction](https://docs.arbitrum.io/how-arbitrum-works/timeboost/gentle-introduction). ### Prerequisites Before starting, ensure you have: 1. An **ERC-20** token address to use as the bid token 2. A Redis server for auctioneer coordination 3. A server to run the auctioneer service 4. A proxy admin contract address ### Overview Enabling Timeboost requires completing these three steps: 1. Deploy the `ExpressLaneAuction` contract 2. Run Auctioneer Services (bid validator and auction server) 3. Configure your sequencer node to support Timeboost ### Step 1: Deploy the `ExpressLaneAuction` contract First, clone the `chain-actions` repository: ```shell git clone https://github.com/OffchainLabs/chain-actions.git cd chain-actions/scripts/foundry/timeboost ``` Create and edit the environment configuration file: ```shell cp .env.sample .env ``` Configure the following parameters in your `.env` file: ```shell ## Configuration for DeployExpressLaneAuction.s.sol PROXY_ADMIN_ADDRESS= # Your proxy admin contract address AUCTIONEER_ADDRESS= # Address that will send resolve auction requests BIDDING_TOKEN_ADDRESS= # Your ERC20 bid token address BENEFICIARY_ADDRESS= # Address to receive bid proceeds AUCTIONEER_ADMIN_ADDRESS= # Admin address for the auctioneer MIN_RESERVE_PRICE_SETTER_ADDRESS= # Address allowed to set minimum reserve price RESERVE_PRICE_SETTER_ADDRESS= # Address allowed to set reserve price RESERVE_PRICE_SETTER_ADMIN_ADDRESS= # Admin for reserve price setter BENEFICIARY_SETTER_ADDRESS= # Address allowed to change beneficiary ROUND_TIMING_SETTER_ADDRESS= # Address allowed to adjust round timing MASTER_ADMIN_ADDRESS= # Master admin address MIN_RESERVE_PRICE=0 # Minimum price for bids (0 recommended for testing) # Round timing configuration (in seconds) ROUND_DURATION_SECONDS=60 # Total duration of each round AUCTION_CLOSING_SECONDS=15 # Time before round end when new bids are closed RESERVE_SUBMISSION_SECONDS=15 # Time allocated for reserve price submission ``` Deploy the contract: ```shell forge script --sender $DEPLOYER --rpc-url $CHILD_CHAIN_RPC --slow ./DeployExpressLaneAuction.s.sol -vvv --verify --broadcast # Use --account XXX / --private-key XXX / --interactive / --ledger to specify the transaction signer ``` Verify successful deployment by checking that the contract returns your configured bid token: ```shell cast call --rpc-url= "biddingToken()(address)" ``` Example output: ```shell 0xYourBidTokenAddress ``` ### Step 2: Run auctioneer services There are two distinct services to run: the bid validator and the auction server. The bid validator verifies submitted bids, while the auction server sends the winning bid onchain. #### Prerequisites The services require the `autonomous-auctioneer` binary, which is included in the Nitro Docker image. Alternatively, you can build it locally by following the [Build Nitro Locally](https://docs.arbitrum.io/run-arbitrum-node/nitro/build-nitro-locally) guide. To build only the `autonomous-auctioneer` component during the local build process: ```shell make target/bin/autonomous-auctioneer ``` #### Running bid validator service Start the bid validator with: ```shell ./autonomous-auctioneer \ --bid-validator.auction-contract-address= \ --bid-validator.rpc-endpoint= \ --auctioneer-server.enable=false \ --bid-validator.redis-url= \ --http.addr=0.0.0.0 \ --http.port= ``` #### Running auction server service Start the auction server with: ```shell ./autonomous-auctioneer \ --auctioneer-server.auction-contract-address= \ --auctioneer-server.db-directory= \ --auctioneer-server.redis-url= \ --auctioneer-server.use-redis-coordinator=false \ --auctioneer-server.sequencer-endpoint= \ --auctioneer-server.wallet.private-key= \ --bid-validator.enable=false ``` ### Step 3: Configure your sequencer node for Timeboost Update your sequencer node configuration to enable Timeboost functionality. Add the following new config to your sequencer's node configuration file: ```json { "http": { "api": [ // existing APIs "auctioneer", "timeboost" ] }, "ws": { "api": [ // existing APIs "auctioneer", "timeboost" ] }, "execution": { "sequencer": { "timeboost": { "enable": true, "auction-contract-address": "", "auctioneer-address": "", "redis-url": "" } } } } ``` ### Verifying your Timeboost setup There are multiple ways to confirm that Timeboost is correctly enabled on your chain: #### Periodic startup logs When you start your sequencer with Timeboost enabled, you'll see periodic logs indicating the start of new express lane auction rounds: ```shell New express lane auction round ``` This log indicates that the Timeboost mechanism is active and running normally. #### Transaction processing confirmation After finishing a bid request, look for messages in your sequencer logs such as: ```shell AuctionResolved: New express lane controller assigned round ``` This message confirms that your sequencer is processing express queue transactions from the express lane controller, and that Timeboost is functioning correctly. #### User interaction verification Users can interact with Timeboost by submitting bids through the `auctioneer_submitBid` endpoint of your auctioneer service. For detailed instructions on how users can interact with Timeboost, see [How to Use Timeboost](https://docs.arbitrum.io/how-arbitrum-works/timeboost/how-to-use-timeboost). A successful bid submission will trigger the auction resolution process and generate the corresponding logs mentioned above. ### Configuring Timeboost's Parameters Below is a table of the configurable parameters and how to think about adjusting their values, should you choose to deploy and enable Timeboost for your chain. | Parameter name | Description | Considerations | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `roundDurationSeconds` | Time during which the sequencer will honor the express lane privileges for transactions signed by the current round’s express lane controller. **Default**: 60 seconds | The larger this value, the more powerful and valuable controlling the express lane will be. Higher values may incentivize more participation and demand for the express lane time advantage. | | `auctionClosingSeconds` | Time before the start of the next round. The Autonomous Auctioneer will not accept bids during this time interval. **Default**: 15 seconds | The larger the value, the more difficult it is for participants to predict the value of the future express lane round, and therefore, their ability and confidence in being able to place an accurate/fair bid for the time advantage. However, a value that is too small may not allow the auctioneer sufficient time to resolve the auction on time during periods of high activity. | | `beneficiary` | An address where proceeds from the Timeboost auction are sent to when `flushBeneficiaryBalance()` gets called on the Auction Contract. **Default**: An address controlled by the chain's owner | Can be any address, including the burn address, that the chain owner designates or controls. | | `_biddingToken` | Address of the token used to make bids in the Timeboost auction. It can be any **ERC-20** token, including the gas token of the network, provided the chosen token address does not have fee-on-transfer, rebasing, transfer hooks, or otherwise non-standard **ERC-20** logic. **Default**: **WETH** | This asset should ideally be liquid and easily obtainable for your auction participants. Furthermore, the less volatile this asset is, the more consistent your auction will be. | | `nonExpressDelayMsec` | The artificial delay applied, by the sequencer, to the arrival timestamp on non-express lane transactions *before* the non-express lane transactions eventually get sequenced. **Default**: 0.2 seconds, or 200 milliseconds | The larger this value, the greater the time advantage will be for the express lane controller, making it easier for the express lane controller to capture MEV opportunities (e.g., backrunning, liquidations, or arbitrage) and therefore the more valuable. However, increasing this value comes at the expense of slower response times for user transactions in the non-express lane. | | `reservePrice` | The minimum bid amount accepted by the auction contract for Timeboost auctions, denominated in `_biddingToken`. **Default**: None | This value should be left empty and only be changed to raise the minimum bid post-deployment of the auction contract (see below). | | `_minReservePrice` | A value that must be equal to or below the `reservePrice` to act as a "floor price" for Timeboost bids. Enforced by the auction contract. **Default**: 0.001 **WETH** | A value that is low enough to make the auction worth while participating in, but not high enough to pose a significant barrier to entry (i.e., ideally, chain owners will want a fair, competitive market for the timeboost time advantage). This value can also be set high such that if someone does bid, the bid proceeds could offset some of the costs spent on running the autonomous auctioneer. | --- > For a complete page index, fetch # Configure assertion control ## Validator action minimum frequency ### Validator node creation frequency: Interval between validator assertions posting The validator assertion posting frequency is controlled by the `--node.staker.make-assertion-interval` (legacy) or `--node.bold.assertion-posting-interval` (BoLD) parameter in the Nitro node's configuration. This parameter sets the minimum time to wait since the last assertion before posting a new one (if the validator configuration is to make new assertions via the `MakeNodes` strategy). The default value is one hour (3600 seconds) for legacy and fifteen minutes (900 seconds) for BoLD. This interval must always exceed the Rollup contract's `minimumAssertionPeriod`, which defaults to 75 L1 blocks (approximately 15 minutes at 12-second block times). * **Configuration Options for legacy**: Configure this in the `node.staker` section, e.g., `"make-assertion-interval": "45m"` for a 45-minute interval. * **Configuration Options for BoLD**: Configure this in the `node.bold` section, e.g., `"assertion-posting-interval": "45m"` for a 45-minute interval. * **Prevention of Issues**: Frequent assertions ensure the chain's state is regularly committed and verifiable on the parent chain, preventing reordering by allowing disputes over any manipulated transaction order. For reorganizations, assertions serve as checkpoints; if a reorganization affects prior batches, validators can challenge and resolve the state to the correct one, thereby maintaining integrity. Infrequent assertions could widen the dispute window vulnerability, but the default balances this with efficiency. * **Recommended Settings**: Set to 20-30 minutes for chains needing low-latency finality, but ensure it's >15 minutes to comply with the minimum period. Higher values (e.g., two hours) are suitable for low-stakes chains to reduce validator operational costs. * **Note**: Validators require staking (bonding **ETH** or tokens on the parent chain) to post assertions, with the minimum bond set via the Rollup contract during chain deployment. If an incorrect assertion is detected, the interval is bypassed, and a new challenge begins. No new assertions get posted if no new batches arrive or force inclusions occur. ### Validator node confirmation frequency: Interval between validator assertions confirming The validator assertion confirming frequency is controlled by the --node.staker.staker-interval (legacy) or `--node.bold.assertion-confirming-interval` (BoLD) parameter in the Nitro node's configuration. This parameter sets the minimum time to wait since the last assertion before confirming a new one (if the validator configuration is to confirm assertions via the `ResolveNodes` strategy). The default value is one minute (1m0s) for both legacy and BoLD. * **Configuration Options for legacy**: Configure this in the `node.staker` section, e.g., `"staker-interval": "45m"` for a 45-minute interval. * **Configuration Options for bold**: Configure this in the `node.bold` section, e.g., `"assertion-confirming-interval": "45m"` for a 45-minute interval. * **Recommended Settings**: Using the default setting will be fine. * **Note**: Validators require staking (bonding **ETH** or tokens on the parent chain) to confirm assertions. ## Reorganization prevention: ### Reorg resistance margin Reorg prevention interacts with batch posting through `maxTimeVariation` and `reorg-resistance-margin`. See the [Configure Sequencer timing adjustments](/launch-arbitrum-chain/chain-config/sequencer/sequencer-timing-adjustments.md) page for the full mechanism. * **Detailed Explanation of the Message**: The message "Disabling batch posting due to batch being within reorg resistance margin from Layer 1 minimum block or timestamp bounds" signifies that the batch poster's logic has detected the proposed batch's minimum required L1 block or timestamp falls within the configured margin of the current L1 chain head. For example, if the margin is ten minutes and the L1 head timestamp is `T`, the poster disables itself if the batch requires a minimum timestamp greater than (`T - 10m`) or a minimum block that is too close to the head. This message serves as a protective mechanism: L1 chains, such as Ethereum, can undergo short reorganizations (e.g., uncle blocks or brief forks), which could invalidate a posted batch if it references a block/timestamp that gets reorganized. By temporarily disabling posting, the system waits for L1 to stabilize, ensuring the batch remains valid once posted. The disablement is transient; posting resumes once the margin clears (e.g., as L1 advances). This message often appears in logs during periods of high L1 volatility or when the sequencer's clock/L1 sync is slightly off. It's normal in such cases, but it may indicate setup issues if it persists. * **Guidance on Configuration**: Set this in the `node.batch-poster` config section, e.g., `"reorg-resistance-margin": "5m"`. Lower values allow for faster posting but increase the risk; higher values enhance safety. Monitor logs for frequent disablements and adjust accordingly based on the parent chain stability (e.g., Ethereum mainnet vs. testnets). * **Recommended Settings**: The default of ten minutes is suitable for most Arbitrum chains settling to Arbitrum One/Nova, as Ethereum reorgs rarely exceed a few blocks (1-2 minutes). For chains on more volatile bases (e.g., alt L1s), increase the time to 15-20 minutes. Test on devnets to observe behavior. * **Trade-offs Between Consistency and Latency**: A higher margin prioritizes consistency by reducing the chance of orphaned posted batches due to L1 reorgs, which could force manual intervention or state rollbacks—critical for high-value chains. However, it increases latency, as batches may wait longer to post, delaying confirmation times (e.g., from seconds to minutes). A lower margin reduces latency for a better user experience (faster transaction finality) but risks more frequent disablements or invalid postings, potentially leading to temporary chain halts or increased operational overhead. Balance based on chain use case: low margin for gaming/dex chains that require speed; high margin for DeFi/financial apps that emphasize reliability. ### Sequencer max time variation The sequencer's max time variation is configurable via the `setMaxTimeVariation` method of the Sequencer Inbox Contract, which will also be set to prevent chain reorganization when there are no batches for an extended period. `MaxTimeVariation` struct: ```solidity struct MaxTimeVariation { uint256 delayBlocks; uint256 futureBlocks; uint256 delaySeconds; uint256 futureSeconds; } ``` The `MaxTimeVariation` struct defines time boundaries that determine whether your chain remains safe or triggers a reorganization (reorg) when posting batches. It uses both block-based and time-based limits to create acceptable windows for message processing between L1 and L2. #### How the safety check works When your chain attempts to send a batch, it performs a critical timing validation by comparing the first transaction timestamp in that batch with the current (batch sending) time against the configured `MaxTimeVariation` limits: * For `MAX` bound violations → Stop adding new messages to the batch * For `MIN` bound violations → Completely prevents batch posting with error: "batch is within reorg resistance margin from Layer 1 minimum block or timestamp bounds" We will explain how the safety check works in more detail in the next section. ### Relationship between `ReorgResistanceMargin` and `MaxTimeVariation` While both `ReorgResistanceMargin` and `MaxTimeVariation` are timing-related security parameters in Arbitrum, they serve complementary but distinct purposes in the batch posting mechanism. #### How they work together `MaxTimeVariation` establishes the fundamental time bounds for message validity, while `ReorgResistanceMargin` adds a safety buffer on top of`MaxTimeVariation`'s minimum bounds: 1. Calculate L1 bounds using `MaxTimeVariation`: ```text l1BoundMaxBlockNumber = latestL1BlockNumber + maxTimeVariation.futureBlocks l1BoundMinBlockNumber = latestL1BlockNumber - maxTimeVariation.delayBlocks l1BoundMaxTimestamp = latestL1Timestamp + maxTimeVariation.futureSeconds l1BoundMinTimestamp = latestL1Timestamp - maxTimeVariation.delaySeconds ``` 2. Add `ReorgResistanceMargin` to L1 minimum bounds: ```text safeBlockThreshold = l1BoundMinBlockNumber + (ReorgResistanceMargin / 12 seconds) safeTimestampThreshold = l1BoundMinTimestamp + (ReorgResistanceMargin / 1 second) ``` 3. Stop adding new messages to the batch if `MAX` bound violations: * If the new message's `timestamp` ≥ `l1BoundMaxTimestamp` or `block number` ≥ `l1BoundMaxBlockNumber`, stop adding new messages to the batch 3. Reject batch if `MIN` bound violations: * If the first message in the batch has a `timestamp` ≤ `safeTimestampThreshold` or `block number` ≤ `safeBlockThreshold`, batch posting is disabled with the error: `"batch is within reorg resistance margin from Layer 1 minimum block or timestamp bounds"` ### Why this relationship matters This layered approach provides defense in depth: * `MaxTimeVariation` ensures message timestamps stay within acceptable bounds for normal operation * `ReorgResistanceMargin` prevents batch posting when approaching those bounds, reducing the risk that an L1 reorganization could make a recently posted batch invalid --- > For a complete page index, fetch # BoLD for Arbitrum chains > **INFO** — PUBLIC PREVIEW DOCUMENT > > This document is currently in **public preview** and may change significantly as feedback is captured from readers like you. Click the **Request an update** button at the top of this document or [join the Arbitrum Discord](https://discord.gg/arbitrum) to share your feedback. ## Launch status and key dates * **Status:** Generally available for all Arbitrum chains, including both L2s and L3s. Already live on Arbitrum One and Arbitrum Nova. * **Arbitrum Sepolia** Dec 11, 2024 * **Arbitrum One** 09:00 ET (GMT-5) on Feb 12, 2025 * **Arbitrum Nova** 09:00 ET (GMT-5) on Feb 12, 2025 ### tldr; Arbitrum BoLD is an upgrade to the dispute protocol on Arbitrum chains that delivers both permissionless validation and core security benefits. As with all features on the Arbitrum stack, Arbitrum chains can adopt BoLD at their own discretion and on their own timeline. To upgrade to BoLD, it is required to upgrade both the Nitro node software and the Rollup's smart contracts on its parent chain. ### Recommended Adoption Path BoLD brings new security benefits to Arbitrum chains, regardless of whether their validators are permissioned or permissionless. These new security benefits include improved resistance to delay attacks and increased censorship resistance for `L3s`. We strongly recommend Arbitrum chains adopt Arbitrum BoLD to register these security benefits while **keeping validation permissioned**. > **WARNING** > > It is strongly recommended that existing and prospective Arbitrum chains upgrade to use Arbitrum BoLD but **keep validation permissioned** because of the increased risks associated with allowing any entity to advance and challenge the state of your chain. The risks are summarized below. Rigorous testing and research has been poured into the parameters chosen for Arbitrum One and so we cannot formally support or endorse use of permissionless Arbitrum BoLD in other configurations. Please note that when a chain upgrades to use Arbitrum BoLD, any withdrawals (to the parent chain) will be delayed by an additional Challenge Period. The default challenge period is 6.4 days. We recommend informing the Offchain Labs teams of your timelines and intent to upgrade so adequate warning can be provided to your chain's users on the [Arbitrum Bridge](https://bridge.arbitrum.io/). Below is a quick breakdown of the benefits of permissioned BoLD vs. permissionless BoLD for your Arbitrum chain: ![AEP scenario 1](/img/orbit-bold-orbit-permissionless-vs-permissioned.png) ### Benefits of adopting Arbitrum BoLD Arbitrum BoLD enables an Arbitrum chain to be permissionlessly validated thanks to several key improvements to the existing dispute protocol. These key improvements benefit an Arbitrum chain even if validator is kept permissioned on a BoLD-enabled Arbitrum chain. Below are some benefits for an Arbitrum chain that come with adopting Arbitrum BoLD—**regardless of whether validation is kept permissioned or not**: #### Improved resistance to delay attacks Disputes on a BoLD-enabled chain are resolved in a round-robin style format where disputes can be concurrently resolved. This is an evolution from the current dispute protocol, where challenges are resolved one-by-one. This evolution means that an upper time bound can be placed on all disputes such that a malicious actor cannot delay the chain indefinitely like they can today. Even when validation is kept permissioned, this upper time bound is critical to mitigating the risk of [delay attacks](https://medium.com/offchainlabs/solutions-to-delay-attacks-on-rollups-434f9d05a07a) by parties on the validator allowlist for an Arbitrum chain. #### Being on the latest version of Arbitrum technology Adopting Arbitrum BoLD for your Arbitrum chain will require upgrading the Nitro node software and deploying a new set of contracts on your parent chain. While not specifically related to Arbitrum BoLD, it is always strongly recommended that Arbitrum chain owners upgrade and keep their chain on the latest stable releases of both Nitro node software and the relevant onchain contracts. This is critical to ensure your Arbitrum chain benefits from the latest security improvements and features that the Offchain Labs team is constantly churning out. #### Secured by interactive fraud-proofs Arbitrum BoLD is not an upgrade to a different type of proving architecture and will continue to be secured with an interactive proving game between validators using fraud proofs. The same single-honest party assumption applies but now with strict improvements to security to the point where chains, like Arbitrum One, can be permissionlessly validated and have their state [assertions](/how-arbitrum-works/deep-dives/assertions.md) be permissionlesly challenged. #### Use of your project's native token as the bonding asset to secure the chain Arbitrum BoLD enables the chain owner to use any **ERC-20** token on the parent chain as the bond for validators to participate in securing the network. By default, this token will be **WETH** for Arbitrum One and we do not recommend teams to use alternative tokens as the bonding asset. For more information on the rationale, we recommend teams consult our documentation to understand [why **WETH** was selected for Arbitrum One](/how-arbitrum-works/bold/gentle-introduction.md#q-why-is-arb-not-the-bonding-token-used-for-bold-on-arbitrum-one) (and not **ARB**). #### Increased censorship resistance for `L3` Arbitrum chains Today, the force inclusion window is a fixed 24 hours. This force inclusion window exists to enable both users and validators to force-include their transactions and assertions on the parent chain, with a 24-hour delay, if the [sequencer](/how-arbitrum-works/deep-dives/sequencer.md) is offline or censoring transactions. Arbitrum BoLD's release will come with an optional *Censorship Timeout* feature that can automatically reduce the force inclusion time window if the parent chain or sequencer is maliciously censoring user transactions/assertions or the Sequencer goes offline. This massively benefits Arbitrum `L3` chains (that settle to a BoLD-enabled parent chain) as it ensures the chain can advance with minimal UX degradation during periods of censorship. You can read more about how this feature works in the [gentle introduction to BoLD](/how-arbitrum-works/bold/gentle-introduction.md#q-how-do-bold-based-l3s-challenge-periods-operate-considering-the-worst-case-scenario). By default, the Censorship Timeout feature is disabled because proper configuration depends on a variety of factors, including the batch posting frequency of the chain and which parent chain(s) an Arbitrum chain settles to. For example, both the maximum delay buffer and the threshold itself should both be higher than your chain's batch posting frequency, otherwise the Censorship Timeout feature will not work as. ### Caveats that come with adopting Arbitrum BoLD for permissionless validation Arbitrum BoLD's implementation and specification have been thoroughly tested and audited. The upgrade to Arbitrum BoLD is not the subject of this section, but rather the caveats and nuances that come with whether to enable permissionless validation. > **WARNING** > > It is strongly recommended that existing and prospective Arbitrum chains upgrade to use Arbitrum BoLD but **keep validation permissioned** because of the increased risks associated with allowing any entity to advance and challenge the state of your chain. The risks are summarized below. Rigorous testing and research has been poured into the parameters chosen for Arbitrum One and so we cannot formally support or endorse use of permissionless Arbitrum BoLD in other configurations. Enabling permissionless validation means that any entity can spin up a validator and open challenges to dispute invalid claims made by other validators on the network. This opens up an Arbitrum chain to the risk of spam and attacks by unknown and malicious entities. To mitigate this risk for Arbitrum One, a considerable amount of research and testing has been done to optimize the trade-offs between deterring attacks and managing the costs of defending Arbitrum for honest parties. This research includes carefully calculating all relevant bond sizes, challenge period durations, and relevant plans for operating the infrastructure. More information on this research can be found in the [BoLD whitepaper](https://arxiv.org/abs/2404.10491). Below are a few examples of various risks that an Arbitrum chain will hold should they pursue permissionless BoLD: #### Risk of resource exhaustion attacks Where malicious entities can acquire and utilize more resources than honest parties can put together during a challenge. Such an attack can take many forms and includes both onchain and offchain computational/infra costs. For example, a well-coordinated attack on an Arbitrum chain could overwhelm honest parties if the malicious actors can spend more gas and computational power and acquire more of the bonding asset than the defenders can. This risk can be mitigated by a combination of high bond sizes, use of a price-independent bonding asset, use of a bonding asset with high liquidity, strong economic guarantees that attackers will lose most of their resources, sufficiently long challenge periods, and robust infrastructure operations and resources that can respond and scale up when necessary. More information on resource exhaustion attacks and how Arbitrum BoLD's design accounts for this risk can be found in [Section 6.1.4 of the BoLD whitepaper](https://arxiv.org/abs/2404.10491). We recommend teams consider a resource exhaustion ratio greater than 5 assuming very high L1 gas costs (like 100 `gwei`/gas). #### Increased infrastructure costs and overhead Related to, and expanding on, the above point about resource exhaustion attacks, the honest parties operating active validators and proposers for a BoLD-enabled chain will need to be ready to vertically scale their infrastructure, and cover the offchain costs of doing so, in the event of an attack. This is because a malicious actor may choose to spam and overwhelm the honest defenders with multiple challenges. Making moves, honest or malicious, costs resources to perform bisections on history committments down to a single step of execution. If this happens, each malicious challenge must be met with an honest counter-challenge during the interactive fraud proof game. Arbitrum chains who decide to adopt Arbitrum BoLD in permissionless mode are strongly encouraged to work with their Rollup-as-a-Service (RaaS) team to: deploy robust monitoring for challenges, set aside a budget to vertically scale up infrastructure and fund counter-challenges, and have an incident response plan drafted and rehearsed to ensure prompt and decisive reactionary steps in the event of an attack. #### Risks to liveness or delays of the chain If the bond sizes are set too low, an adversary can cheaply create a challenge and delay confirmation of an assertion for up to an entire extra challenge period if they can censor honest BoLD moves. Remember that challenges, while time-bound, still take time to complete. Delaying the confirmation of assertions for a chain could negatively impact the chain in many ways that an attacker could benefit from (e.g., profiting from price volatility and price impacts on the Arbitrum chain's token may make delaying the chain worthwhile for an attacker). We recommend teams set bond sizes to be much greater than the opportunity cost of a week of delay, based on your chain's TVL (e.g., if your chain's TVL is $1B, then the opportunity cost of $1B should be used as a *floor* for the block level bond amount size). We further recommend that the bonding token used is highly liquid on the parent chain and relatively non-volatile. ### Conclusion for Arbitrum chains considering BoLD Permissionless Validation Due to the uniquely different tokenomics, sizes, and varying types of Arbitrum chains deployed (or in active development) today, Offchain Labs does not provide a "one-size-fits-all" recommendation for how best to safely set up and enable permissionless validation for Arbitrum chains. Instead, we recommend teams adopt Arbitrum BoLD but keep validation permissioned. Should Arbitrum chain teams strongly desire to adopt Arbitrum BoLD in permissionless mode, we do not endorse using configurations that differ from those on [Arbitrum One](https://github.com/OffchainLabs/nitro-contracts/blob/bold-merge/scripts/files/configs/arb1.ts). We especially do not recommend teams use custom **ERC-20** tokens as the bonding asset and/or with low bond minimums. If your team would like to have permissionless validation for your Arbitrum chain, please reach out to us [via this form](https://docs.google.com/forms/d/e/1FAIpQLSe5YWxFbJ8DgWcDNbIW2YYuTRmegtx2FHObym00_sOt0kq4wA/viewform) so that we can schedule some time to understand your needs better. ### How to adopt Arbitrum BoLD As mentioned earlier, the upgrade to the dispute protocol involves both a Nitro node software upgrade and the deployment/upgrade of new smart contracts on your Arbitrum chain's parent chain. To read more about Arbitrum BoLD, please refer to the [Gentle Introduction for BoLD](/how-arbitrum-works/bold/gentle-introduction.md). > **CAUTION** > > The recommendation is to keep Arbitrum chains validation permissioned by having `disableValidatorWhitelist` be `false` (which is the default) and by having a list of validators on the allowlist via the `validators[]` array. Furthermore, we recommend keeping the [Censorship Timeout](/how-arbitrum-works/deep-dives/sequencer.md#censorship-timeout) feature disabled. > **INFO** — This is not an ArbOS upgrade > > Enabling BoLD in your chain involves updating your chain's Nitro contracts and ensuring that your nodes are running the expected minimum (or higher) version. > > However, enabling BoLD does not require upgrading your [ArbOS](/how-arbitrum-works/deep-dives/arbos.md) version. This how-to provides step-by-step instructions for Arbitrum chain operators who want to enable BoLD on their chain. Familiarity with Nitro, [BoLD](/how-arbitrum-works/bold/gentle-introduction.md), and [chain ownership](/launch-arbitrum-chain/operate/ownership-and-access.md) is expected. ### Overview To enable BoLD in your Arbitrum chain, you'll have to perform these actions: 1. Make sure your nodes are running at least [Nitro v3.5.4](https://github.com/OffchainLabs/nitro/releases/tag/v3.5.4) and enable the required parameters after the upgrade 2. Upgrade your Nitro contracts to [v3.1.0](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v3.1.0) Let's dive into it: ### 1. Upgrade your nodes to Nitro v3.5.4 or higher Before updating the contracts, make sure your nodes are ready for the update. Nitro v3.5.4 introduced compatibility with pre-BoLD and BoLD chains to ensure a smooth upgrade, but Nitro v3.6.2 has many recommended improvements. Nodes will automatically detect whether the chain is running pre-BoLD or BoLD Rollup and challenge contracts and will perform the appropriate calls depending on that check. Most of the parameters used in Nitro before v3.5.4 will stay the same when running a higher version but, depending on the type of node, you'll have to include a few more BoLD-specific parameters after the upgrade: * For validator nodes: add `--node.staker.strategy=` (--node.bold.strategy is deprecated and only available before Nitro v3.8.0) to configure the validator to create and/or confirm assertions in the new Rollup contract (find more information in [How to run a validator](/run-arbitrum-node/more-types/run-validator-node.md#step-1-configure-and-run-your-validator)) * For all other types of node before Nitro v3.6.0: add `--node.bold.enable=true` to enable [watchtower mode](/run-arbitrum-node/run-full-node.md#watchtower-mode) * For all other types of node after Nitro v3.6.0: [watchtower mode](/run-arbitrum-node/run-full-node.md#watchtower-mode) is automatically enabled Additionally, after performing the upgrade, the `--chain.info-json` object also needs to be modified: * Update the new Rollup address in the `rollup.rollup` field * Add the bond token in a new `rollup.stake-token` field ### 2. Upgrade your Nitro contracts to v3.1.0 This section explains how to upgrade your chain contracts to [v3.1.0](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v3.1.0); this guide follows the same steps outlined in the [3.1.0 upgrade guide](https://github.com/OffchainLabs/chain-actions/tree/main/scripts/foundry/contract-upgrades/3.1.0) in the `chain-actions` repo. Note that this process expects your chain to use the canonical contracts. If your chain uses customized contracts, you might need a different updating script/contract than the one available. During the upgrade operation, the following actions will be performed: 1. Upgrade the `Bridge`, `Inbox`, `RollupEventInbox`, `Outbox`, and `SequencerInbox` contracts to v3.1.0 2. Deploy the new v3.1.0 BoLD `ChallengeManager` 3. Migrate the v2 Rollup contract into a new v3.1.0 Rollup contract address 4. Set up the Rollup contract according to the new configuration and use the latest confirmed assertion on the old Rollup as the genesis of the new Rollup #### Step 0: Pre-requisites To effectively upgrade your Nitro contracts using the BoLD upgrade action, your contracts must be in one of these versions: * `Inbox`: v1.1.0 - v2.1.3 inclusive * `Outbox`: any * `SequencerInbox`: v1.2.1 - v2.1.3 inclusive * `Bridge` * eth chain: v1.1.0 - v2.1.3 inclusive * custom-fee token chain: v2.0.0 - v2.1.3 inclusive * `RollupProxy`: v1.1.0 - v2.1.3 inclusive * `RollupAdminLogic`: v2.0.0 - v2.1.3 inclusive * `RollupUserLogic`: v2.0.0 - v2.1.3 inclusive * `ChallengeManager`: v2.0.0 - v2.1.3 inclusive To determine the exact version your contracts use, [follow these instructions](https://github.com/OffchainLabs/chain-actions#check-version-and-upgrade-path). #### Step 1: Clone the `nitro-contracts` repository and build the contracts You'll use a [`BOLDUpgradeAction.sol`](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/BOLDUpgradeAction.sol) contract in the `nitro-contracts` repository to perform the upgrade operation. First clone the `nitro-contracts` repository and checkout the appropriate version: ```shell $ git clone https://github.com/OffchainLabs/nitro-contracts.git $ cd nitro-contracts $ git checkout v3.1.0-scripts ``` Once you have the correct version, install the dependencies and build the contracts: ```shell $ yarn install $ yarn build:all ``` #### Step 2: Configure your chain parameters The BoLD upgrade action contract that performs the upgrade will need to include your chain information as parameters. To set this configuration, copy the [`scripts/files/configs/custom.ts`](https://github.com/OffchainLabs/nitro-contracts/blob/9d0e90ef588f94a9d2ffa4dc22713d91a76f57d4/scripts/files/configs/custom.ts) file and adjust the configuration to match that of your chain. #### Step 3: Prepare and deploy the upgrade contract You'll now prepare the upgrade contract with the parameters you just set and then deploy it in the parent chain of our chain. First, create a `.env` file based on the `.env-sample` file. ```shell $ cp .env-sample .env ``` Ensure you update the `CONFIG_NETWORK_NAME` env variable to match the filename of the configuration file you created in the previous step. ```shell CONFIG_NETWORK_NAME=custom ``` Now, you can run the `prepare` script to deploy the action contract with the specified configuration parameters. Note that: * You can use any account to deploy the contract, so `L1_PRIV_KEY` does not necessarily need to be the chain owner. * The `network` parameter should be your parent chain. You can find the identifiers of these networks in the `hardhat.config.ts`. Note that if your parent chain is an L1, you'll have to configure an additional `INFURA_KEY` env variable for its endpoint. ```shell $ L1_PRIV_KEY=xxx yarn script:bold-prepare --network {mainnet|arb1|nova|base|sepolia|arbSepolia} ... Deployed contracts written to: `scripts/files/sepoliaDeployedContracts.json` Done. ``` Optionally, the script can try to verify the deployed contracts by adding the following parameters: * Set `DISABLE_VERIFICATION` to `false`. * Use the correct key for verifying the contracts on the block explorer depending on your parent chain: `ETHERSCAN_API_KEY | ARBISCAN_API_KEY | NOVA_ARBISCAN_API_KEY | BASESCAN_API_KEY`. ```shell $ L1_PRIV_KEY=xxx DISABLE_VERIFICATION=false ARBISCAN_API_KEY=xxx yarn script:bold-prepare --network {mainnet|arb1|nova|base|sepolia|arbSepolia} ``` > **NOTE** > > You can use any account to deploy the contract, so `L1_PRIV_KEY` does not necessarily need to be the chain owner. #### Step 4: Gather the information of the current state of the chain The next script will gather information about the chain's current state (the last confirmed assertion), allowing you to initialize the new Rollup contract with that confirmed assertion. We recommended stopping all validators at this point to prevent them from confirming new assertions that might block the upgrade in the next step. > **INFO** — Last confirmed assertion > > As mentioned, this script will try to find the last confirmed assertion of your chain. Please note that: > > * If a new assertion is confirmed between Steps 4 and 5, step 5 will revert and Step 4 must be repeated. > * The script looks for the `NodeCreated` event of the last confirmed assertion in the last 100,000 blocks. If the `NodeCreated` event was emitted in an older block, it won't be able to find it. > **NOTE** — Upgrades executed by a multisig or security council > > If Step 5 requires signatures that take hours or days to collect, you do not need to keep validators stopped for that entire period. The payload your signers approve does not encode assertion data, so the lookup this script populates can be refreshed at any point before execution without invalidating collected signatures. To learn how to sequence this, see the [BoLD upgrade playbook](/launch-arbitrum-chain/operate/bold-upgrade-playbook.md). ```shell $ L1_PRIV_KEY=xxx yarn script:bold-populate-lookup --network {mainnet|arb1|nova|base|sepolia|arbSepolia} ... Done. ``` Note that: * You can use any account in this step to call the contract, so `L1_PRIV_KEY` does not necessarily need to be the chain owner's. #### Step 5: Run the upgrade script You are now ready to perform the upgrade. The following script will either perform the upgrade directly or print the upgrade payload depending on the private key used: * If `L1_PRIV_KEY` is the chain owner's, the script will perform the upgrade directly. Note that it will not ask for confirmation before sending the transaction. * If `L1_PRIV_KEY` is NOT the chain owner's, the script will print the upgrade payload. > **WARNING** > > The following script will send the upgrade transaction directly if the `L1_PRIV_KEY` specified is the private key of the chain owner. Please note that the script **does not ask for confirmation**. ```shell $ L1_PRIV_KEY=xxx yarn script:bold-local-execute --network {mainnet|arb1|nova|base|sepolia|arbSepolia} upgrade executor: 0x5FEe78FE9AD96c1d8557C6D6BB22Eb5A61eeD315 execute(...) call to upgrade executor: 0x1cff79cd000000000000000000000000f8199ca3702c09c78b957d4d820311125753c6d2000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a4ebe03a93000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000008a8f0a24d7e58a76fc8f77bb68c7c902b91e182e00000000000000000000000087630025e63a30ecf9ca9d580d9d95922fea6af0000000000000000000000000c32b93e581db6ebc50c08ce381143a259b92f1ed00000000000000000000000000000000000000000000000000000000 ``` #### Step 6: Update your nodes' configurations and restart them As stated at the beginning, you need to add a few parameters to your node configuration for it to support BoLD: * For validator nodes: add `--node.staker.strategy=` (--node.bold.strategy is deprecated and only available before Nitro v3.8.0) to configure the validator to create and/or confirm assertions in the new Rollup contract (find more information in [How to run a validator](/run-arbitrum-node/more-types/run-validator-node.md#step-1-configure-and-run-your-validator)) * For all other types of node before Nitro v3.6.0: add `--node.bold.enable=true` to enable [watchtower mode](/run-arbitrum-node/run-full-node.md#watchtower-mode) * For all other types of node after Nitro v3.6.0: [watchtower mode](/run-arbitrum-node/run-full-node.md#watchtower-mode) is automatically enabled Additionally, the `--chain.info-json` object also needs to be modified: * Update the `rollup.rollup` field to point at the new Rollup contract address * Add a new `rollup.stake-token` field with the address of the bond token contract After updating the configuration, restart your node. > **INFO** — Validator nodes shutdown > > When enabling BoLD on a validator, it will default to read only finalized information from its parent chain. If you run your node before the blocks that contain the upgrade transactions are finalized, the node will stop with the following message: > > ```shell > error initializing staker: could not create assertion chain: no contract code at given address > ``` > > In this case, wait until those blocks finalize, then start your node again. #### Step 7: Monitor the validation process Once the upgrade executes, monitor assertions to ensure they are created and confirmed in the new Rollup contract. Note that the new events emitted are `AssertionCreated` (which should appear every time an assertion is posted, by default this is 15 minutes) and `AssertionConfirmed` (which should only appear after a challenge period has elapsed, by default this is seven days). If assertions don't appear, or your validators fail to start or bond on the new Rollup contract, see [Validator troubleshooting](/launch-arbitrum-chain/operate/validator-troubleshooting.md). ### Table of Arbitrum BoLD parameters The following configuration parameters can be applied when deploying or managing your Arbitrum chain. For an example of a configuration, feel free to reference this [sample configuration](#how-to-adopt-arbitrum-bold) in our guide on how to upgrade your chain to use BoLD. | Parameter | Description | Recommended default | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `excessStakeReceiver` | The address that confiscated bonds will be sent to. Bonds are confiscated from malicious actors who dispute and lose the interactive fraud proof game | The chain owner's address | | `challengeGracePeriodBlocks` | Amount of time to wait before a challenge period formally expire to allow for the chain owner or security council to intervene to ensure correctness of an assertion | 48 hours worth of Ethereum blocks for an L2 Arbitrum chain (settling to Ethereum) or an L3 Arbitrum chain settling to Arbitrum One or Arbitrum Nova. If the chain settles to a different type of parent chain, you must use its parent chain's `block.number` timing | | `confirmPeriodBlocks` | The amount of time that an assertion must exist on the parent chain before it can be confirmed by the protocol, measured using the parent chain's `block.number` timing | 7 days worth of Ethereum blocks for an L2 Arbitrum chain (settling to Ethereum) or an L3 Arbitrum chain settling to Arbitrum One or Arbitrum Nova. If the chain settles to a different type of parent chain, you must use its parent chain's `block.number` timing | | `challengePeriodBlocks` | The amount of time that a layer zero edge (otherwise known as a root/refinement node) needs to have accumulated to be confirmed. Usually the same as the `confirmPeriodBlocks`, measured in the number of parent chain `block.number` timing | 7 days worth of Ethereum blocks for an L2 Arbitrum chain (settling to Ethereum) or an L3 Arbitrum chain settling to Arbitrum One or Arbitrum Nova. If the chain settles to a different type of parent chain, you must use its parent chain's `block.number` timing | | `stakeToken` | Address for the token, on the parent chain, to be used by a validator to become an assertion proposer | **WETH** | | `stakeAmt` | The minimum amount of the `stakeToken` required for a validator to become an assertion proposer. Please consult the [Economics of Disputes](/how-arbitrum-works/bold/bold-economics-of-disputes.md) page to learn more about how to think about setting this value for your chain in a permissionless setting (not recommended) | 1 **WETH** | | `miniStakeAmounts` | An array of the required amounts of the `stakeToken` for each level of the interactive dispute game. There are three levels to BoLD where participants must dispute assertions down until there is only a single step of execution to prove on Ethereum (to determine a winner) | `[0, 1, 1]` **WETH** | | `chainId` | Your chain's ID | Your chain's ID | | `minimumAssertionPeriod` | The minimum amount of time that a validator must wait before posting a new assertion, measured using the parent chain's `block.number` timing | 15 minutes worth of Ethereum blocks for an L2 Arbitrum chain (settling to Ethereum) or an L3 Arbitrum chain settling to Arbitrum One or Arbitrum Nova. If the chain settles to a different type of parent chain, you must use its parent chain's blocks in the calculation | | `validatorAfkBlocks` | The validator whitelist is removed if this amount of time elapses and no assertions are confirmed by the protocol on the parent chain. This parameter is ignored if the `disableValidatorWhitelist` is true, indicating that there is no whitelist | 28 days (4 weeks) worth of Ethereum blocks for an L2 Arbitrum chain (settling to Ethereum) or an L3 Arbitrum chain settling to Arbitrum One or Arbitrum Nova. If the chain settles to a different type of parent chain, you must use its parent chain's blocks in the calculation | | `disableValidatorWhitelist` | Enables or disables the validator whitelist - effectively toggling between permissioned and permissionless BoLD. It is highly recommended that this value be set to `false`. More information can be found in the [BoLD adoption for Arbitrum chains](/launch-arbitrum-chain/chain-config/validation/bold.md) guide. | `false` | | `blockLeafSize` | Maximum number of blocks between assertions. It is not recommended to change this value. | 2^26 | | `bigStepLeafSize` | Maximum number of steps in the "big step" level history committment. The product of `bigStepLeafSize`, `smallStepLeafSize`, and `numBigStepLevel` should equal to the maximum number of WAVM opcodes theoretically possible in the execution of an Arbitrum block, with a small buffer. It is not recommended to change this value. | 2^19 | | `smallStepLeafSize` | Maximum number of steps in the "small step" level history committment. The product of `bigStepLeafSize`, `smallStepLeafSize`, and `numBigStepLevel` should equal to the maximum number of WAVM opcodes theoretically possible in the execution of an Arbitrum block, with a small buffer. It is not recommended to change this value. | 2^23 | | `numBigStepLevel` | Number of "big step" levels. It is not recommended to change this value. | 1 | | `maxDataSize` | Maximum size of data that can be posted onto the parent chain, in KB | `117964` for L2s, and `104857` for L3s | | `isDelayBufferable` | A parameter to enable or disable the delay buffer, otherwise known as the [Censorship Timeout](/how-arbitrum-works/deep-dives/sequencer.md#censorship-timeout) feature. | `false` | | `bufferConfig.max` | The maximum amount of time that the delay buffer can be. More information on how the delay buffer value changes over time and how it is used to calculate the force inclusion window can be found [here](/how-arbitrum-works/deep-dives/sequencer.md#censorship-timeout). | `2^32`—note that this configuration value is measured using Ethereum blocks for an L2 Arbitrum chain (settling to Ethereum) or an L3 Arbitrum chain settling to Arbitrum One or Arbitrum Nova. If the chain settles to a different type of parent chain, you must use its parent chain's `block.number` timing. It is recommended that you set this value to be higher than the batch posting frequency of your chain and ideally higher than the `bufferConfig.threshold` of your chain. | | `bufferConfig.threshold` | The minimum amount of time that the force inclusion window can be reduced to, in the case of prolonged sequencer censorship and/or unexpected sequencer outages. The `delayBuffer`, starting from `bufferConfig.max`, is decremented by the difference between a delayed message's delay beyond `bufferConfig.threshold` so it is important to set the threshold to some value greater than the regular batch posting frequency of your chain and also greater than `delayBlocks` on your chain | `2^32`—note that this configuration value is measured using Ethereum blocks for an L2 Arbitrum chain (settling to Ethereum) or an L3 Arbitrum chain settling to Arbitrum One or Arbitrum Nova. If the chain settles to a different type of parent chain, you must use its parent chain's `block.number` timing. It is recommended that you set this value to be higher than batch posting frequency of your chain, but lower than the `delayBlocks` of your chain. | | `bufferConfig.replenishRateInBasis` | The rate at which the delay buffer will replenish linearly | 500 (or 5% replenishment rate), meaning that one minute will be replenished for every 20 minutes where there are no messages delayed beyond `bufferConfig.threshold` | | `validators` | An array of addresses that are allowed to post assertions to the parent chain, when BoLD is in permissioned mode (i.e., when `disableValidatorWhitelist` is `false`) | The list of whitelisted validators allowed to progress the chain (by regularly posting assertions to the parent chain) | --- > For a complete page index, fetch # Bond and validator configurations Arbitrum chains are customizable Layer 3 (L3) chains that settle to an Arbitrum Layer 2 (L2) chain, such as Arbitrum One. They support validator configurations to ensure chain security through bonding and [assertion](/how-arbitrum-works/deep-dives/assertions.md) challenges. Validators post assertions about the chain's state on the parent L2 chain and can challenge incorrect assertions. Arbitrum Chains can be permissioned, meaning validators must be allowlisted. For chains that use that elect to use the [BoLD](/how-arbitrum-works/bold/gentle-introduction.md) protocol, permissionless validation is an option (BoLD also supports permissioned validation). Bonding is required for active validation, where validators place bond funds to participate. If a validator loses a challenge (e.g., due to a faulty assertion), their bond is escrowed or burned. Configurations such as the `stakeToken`, `baseStake`, and `loserStakeEscrow` are configurable during chain deployment or post-deployment via contract calls. Arbitrum chains may utilize the BoLD (Bounded Liquidity Delay) protocol for efficient dispute resolution, which affects bonding tokens (e.g., **WETH** for BoLD-enabled chains like Arbitrum One/Nova, or **ETH**/native for other chains). ## `stakeToken` The bonded token is the asset that validators must bond to participate in asserting the chain's state on the parent L2 chain. It serves as collateral for challenges. A bonded token can be **WETH** or the parent chain's native token. ### Configuration details * Can be **ETH** (native gas token) or any **ERC-20** token. * **For BoLD-enabled chains** (e.g., settling to Arbitrum One or Nova), it defaults to **WETH**. * **For non-BoLD chains**, it defaults to the parent chain's native token (usually **ETH**). * Currently, its value is often hardcoded to **ETH** in basic deployments, but customizable to an **ERC-20** contract address in advanced setups. Future updates will expand **ERC-20** support. ### How to configure 1. **Prepare the chain configuration**: Generate the base chain config using `prepareChainConfig`. Set the `chainId` and initial owner. ```typescript import { prepareChainConfig } from '@arbitrum/chain-sdk'; const chainConfig = prepareChainConfig({ chainId: 123456, // Replace with your desired chain ID arbitrum: { InitialChainOwner: '0xYourOwnerAddressHere', // Wallet address that will own the chain DataAvailabilityCommittee: false, // Set to true for AnyTrust chains (DAC) }, }); ``` 2. **Set up the public client for the parent chain**: Create a public client to interact with the parent chain. ```typescript import { createPublicClient, http } from 'viem'; import { sepolia } from 'viem/chains'; // Example: Use 'mainnet' or 'arbitrumOne' as needed const parentChainPublicClient = createPublicClient({ chain: sepolia, // Replace with your parent chain (e.g., arbitrumOne) transport: http('https://your-parent-chain-rpc-url'), // Replace with actual RPC URL }); ``` 3. **Prepare deployment parameters, including `stakeToken`**: Use `createRollupPrepareDeploymentParamsConfig` to define the rollout config. This is where you configure the `stakeToken` (the **ERC-20** address on the parent chain) and `baseStake` (minimum stake amount in wei). ```typescript import { createRollupPrepareDeploymentParamsConfig } from '@arbitrum/chain-sdk'; const createRollupConfig = await createRollupPrepareDeploymentParamsConfig(parentChainPublicClient, { chainId: 123456, // Must match the chainId from Step 1 owner: '0xYourOwnerAddressHere', // Must match InitialChainOwner from Step 1 chainConfig, stakeToken: '0xYourStakeTokenERC20AddressHere', // ERC-20 token address on parent chain for validator staking baseStake: 1000000000000000000n, // Example: 1 token (adjust based on token decimals) // Optional: Other params like confirmPeriodBlocks, loserStakeEscrow, etc. }); ``` 4. **Deploy the chain**: Use `createRollup` to deploy. Provide validator and Batch Poster addresses. This step sends the transaction to the parent chain. ```typescript import { createWalletClient } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { createRollup } from '@arbitrum/chain-sdk'; const deployerAccount = privateKeyToAccount('0xYourDeployerPrivateKeyHere'); // Securely manage this const walletClient = createWalletClient({ account: deployerAccount, chain: sepolia, // Match parent chain transport: http('https://your-parent-chain-rpc-url'), }); const createRollupResults = await createRollup({ params: { config: createRollupConfig, batchPosters: ['0xYourBatchPosterAddressHere'], // Address for posting batches validators: ['0xYourValidatorAddressHere'], // Validator addresses // Optional: nativeToken: '0xCustomGasTokenAddress' for custom fee tokens }, account: deployerAccount, publicClient: parentChainPublicClient, walletClient, }); console.log('Deployment Transaction Hash:', createRollupResults.transactionHash); console.log('Core Contracts:', createRollupResults.coreContracts); ``` 5. **Post-deployment (if using AnyTrust)**: If `DataAvailabilityCommittee` is `true`, set the DAC keyset in the `SequencerInbox`. ```typescript import { setValidKeyset } from '@arbitrum/chain-sdk'; // Generate or provide your keyset (BLS public keys for the committee) const keyset = '0xYourGeneratedKeysetHere'; const setKeysetResults = await setValidKeyset({ coreContracts: createRollupResults.coreContracts, keyset, publicClient: parentChainPublicClient, walletClient, }); ``` ### Additional notes * Ensure the `stakeToken` is a compliant **ERC-20** on the parent chain; mismatches can cause deployment failures. * Test on a testnet (e.g., Sepolia) first, as deployments are irreversible and cost gas. * The owner address gains control over upgrades and settings—use a secure multisig in production. * For production, consider a Rollup-as-a-Service (RaaS) provider for monitoring and scaling. * **Verification**: This SDK-based method deploys directly via smart contract interactions and does not involve any UI or portal (the portal is for asset bridging, not chain deployment). ## `baseStake` The `baseStake` is the minimum amount of the bond token that validators must deposit to bond and post assertions. ### Configuration details * Specified as a float value (e.g., in `wei` for **ETH**). * Must be greater than 0. * **Balances security**: Low values ease entry but risk malicious challenges; high values deter attacks but exclude smaller validators. ### How to configure 1. **Prepare the chain configuration**: Generate the base chain config using `prepareChainConfig`. Set the `chainId` and initial owner. ```typescript import { prepareChainConfig } from '@arbitrum/chain-sdk'; const chainConfig = prepareChainConfig({ chainId: 123456, // Replace with your desired chain ID arbitrum: { InitialChainOwner: '0xYourOwnerAddressHere', // Wallet address that will own the chain DataAvailabilityCommittee: false, // Set to true for AnyTrust chains (DAC) }, }); ``` 2. **Set up the public client for the parent chain**: Create a public client to interact with the parent chain. ```typescript import { createPublicClient, http } from 'viem'; import { sepolia } from 'viem/chains'; // Example: Use 'mainnet' or 'arbitrumOne' as needed const parentChainPublicClient = createPublicClient({ chain: sepolia, // Replace with your parent chain (e.g., arbitrumOne) transport: http('https://your-parent-chain-rpc-url'), // Replace with actual RPC URL }); ``` 3. **Prepare deployment parameters, including `stakeToken`**: Use `createRollupPrepareDeploymentParamsConfig` to define the rollout config. This is where you configure the `stakeToken` (the **ERC-20** address on the parent chain) and `baseStake` (minimum stake amount in wei). ```typescript import { createRollupPrepareDeploymentParamsConfig } from '@arbitrum/chain-sdk'; const createRollupConfig = await createRollupPrepareDeploymentParamsConfig(parentChainPublicClient, { chainId: 123456, // Must match the chainId from Step 1 owner: '0xYourOwnerAddressHere', // Must match InitialChainOwner from Step 1 chainConfig, stakeToken: '0xYourStakeTokenERC20AddressHere', // ERC-20 token address on parent chain for validator staking baseStake: 1000000000000000000n, // Example: 1 token (adjust based on token decimals) // Optional: Other params like confirmPeriodBlocks, loserStakeEscrow, etc. }); ``` 4. **Deploy the chain**: Use `createRollup` to deploy. Provide validator and batch poster addresses. This step sends the transaction to the parent chain. ```typescript import { createWalletClient } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { createRollup } from '@arbitrum/chain-sdk'; const deployerAccount = privateKeyToAccount('0xYourDeployerPrivateKeyHere'); // Securely manage this const walletClient = createWalletClient({ account: deployerAccount, chain: sepolia, // Match parent chain transport: http('https://your-parent-chain-rpc-url'), }); const createRollupResults = await createRollup({ params: { config: createRollupConfig, batchPosters: ['0xYourBatchPosterAddressHere'], // Address for posting batches validators: ['0xYourValidatorAddressHere'], // Validator addresses // Optional: nativeToken: '0xCustomGasTokenAddress' for custom fee tokens }, account: deployerAccount, publicClient: parentChainPublicClient, walletClient, }); console.log('Deployment Transaction Hash:', createRollupResults.transactionHash); console.log('Core Contracts:', createRollupResults.coreContracts); ``` 5. **Post-deployment (if using AnyTrust)**: If `DataAvailabilityCommittee` is `true`, set the DAC keyset in the `SequencerInbox`. ```typescript import { setValidKeyset } from '@arbitrum/chain-sdk'; // Generate or provide your keyset (BLS public keys for the committee) const keyset = '0xYourGeneratedKeysetHere'; const setKeysetResults = await setValidKeyset({ coreContracts: createRollupResults.coreContracts, keyset, publicClient: parentChainPublicClient, walletClient, }); ``` ### Additional notes * **Validators and staking**: After deployment, validators bond by depositing at least `baseStake` of the `stakeToken` into the Rollup contract. This can be done via contract calls (e.g., using the `RollupUserLogic` interface). * **Warnings**: * Ensure the `stakeToken` is a compliant **ERC-20** on the parent chain; mismatches can cause deployment failures. * Test on a testnet (e.g., Sepolia) first, as deployments are irreversible and cost gas. * The owner address gains control over upgrades and settings—use a secure multisig in production. * For production, consider a Rollup-as-a-Service (RaaS) provider for monitoring and scaling. * **Verification**: This SDK-based method deploys directly via smart contract interactions and does not involve any UI or portal (the portal is for asset bridging, not chain deployment). ## `loserStakeEscrow` The `loserStakeEscrow` is the address where a validator's bonded funds are sent if they lose a challenge (e.g., due to an incorrect assertion). This mechanism acts as a penalty. The default configuration has no default specified; must be configured. ### Configuration details * Funds are escrowed rather than immediately burned, allowing potential recovery or governance decisions. * **Recommended**: Set to an address controlled by the chain owner(s) for management, or a burn address (e.g., `0x000000000000000000000000000000000000dEaD`) if funds should be permanently removed. ### Configuring during deployment 1. **Prepare chain config**: ```typescript import { prepareChainConfig } from '@arbitrum/chain-sdk'; const chainConfig = prepareChainConfig({ chainId: 123456, // Your unique chain ID arbitrum: { InitialChainOwner: '0xYourOwnerAddressHere', DataAvailabilityCommittee: false, // True for AnyTrust chains }, }); ``` 2. **Set up parent chain client**: ```typescript import { createPublicClient, http } from 'viem'; import { sepolia } from 'viem/chains'; const parentChainPublicClient = createPublicClient({ chain: sepolia, // Replace with your parent chain transport: http('https://your-parent-rpc-url'), }); ``` 3. **Prepare deployment params, including `loserStakeEscrow`**: Set `loserStakeEscrow` as an address (string). There is no explicit default documented, but if omitted, it may revert to a system default (e.g., zero address)—always specify for control. ```typescript import { createRollupPrepareDeploymentParamsConfig } from '@arbitrum/chain-sdk'; const createRollupConfig = await createRollupPrepareDeploymentParamsConfig(parentChainPublicClient, { chainId: 123456, owner: '0xYourOwnerAddressHere', chainConfig, loserStakeEscrow: '0xYourEscrowAddressHere', // e.g., owner-controlled or burn address // Optional: baseStake: 100000000000000000n, stakeToken: '0xERC20Address', etc. }); ``` 4. **Deploy the chain**: ```typescript import { createWalletClient } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { createRollup } from '@arbitrum/chain-sdk'; const deployerAccount = privateKeyToAccount('0xYourPrivateKey'); const walletClient = createWalletClient({ account: deployerAccount, chain: sepolia, transport: http('https://your-parent-rpc-url'), }); const createRollupResults = await createRollup({ params: { config: createRollupConfig, batchPosters: ['0xYourBatchPosterAddress'], validators: ['0xYourValidatorAddress'], }, account: deployerAccount, publicClient: parentChainPublicClient, walletClient, }); console.log('Rollup Address:', createRollupResults.coreContracts.rollup); ``` 5. **For AnyTrust chains**: If `DataAvailabilityCommittee` is `true`, configure the DAC keyset post-deployment using `setValidKeyset` from the SDK. ### Updating `loserStakeEscrow` post-deployment The chain owner can update it by calling `setLoserStakeEscrow` on the deployed Rollup contract (address from `createRollupResults.coreContracts.rollup`): ```typescript import { parseAbi } from 'viem'; const rollupAbi = parseAbi(['function setLoserStakeEscrow(address newLoserStakedEscrow) external']); await walletClient.writeContract({ address: '0xYourRollupAddress', abi: rollupAbi, functionName: 'setLoserStakeEscrow', args: ['0xNewEscrowAddressHere'], }); ``` ### Additional notes * This process requires the caller to be the owner. Changes affect how challenge losses are handled, so test thoroughly. * Setting `loserStakeEscrow` to a burn address increases the cost of failed challenges, enhancing security. * Deploy on testnets first; mainnet deployments are costly and permanent. ## Validator configurations and how to set them up Validators are configured during chain deployment and can be run post-launch. Arbitrum chains support permissioned validators, that can be added to an allowlist. ### Step 1: Configure and run the validator node Start with the base configuration for a full node, then add validator-specific flags. Use a Docker command to run the Nitro node image (replace placeholders like version numbers, RPC URLs, and chain IDs with your specifics). For example, on Arbitrum One (chain ID 42161): ```shell docker run --rm -it -v /path/to/local/dir/arbitrum:/home/user/.arbitrum offchainlabs/nitro-node:v3.8.0-62c0aa7 \ --parent-chain.connection.url=https://your-l1-rpc-url:8545 \ --chain.id=42161 \ --node.staker.enable=true \ --node.staker.strategy=Defensive \ --node.staker.parent-chain-wallet.password="YOUR_SECURE_PASSWORD" ``` Key configuration parameters to include: * `--node.staker.enable=true`: This flag enables validation mode. * `--node.staker.strategy=[Strategy]`: Choose a strategy based on your intended behavior: * `Defensive`: Monitors the chain and challenges incorrect assertions by posting a bond (recommended for most users). * `StakeLatest`: Bonds on the latest correct assertion and challenges bad ones (available only on pre-BoLD chains). * `ResolveNodes`: Bonds on the latest assertion, resolves unconfirmed ones, and challenges bad assertions. * `MakeNodes`: Creates new assertions, resolves unconfirmed ones, and challenges bad ones (use cautiously, as it may lead to reverted transactions if multiple validators act at once). * `Watchtower`: Passively monitors and logs errors without active challenging (enabled by default; no wallet needed, but set `--node.staker.enable=false` to disable if not wanted). * `--node.staker.parent-chain-wallet.private-key=[0xYourPrivateKey]` or `--node.staker.parent-chain-wallet.password=[YourPassword]`: Provides access to the wallet for onchain operations. Use a secure method; password-protected keystores are safer than direct private keys. * `--node.bold.enable=true`: Enable this if the chain has BoLD activated (required for versions before Nitro v3.6.0). For custom Arbitrum chains: * Add `--chain.info-json=[JSON string or file path with Arbitrum chain info]` to specify the chain's details. * BoLD parameters may not be needed if BoLD isn't activated on that chain. Run the command in a persistent setup (e.g., using Docker Compose or a systemd service) to keep the node online. ### Step 2: Verify the validator is running correctly * Monitor the node logs for confirmation messages: * Look for `INFO [...] running as validator txSender=[your_wallet_address] actingAsWallet=[your_wallet_address] whitelisted=true strategy=[YourChosenStrategy]`. This indicates the node is in validator mode with a valid wallet. * `validation succeeded`: Shows the node is successfully validating blocks. * `found correct assertion`: Confirms the node is detecting and agreeing with onchain assertions. * If issues arise, check for errors related to wallet funding, RPC connectivity, or chain syncing. ### Advanced: Creating a dedicated validator wallet If you need a new wallet specifically for validation, use Nitro to generate one: ```shell docker run --rm -it -v /path/to/local/dir/arbitrum:/home/user/.arbitrum offchainlabs/nitro-node:v3.8.0-62c0aa7 \ --parent-chain.connection.url=https://your-l1-rpc-url:8545 \ --chain.id=42161 \ --node.staker.enable=true \ --node.staker.parent-chain-wallet.only-create-key \ --node.staker.parent-chain-wallet.password="YOUR_SECURE_PASSWORD" ``` This creates a wallet file in your mounted directory (e.g., under `arb1/wallet/` for Arbitrum One). Back it up securely, as it's needed to withdraw any staked funds later. Load it in your node run command using the password. ### For permissioned chains If the Arbitrum chain is permissioned (not fully permissionless), add your validator wallet address to the allowlist: 1. Identify the `upgradeExecutor` contract address for the chain. 2. Call the `executeCall` method on it: * Set `target` to the Rollup contract address. * Set `targetCalldata` to `0xa3ffb772` followed by your validator address (this is the encoded signature for `setValidator(address[],bool[])`). 3. Verify by calling `isValidator(your_address)` on the Rollup contract, which should return `true`. This step requires administrative access to the chain. Keep your node synced and monitor it regularly to ensure it contributes effectively to chain security. Always use the latest Nitro version for security and compatibility. > **INFO** — Additional information > > Refer to the [Run a full node](/run-arbitrum-node/run-full-node.md) article for instructions on how to get up and running. ### Assertion interval Default to 15 minutes for new assertions (via `--node.bold.assertion-posting-interval` (BoLD) or `--node.staker.make-assertion-interval` (Legacy \~1 hour is default)); must exceed the Rollup's `minimumAssertionPeriod` (\~15 minutes). For production, run multiple validators in a network. Test on devnets first. Always back up keys, as they're needed to withdraw bonds. If issues arise, refer to the official Arbitrum docs for updates, as configurations evolve. --- > For a complete page index, fetch # Customizable challenge period The challenge period defines the time frame during which state updates ([assertions](/how-arbitrum-works/deep-dives/assertions.md)) submitted to the parent chain remain open for scrutiny and potential challenges before they are finalized. This mechanism ensures that participants in the system have the opportunity to verify the validity of state updates and raise challenges if necessary. The length of the Challenge Period is measured based on the parent chain's notion of time, typically reflected in `block.number`. For L3s settling to Arbitrum chains, this period is determined by L1 block progression rather than Arbitrum's (L2) blocks. In addition to the main challenge period, **an extra challenge period** provides a buffer to resolve any pending challenges after the main period ends. Together, these parameters help balance security and confirm the chain's state. ## Default challenge period and extra challenge period > **INFO** — How time is measured in challenges > > Chains settling to Ethereum or Arbitrum chains use `block.number` for all block calculations, which corespond to the chain's view of Ethereum's block number. For example, an L3 Arbitrum chain settling to Arbitrum One, will calculate block progression based on Ethereum's (L1) block number. By default, the challenge period lasts approximately one week, which equates to roughly 45,818 L1 blocks for chains that settle to Ethereum or an Arbitrum chain. The default duration design is to provide sufficient time for validators to detect and challenge fraudulent assertions. On the other hand, the extra challenge period adds a buffer of 40 minutes by default, 200 L1 blocks for chains settling to Ethereum or an Arbitrum chain. This time ensures that any last-minute challenges or ongoing dispute resolution processes can be completed before the Rollup finalizes its state. These default values are selected to carefully balance security and performance for most Rollup use cases. However, developers and Arbitrum chain owners may wish to customize these parameters to suit their specific requirements. ## Customizing the challenge period ### Challenge period blocks The main challenge period configuration uses the `confirmPeriodBlocks` parameter, which specifies the duration of the challenge window based on the parent chain’s notion of `block.number`. This parameter can is customizable in two ways: 1. **During deployment**: Developers can specify the desired value in the `confirmPeriodBlocks` field of the `RollupCreator` configuration when deploying the Rollup. 2. **Post-deployment**: The chain owner can update this value dynamically by calling the `Rollup.setConfirmPeriodBlocks(newValue)` function. For example, setting `confirmPeriodBlocks` to 30,000 blocks reduces the challenge period to approximately 4.5 days. This configuration might be suitable for applications prioritizing faster state confirmation, while increasing the value would extend the challenge period, improving security. ### Extra challenge period blocks The extra challenge period is governable using the `extraChallengeTimeBlocks` parameter, which defines the additional buffer duration after the main challenge period. This period ensures that pending challenges are processed before the Rollup state gets finalized. By default, the extra challenge period is set to **200 blocks**, providing a short but sufficient buffer for most networks. However, developers can increase this value for applications requiring additional dispute resolution time or operate in environments with higher latency between the parent and child chain. Like the main challenge period, this parameter is customizable in two ways: 1. **During deployment**: The value can be set in the `extraChallengeTimeBlocks` field of the `RollupCreator` configuration. 2. **Post-deployment**: The chain owner can dynamically adjust the parameter using the `Rollup.setExtraChallengeTimeBlocks(newExtraTimeBlocks)` function. For example, the following command can update the extra challenge period to 300 blocks based on the parent chain’s notion of `block.number`: ```shell cast send "setExtraChallengeTimeBlocks(uint256)" 300 \ --rpc-url \ --private-key ``` Replace: `` with the contract address of the Rollup admin. `` with the appropriate RPC endpoint. `` with the private key of the authorized admin account (e.g., chain owner). ## Recommended values and best practices For Arbitrum chains aligned with Arbitrum One's configuration, the recommended settings are: * **Challenge period blocks**: 45,818 Ethereum blocks (approximately one week). * **Extra challenge period blocks**: 200 Ethereum blocks (approximately 40 minutes). These values offer a robust and balanced setup for most Rollup use cases. Developers should consider their application’s requirements when adjusting these parameters: * **Shorter periods**: Suitable for applications that benefit from faster state confirmation, such as Rollups prioritizing quicker exits or user withdrawals. For chains settling to Ethereum or an Arbitrum chain, this reduces challenge duration based on L1 block times. * **Longer periods**: Recommended for applications requiring higher security, such as cross-chain asset transfers or large-value transactions. The actual duration depends on the parent chain’s block intervals (e.g., Ethereum vs. other chains). --- > For a complete page index, fetch # Enable fast withdrawals on your Arbitrum chain Optimistic Rollups must sustain a multi-day challenge period to allow time for fraud proofs. This delays finality for users and dApps, resulting in multi-day withdrawal times and cross-chain communication delays. Fast withdrawals is a new configuration allowing Arbitrum chains to achieve fast finality. When an Arbitrum chain operates on Fast Withdrawals, its transactions will be processed by a committee of validators. Transactions reaching a unanimous vote across the committee will be immediately confirmed. This will allow: * Setting up a withdrawal frequency of any time period (up to 15 minutes) * Users' withdrawals Confirmation on the parent chain at frequencies up to \~15 minutes * Cross-chain dApps to read the finalized state at the same rate as the fast withdrawal frequency Enabling this feature shifts the chain from relying solely on time-based fraud proofs to a committee-driven confirmation process, where validators must agree unanimously. A minimum of three validators is recommended to reduce trust risks. This feature is primarily recommended for AnyTrust chains, as it leverages the existing DAC trust model without adding new assumptions. Ideally, DAC members run the validators. For pure Rollup chains, enabling this feature effectively introduces a committee trust layer, making it less "trust-minimized" than standard mode. Once enabled, the chain processes a potential backlog of unconfirmed assertions before operating at full speed. Confirmations may be slightly delayed under high load (e.g., >1 Mgas/s throughput), but security remains intact. ## Recommended configuration While any Arbitrum chain can adopt Fast Withdrawals, we only *recommend* that fast withdrawals be adopted by [AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) chains with a minimum validator and DAC member requirement. We explain both these recommendations below: ### Fast withdrawals for AnyTrust chains As AnyTrust chains are an optimum (an Optimistic Rollup using a separate data availability layer), AnyTrust chains are already placing a trust assumption on their Data Availability Committee (DAC) to provide the data needed for fraud proofs and recreating the chain. The optimal setup for an AnyTrust chain is to have all DAC members *also* run validators as part of the fast withdrawals committee. This will leverage the existing trust assumption placed on the DAC operators such that **enabling fast withdrawals does not add any new trusted parties.** It is possible for an Arbitrum chain Rollup to adopt fast withdrawals. However, it would technically no longer be a Rollup as the minimum trust assumption will shift to the trust placed in the Fast Confirmations committee. ### Minimum validator and DAC nodes We recommend that any Fast Withdrawals-enabled chain have at least three DAC members and three validators acting in the fast withdrawals committee. Given that fast withdrawals will enable confirmation of new Rollup state much faster than the usual 6.4-day challenge period (15 minutes for BoLD-enabled L2s and L3s, and 15 seconds for pre-BoLD L3s), it becomes even more important to have additional parties involved in validation to further reduce trust assumptions. This requirement can be be met with three total operators, who each run a single DAS node and a single validator. ## Technical lower bound for fast withdrawals Once fast withdrawals is enabled, the committee will confirm transactions at the configured frequency. However, a higher network load can cause the fast withdrawals committee to experience slight delays from the configured rate. * **Pre-BoLD chains** * For low-to-medium activity chains (< 1 Mgas/s), 15 seconds is considered to be the sustained lower bound for Fast Withdrawals. * For chains with higher throughput (>1 Mgas/s), the practical lower bound for fast withdrawals is between 1-2 minutes. * **BoLD enabled chains** * For all L3s utilizing BoLD, the practical lower bound for fast withdrawals is 15 minutes. This is because BoLD-enabled chains cannot handle a reorg of the parent chain. Hence, assertions must be posted on an L2 block that has already achieved finality on L1. For L2s settling on Ethereum, this takes \~15 minutes. Chain owners and operators should be aware that the fast withdrawals committee may take longer to confirm new assertions under conditions with greater network load. This behavior is to be expected and does not interfere with the security or trust model of the fast withdrawals committee. ## Practical lower bounds concerning parent chain finality While a fast withdrawals-enabled chain can be configured to finality in as little as 15 seconds (pre-BoLD only), there are externalities on the parent chain and from cross-chain messaging layers that must be considered. For an Ethereum-based Layer-2, we recommend that the fast withdrawal frequency remain above 12.8 minutes, which is the time for Ethereum to achieve finality. For non-Ethereum L1s, we similarly recommend staying above the accepted finality threshold specific to that L1. For an Arbitrum One-based Layer-3, there are three tiers of finality to consider: 1. Soft finality from the [Sequencer](/how-arbitrum-works/deep-dives/sequencer.md)'s confirmation of transaction inclusion (\~250ms) 2. Safe finality from batch inclusion after Arbitrum One's [assertion](/how-arbitrum-works/deep-dives/assertions.md) is included in an Ethereum block. 3. Hard finality after the Ethereum block containing Arbitrum One's batch is finalized on Ethereum (\~15 minutes). For BoLD-enabled chains, this finality is a mandatory threshold for fast withdrawals. ## Adoption instructions (example script) To enable the fast withdrawals feature, there are three actions to take: 1. Make sure the chain is using `nitro-contracts` v2.1.0 or above 2. Activate the fast withdrawals feature 3. Upgrade the node software to nitro v3.1.2 or above ### Upgrading to `nitro-contracts` v2.1.0 As mentioned above, the fast withdrawals feature is available for chains that are using `nitro-contracts` v2.1.0 or above, especially the `RollupAdminLogic` and the `RollupUserLogic` contracts. You can check what nitro-contracts version your chain is using by running the [Arbitrum chain versioner script](https://github.com/OffchainLabs/chain-actions/blob/main/README.md#check-version-and-upgrade-path). If your chain is not running with `nitro-contracts` v2.1.0 or above, you’ll need to perform an upgrade to enable this version. The [Arbitrum chain versioner script](https://github.com/OffchainLabs/chain-actions/blob/main/README.md#check-version-and-upgrade-path) will provide the upgrade paths needed to reach v2.1.0, but basically: * If the chain is running nitro-contracts v1.1.x, you need to [upgrade first to v1.2.1](https://github.com/OffchainLabs/chain-actions/blob/main/scripts/foundry/contract-upgrades/1.2.1/README.md). * If the chain is running nitro-contracts v1.2.1, you need to [upgrade to v2.1.0](https://github.com/OffchainLabs/chain-actions/blob/main/scripts/foundry/contract-upgrades/2.1.0/README.md). Upgrading to the new `nitro-contracts` version also requires updating the node software. For v2.1.0, validator nodes and the batch poster node should run [nitro v3.1.2](https://github.com/OffchainLabs/nitro/releases/tag/v3.1.2) or above. Suppose you’re upgrading your `nitro-contracts` from v1.2.1 to v2.1.0 and using the standard WASM module root (without customizations). In that case, there are [action contracts available in the supported chains](https://github.com/OffchainLabs/chain-actions/blob/main/scripts/foundry/contract-upgrades/2.1.0/README.md#deployed-instances). If you’re using a customized nitro software, with a different WASM module root, you can still deploy the action contract referencing your modified WASM module root (pre and post upgrade). ### Activating fast withdrawals Once the chain runs `nitro-contracts` v2.1.0 or above, the new fast withdrawal parameters will be available in the `RollupAdminLogic` and the `RollupUserLogic` contracts. Both the Arbitrum SDK and the Chain SDK actions repository provide configurable scripts to activate and configure fast withdrawals on an AnyTrust chain. You can use either of those to activate the feature. Both scripts perform the same actions. > **INFO** > > Even though two scripts are available to activate Fast Withdrawals, you only need to execute one of them. Both scripts perform the same actions. #### Arbitrum Chain SDK script The Chain SDK provides an [example script](https://github.com/OffchainLabs/arbitrum-chain-sdk/tree/main/examples/setup-fast-withdrawal) to set up a fast withdrawal committee by performing the following operations: 1. Create a new `n/n` Safe wallet with the specified validators as signers 2. Add the specified validators to the Rollup validators allowlist 3. Set the new Safe wallet as the `anytrustFastConfirmer` in the Rollup contract 4. Set the new `minimumAssertionPeriod` if needed 5. Show how to configure the batch poster and validator nodes To configure the script, you need to specify the following environment variables: | Variable Name | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CHAIN_OWNER_PRIVATE_KEY` | Private key of the account with executor privileges in the UpgradeExecutor admin contract for the chain. It will be the deployer of the multi-sig Safe wallet. | | `PARENT_CHAIN_ID` | ChainId of the parent chain. | | `ROLLUP_ADDRESS` | Address of the Rollup contract. | | `FC_VALIDATORS` | Array of fast-withdrawal validators. They will be added as signers to the multisig Safe wallet and to the Rollup's validator allowlist. It is recommended that these are DAC members of the AnyTrust chain. | | `MINIMUM_ASSERTION_PERIOD` | Optional parameter that defaults to 75 blocks (\~15 minutes). Minimum number of blocks that have to pass in between assertions (measured in block number of the first non-Arbitrum ancestor chain (i.e., Ethereum blocks for L2 chains and L3 chains settling to an Arbitrum chain, and parent chain blocks for chains settling to a non-Arbitrum L2 chain)). | Finally, follow these steps to execute the script (from the `examples/setup-fast-withdrawal` folder): 1. Install dependencies ```shell yarn install ``` 2. Create a `.env` file and add the env vars ```shell cp .env.example .env ``` 3. Run the script ```shell yarn run dev ``` #### Arbitrum chain actions script The Arbitrum chain actions repository also provides an [action script](https://github.com/OffchainLabs/chain-actions/blob/main/scripts/foundry/fast-confirm/README.md) to activate fast withdrawals by performing the following operations: 1. Make sure the "Validate fast confirmation" has not been enabled yet 2. Create a Safe contract for the fast confirmation committee 3. Set the Safe contract as the fast confirmer on the Rollup 4. Set the Safe contract as a validator on the Rollup 5. Set `setMinimumAssertionPeriod` to 1 block to allow more frequent assertion To configure the action script, you need to specify the following environment variables: | Variable Name | Description | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `UPGRADE_ACTION_ADDRESS` | Address of the upgrade action to execute. A standard version is deployed in all [supported chains](https://github.com/OffchainLabs/chain-actions/blob/main/scripts/foundry/fast-confirm/README.md#deployed-instances). If you need to deploy your own action, execute the first step of [this process](https://github.com/OffchainLabs/chain-actions/blob/main/scripts/foundry/fast-confirm/README.md#how-to-use-it). | | `PARENT_UPGRADE_EXECUTOR_ADDRESS` | Private key of the account with executor privileges in the UpgradeExecutor admin contract on the parent chain. It will be the deployer of the multi-sig Safe wallet. | | `PARENT_CHAIN_RPC` | RPC endpoint of the parent chain. | | `ROLLUP` | Address of the Rollup contract. | | `FAST_CONFIRM_COMMITTEE` | Comma-separated list of fast-withdrawal validators. They must be allowlisted validators in the Rollup contract. They will be added as signers to the multisig Safe wallet. It is recommended that these are DAC members of the AnyTrust chain. | Finally, follow these steps to execute the script (from the `scripts/foundry/fast-confirm` folder): 1. Install dependencies ```shell yarn install ``` 2. Create a `.env` file and add the env vars ```shell cp .env.example .env ``` 3. Execute the action. The upgrade can be executed using `cast` CLI command ([cast is a part of the Foundry tools](https://book.getfoundry.sh/cast/)), using the owner account (the one with executor rights on parent chain `UpgradeExecutor`) to send the transaction: ```shell (export $(cat .env | xargs) && cast send $PARENT_UPGRADE_EXECUTOR_ADDRESS "execute(address, bytes)" $UPGRADE_ACTION_ADDRESS $(cast calldata "perform(address, address[])" $ROLLUP \[$FAST_CONFIRM_COMMITTEE\]) --rpc-url $PARENT_CHAIN_RPC --account EXECUTOR) # use --account XXX / --private-key XXX / --interactive / --ledger to set the account to send the transaction from ``` > **NOTE** > > If you have a multisig as executor, you can use the following command to create the payload for calling into the `PARENT_UPGRADE_EXECUTOR`: > > ```shell > (export $(cat .env | xargs) && cast calldata "execute(address, bytes)" $UPGRADE_ACTION_ADDRESS $(cast calldata "perform(address, address[])" $ROLLUP \[$FAST_CONFIRM_COMMITTEE\])) > ``` ### Configure fast withdrawals on nitro v3.1.2 or above To enable fast withdrawals on your chain, the batch poster and the validators of the chain need to be running [nitro v3.1.2](https://github.com/OffchainLabs/nitro/releases/tag/v3.1.2) or above. The following parameters need to be configured in those nodes. #### Batch poster | Option | Description | | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--node.batch-poster.max-delay=0h15m0s` | Since batches need to be posted so validators can create and confirm assertions, the maximum delay should be set to an amount close to the `minimumAssertionPeriod` defined in the Rollup contract. Modify `0h15m0s` to the configured value. | #### Validators | Option | Description | | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--node.bold.enable-fast-confirmation=true` | \[BoLD only] Enables fast withdrawals in the validator node | | `--node.bold.assertion-posting-interval=0h15m0s` | \[BoLD only] Since assertions need to be created for them to be confirmed, the minimum interval to create these assertions should be set to an amount close to the `minimumAssertionPeriod` defined in the Rollup contract. Modify `0h15m0s` to the configured value. | | `--node.staker.enable-fast-confirmation=true` | \[Pre-BoLD only] Enables fast withdrawals in the validator node | | `--node.staker.make-assertion-interval=0h15m0s` | \[Pre-BoLD only] Since assertions need to be created for them to be confirmed, the minimum interval to create these assertions should be set to an amount close to the `minimumAssertionPeriod` defined in the Rollup contract. Modify `0h15m0s` to the configured value. | > **NOTE** > > Immediately after configuring fast withdrawals—your chain may not be operating fully at speed yet. This is because the validators have to work through the backlog of assertions which were not yet confirmed. You will see a series of `NodeCreated` and `NodeConfirmed` events. Once the backlog has been processed, your chain should operate fully at speed. --- > For a complete page index, fetch # Canonical factory contracts Deploying new Arbitrum chains is usually done through a `RollupCreator` contract that processes the creation of the needed contracts and sends the initialization messages from the parent to the child chain. Similarly, creating a token bridge for an Arbitrum chain is usually done using a `TokenBridgeCreator` contract that creates the token bridge contracts in both the parent and child chains (this last one via [Parent-to-child messages](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md)). This page describes the benefits of using these canonical factory contracts and lists their addresses in all supported chains. > **INFO** — Use the Arbitrum Chain SDK > > You can use these contracts to create Arbitrum chains. However, it is strongly recommended to go through the [Chain SDK](/launch-arbitrum-chain/quickstart/sdk-introduction.md) to interact with them. Doing so prevents misconfiguring parameters and using appropriate defaults for most of them. ## Benefits of using the canonical factory contracts The main benefits of using the canonical factory contracts are: * **End-to-end deployment and initialization**: The canonical contracts will create all needed contracts, initialize them, and configure them with the parameters passed. Additionally, they will send the appropriate initialization messages to the chain's Inbox contract so the sequencer can process them upon starting. This process is done atomically in a single transaction, guaranteeing a successful chain or token bridge creation. * **Gas efficiency**: The canonical contract design is to be gas efficient. Creating and initializing all contracts for a chain or token bridge is a gas-consuming process, so special care has been taken to fit everything into one transaction. * **Updated with the latest features**: Whenever a new version is available for the contracts of an Arbitrum chain, the canonical factory contracts get updated, so new Rollups and token bridges are created using the updated version. ## Addresses of the canonical factory contracts This table shows the addresses of the deployed and maintained canonical factory contracts. | Network | Chain id | `RollupCreator` | `TokenBridgeCreator` | | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Ethereum | 1 | [0x43698080f40dB54DEE6871540037b8AB8fD0AB44](https://etherscan.io/address/0x43698080f40dB54DEE6871540037b8AB8fD0AB44) | [0x60D9A46F24D5a35b95A78Dd3E793e55D94EE0660](https://etherscan.io/address/0x60D9A46F24D5a35b95A78Dd3E793e55D94EE0660) | | Arbitrum One | 42161 | [0xB90e53fd945Cd28Ec4728cBfB566981dD571eB8b](https://arbiscan.io/address/0xB90e53fd945Cd28Ec4728cBfB566981dD571eB8b) | [0x2f5624dc8800dfA0A82AC03509Ef8bb8E7Ac000e](https://arbiscan.io/address/0x2f5624dc8800dfA0A82AC03509Ef8bb8E7Ac000e) | | Arbitrum Nova | 42170 | [0xF916Bfe431B7A7AaE083273F5b862e00a15d60F4](https://nova.arbiscan.io/address/0xF916Bfe431B7A7AaE083273F5b862e00a15d60F4) | [0x8B9D9490a68B1F16ac8A21DdAE5Fd7aB9d708c14](https://nova.arbiscan.io/address/0x8B9D9490a68B1F16ac8A21DdAE5Fd7aB9d708c14) | | Base | 8453 | [0xDbe3e840569a0446CDfEbc65D7d429c5Da5537b7](https://basescan.org/address/0xDbe3e840569a0446CDfEbc65D7d429c5Da5537b7) | [0x4C240987d6fE4fa8C7a0004986e3db563150CA55](https://basescan.org/address/0x4C240987d6fE4fa8C7a0004986e3db563150CA55) | | Sepolia | 11155111 | [0x687Bc1D23390875a868Db158DA1cDC8998E31640](https://sepolia.etherscan.io/address/0x687Bc1D23390875a868Db158DA1cDC8998E31640) | [0x7edb2dfBeEf9417e0454A80c51EE0C034e45a570](https://sepolia.etherscan.io/address/0x7edb2dfBeEf9417e0454A80c51EE0C034e45a570) | | Arbitrum Sepolia | 421614 | [0x5F45675AC8DDF7d45713b2c7D191B287475C16cF](https://sepolia.arbiscan.io/address/0x5F45675AC8DDF7d45713b2c7D191B287475C16cF) | [0x56C486D3786fA26cc61473C499A36Eb9CC1FbD8E](https://sepolia.arbiscan.io/address/0x56C486D3786fA26cc61473C499A36Eb9CC1FbD8E) | | Base Sepolia | 84532 | [0x70cA29dA3B116A2c4A267c549bf7947d47f41e22](https://sepolia.basescan.org/address/0x70cA29dA3B116A2c4A267c549bf7947d47f41e22) | [0xFC71d21a4FE10Cc0d34745ba9c713836f82f8DE3](https://sepolia.basescan.org/address/0xFC71d21a4FE10Cc0d34745ba9c713836f82f8DE3) | ## How to deploy new factory contracts This section describes the process of deploying a new `RollupCreator` and a new `TokenBridgeCreator` on a parent chain. > **WARNING** — The use of non-canonical factory contracts is unsupported > > This section describes the process of deploying new factory contracts. This can be useful when creating Arbitrum chains on a parent chain that doesn't have canonical factory contracts, or when modifying the core or token bridge contracts. However, keep in mind that using factory contracts different than the ones listed above (canonical) is not supported and might lead to unexpected issues. ### Deploying a `RollupCreator` To deploy the `RollupCreator` and the core contract templates, we'll use the [`nitro-contracts`](https://github.com/OffchainLabs/nitro-contracts) repository. #### Step 1: Clone `nitro-contracts` repo and checkout the desired release ```shell git clone https://github.com/OffchainLabs/nitro-contracts.git cd nitro-contracts git checkout vX.Y.Z ``` #### Step 2: Install the dependencies and build the contracts ```shell yarn install yarn build:all ``` #### Step 3: Create the `.env` file and set the desired env parameters ```shell cp .env-sample .env ``` We can use the following parameters: * `DEVNET_PRIVKEY` or `MAINNET_PRIVKEY`: The private key of the account that will deploy the contracts. Use one or the other depending on the type of network where the contracts will be deployed: testnet or mainnet. Will be used in the `hardhat.config.ts` file. * `DISABLE_VERIFICATION`: Whether to disable verification of contracts in the block explorer or not. * `ETHERSCAN_API_KEY`: The key to access the API of the block explorer used. Will be used in the `hardhat.config.ts` file. #### Step 4: Check hardhat configuration Now we can open the `hardhat.config.ts` file and verify that the network where the contracts are going to be deployed exists in the file. If it exists, we can verify that it's using the environment variables specified above. If it doesn't exist, we can add the network to the configuration file. #### Step 5: Create a `config.ts` file and specify the right maxDataSize ```shell cp scripts/config.ts.example scripts/config.ts ``` Then we can modify the constant `maxDataSize` to one of the following values: * `117964` is the parent chain is Ethereum * `104857` if the parent chain is a Layer 2 chain #### Step 6: Run the deployment script ```shell yarn deploy-factory --network parentChainNetwork ``` This last step will create the template contracts and the `RollupCreator` contract, and will show all addresses in the output. It will optionally try to verify the contracts if the configuration is present. ### Deploying a `TokenBridgeCreator` To deploy the `TokenBridgeCreator` and the token bridge contract templates, we'll use the [token-bridge-contracts](https://github.com/OffchainLabs/token-bridge-contracts) repository. #### Step 1: Clone `token-bridge-contracts` repo and checkout the desired release ```shell git clone https://github.com/OffchainLabs/token-bridge-contracts.git cd token-bridge-contracts git checkout vX.Y.Z ``` #### Step 2: Install the dependencies and build the contracts ```shell yarn install yarn build ``` #### Step 3: Create the .env file and set the desired env parameters ```shell cp .env-sample .env ``` We can use the following parameters: * `BASECHAIN_RPC`: RPC of the parent chain * `BASECHAIN_DEPLOYER_KEY`: Private key of the account that will be deploying the contracts * `BASECHAIN_WETH`: Address of the WETH contract on the parent chain (should be the zero address for custom gas token chains) * `GAS_LIMIT_FOR_L2_FACTORY_DEPLOYMENT`: The gas limit for deploying the child chain factory contracts. It is recommended to leave the default value `6000000` * `ARBISCAN_API_KEY`: (Optional) The key to access the API of the block explorer used for verifying the contracts #### Step 4: Run the deployment script ```shell yarn deploy:token-bridge-creator ``` This step will create the template contracts and the `TokenBridgeCreator` contract, and will show all addresses in the output. --- > For a complete page index, fetch # How to configure your Arbitrum chain's node using the Chain SDK > **INFO** — RaaS providers > > It is highly recommended that you work with a Rollup-as-a-Service (RaaS) provider to deploy a production chain. You can find a list of [RaaS providers](/launch-arbitrum-chain/integrations/infrastructure-providers.md#rollup-as-a-service-raas-providers) in our integrations directory. Once you have successfully deployed and initialized the Arbitrum chain core contracts, the next step is to configure and run an Arbitrum Nitro node for your chain. You configure a Nitro node using a `JSON` file describing all the parameters for the node, including settings for the batch poster, validator, and the chain itself. See the [Overview](/launch-arbitrum-chain/quickstart/sdk-introduction.md) for an introduction to creating and configuring an Arbitrum chain. Before reading this guide, we recommend that you're familiar with the general process for creating new chains explained in the introduction and the first section of [How to deploy an Arbitrum chain](/launch-arbitrum-chain/deploy/deploy-chain.md). ## Structure of a Nitro node configuration JSON object When starting up the node, a Nitro node reads its configuration from a JSON object, usually provided in a file. This object has the following structure: ```typescript { "chain": { "info-json": "[{...}]", "name": "MyArbitrumChain", }, "parent-chain": { "connection": { "url": "http://parentChainRpcUrl", }, }, "http": { "addr": "0.0.0.0", "port": 8449, "vhosts": "*", "corsdomain": "*", "api": ["eth", "net", "web3", "arb", "debug"], }, "node": { // Node specific settings including sequencer, batch-poster and validator }, "execution": { // Execution-client specific settings } }; ``` The following table briefly describes the type of parameters that can be configured in each root property: | Property | Description | | -------------- | ------------------------------------------------------------------------------------ | | `chain` | Information about the chain, including the chain ID, its name, and the chain config. | | `parent-chain` | Information for accessing the parent chain. | | `http` | Configuration parameters for the HTTP server. | | `node` | Node settings, including sequencer, batch-poster and validator if enabled. | | `execution` | Execution settings, including archive mode and block production parameters. | ### Additional configuration for Arbitrum AnyTrust chains: For [Arbitrum AnyTrust chains](/how-arbitrum-works/deep-dives/anytrust-protocol.md), the Nitro node configuration object has an additional `da.anytrust` segment under the `node` field to configure the AnyTrust-specific properties (prior to Nitro v3.10.0, this segment was named `data-availability` and also held the `sequencer-inbox-address` and `parent-chain-node-url` keys, which current versions no longer accept): ```typescript { ... "node": { ... "da": { "anytrust": { "enable": true, "rest-aggregator": { "enable": true, "urls": "http://localhost:9877", }, "rpc-aggregator": { "enable": true, "assumed-honest": 3, "backends": "[...]", }, }, }, } ... }; ``` You can find information about what these parameters configure in [How to configure a DAC](/launch-arbitrum-chain/chain-config/data-availability/configure-dac.md). ## How to generate a node's configuration file using the Arbitrum Chain SDK Let's look at what methods to use for generating a configuration file for the Nitro node of your Arbitrum chain, using the Chain SDK. > **INFO** — Example script > > The Arbitrum Chain SDK includes an example script for generating a node configuration file. We recommend that you first understand the process described in this section and then check the [prepare-node-config](https://github.com/OffchainLabs/arbitrum-chain-sdk/blob/main/examples/prepare-node-config/index.ts) script. ### 1. Gather information from the deployed chain To generate the configuration file for your node, you'll need the following information: * **Core contracts**: you'll have to configure your node with all the core contracts created on deployment. * **Chain config**: the same configuration used when deploying the chain must be passed to the node. * Private keys of the accounts that will operate the chain, like the batch poster and the validator * Any extra configuration desired for the sequencer, batch poster and validator, like batch posting frequency or maximum block speed. ### 2. Generate the node configuration object The `prepareNodeConfig` method generates a JSON object with the configuration for the node. It sets the appropriate defaults for most parameters, allowing you to override any of these defaults. Below is an example of how to use `prepareNodeConfig` to obtain the node configuration for an Arbitrum chain deployed on transaction `txHash` > **NOTE** > > This transaction hash is not strictly required; it's only for obtaining the core contracts and chain config to use in the node. ```typescript import { createPublicClient, http } from 'viem'; import { createRollupPrepareTransaction, createRollupPrepareTransactionReceipt, ChainConfig, prepareNodeConfig } from '@arbitrum/chain-sdk'; const parentChainPublicClient = createPublicClient({ chain: parentChain, transport: http(), }); // get the transaction const tx = createRollupPrepareTransaction(await parentChainPublicClient.getTransaction({ hash: txHash })); // get the transaction receipt const txReceipt = createRollupPrepareTransactionReceipt(await parentChainPublicClient.getTransactionReceipt({ hash: txHash })); // get the chain config and core contracts const config = tx.getInputs()[0].config; const chainConfig: ChainConfig = JSON.parse(config.chainConfig); const coreContracts = txReceipt.getCoreContracts(); const nodeConfig = prepareNodeConfig({ chainName: 'MyArbitrumChain', chainConfig, coreContracts, batchPosterPrivateKey, validatorPrivateKey, stakeToken: config.stakeToken, parentChainId: parentChain.id, parentChainRpcUrl: parentChain.rpcUrls.default.http[0], }); ``` `prepareNodeConfig` will also generate the specific configuration for AnyTrust chains if it detects that the chain configuration includes the appropriate flag. After generating the node configuration object, it can be saved to a file for later use by the Nitro node. ### 3. Next step You can now run the Nitro node for your Arbitrum chain with the node configuration generated. You can find instructions for running a node in [How to run a full node](/run-arbitrum-node/run-full-node.md). --- > For a complete page index, fetch # How to deploy an Arbitrum chain using the Chain SDK > **INFO** — RaaS providers > > It is highly recommended that you work with a Rollup-as-a-Service (RaaS) provider to deploy a production chain. You can find a list of [RaaS providers](/launch-arbitrum-chain/integrations/infrastructure-providers.md#rollup-as-a-service-raas-providers) in our integrations directory. Creating a new Arbitrum chain involves deploying a set of contracts on your chain's parent chain. These contracts are: * **Bridge contracts**: Used to send cross-chain messages between the Arbitrum chain and its parent chain, including batches posted by the sequencer * **Rollup contracts**: Used by validators to create and confirm [assertions](/how-arbitrum-works/deep-dives/assertions.md) of the current state of the Arbitrum chain * **Challenge protocol contracts**: Used by validators to dispute current assertions of the state of the chain, and ultimately resolve those disputes You can explore the code of these contracts in the [nitro-contracts repository](https://github.com/OffchainLabs/nitro-contracts). Upon deployment, an Arbitrum chain can be configured as a Rollup or [AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) chain, and use **ETH** or any standard **ERC-20** token as its gas token. This page explains how to deploy an Arbitrum chain using the Arbitrum Chain SDK. See the [Overview](/launch-arbitrum-chain/quickstart/sdk-introduction.md) for an introduction to the process of creating and configuring an Arbitrum chain. > **INFO** — About custom gas token Arbitrum chains > > Custom gas token Arbitrum chains let participants pay transaction fees in an **ERC-20** token instead of **ETH**. Standard **ERC-20** tokens can be used as gas tokens, while more complex tokens with additional functionality must fulfill [these requirements](/launch-arbitrum-chain/chain-config/costs/custom-gas-token-rollup.md#requirements-of-the-custom-gas-token) to be used as gas tokens. Remember that the **ERC-20** token to be used must be deployed on your chain's parent chain. ## Parameters used when deploying a new chain Before we describe the process of creating a chain using the Chain SDK, let's see what configuration options we have available when creating a chain. Deploying a new Arbitrum chain is done through a [`RollupCreator`](/launch-arbitrum-chain/deploy/canonical-factory-contracts.md) contract that processes the creation of the needed contracts and sends the initialization messages from the parent chain to the newly created Arbitrum chain. `RollupCreator` has a `createRollup` function that deploys your chain's core contracts to the parent chain. `createRollup` takes a complex struct called `RollupDeploymentParams` as its only input. This struct defines the parameters of the Arbitrum chain to be created. ```solidity struct RollupDeploymentParams { Config config; address[] validators; uint256 maxDataSize; address nativeToken; bool deployFactoriesToL2; uint256 maxFeePerGasForRetryables; address[] batchPosters; address batchPosterManager; } ``` The following table describes `RollupDeploymentParams`'s parameters: | Parameter | Type | Description | | --------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `config` | Config | The chain's configuration, explained below. | | `validators` | address\[] | Initial set of validator addresses. Validators are responsible for validating the chain state and posting assertions (`RBlocks`) back to the parent chain. They also monitor the chain and initiate challenges against potentially faulty assertions submitted by other validators. | | `maxDataSize` | uint256 | Maximum message size for the Inbox contract (`117964` for L2 chains, and `104857` for L3 chains). | | `nativeToken` | address | Address of the token contract in the parent chain, used for paying gas fees on the Arbitrum chain. It can be set to **ETH** for regular chains or to any **ERC-20** token for custom gas token Arbitrum chains. | | `deployFactoriesToL2` | bool | Whether or not to deploy several deterministic factory contracts to the Arbitrum chain. | | `maxFeePerGasForRetryables` | uint256 | Gas price bid to use when sending the retryable tickets. | | `batchPosters` | address\[] | Initial set of batch poster addresses. Batch posters batch and compress transactions on the Arbitrum chain and transmit them back to the parent chain. | | `batchPosterManager` | address | Address of the account responsible for managing currently active batch posters. Not mandatory, as these actions can also be taken by the chain owner. | The `Config` struct used in the previous configuration looks like this: ```solidity struct Config { uint64 confirmPeriodBlocks; address stakeToken; uint256 baseStake; bytes32 wasmModuleRoot; address owner; address loserStakeEscrow; uint256 chainId; string chainConfig; uint256 minimumAssertionPeriod; uint64 validatorAfkBlocks; uint256[] miniStakeValues; ISequencerInbox.MaxTimeVariation sequencerInboxMaxTimeVariation; uint256 layerZeroBlockEdgeHeight; uint256 layerZeroBigStepEdgeHeight; uint256 layerZeroSmallStepEdgeHeight; AssertionState genesisAssertionState; uint256 genesisInboxCount; address anyTrustFastConfirmer; uint8 numBigStepLevel; uint64 challengeGracePeriodBlocks; BufferConfig bufferConfig; } ``` Most of these parameters don't need to be configured, since the Chain SDK will provide the right default values for them. However, the following table describes some of the parameters that you might want to configure: | Parameter | Type | Description | | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `confirmPeriodBlocks` | uint64 | Sets the challenge period in terms of blocks, which is the time allowed for validators to dispute or challenge state assertions. Learn more about it in [Customizable challenge period](/launch-arbitrum-chain/chain-config/validation/challenge-period.md). | | `stakeToken` | address | Address of the token that validators must bond to participate in the chain's validation process. | | `baseStake` | uint256 | Arbitrum chain validator nodes must bond a certain amount to incentivize honest participation. This parameter specifies this amount. | | `wasmModuleRoot` | address | Hash of the WASM module root to be used when validating. | | `owner` | address | Account address responsible for deploying, owning, and managing your Arbitrum chain's base contracts on its parent chain. You **will** have to fund this address with enough **ETH** to cover the gas costs of deploying your core contracts to L2. | | `loserStakeEscrow` | address | Address that will receive any extra bonds deposited in the same assertion (when creating disputes). | | `chainId` | uint256 | Your chain's unique identifier. It differentiates your chain from others in the ecosystem. Don't worry about this; it's inconsequential for devnets. In production scenarios, you'll want to use a unique integer identifier that represents your chain's network on chain indexes like [Chainlist.org](http://chainlist.org). | | `chainConfig` | string | Additional chain configuration, explained below. | | `chainName` | string | This name provides a way for people to distinguish your Arbitrum chain from other Arbitrum chains. You’ll want to make this a name that you can easily remember, and that your users and developers will recognize. | The `chainConfig` parameter within the `Config` struct is a stringified `JSON` object that looks like this: ```typescript { chainId: number; homesteadBlock: number; daoForkBlock: null; daoForkSupport: boolean; eip150Block: number; eip150Hash: string; eip155Block: number; eip158Block: number; byzantiumBlock: number; constantinopleBlock: number; petersburgBlock: number; istanbulBlock: number; muirGlacierBlock: number; berlinBlock: number; londonBlock: number; clique: { period: number; epoch: number; } arbitrum: { EnableArbOS: boolean; AllowDebugPrecompiles: boolean; DataAvailabilityCommittee: boolean; InitialArbOSVersion: number; InitialChainOwner: Address; GenesisBlockNum: number; MaxCodeSize: number; MaxInitCodeSize: number; } } ``` Again, most of these parameters don't need to be configured, since the Chain SDK will provide the right default values for them. However, the following table describes some of the parameters that you might want to configure: | Parameter | Type | Description | | ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `chainId` | number | Your chain's unique identifier. It differentiates your chain from others in the ecosystem. | | `arbitrum.DataAvailabilityCommittee` | bool | Whether or not to use a Data Availability Committee (DAC) to store your chain's data (this should be `false` for Rollup chains, and `true` for AnyTrust chains). | | `arbitrum.InitialArbOSVersion` | number | ArbOS version to use (it should be the latest ArbOS version available). | | `arbitrum.InitialChainOwner` | address | Account address responsible for deploying, owning, and managing your Arbitrum chain's base contracts on its parent chain. | | `arbitrum.MaxCodeSize` | number | Sets the maximum size for contract bytecodes on the chain (it's recommended to use the default 24Kb, and not set it higher than 96Kb). For more information, refer to [Smart contract size limit](/launch-arbitrum-chain/chain-config/execution/smart-contract-size-limit.md). | | `arbitrum.MaxInitCodeSize` | number | Maximum initialization bytecode size allowed (usually double the amount set in `MaxCodeSize`). | > **INFO** > > The `chainId` and `InitialChainOwner` parameters must be equal to the `chainId` and `owner` defined in the `Config` struct. #### `MaxCodeSize` limitations * Entering a contract requires reading its code from the database, which means larger code requires more work. The EVM doesn't charge differently based on code size, but the computation cost could vary. However, that doesn't seem to be a strict limit compared to the readability aspect. * Reading code occurs via a preimage fetch (`code hash -> code`). The limit of preimage size is that a one-step proof (OSP) of a preimage must include the full preimage as part of the transaction. #### `MaxInitCodeSize` limitations **For L2s building on top of Ethereum (parent chain):** The effective limit on calldata seems to be 128KB: * Some clients (Geth, AFAU) use 128KB as the limit, even though the protocol doesn't enforce it * 128KB of calldata, with 40 gas per byte (according to [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623)), would put the gas at 6M, which is within the limit for a transaction The conclusion is that using a 96KB code size limit is conservative and safe. **For L3s building on top of Arbitrum One:** * The sequencer has a default limit on `TxMaxDataSize` of `95000`, so L3s should use a lower preimage limit than that, roughly 85KB. **Other chains:** * Should deduct limits from their prospective parent chain **Safe values** | Configuration | Max contract size limit | Init code size | | ------------- | ----------------------- | -------------- | | Config A | 24kb | 48kb | | Config B | 48kb | 96kb | ::: ## How to create a new Arbitrum chain using the Chain SDK Now, let's look at the methods to use when creating a new Arbitrum chain with the Chain SDK. > **INFO** — Example script > > The Chain SDK includes an example script for creating an Arbitrum chain. We recommend that you first understand the process described in this section and then check the following example scripts: > > * [`create-rollup-eth`](https://github.com/OffchainLabs/arbitrum-chain-sdk/blob/main/examples/create-rollup-eth/index.ts) for creating an Arbitrum AnyTrust chain with **ETH** as the gas token. > * [`create-rollup-custom-fee-token`](https://github.com/OffchainLabs/arbitrum-chain-sdk/tree/main/examples/create-rollup-custom-fee-token) for creating an Arbitrum AnyTrust chain with an **ERC-20** as the gas token. Here are the steps involved in the deployment process: 1. [Create the chain's configuration object](#1-create-the-chains-configuration-object) 2. [Deploy the Arbitrum chain](#2-deploy-the-arbitrum-chain) 3. [Understand the returned data](#3-understand-the-returned-data) 4. [Set the DAC keyset in the `SequencerInbox` (for AnyTrust chains)](#4-set-the-dac-keyset-in-the-sequencerinbox-for-anytrust-chains) 5. [Next step](#5-next-step) ### 1. Create the chain's configuration object The `prepareChainConfig` function creates a `chainConfig` structure like the one defined in the previous section. It sets the appropriate defaults for most of the parameters, allowing you to override any of these defaults. However, the `chainId` and `InitialChainOwner` parameters must be set to the desired values. Below is an example of how to use `prepareChainConfig` to obtain the chain configuration for a Rollup chain with a specific `chainId` and `InitialChainOwner`: ```typescript import { prepareChainConfig } from '@arbitrum/chain-sdk'; const chainConfig = prepareChainConfig({ chainId: 123_456, arbitrum: { InitialChainOwner: 0x123...890, // Set the following parameter to `true` to deploy instead an AnyTrust chain DataAvailabilityCommittee: false, }, }); ``` Once we have the `chainConfig`, we can use the function `createRollupPrepareDeploymentParamsConfig` to craft a `Config` structure like the one defined in the section above. Again, this function will set the appropriate defaults for most parameters, allowing you to override any of these defaults. However, the `chainId` and `owner` parameters must be set to the desired values. Additionally, a public client of the parent chain must be passed as an argument to the function. Below is an example of how to use `createRollupPrepareDeploymentParamsConfig` to obtain the chain configuration for a chain with a specific `chainId` and `owner`: ```typescript import { createPublicClient, http } from 'viem'; import { createRollupPrepareDeploymentParamsConfig } from '@arbitrum/chain-sdk'; import { arbitrumSepolia } from 'viem/chains'; const parentChain = arbitrumSepolia; const parentChainPublicClient = createPublicClient({ chain: parentChain, transport: http(parentChainRPC), }); const createRollupConfig = createRollupPrepareDeploymentParamsConfig(parentChainPublicClient, { chainId: 123_456, owner: 0x123...890, chainConfig: chainConfig, }); ``` > **INFO** > > The above defaults to Arbitrum Sepolia. We've added the import `viem/chains` and the chain info to demonstrate from a popular library (`viem`) how to import the chain information already available in the Arbitrum Chain SDK. ### 2. Deploy the Arbitrum chain With the new crafted configuration, we can call the `createRollup` method which will send the transaction to the `RollupCreator` contract and wait until it is executed. Besides the `Config` structure created in the previous step, other parameters from the `RollupDeploymentParams` structure can be passed to override the defaults set by the Chain SDK. Batch poster and validator addresses must be set to the desired values. Additionally, a public client of the parent chain and a deployer PrivateKeyAccount must be passed as arguments to the function. > **INFO** — Additional step for custom gas token chains > > If you want to configure a custom gas token, the deployer needs to give allowance to the `RollupCreator` contract before starting the deployment process, so that it can spend enough tokens to send the correspondant `Parent-to-Child` messages during the deployment process. This process is handled within the `createRollup` function, but the deployer must own enough tokens to create these messages. If you want to configure a custom gas token, you can pass the address in the parent chain of the **ERC-20** token to use in the `nativeToken` parameter of the `createRollup` function. Below is an example of how to use `createRollup` using the `createRollupConfig` crafted in the previous step: ```typescript import { createPublicClient, http } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { createRollup } from '@arbitrum/chain-sdk'; const deployer = privateKeyToAccount(deployerPrivateKey); const parentChainPublicClient = createPublicClient({ chain: parentChain, transport: http(), }); const createRollupResults = await createRollup({ params: { config: createRollupConfig, batchPosters: [batchPoster], validators: [validator], // Uncomment the following line and add the address of the custom gas token // nativeToken: '0xAddressInParentChain', }, account: deployer, parentChainPublicClient, }); ``` ### 3. Understand the returned data After calling `createRollup`, an object of type `CreateRollupResults` is returned with the following fields: ```typescript type CreateRollupResults = { // The transaction sent transaction: CreateRollupTransaction; // The transaction receipt transactionReceipt: CreateRollupTransactionReceipt; // An object with the addresses of the contracts created coreContracts: CoreContracts; }; ``` ### 4. Set the DAC keyset in the `SequencerInbox` (for AnyTrust chains) If you're creating an AnyTrust chain, the next step is to set up the keyset of your Data Availability Committee (DAC) on the `SequencerInbox` contract. This process involves setting up the Data Availability Servers (DAS) and generating the keyset with all DAS keys. See [How to configure a DAC](/launch-arbitrum-chain/chain-config/data-availability/dac-get-started.md) to learn more about setting up a DAC. > **INFO** > > The Arbitrum Chain SDK includes an example script for setting up the keyset in the `SequencerInbox`. We recommend that you first understand the process described in this section and then check the [`set-valid-keyset`](https://github.com/OffchainLabs/arbitrum-chain-sdk/blob/main/examples/set-valid-keyset/index.ts) script. The Chain SDK includes a `setValidKeyset` function to help set the keyset in the SequencerInbox. From the last step, you can gather the `sequencerInbox` and `upgradeExecutor` addresses and pass them to the function along with the `keyset`, a public client of the parent chain, and a wallet client of an account that has executor privileges in the `UpgradeExecutor` contract (to learn more about `UpgradeExecutor`, see [Ownership structure and access control](/launch-arbitrum-chain/operate/ownership-and-access.md)). Below is an example of how to use `setValidKeyset` using the parameters described above: ```typescript import { createPublicClient, createWalletClient, http } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { setValidKeyset } from '@arbitrum/chain-sdk'; const deployer = privateKeyToAccount(deployerPrivateKey); const parentChainPublicClient = createPublicClient({ chain: parentChain, transport: http(), }); const deployerWalletClient = createWalletClient({ account: deployer, chain: parentChain, transport: http(), }); const transactionReceipt = await setValidKeyset({ coreContracts: { upgradeExecutor: '0xUpgradeExecutor', sequencerInbox: '0xSequencerInbox', }, keyset: generatedKeyset, publicClient: parentChainPublicClient, walletClient: deployerWalletClient, }); ``` This function will send the transaction and wait for its execution, returning the transaction receipt. ### 5. Next step Once the chain's contracts are created, you can move to the next step: [configure your Arbitrum chain's node](/launch-arbitrum-chain/deploy/configure-node.md). --- > For a complete page index, fetch # Deploy a token bridge using the Chain SDK > **INFO** — RaaS providers > > It is highly recommended that you work with a Rollup-as-a-Service (RaaS) provider to deploy a production chain. You can find a list of [RaaS providers](/launch-arbitrum-chain/integrations/infrastructure-providers.md#rollup-as-a-service-raas-providers) in our integrations directory. The Arbitrum stack doesn't natively support specific token bridging standards at the protocol level. Instead, Offchain Labs designed a "canonical token bridge" that ensures **ERC-20** token transfers between the parent and child chains. The token bridge architecture includes contracts deployed on the parent and child chains. These entities communicate via the retryable ticket protocol, ensuring efficient and secure interactions. Once you have deployed your Arbitrum chain and have a node running, you can deploy a token bridge for your chain. See the [Overview](/launch-arbitrum-chain/quickstart/sdk-introduction.md) for an introduction to creating and configuring an Arbitrum chain. Before reading this guide, we recommend: * Becoming familiar with the general process of creating new chains explained in [How to deploy an Arbitrum chain](/launch-arbitrum-chain/deploy/deploy-chain.md) * Learning about the canonical token bridge in the [Token bridging](/how-arbitrum-works/deep-dives/token-bridging.md) section ## Parameters used when deploying a token bridge Before we describe the process of deploying a token bridge using the Chain SDK, let's look at the parameters we need to pass to the token bridge creator contract. Deploying a new token bridge for an Arbitrum chain is done through a [`TokenBridgeCreator`](/launch-arbitrum-chain/deploy/canonical-factory-contracts.md) contract that processes the creation of the needed contracts and sends the appropriate `ParentToChild` messages from the parent chain to the child chain so the counterpart contracts of the token bridge are created in the Arbitrum chain. `TokenBridgeCreator` has a `createTokenBridge` function that creates the parent chain contracts of the token bridge and sends the creation message to the Arbitrum chain via retryable tickets. `createTokenBridge` takes four parameters as input: ```solidity address inbox, address rollupOwner, uint256 maxGasForContracts, uint256 gasPriceBid ``` The following table describes these parameters: | Parameter | Type | Description | | -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------- | | `inbox` | address | Address of the Inbox contract of the chain. This is used to uniquely identify the chain. | | `rollupOwner` | address | Account address responsible for deploying, owning, and managing your Arbitrum chain's base contracts on its parent chain. | | `maxGasForContracts` | uint256 | Gas limit used for executing the retryable ticket on the child chain. | | `gasPriceBid` | uint256 | Max gas price used for executing the retryable ticket on the child chain. | When creating the token bridge through the Chain SDK, the parameters `maxGasForContracts` and `gasPriceBid` don't need to be configured, since the SDK will calculate the right values. ## How to deploy a token bridge using the Arbitrum Chain SDK Let's look at the methods to create a token bridge using the Chain SDK. > **INFO** — Example script > > The Arbitrum Chain SDK includes an example script for deploying a token bridge. We recommend that you first understand the process described in this section and then check the [create-token-bridge-eth](https://github.com/OffchainLabs/arbitrum-chain-sdk/blob/main/examples/create-token-bridge-eth/index.ts) and [create-token-bridge-custom-fee-token](https://github.com/OffchainLabs/arbitrum-chain-sdk/blob/main/examples/create-token-bridge-custom-fee-token/index.ts) scripts. Deploying a token bridge for a chain involves the following steps: 1. [Approve the custom gas token (if configured)](#1-approve-the-custom-gas-token-if-configured) 2. [Deploy the token bridge](#2-deploy-the-token-bridge) 3. [Wait for retryable tickets to execute](#3-wait-for-retryable-tickets-to-execute) 4. [Obtain the token bridge contracts (optional)](#4-obtain-the-token-bridge-contracts-optional) 5. [Set up the WETH gateway](#5-set-up-the-weth-gateway) ### 1. Approve the custom gas token (if configured) > **NOTE** > > This step is only a requirement for Arbitrum chains configured to use a custom gas token. Because the token bridge creation involves sending a retryable ticket to the Arbitrum chain, the `TokenBridgeCreator` needs to be able to send the appropriate custom gas token amount for its execution on the child chain. That means that before calling the `TokenBridgeCreator`, we need to grant allowance to the contract to move our custom gas token. To facilitate this process, the Chain SDK provides two functions: 1. `createTokenBridgeEnoughCustomFeeTokenAllowance`: This method verifies that the `TokenBridgeCreator` contract has enough allowance to pay for the fees associated with the token bridge deployment. 2. `createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest`: This function assists in generating the raw transaction required to approve the custom gas token for the `TokenBridgeCreator` contract. Both functions take the following parameters: * `nativeToken`: the address of the custom gas token contract in the parent chain * `owner`: the address of the chain owner * `publicClient`: a viem's public client for the parent chain The following example shows how to use these functions: ```typescript import { createPublicClient, http } from 'viem'; import { createTokenBridgeEnoughCustomFeeTokenAllowance, createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest } from '@arbitrum/chain-sdk'; const parentChainPublicClient = createPublicClient({ chain: parentChain, transport: http(), }); const allowanceParams = { nativeToken, owner: rollupOwner.address, publicClient: parentChainPublicClient, }; if (!(await createTokenBridgeEnoughCustomFeeTokenAllowance(allowanceParams))) { const approvalTxRequest = await createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest(allowanceParams); // sign and send the transaction const approvalTxHash = await parentChainPublicClient.sendRawTransaction({ serializedTransaction: await rollupOwner.signTransaction(approvalTxRequest), }); // get the transaction receipt after waiting for the transaction to complete const approvalTxReceipt = await parentChainPublicClient.waitForTransactionReceipt({ hash: approvalTxHash, }); } ``` ### 2. Deploy the token bridge To initiate the token bridge deployment process, we can call the `createTokenBridgePrepareTransactionRequest` function, which will craft a transaction request to be signed by the chain owner and sent to the `TokenBridgeCreator` contract. After that, we wait for the transaction to be executed and retrieve its receipt with `createTokenBridgePrepareTransactionReceipt`. You'll notice that in this case we use the `rollup` contract instead of the `inbox` contract as input for the `createTokenBridgePrepareTransactionRequest` function. Both contracts can uniquely identify a chain, so either can be used to find the right Inbox contract, but only the latter can be sent to the `TokenBridgeCreator` contract. Below is an example of how to use these functions: ```typescript import { createPublicClient, http } from 'viem'; import { createTokenBridgePrepareTransactionRequest, createTokenBridgePrepareTransactionReceipt } from '@arbitrum/chain-sdk'; const parentChainPublicClient = createPublicClient({ chain: parentChain, transport: http(), }); const orbitChainPublicClient = createPublicClient({ chain: orbitChain, transport: http(), }); const txRequest = await createTokenBridgePrepareTransactionRequest({ params: { rollup: coreContracts.rollup, rollupOwner: rollupOwner.address, }, parentChainPublicClient, account: rollupOwner.address, }); // sign and send the transaction const txHash = await parentChainPublicClient.sendRawTransaction({ serializedTransaction: await rollupOwner.signTransaction(txRequest), }); // get the transaction receipt after waiting for the transaction to complete const txReceipt = createTokenBridgePrepareTransactionReceipt(await parentChainPublicClient.waitForTransactionReceipt({ hash: txHash })); ``` ### 3. Wait for retryable tickets to execute After the transaction executes on the parent chain, we wait for the generated retryable tickets to execute on the child chain. To do this, we use a `waitForRetryable` method available in the `txReceipt` object returned by `createTokenBridgePrepareTransactionReceipt`. Remember that these retryable tickets intend to create the counterpart contracts of the token bridge in the child chain so that they can communicate. The first retryable ticket creates a creator contract on the child chain configured with the templates of all the token bridge contracts. The second retryable creates the actual counterpart contracts of the token bridge. Example: ```typescript // wait for retryables to execute console.log(`Waiting for retryable tickets to execute on the Orbit chain...`); const orbitChainRetryableReceipts = await txReceipt.waitForRetryables({ orbitPublicClient: orbitChainPublicClient, }); console.log(`Retryables executed`); console.log(`Transaction hash for first retryable is ${orbitChainRetryableReceipts[0].transactionHash}`); console.log(`Transaction hash for second retryable is ${orbitChainRetryableReceipts[1].transactionHash}`); if (orbitChainRetryableReceipts[0].status !== 'success') { throw new Error(`First retryable status is not success: ${orbitChainRetryableReceipts[0].status}. Aborting...`); } if (orbitChainRetryableReceipts[1].status !== 'success') { throw new Error(`Second retryable status is not success: ${orbitChainRetryableReceipts[1].status}. Aborting...`); } ``` ### 4. Obtain the token bridge contracts (optional) Once the token bridge deployment is successful, you can use the `getTokenBridgeContracts` method to retrieve all the token bridge contracts' addresses: ```typescript const tokenBridgeContracts = await txReceipt.getTokenBridgeContracts({ parentChainPublicClient, }); ``` ### 5. Set up the WETH gateway > **NOTE** > > That step only applies to ETH-based Arbitrum chains (i.e., not custom gas token chains). The canonical bridge design has a separate custom gateway for **WETH** to bridge it in and out of the Arbitrum chain. > > You can find more info about **WETH** gateways in our ["other gateways flavors" documentation](/how-arbitrum-works/deep-dives/token-bridging.md#other-flavors-of-gateways). Once the token bridge deploys, if the chain uses **ETH** as the gas token, you must set a special gateway to bridge **WETH**. This gateway unwraps **WETH** to bridge it as **ETH** and wraps it back to **WETH** on the destination chain. You can use the methods `createTokenBridgePrepareSetWethGatewayTransactionRequest` and `createTokenBridgePrepareSetWethGatewayTransactionReceipt` to set this gateway, in a similar way to what we used to send the `createTokenBridge` request earlier. This action also sends a retryable ticket to the child chain to create and configure the **WETH** gateway, so you should wait to verify that the ticket executes successfully. Below is an example of how to use these functions: ```typescript import { createPublicClient, http } from 'viem'; import { createTokenBridgePrepareSetWethGatewayTransactionRequest, createTokenBridgePrepareSetWethGatewayTransactionReceipt } from '@arbitrum/chain-sdk'; const parentChainPublicClient = createPublicClient({ chain: parentChain, transport: http(), }); const orbitChainPublicClient = createPublicClient({ chain: orbitChain, transport: http(), }); const setWethGatewayTxRequest = await createTokenBridgePrepareSetWethGatewayTransactionRequest({ rollup: coreContracts.rollup, parentChainPublicClient, account: rollupOwner.address, }); // sign and send the transaction const setWethGatewayTxHash = await parentChainPublicClient.sendRawTransaction({ serializedTransaction: await rollupOwner.signTransaction(setWethGatewayTxRequest), }); // get the transaction receipt after waiting for the transaction to complete const setWethGatewayTxReceipt = createTokenBridgePrepareSetWethGatewayTransactionReceipt(await parentChainPublicClient.waitForTransactionReceipt({ hash: setWethGatewayTxHash })); // Wait for retryables to execute const orbitChainSetWethGatewayRetryableReceipt = await setWethGatewayTxReceipt.waitForRetryables({ orbitPublicClient: orbitChainPublicClient, }); console.log(`Retryables executed`); console.log(`Transaction hash for retryable is ${orbitChainSetWethGatewayRetryableReceipt[0].transactionHash}`); if (orbitChainSetWethGatewayRetryableReceipt[0].status !== 'success') { throw new Error(`Retryable status is not success: ${orbitChainSetWethGatewayRetryableReceipt[0].status}. Aborting...`); } ``` > **WARNING** — You must verify Arbitrum chain contracts' source code > > We have provided a script that will perform the source code verification of all the contracts deployed by the `L1AtomicTokenBridgeCreator` to the specific Arbitrum chain. The script is available in the [Token Bridge Contracts repo](https://github.com/OffchainLabs/token-bridge-contracts/blob/main/docs/deployment.md#verify-orbit-contracts-source-code-on-the-blockscout). --- > For a complete page index, fetch # How to customize ArbOS on your Arbitrum chain Info ### Customizations require expertise Customizing your chain is a core benefit of building with Arbitrum chains. We strongly recommend that teams interested in customizations work alongside a partner with ArbOS and Nitro software expertise, such as a Rollup-as-a-Service team. Working alongside an experienced Arbitrum chain operator can help your team navigate the complex tradeoff space of rollup customizations, which can include performance, security, and cost considerations. Offchain Labs is positioned to train and enable Rollup-as-a-Service in their work with clients to scale support to the Arbitrum chain ecosystem as a whole. As such, Offchain Labs does not necessarily have the capacity to review code changes made by individual Arbitrum chains. We encourage you to leverage your in-house expertise, collaborate with expert partners, and allocate appropriate resources for both an initial implementation (including an audit) and ongoing maintenance and security management of your customization. Customizing the [ArbOS](/how-arbitrum-works/deep-dives/arbos.md) version involves modifying the Nitro codebase to introduce a new, version-controlled iteration of ArbOS (Arbitrum's operating system-like layer). This lets you activate custom behaviors, features, or state changes while maintaining backward compatibility and determinism for fraud proofs. This is an advanced customization primarily for live chains, allowing developers to extend or alter the State Transition Function (STF)—the core logic for block production and state updates—while ensuring safe, non-disruptive upgrades. Unlike standard upgrades (e.g., to canonical versions like ArbOS 20 "Atlas"), customization involves creating intermediate or project-specific versions (e.g., ArbOS 32, a fork of 31). This lets you incorporate bespoke elements such as new precompiles, EVM opcodes, or state variables without conflicting with official releases. This customization is recommended for teams with expertise or partners (e.g., Rollup-as-a-Service providers), as it requires audits, maintenance, and careful handling to avoid issues like chain re-orgs or failed fraud proofs. ## Cases where you may want to consider customizing your own ArbOS upgrade 1. When you want to make changes to your Nitro code that affect the State Transition Function, or STF (you may refer to the [Customize STF docs](/launch-arbitrum-chain/extend-the-protocol/stf.md#introduction)), and 2. If your desired changes need to be made to a live and operational Arbitrum chain If your changes meet both those two points, then you will need a custom ArbOS upgrade. Also, if you made changes to a live and operational chain and want to upgrade them later in the future, then you will likely need an ArbOS upgrade to facilitate the upgrade. ## Where should I insert ArbOS Upgrade related code? Below, you will find four examples of ArbOS-related code changes and, generally, how to make them: ### 1. Add a new method to existing precompile on a specific ArbOS version After you add `sayHi()` to `ArbSys.go` according to the guide in [customize precompile Option 1](/launch-arbitrum-chain/extend-the-protocol/precompiles.md#option-1-add-new-methods-to-an-existing-precompile), you need to continue to modify [precompile.go](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/precompile.go). For example, the original code is: ```go ArbSys := insert(MakePrecompile(pgen.ArbSysMetaData, &ArbSys{Address: types.ArbSysAddress})) arbos.ArbSysAddress = ArbSys.address arbos.L2ToL1TransactionEventID = ArbSys.events["L2ToL1Transaction"].template.ID arbos.L2ToL1TxEventID = ArbSys.events["L2ToL1Tx"].template.ID ``` You need to append the following code to it: ```go ArbSys := insert(MakePrecompile(pgen.ArbSysMetaData, &ArbSys{Address: types.ArbSysAddress})) arbos.ArbSysAddress = ArbSys.address arbos.L2ToL1TransactionEventID = ArbSys.events["L2ToL1Transaction"].template.ID arbos.L2ToL1TxEventID = ArbSys.events["L2ToL1Tx"].template.ID // The arbos version control logic ArbOwner.methodsByName["SayHi"].arbosVersion = ${The arbos version you want to activate this method} ``` In this way, this method will be executed normally and return results only after you update ArbOS to the target version. ### 2. Create a new precompile contract on a specific ArbOS version After you add a new precompile named `ArbHi` according to the guide in [customize precompile Option 2](/launch-arbitrum-chain/extend-the-protocol/precompiles.md#option-2-create-a-new-precompile) and make changes to [`precompile.go`](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/precompile.go), you also need to make the following changes: ```go ArbHi := insert(MakePrecompile(pgen.ArbHiMetaData, &ArbHi{Address: types.ArbHiAddress})) // types.ArbHiAddress here is an example address // Set activate version to the precompile ArbHi.arbosVersion = ${The arbos version you want to activate this precompile} // Set activate version to all method for _, method := range ArbHi.methods { method.arbosVersion = ${The arbos version you want to activate this precompile} } ``` In this way, `ArbHi` and all its methods will be activated after the ArbOS version you set. ### 3. Create a new ArbOS state on a specific ArbOS version After you add a new state `myNumber` according to the guide in [customize precompile Option 5](/launch-arbitrum-chain/extend-the-protocol/precompiles.md#option-5-call-and-modify-state), you also need to rewrite [`UpgradeArbosVersion`](https://github.com/OffchainLabs/nitro/blob/v3.1.0/arbos/arbos/arbosState/arbosstate.go#L253) in [`arbosstate.go`](https://github.com/OffchainLabs/nitro/blob/v3.1.0/arbos/arbosState/arbosstate.go): Add your expected ArbOS version to the switch case statement of `nextArbosVersion`. Here we will take ArbOS V21 as an example: ```go ensure := func(err error) { if err != nil { message := fmt.Sprintf( "Failed to upgrade ArbOS version %v to version %v: %v", state.arbosVersion, state.arbosVersion+1, err, ) panic(message) } } nextArbosVersion := state.arbosVersion + 1 switch nextArbosVersion { case 1: //..... case 2: //.... //..... case 21: // Set your new ArbOS state value here ensure(state.SetNewMyNumber(${random number})) //.... } ``` Here, we will ensure that the initial value of $(a random number) after ArbOS is upgraded to v21. > **CAUTION** > > It should be noted that when you initialize the state (initial code is in [customize precompile Option 5](/launch-arbitrum-chain/extend-the-protocol/precompiles.md#option-5-call-and-modify-state)), you need to initialize it to `0` or `null` value first to avoid a potential re-org on your chain. Also, please make sure your program cannot call the `state.SetNewMyNumber` or other functions that may change the value of `myNumber` before ArbOS v21. To prevent this, if you are using an external call to the precompile contract to change the value, you can refer to [point 1](/launch-arbitrum-chain/extend-the-protocol/arbos.md#1-add-a-new-method-to-existing-precompile-on-a-specific-arbos-version) or [point 2](/launch-arbitrum-chain/extend-the-protocol/arbos.md#2-create-a-new-precompile-contract-on-a-specific-arbos-version) to set the activation time of the precompile contract method. If your nitro code needs to call this method to change the state, you can continue reading [Step 4](/launch-arbitrum-chain/extend-the-protocol/arbos.md#4-any-changes-in-the-stf-logic-that-will-affect-the-final-execution-result-arbos-version-control). ### 4. Any changes in the STF logic that will affect the final execution result (ArbOS version control) If you change the logic in STF, it will cause the execution result of the transaction to be different, you need to keep the original execution logic and put the new logic into another branch. You can use an `if else` statement to control it. For example, we change the `SayHi` return after upgrading the ArbOS version: ```go // The method in ArbHi precompile func (con *ArbHi) SayHi(c ctx, evm mech) (string, error) { if p.state.ArbOSVersion() >= ${The arbos you want to upgrade to} { // Your new logic code return "hi, new ArbOS version", nil } else { // The old logic code needs to be kept return "hi", nil } } ``` In the above code, we use precompiles as an example. Some logic might also affect the STF, such as the methods in [`block_process.go`](https://github.com/OffchainLabs/nitro/blob/v3.11.3/arbos/block_processor.go), [`internal_tx.go`](https://github.com/OffchainLabs/nitro/blob/v3.11.3/arbos/internal_tx.go), [`tx_processor.go`](https://github.com/OffchainLabs/nitro/blob/v3.11.3/arbos/tx_processor.go) and so on. The aforementioned ArbOS control methods will be needed for interacting with different versions of the STF logic. > **TIP** — Backward compatibility > > Wasm module roots are backward compatible, so upgrading them before an ArbOS version upgrade will not disrupt your chain's functionality. ## Upgrade WASM Module Root on the parent chain After you have made your custom ArbOS changes, you will need to update the WASM Module root recorded on the parent chain. This is because an Arbitrum chain may execute the validation and fraud proofs on the parent chain. Please continue reading [Customize STF](/launch-arbitrum-chain/extend-the-protocol/stf.md#step-3-run-the-node) for follow-up operations. ## Schedule ArbOS upgrade After you add ArbOS version control to the Nitro code, you can update ArbOS. You can refer to the [ArbOS upgrade](/launch-arbitrum-chain/operate/arbos-upgrade.md) document to upgrade. It is recommended that teams who opt to version control their custom ArbOS version choose an ArbOS version number that builds on top of the canonical releases shipped by Offchain Labs. As an example, if your team is customizing ArbOS 31, we recommend versioning your ArbOS version as ArbOS 32. It should be noted that if you set a higher ArbOS version as the upgrade target, all the features added between the current and target versions will be activated. For example, if your current version is ArbOS v18 and you set the target version to v25, all the features between v18 and v25 will be loaded. --- > For a complete page index, fetch # How to integrate with the DA API Info ### Customizations require expertise Customizing your chain is a core benefit of building with Arbitrum chains. We strongly recommend that teams interested in customizations work alongside a partner with ArbOS and Nitro software expertise, such as a Rollup-as-a-Service team. Working alongside an experienced Arbitrum chain operator can help your team navigate the complex tradeoff space of rollup customizations, which can include performance, security, and cost considerations. Offchain Labs is positioned to train and enable Rollup-as-a-Service in their work with clients to scale support to the Arbitrum chain ecosystem as a whole. As such, Offchain Labs does not necessarily have the capacity to review code changes made by individual Arbitrum chains. We encourage you to leverage your in-house expertise, collaborate with expert partners, and allocate appropriate resources for both an initial implementation (including an audit) and ongoing maintenance and security management of your customization. This guide will help your team implement their own Data Availability (DA) provider that integrates with Arbitrum Nitro using the DA API. **The DA API is experimental until Nitro officially announces its availability in a release.** ## 1. What is the DA API? The DA API is an extensibility feature in Arbitrum Nitro that allows external data availability providers to integrate with Nitro without requiring any fork of Nitro core or contracts. This extensibility will enable your team to build and deploy custom DA solutions tailored to your specific needs while maintaining full compatibility with the Nitro stack. ### Why use the DA API? * **No forking required**: Integrate your DA system without modifying Nitro or contracts * **Pluggable architecture**: Define your own certificate formats and validation logic * **Fraud-proof compatible**: Full support for [BoLD](/how-arbitrum-works/bold/gentle-introduction.md) challenge protocol and fraud proofs * **Multi-provider support**: Multiple DA systems can coexist on the same chain ### Architecture overview The DA API has four main components: 1. **Reader RPC methods**: Recovers batch data from certificates and collects preimages for fraud proofs 2. **Writer RPC method**: Stores batch data and generates certificates 3. **Validator RPC methods**: Generates proofs for Fraud proof validation 4. **Onchain validator contract** (Solidity): Validates proofs onchain during fraud proof challenges These components work together to enable: * **Normal execution**: [Batch Poster](/how-arbitrum-works/deep-dives/assertions.md) stores data -> generates certificate -> posts to L1 * **Validation**: Node reads certificate -> recovers data -> executes * **Fraud proofs**: Prover generates proof -> enhances with DA data -> validates onchain ### Who is this guide for? This guide is for development teams who want to: * Integrate an existing DA system with Nitro's DA API * Understand how the DA API works under the hood **Prerequisites**: Familiarity with JSON-RPC, Solidity, Ethereum L1/L2 architecture, and Arbitrum Nitro basics. Experience with any backend programming language. ### Reference implementation The Nitro repository includes `ReferenceDA`, a complete working example of a DA provider: * Go implementation: `daprovider/referenceda/` * Solidity contract: `contracts-local/src/osp/ReferenceDAProofValidator.sol` * Example server: `cmd/daprovider/` Use `ReferenceDA` as a reference when building your own provider. It demonstrates all the RPC methods and patterns described in this guide. ## 2. Quickstart guide To create a DA provider, you need to implement two components: #### 1. JSON-RPC server (any language) Your DA provider exposes JSON-RPC methods that Nitro nodes call to store and retrieve data. You can implement this in any language. **Reader methods** (data retrieval): * `daprovider_getSupportedHeaderBytes`: Returns header bytes identifying your provider * `daprovider_recoverPayload`: Recovers batch data from certificate (normal execution) * `daprovider_collectPreimages`: Collects preimages for validation and fraud-proof replay (validation) * `daprovider_recoverPayloadAndPreimages`: Recovers batch data from certificate and also collects preimages for validation **Writer methods** (for batch posting): * `daprovider_getMaxMessageSize`: Returns your provider's current maximum batch size * `daprovider_store`: Stores batch data and returns certificate * `daprovider_startChunkedStore`, `daprovider_sendChunk`, `daprovider_commitChunkedStore` - Optional streaming protocol for large batches (see [Appendix A](#appendix-a-streaming-protocol)) **Validator methods** (fraud-proof generation): * `daprovider_generateReadPreimageProof`: Generates a proof for reading preimage data * `daprovider_generateCertificateValidityProof`: Generates proof of certificate validity #### 2. Onchain validator contract (Solidity) * Implements `ICustomDAProofValidator` interface * `validateReadPreimage()`: Validates preimage read proofs onchain * `validateCertificate()`: Validates certificate authenticity onchain ### Development workflow 1. **Design your certificate format** ([Section 6](#6-designing-your-certificate-format)) 2. **Implement reader RPC methods** to recover data from certificates ([Section 3](#3-implementing-reader-rpc-methods)) 3. **Implement writer RPC method** to generate certificates ([Section 4](#4-implementing-the-writer-rpc-methods)) 4. **Implement validator RPC methods** to generate proofs ([Section 5](#5-implementing-validator-rpc-methods)) 5. **Implement the onchain validator contract** for proof validation (Section 7) 6. **Create your JSON-RPC server** exposing these methods ([Section 9](#9-configuration--deployment)) 7. **Test your integration** end-to-end ([Section 10](#10-testing-your-integration)) 8. **Deploy** your server and configure Nitro nodes ([Section 9](#9-configuration--deployment)) ### Quick reference: `ReferenceDA` example `ReferenceDA` is a complete working example you can study: **`ReferenceDA` implementation**: * **Certificate format**: 99 bytes (header + SHA256 + ECDSA signature), see `daprovider/referenceda/certificate.go` * **JSON-RPC server**: Complete working server at `cmd/daprovider/daprovider.go` * Implements all required RPC methods * Written in Go, but you can use any language * Shows configuration, server setup, and lifecycle management * **Onchain contract**: `contracts-local/src/osp/ReferenceDAProofValidator.sol` * Implements `ICustomDAProofValidator` * Uses ECDSA signature verification with trusted signer mapping ## 3. Implementing reader RPC methods Your JSON-RPC server must implement three Reader methods that Nitro nodes call to retrieve batch data. Implementation is possible in any language. ### Method 1: `daprovider_getSupportedHeaderBytes` Returns the header byte that identifies your DA provider in [Sequencer](/how-arbitrum-works/deep-dives/sequencer.md) messages. **Parameters**: None **Returns**: ```json { "headerBytes": "0x01" // Hex-encoded single byte } ``` **Example**: * External DA providers return `"0x01"` (DA API header byte) * AnyTrust returns `"0x8088"` (two registrations: `0x80` and `0x88`) **Purpose**: Nitro uses this to register your provider in its internal routing table. When it sees a sequencer message with this header byte, it routes the request to your server. Each byte in the returned hex string registers a separate header byte. The DA provider is responsible for any demultiplexing to different backends internally based on certificate contents. ### Method 2: `daprovider_recoverPayload` Recovers the full batch payload data from a certificate. Called during normal node execution. **Parameters**: ```json { "batchNum": "0x1a2b", // Batch number (hex-encoded uint64) "batchBlockHash": "0x1234...", // Block hash when batch was posted "sequencerMsg": "0xabcd..." // Full sequencer message including certificate } ``` **Returns**: ```json { "Payload": "0x5678..." // Hex-encoded batch data } ``` **Implementation requirements**: 1. **Extract the certificate** from `sequencerMsg`: ```shell sequencerMsg format: [SequencerHeader(40 bytes), DACertificateFlag(0x01), Certificate(...)] ``` Skip the first 40 bytes, then extract your certificate. 2. **Validate the certificate**: * Check certificate format/structure * Verify signature or proof * Confirm the certificate is authentic according to your DA system's rules 3. **Retrieve the batch data** using information in the certificate 4. **Verify data integrity**: * Check that the data matches the commitment in the certificate * Example: `sha256(data) == certificate.dataHash` 5. **Return the payload** or error **Error Handling**: Your implementation must distinguish between three scenarios: **1. Invalid certificate -> CertificateValidationError** * **When**: Certificate is invalid (bad format, bad signature, untrusted signer, etc.) * **Return**: JSON-RPC error with message containing `"certificate validation failed"` * **Nitro behavior**: Treats batch as empty (zero transactions), syncing continues * **Example**: `"certificate validation failed: untrusted signer"` **2. Other errors -> Syncing Stops** * **When**: Storage failure, network error, RPC timeout, database down * **Return**: JSON-RPC error with any other message * **Nitro behavior**: **Stops syncing immediately** * **Example**: `"storage unavailable: database connection lost"` **3. Empty batch -> Valid Response** * **When**: Certificate is valid, but batch data is actually empty * **Return**: Successful response with `"Payload": null` or `"Payload": "0x"` * **Nitro behavior**: Processes empty batch (zero transactions), syncing continues * **Important**: Only return nil/empty if the batch is truly empty, not as an error signal > **DANGER** — Critical rule > > The DA provider is a **trusted component**. Nitro cannot validate what you return, so you must: > > * Return proper errors when there are errors (don't return nil payload) > * Use CertificateValidationError for invalid certificates (allows processing to continue) > * Use other errors for infrastructure failures (stops syncing until resolved) **Detection mechanism**: Nitro detects CertificateValidationError by checking if the error message **contains the string** `"certificate validation failed"`. The error can include additional context after this string.
Success: returns data ```json { "jsonrpc": "2.0", "id": 1, "result": { "Payload": "0x000123456789abcdef..." } } ```
Success with empty batch ```json { "jsonrpc": "2.0", "id": 1, "result": { "Payload": "0x" } } ```
Invalid certificate ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32000, "message": "certificate validation failed: untrusted signer" } } ```
Storage error: syncing stops ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32000, "message": "storage unavailable: database connection timeout" } } ```
### Method 3: `daprovider_collectPreimages` Collects preimage mappings needed for fraud-proof replay (called during validation). **Parameters**: Same as `daprovider_recoverPayload` ```json { "batchNum": "0x1a2b", "batchBlockHash": "0x1234...", "sequencerMsg": "0xabcd..." } ``` **Returns**: ```json { "Preimages": { "0xabcd1234...": { "Data": "0x5678...", "Type": 3 } } } ``` **Implementation requirements**: 1. **Do the same work as `recoverPayload`**: * Extract certificate * Validate certificate * Retrieve batch data 2. **Build the preimage mapping**: * Compute `certHash = keccak256(certificate)` * Map `certHash -> batch data` * Set preimage type to `3` (DACertificatePreimageType) **Critical**: The preimage key **must be `keccak256(certificate)`**, not your internal hash. The fraud-proof replay binary expects this specific key. **Example response**: ```json { "Preimages": { "0xabcd1234...certHash": { "Data": "0x5678...batchData", "Type": 3 } } } ``` **Error handling**: `collectPreimages` has **identical error handling behavior** to `recoverPayload`: * Invalid certificate -> return error containing `"certificate validation failed"` (validator continues, treating batch as empty) * Other errors -> return other error (validator stops) * Empty batch -> return empty preimages map (valid, validator continues) See the [Error Handling section in `recoverPayload`](#method-2-daprovider_recoverpayload) above for complete details. **Why two separate methods?** * `recoverPayload`: Fast path for normal execution (just needs the data) * `collectPreimages`: Validation path (needs data + keccak256 mapping for fraud proofs) This separation avoids unnecessary work in each context. ### Method 4: `daprovider_recoverPayloadAndPreimages` Recovers payload and also collects preimage mappings needed for fraud-proof replay (called during validation). **Parameters**: Same as `daprovider_recoverPayload` ```json { "batchNum": "0x1a2b", "batchBlockHash": "0x1234...", "sequencerMsg": "0xabcd..." } ``` **Returns**: ```json { "Payload": "0x5678..." // Hex-encoded batch data, "Preimages": { "0xabcd1234...": { "Data": "0x5678...", "Type": 3 } } } ``` **Why this method?** * It is required to enable collection of preimages during normal execution * This avoids duplicating of code in nitro to be able to collect preimages for validation ### Parameter encoding **All `uint64` parameters must be hex-encoded strings with `0x` prefix**: * Correct: `"0x1a2b"`, `"0x0"`, `"0xff"` * Incorrect: `42`, `"42"`, `"0x"` **All byte arrays use hex encoding with `0x` prefix**: * Correct: `"0xabcdef"`, `"0x01ff"` * Incorrect: `"abcdef"`, `[0xab, 0xcd]` ### Reference: `ReferenceDA` implementation The `ReferenceDA` server (`cmd/daprovider/`) shows a complete working implementation in Go. Key logic: **Certificate extraction**: ```go certBytes := sequencerMsg[40:] // Skip 40-byte sequencer header ``` **Certificate validation** (calls L1 contract): ```go validator, _ := NewReferenceDAProofValidator(validatorAddr, l1Client) err := cert.ValidateWithContract(validator, &bind.CallOpts{}) ``` **Preimage recording**: ```go certHash := crypto.Keccak256Hash(certBytes) preimages[certHash.Hex()] = PreimageResult{ Data: hexutil.Encode(payload), Type: 3, // DACertificatePreimageType } ``` While `ReferenceDA` is written in Go, you can implement these methods in any language. You need to: * Parse hex-encoded JSON-RPC requests * Query an Ethereum L1 node * Query your DA system * Store and retrieve data * Compute keccak256 hashes ## 4. Implementing the writer RPC methods Your JSON-RPC server implements Writer methods that the Batch Poster calls to store batch data and get a certificate. ### Method: `daprovider_getMaxMessageSize` Returns the maximum message size your DA provider will accept. The batch poster calls this before building each batch to determine the size limit. **Parameters**: None **Returns**: ```json { "maxSize": 1000000 // Maximum message size in bytes (integer) } ``` **Implementation requirements**: * MUST return a positive value (> 0) * Returning 0 or negative will cause an error in the batch poster * Value may change dynamically between calls (e.g., when your provider falls back to a backend with lower limits) **Example response**: ```json { "jsonrpc": "2.0", "id": 1, "result": { "maxSize": 1000000 } } ``` ### Method: `daprovider_store` Stores batch data and returns a certificate that will post to L1. **Parameters**: ```json { "message": "0x1234...", // Batch data to store (hex-encoded) "timeout": "0x67a30580" // Expiration time as Unix timestamp (hex-encoded uint64) } ``` The `timeout` parameter specifies when the stored data should expire: * **Unix timestamp** (seconds since January 1, 1970 UTC) * **Minimum retention**: DA provider must retain data **at least** until this time * **Calculated by batch poster** as: `current_time + retention_period` **Returns**: ```json { "serialized-da-cert": "0x01..." // Certificate (hex-encoded) } ``` **Implementation Requirements**: 1. **Store the batch data** in your DA system: * Must be retrievable later using the certificate 2. **Generate a certificate**: * Must start with bytes `0x01` (DA API header) + your optional provider type byte(s). Refer to the description in the section on `daprovider_getSupportedHeaderBytes` * Must contain enough information to retrieve the data later * Should include a data commitment (hash, Merkle root, etc.) * Should include proof of authenticity (signature, BLS Signature, etc.) 3. **Return the certificate** as hex-encoded bytes **Example Request**: ```json { "jsonrpc": "2.0", "id": 1, "method": "daprovider_store", "params": [ { "message": "0x00012345...", "timeout": "0x67a30580" } ] } ``` **Example Response (Success)**: ```json { "jsonrpc": "2.0", "id": 1, "result": { "serialized-da-cert": "0x011234567890abcdef..." } } ``` ### Certificate format Your certificate should: * **Start with `0x01`** (DA API header byte, defined as `DACertificateMessageHeaderFlag`) * **Optional provider type bytes**: Your provider type identifier, see description in the [section on `daprovider_getSupportedHeaderBytes`](#method-1-daprovider_getsupportedheaderbytes) * **Contain a data commitment**: Hash, Merkle root, KZG commitment, etc. * **Contain validity proof**: Signature, BLS signature, or other authentication * **Be compact**: Certificates are posted to L1 as calldata **`ReferenceDA` Example** (99 bytes total): ```shell [0] : `0x01` (DA API header) [1] : `0xFF` (`ReferenceDA` provider type) [2-33] : SHA256(batch data) - 32 bytes [34-98] : ECDSA signature (v, r, s) - 65 bytes ``` See [Section 6](#6-designing-your-certificate-format) for detailed guidance on certificate design. ### Dynamic batch resizing If a batch exceeds your current size limit, you can signal this by returning an error containing: ```shell "message too large for current DA backend" ``` When the batch poster receives this error, it will: 1. Call `daprovider_getMaxMessageSize` again (which may return a smaller value) 2. Rebuild the batch with the new size constraints 3. Retry the store with the same writer This is useful if your provider has internal fallback scenarios. For example, if your primary backend is temporarily constrained, you can signal a smaller size limit and the batch poster will automatically adapt without falling back to a different DA provider entirely. **Example resize response**: ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32000, "message": "message too large for current DA backend: max size is 500000 bytes" } } ``` ### Fallback mechanism The Batch Poster supports multiple DA writers in a sequential fallback chain (External DA -> AnyTrust if enabled -> EthDA). If your server wants to trigger fallback to the next writer (e.g., temporary unavailability, overload), return an error containing the string: ```shell "DA provider requests fallback to next writer" ``` **Example fallback response**: ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32000, "message": "DA provider requests fallback to next writer: storage temporarily unavailable" } } ``` > **INFO** — Important > > The fallback error and the resize error are the **only** ways to trigger automatic recovery. Any other error will **stop the batch posting entirely** without trying other writers or resizing. This design prevents expensive surprise costs from fixable infrastructure issues. ### Error handling **Return errors for**: * Storage failures (disk full, network down) * Invalid batch data * Timeout exceeded * System overload (use fallback error) **Example error response**: ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32000, "message": "storage failed: disk quota exceeded" } } ``` ### Reference: `ReferenceDA` implementation The `ReferenceDA` server shows a complete implementation. Key logic: **Store batch data** (in-memory for demo, use real storage in production): ```go storage.Store(message) ``` **Generate certificate** with SHA256 hash + ECDSA signature: ```go dataHash := sha256.Sum256(message) sig, _ := signer(dataHash[:]) certificate := []byte{ 0x01, // DA API header 0xFF, // ReferenceDA type } certificate = append(certificate, dataHash[:]...) // 32 bytes certificate = append(certificate, sig...) // 65 bytes (v, r, s) ``` **Return certificate**: ```go return StoreResult{ SerializedDACert: hexutil.Encode(certificate), } ``` While `ReferenceDA` uses Go, you can implement `daprovider_store` in any language. You need to be able to: * Accept JSON-RPC requests * Store data persistently * Generate cryptographic signatures/commitments * Return hex-encoded responses ### Streaming protocol methods (optional) For large batches exceeding HTTP body limits (default: 5MB), you'll need to implement these three additional RPC methods: * `daprovider_startChunkedStore`: Initiates chunked storage session * `daprovider_sendChunk`: Sends individual chunks (can be sent in parallel) * `daprovider_commitChunkedStore`: Finalizes stream and returns certificate See [Appendix A: Streaming Protocol](#appendix-a-streaming-protocol) for complete specifications, parameter details, and implementation guidance. ## 5. Implementing validator RPC methods Your JSON-RPC server implements two Validator methods that generate cryptographic proofs for fraud proof validation. These are **critical for security**—the fraud-proof system must be able to prove both valid and invalid certificates onchain. ### Method 1: `daprovider_generateReadPreimageProof` Generates an **opening proof** for reading a specific **32-byte range** of the committed batch data during fraud proof validation. The `offset` parameter specifies the **32-byte-aligned** starting position (must be a multiple of 32). **Parameters**: ```json { "certHash": "0xabcd...", // keccak256 hash of the certificate "offset": "0x0", // 32-byte-aligned offset (must be multiple of 32, hex-encoded uint64) "certificate": "0x01..." // Full certificate bytes } ``` > **NOTE** > > The `offset` must be 32-byte aligned (0, 32, 64, 96, ...). The proof covers exactly 32 bytes starting at this offset. **Returns**: ```json { "proof": "0x..." // Your DA-system-specific proof data (hex-encoded) } ``` **Implementation approaches**: Your implementation can use one of two approaches: **1. Simple approach (like `ReferenceDA`)**: Include the full preimage * Proof contains the entire batch data * Easy to implement, returns the stored data * **Inefficient for large batches**: 5MB batch = 5MB proof **2. Advanced approach**: Use cryptographic opening proofs * Proof contains only a commitment opening for the requested byte range * **Efficient for large batches**: 5MB batch = typically <1KB proof * Requires a cryptographic commitment scheme (see examples below) **Implementation steps**: 1. **Parse the certificate** to extract information about the stored data 2. **Retrieve the batch data** from your DA storage 3. **Build a proof** using one of these approaches: **Simple (Full Preimage)**: * Include the entire batch payload in the proof * Format: `[version, preimageSize, preimageData]` **Advanced (Opening Proof)**: * Generate a cryptographic opening for the 32-byte range at `offset` * Include only the commitment opening, not the full data * See cryptographic scheme examples below 4. **Return the proof** as hex-encoded bytes **Cryptographic commitment scheme examples**: If you're implementing the advanced approach with opening proofs, here are common schemes: **KZG polynomial commitments** (used in EIP-4844, Celestia, EigenDA): * Commit to batch data as a polynomial * Generate a **point evaluation proof** for the 32-byte chunk at `offset` * Proof size: \~48 bytes (constant, regardless of batch size) * Verification: onchain pairing check proves polynomial evaluates correctly at that position * Example: EIP-4844 uses `POINT_EVALUATION_PRECOMPILE` for this ```shell Proof format: [commitment (48 bytes), evaluation (32 bytes), proof (48 bytes)] ``` **Merkle tree commitments**: * Organize batch into 32-byte chunks as tree leaves * Generate an **inclusion proof** for the leaf at position `offset / 32` * Proof size: \~log₂(n) \* 32 bytes (e.g., 512 bytes for 64K leaves) * Verification: Hash authentication path to prove chunk inclusion * Common in Bitcoin, Ethereum state trees ```shell Proof format: [leaf_data, sibling_hash₁, sibling_hash₂, ..., sibling_hashₙ] ``` **Vector commitments**: * Commit to batch as a vector of elements * Generate a **position opening** for the specific index/range * Proof size: Constant (scheme-dependent, often \~32-96 bytes) * Verification: Algebraic check proves correct opening * Used in some modern DA systems ```shell Proof format: [element_value, opening_proof, auxiliary_data] ``` **Comparison**: | Approach | Proof Size (5MB batch) | Pros | Cons | | ---------------------- | ---------------------- | ------------------------ | ----------------------------------------------- | | **Full Preimage** | 5MB | Simple to implement | Huge proofs, expensive L1 verification | | **KZG Commitments** | \~128 bytes | Constant size, efficient | Requires trusted setup, pairing-friendly curves | | **Merkle Trees** | \~512 bytes | No trusted setup, simple | Logarithmic size, multiple hashes to verify | | **Vector Commitments** | \~64 bytes | Constant size, flexible | More complex cryptography | **What happens next**: * The proof enhancer prepends `[certSize(8), certificate]` to your proof * The complete proof is sent to your onchain `validateReadPreimage()` function * Your contract extracts up to 32 bytes starting at `offset` and returns them **Example request**: ```json { "jsonrpc": "2.0", "id": 1, "method": "daprovider_generateReadPreimageProof", "params": [ { "certHash": "0xabcd1234...", "offset": "0x0", "certificate": "0x01..." } ] } ``` **Example Response**: ```json { "jsonrpc": "2.0", "id": 1, "result": { "proof": "0x0100000000000001005678..." } } ``` **`ReferenceDA` proof format** (Simple Approach): `ReferenceDA` uses the simple approach, including the full preimage in the proof: ```shell [0] : version (`0x01`) [1-8] : preimageSize (8 bytes, big-endian uint64) [9...] : preimageData (full batch payload - entire 5MB for a 5MB batch!) ``` This method is easy to implement but **inefficient** for large batches. Production DA systems should consider using cryptographic commitments (e.g., KZG, Merkle, etc.) to generate compact opening proofs instead. ### Method 2: `daprovider_generateCertificateValidityProof` Generates a proof of whether a certificate is valid or invalid according to your DA system's rules. **Parameters**: ```json { "certificate": "0x01..." // Certificate to validate } ``` **Returns**: ```json { "proof": "0x..." // Validity proof (hex-encoded) } ``` > **DANGER** — Critical rule > > Invalid certificates (bad format, bad signature, untrusted signer) must return a successful response with `claimedValid=0` in the proof. Do **not** return an error. **Only return errors for**: * Network failures (can't reach L1, database down) * RPC timeouts * Other transient issues **Why?** The fraud-proof system needs to prove "this certificate is invalid" onchain. If you return an error, this proof becomes impossible. **Implementation requirements**: 1. **Validate the certificate**: * Check format/structure * Verify signature or cryptographic proof * Check against trusted signers (if applicable) 2. **Determine validity**: * Valid -> `claimedValid = 1` * Invalid (any reason) -> `claimedValid = 0` 3. **Build proof** containing: * `claimedValid` byte (0 or 1) * Any additional data your onchain validator needs * For `ReferenceDA`: `[claimedValid(1 byte), version(1 byte)]` 4. **Return the proof** (**not** an error, even for invalid certs!) **Example request**: ```json { "jsonrpc": "2.0", "id": 1, "method": "daprovider_generateCertificateValidityProof", "params": [ { "certificate": "0x01ff1234..." } ] } ``` **Example responses**
Valid certificate ```json { "jsonrpc": "2.0", "id": 1, "result": { "proof": "0x0101" // claimedValid=1, version=1 } } ```
invalid certificate - still success! ```json { "jsonrpc": "2.0", "id": 1, "result": { "proof": "0x0001" // claimedValid=0, version=1 } } ```
network error - only now return Error ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32000, "message": "failed to query L1 contract: connection timeout" } } ```
**`ReferenceDA` Proof format**: ```shell [0] : `claimedValid` (`0x00` = invalid, `0x01` = valid) [1] : version (`0x01`) ``` ### Proof flow summary **What the proof enhancer does**: 1. Receives your custom proof from the RPC method 2. Prepends standardized header: `[certSize(8 bytes), certificate, ...]` 3. Sends complete proof to your onchain validator contract **For `generateReadPreimageProof`**: ```shell Complete proof = [machineProof..., certSize(8), certificate, yourCustomProof] ↑ [version, size, preimageData] ``` **For `generateCertificateValidityProof`**: ```shell Complete proof = [machineProof..., certSize(8), certificate, claimedValid(1), yourCustomProof] ↑ [0 or 1, version, ...] ``` ### Reference: `ReferenceDA` implementation The `ReferenceDA` server demonstrates the complete pattern: **GenerateCertificateValidityProof** key logic: ```go // Parse certificate cert, err := Deserialize(certificate) if err != nil { // Invalid format -> claimedValid=0, not an error! return proof{claimedValid: 0, version: 1} } // Verify signature signer, err := cert.RecoverSigner() if err != nil { // Invalid signature -> claimedValid=0 return proof{claimedValid: 0, version: 1} } // Query L1 contract for trusted signers isTrusted, err := l1Contract.TrustedSigners(signer) if err != nil { // Network error -> return actual error return error(err) } // Build proof return proof{claimedValid: boolToByte(isTrusted), version: 1} ``` **GenerateReadPreimageProof** key logic: ```go // Parse certificate to get data hash cert, err := Deserialize(certificate) // Retrieve full batch data from storage preimageData, err := storage.GetByHash(cert.DataHash) // Build proof with full data return proof{ version: 1, preimageSize: len(preimageData), preimageData: preimageData, } ``` While `ReferenceDA` uses Go, you can implement these methods in any language. ## 6. Designing your certificate format The certificate is the core data structure in your DA system. It's posted to L1 and used to retrieve batch data. ### Header requirements All DA API certificates must start with: 1. **Byte 0**: `0x01` (defined as `daprovider.DACertificateMessageHeaderFlag`) 2. **Bytes 1-n**: Your certificate may optionally identify itself with provider bytes, which Nitro uses for routing the request to the right provider server. ### What to include Your certificate should contain: 1. **Data commitment**: A hash or commitment to the batch payload * `ReferenceDA` uses the SHA256 hash of the payload * You could use keccak256, SHA3, Merkle root, KZG commitment, etc. 2. **Validity proof**: Information that proves the certificate is authentic * `ReferenceDA` uses ECDSA signature over the data hash * You could use BLS signatures, Merkle proofs, aggregated signatures, etc. 3. **Any other metadata**: Whatever your onchain validator needs * Timestamps, version numbers, provider IDs, etc. ### Size considerations Certificates post to L1 as calldata, so **smaller is better** for gas costs. * `ReferenceDA`: 99 bytes (header(2) + hash(32) + signature(65)) ### Format flexibility The DA API treats certificates as **opaque blobs**. The core Nitro system only cares about: * The initial `0x01` DACertificateMessageHeaderFlag, plus optional provider bytes for routing to the correct external DA system * `keccak256(certificate)` as the preimage key * The certificate is posted to L1 intact Everything else is up to you. Your Reader, Writer, Validator, and onchain contract define the format and validation rules. ### `ReferenceDA` certificate format File: `daprovider/referenceda/certificate.go` ```shell Byte Layout (99 bytes total): [0] : `0x01` (DA API header, DACertificateMessageHeaderFlag) [1] : `0xFF` (`ReferenceDA` provider type) [2-33] : SHA256 hash of payload (32 bytes) [34] : ECDSA signature `V` value (1 byte) [35-66] : ECDSA signature `R` value (32 bytes) [67-98] : ECDSA signature `S` value (32 bytes) ``` **Design rationale**: `ReferenceDA` is a simple example to demonstrate basic features needed by a certificate: * SHA256 provides data commitment * ECDSA signature proves authenticity (verifiable onchain with `ecrecover`) * Trusted signer mapping determines validity (configured on validator contract) ## 7. Implementing the onchain validator contract The onchain validator contract validates proofs during fraud-proof challenges. This contract is the security-critical component that ensures only valid data is accepted. ### Interface definition File: `contracts/src/osp/ICustomDAProofValidator.sol` ```solidity interface ICustomDAProofValidator { /** * @notice Validates a proof for reading a preimage at a specific offset * @param certHash Keccak256 hash of the certificate * @param offset Offset in the preimage to read * @param proof Complete proof data (format: [certSize(8), certificate, customData...]) * @return preimageChunk Up to 32 bytes of preimage data at the specified offset */ function validateReadPreimage( bytes32 certHash, uint256 offset, bytes calldata proof ) external view returns (bytes memory preimageChunk); /** * @notice Validates whether a certificate is authentic * @param proof Complete proof data (format: [certSize(8), certificate, claimedValid(1), validityProof...]) * @return isValid True if certificate is valid, false otherwise * * IMPORTANT: Must **not** revert for invalid certificates, will return false instead. */ function validateCertificate( bytes calldata proof ) external view returns (bool isValid); } ``` ### `validateReadPreimage` implementation This function must: 1. **Extract the certificate** from the proof (first 8 bytes are certificate size, followed by certificate) 2. **Verify the certificate hash** matches `certHash` (security critical!) 3. **Extract your custom proof data** (everything after the certificate) 4. **Validate the custom proof** according to your DA system's rules 5. **Extract and return** up to 32 bytes of preimage data starting at `offset` **Security requirements**: * MUST verify `keccak256(certificate)` matches the provided `certHash` * MUST validate that the preimage data matches the commitment in the certificate * Can revert on invalid proofs (this is a read operation, not validity determination) ### `validateCertificate` implementation This function must: 1. **Extract the certificate** from proof 2. **Extract claimedValid** byte (at `proof[8 + certSize]`) 3. **Validate the certificate** according to your DA system's rules 4. **Return true if valid, false if invalid** > **DANGER** — Critical Requirement > > This function **must not revert for invalid certificates**. It should: > > * Return `true` for valid certificates > * Return `false` for invalid certificates (bad format, bad signature, untrusted signer, etc.) > * Only revert for truly unexpected conditions (e.g., internal contract errors) **Why?** The fraud-proof system needs to be able to prove "this certificate is invalid" onchain. If the function reverts, this proof becomes impossible. ### Proof format The `OneStepProverHostIo` contract passes proofs in this format: **For validateReadPreimage**: ```shell [certSize(8 bytes), certificate, yourCustomProofData] ``` **For validateCertificate**: ```shell [certSize(8 bytes), certificate, claimedValid(1 byte), yourCustomProofData] ``` Extract components like: ```solidity // Extract certificate size uint64 certSize = uint64(bytes8(proof[0:8])); // Extract certificate bytes calldata certificate = proof[8:8 + certSize]; // Extract custom proof (for validateReadPreimage) bytes calldata customProof = proof[8 + certSize:]; // Extract claimedValid and custom proof (for validateCertificate) uint8 claimedValid = uint8(proof[8 + certSize]); bytes calldata customProof = proof[8 + certSize + 1:]; ``` ### Security checks performed by OSP The `OneStepProverHostIo` contract performs critical security checks before calling your validator: 1. **Certificate Hash Verification**: Verifies that the `keccak256(certificate) == certHash` (prevents certificate substitution) 2. **Claim Verification** (for validateCertificate): Verifies prover's `claimedValid` matches validator's return value You don't need to implement these checks—the OSP enforces them. You only need to validate the format of your certificate and the proofs. ### Example: `ReferenceDA` validator contract File: `contracts-local/src/osp/ReferenceDAProofValidator.sol` **Constructor and Storage**: ```solidity mapping(address => bool) public trustedSigners; constructor(address[] memory _trustedSigners) { for (uint256 i = 0; i < _trustedSigners.length; i++) { trustedSigners[_trustedSigners[i]] = true; } } ``` **validateCertificate**: ```solidity function validateCertificate( bytes calldata proof ) external view returns (bool) { // 1. Extract certificate uint64 certSize = uint64(bytes8(proof[0:8])); bytes calldata certificate = proof[8:8 + certSize]; // 2. Validate certificate structure if (certificate.length != 99) return false; if (certificate[0] != 0x01) return false; // DA API header if (certificate[1] != 0xFF) return false; // ReferenceDA type // 3. Extract certificate components bytes32 dataHash = bytes32(certificate[2:34]); uint8 v = uint8(certificate[34]); bytes32 r = bytes32(certificate[35:67]); bytes32 s = bytes32(certificate[67:99]); // 4. Recover signer using ecrecover address signer = ecrecover(dataHash, v, r, s); if (signer == address(0)) return false; // 5. Check if the signer is trusted return trustedSigners[signer]; } ``` **validateReadPreimage**: ```solidity function validateReadPreimage( bytes32 certHash, uint256 offset, bytes calldata proof ) external view returns (bytes memory) { // 1. Extract certificate (99 bytes for ReferenceDA) bytes calldata certificate = proof[8:8 + 99]; // 2. Extract custom proof: [version(1), preimageSize(8), preimageData] uint8 version = uint8(proof[8 + 99]); uint64 preimageSize = uint64(bytes8(proof[8 + 99 + 1:8 + 99 + 9])); bytes calldata preimageData = proof[8 + 99 + 9:8 + 99 + 9 + preimageSize]; // 3. Extract the data hash from the certificate bytes32 dataHash = bytes32(certificate[2:34]); // 4. Verify that the preimage matches the certificate's hash if (sha256(preimageData) != dataHash) { revert("Preimage hash mismatch"); } // 5. Returns up to 32 bytes at offset if (offset >= preimageSize) { return new bytes(0); } uint256 remainingBytes = preimageSize - offset; uint256 chunkSize = remainingBytes > 32 ? 32 : remainingBytes; bytes memory chunk = new bytes(chunkSize); for (uint256 i = 0; i < chunkSize; i++) { chunk[i] = preimageData[offset + i]; } return chunk; } ``` **Key points**: * validateCertificate returns `false` for invalid certificates, never reverts * Uses `ecrecover` for signature verification * Validates that the SHA256 hash matches * Extracts 32-byte chunks for preimage reads ## 8. Understanding proof enhancement Proof enhancement is the bridge between the WASM execution environment (which has no network access) and onchain fraud-proof validation (which requires DA-specific data). ### The challenge During fraud-proof challenges: * The prover (WASM binary) runs in a fully deterministic environment **without network access** * When it encounters DA API operations, it can't call your DA provider to get proofs * But the onchain validator needs these proofs to verify the fraud proof ### How proof enhancement works **Step 1: WASM signals enhancement needed** When the replay binary encounters a DA API operation (reading preimage or validating certificate), it: 1. Sets the `ProofEnhancementFlag (0x80)` in the machine status byte (first byte of proof) 2. Appends marker data to the end of the proof: * `0xDA` for preimage read operations * `0xDB` for certificate validation operations 3. Returns the incomplete proof **Step 2: Proof enhancer detects and routes** The validator's proof enhancement manager (file: `validator/proofenhancement/proof_enhancer.go`): 1. Detects the enhancement flag in the proof 2. Reads the marker byte to determine operation type 3. Routes to the appropriate enhancer (`ReadPreimage` or `ValidateCertificate`) **Step 3: Certificate retrieved from L1** The enhancer retrieves the certificate **from L1, not from the DA provider**: 1. Finds which batch contains the message: `inboxTracker.FindInboxBatchContainingMessage(messageNum)` 2. Gets sequencer message bytes: `inboxReader.GetSequencerMessageBytes(ctx, batchNum)` 3. Extracts certificate: `certificate = sequencerMessage[40:]` (skip 40-byte header) 4. Validates certificate hash matches what the proof expects **Step 4: Validator RPC called** The enhancer calls your Validator interface: * For preimage reads: `validator.GenerateReadPreimageProof(certHash, offset, certificate)` * For certificate validation: `validator.GenerateCertificateValidityProof(certificate)` **Step 5: Complete proof built** The enhancer builds the complete proof: ```shell [...originalMachineProof, certSize(8), certificate, customProof] ``` The marker data has been **removed**—its only purpose was for enhancement routing. **Step 6: Proof submitted to OSP** The BOLD State Provider submits the enhanced proof to the OneStepProverHostIo contract, which validates it onchain. ### Why certificates come from L1 Certificates **always** come from L1 Sequencer Inbox messages\*\*, never from external DA providers. This process ensures: * Proofs are always verifiable without network dependencies * No trust in the DA provider's availability during challenges * Complete determinism and reproducibility The sequencer message format is: ```shell [SequencerHeader(40 bytes), DACertificateFlag(0x01), Rest of certificate(...)] ``` Certificates get included in L1 calldata and are always available. ### What your validator must provide Your `Validator` interface implementation must return proofs that: * Match the format your onchain validator contract expects * Contain all data needed for onchain verification * Don't require any additional network calls or external data For `ReferenceDA`: * `ReadPreimage` proof: `[version(1), preimageSize(8), preimageData]` * Validity proof: `[claimedValid(1), version(1)]` ## 9. Configuration & deployment This section covers creating the Arbitrum chain with custom DA support, configuring Nitro nodes to connect to your DA provider and deploying your DA provider server. ### Create the Arbitrum chain with custom DA support To use a custom DA validator contract for the onchain proving process, the minimum nitro-contracts version required is v3.2, which is available in the `deploy-custom-da-val` branch on the nitro-contracts repository. > **INFO** > > Nitro contracts v3.2 have not been publicly released yet and is not supported in the latest version of the Arbitrum Chain SDK. The following instructions specify how to deploy a RollupCreator factory contract and create a new chain with it. However, it's recommended to use the canonical RollupCreator factory contract and the Arbitrum Chain SDK once they are available. You can deploy a RollupCreator factory contract following the instructions in [How to deploy new factory contracts](/launch-arbitrum-chain/deploy/canonical-factory-contracts.md#deploying-a-rollupcreator). Before creating the Arbitrum chain, you must have the onchain custom validator contract and the OneStepProof (OSP) contracts deployed on the parent chain. Deploy the onchain validator contract first, and then use the script available at `scripts/deployOsp.ts` to deploy the OSP contracts. Specify the address of the custom validator contract in a `CUSTOM_DA_VALIDATOR` env variable and use hardhat to run the script: ```shell yarn run hardhat run scripts/deployOsp.ts --network parentChainNetwork ``` This will deploy all OSP contracts. Take note of the `OneStepProofEntry` contract address since we'll needed when creating the chain. Finally, create the chain with custom DA support following the instructions in the nitro-contracts [deployment doc](https://github.com/OffchainLabs/nitro-contracts/blob/deploy-custom-da-val/docs/deployment.md#3-create-new-rollup-chains). You'll have to specify the address of the OneStepProofEntry contract in the `config` used to create the chain, [here](https://github.com/OffchainLabs/nitro-contracts/blob/deploy-custom-da-val/scripts/config.example.ts#L72). Once the chain's contracts are deployed, you can configure your nitro node following the instructions in the next section. ### Nitro node configuration Nitro nodes connect to DA providers via JSON-RPC. Configure your provider with these flags: **Single Provider**: ```shell --node.da.external-provider.enable --node.da.external-provider.with-writer --node.da.external-provider.rpc.url=http://your-da-provider:8547 ``` **Multiple Providers**: ```shell --node.da.external-providers='[ {"rpc":{"url":"http://provider1:8547"},"with-writer":true}, {"rpc":{"url":"http://provider2:8547"},"with-writer":true} ]' ``` **RPC Connection Options**: ```shell --node.da.external-provider.rpc.url # RPC endpoint URL --node.da.external-provider.rpc.timeout # Per-response timeout (0 = disabled) --node.da.external-provider.rpc.jwtsecret # Path to JWT secret file for auth --node.da.external-provider.rpc.connection-wait # How long to wait for initial connection --node.da.external-provider.rpc.retries=3 # Number of retry attempts --node.da.external-provider.rpc.retry-delay # Delay between retries ``` **Batch Poster Configuration**: ```shell --node.batch-poster.disable-dap-fallback-store-data-onchain # Disable fallback to EthDA (calldata/blobs) ``` > **NOTE** > > The maximum batch size is determined dynamically by querying your DA provider's `daprovider_getMaxMessageSize` method before each batch is built. There is no static configuration for maximum batch size. ### JWT authentication Secure communication between Nitro nodes and DA providers using JWT: **Generate JWT Secret**: ```shell openssl rand -hex 32 > jwt.hex ``` **Configure Nitro Node**: ```shell --node.da.external-provider.rpc.jwtsecret=/path/to/jwt.hex ``` **Configure DA Provider Server**: Your server implementation should validate JWT tokens in the same way. ### Creating a DA provider server Your DA provider server exposes JSON-RPC methods that Nitro nodes call. You can implement this in any language as long as it speaks JSON-RPC over HTTP. **Required RPC Methods**: **Reader Methods**: * `daprovider_getSupportedHeaderBytes`: Returns header byte strings * `daprovider_recoverPayload`: Recovers batch payload * `daprovider_collectPreimages`: Collects preimages for validation * `daprovider_recoverPayloadAndPreimages`: Recovers batch data and also collects preimages for validation **Writer Methods** (optional, for batch posting): * `daprovider_getMaxMessageSize`: Returns maximum batch size * `daprovider_store`: Stores batch and returns certificate **Validator Methods**: * `daprovider_generateReadPreimageProof`: Generates preimage read proof * `daprovider_generateCertificateValidityProof`: Generates validity proof **Example: cmd/daprovider Server (Go)** The Nitro repository includes a complete reference implementation at `cmd/daprovider/daprovider.go` written in Go: ```go func main() { // 1. Parse configuration config, err := parseDAProvider(os.Args[1:]) // 2. Create a DA provider factory based on the mode providerFactory, err := factory.NewDAProviderFactory( config.Mode, // "anytrust" or "referenceda" &config.Anytrust, // AnyTrust config &config.ReferenceDA, // ReferenceDA config dataSigner, // Optional data signer l1Client, // L1 client l1Reader, // L1 reader seqInboxAddr, // Sequencer inbox address enableWriter, // Enable writer interface ) // 3. Create a reader/writer/validator reader, _, err := providerFactory.CreateReader(ctx) writer, _, err := providerFactory.CreateWriter(ctx) validator, _, err := providerFactory.CreateValidator(ctx) // 4. Start JSON-RPC server headerBytes := providerFactory.GetSupportedHeaderBytes() providerServer, err := dapserver.NewServerWithDAPProvider( ctx, &config.ProviderServer, reader, writer, validator, headerBytes, data_streaming.PayloadCommitmentVerifier(), ) // 5. Run until interrupted <-sigint providerServer.Shutdown(ctx) } ``` **Running the `ReferenceDA` example**: ```shell ./bin/daprovider \ --mode=referenceda \ --referenceda.enable \ --referenceda.signing-key.private-key= \ --referenceda.validator-address= \ --parent-chain.node-url= \ --provider-server.addr=0.0.0.0 \ --provider-server.port=8547 \ --provider-server.enable-da-writer ``` ### Multi-provider registry Nitro supports multiple DA providers simultaneously using single-byte header matching: **How it works**: 1. Each provider returns supported header bytes via `getSupportedHeaderBytes()` 2. Registry maps each header byte to a Reader/Validator pair 3. When processing a message, Nitro checks the first byte after the sequencer header and routes to the correct provider 4. Duplicate header bytes are rejected at registration **Example**: * AnyTrust: `0x80`, `0x88` * External DA (including `ReferenceDA`): `0x01` Multiple providers can coexist on the same chain. External DA providers handle any internal demultiplexing (e.g., routing to different backends) based on certificate contents after the header byte. ## 10. Testing your integration Thorough testing is critical for DA provider integrations, as bugs can lead to data loss or fraud-proof failures. ### Unit testing Test each component in isolation: **Reader tests**: * Certificate extraction from sequencer messages * Certificate deserialization (valid and invalid formats) * Certificate validation (valid/invalid signatures, trusted/untrusted signers) * Data retrieval from storage * Hash verification (data matches certificate commitment) * Preimage recording (correct mapping from keccak256(cert) to payload) **Writer tests**: * Certificate generation * Data storage * Certificate serialization * Signature creation * Error handling and fallback mechanism **Validator tests**: * `GenerateReadPreimageProof` with various offsets * `GenerateCertificateValidityProof` for valid certificates (returns claimedValid=1) * `GenerateCertificateValidityProof` for invalid certificates (returns claimedValid=0, NOT error) * Proof format correctness **Onchain contract tests**: * `validateCertificate` with valid certificates (returns `true`) * `validateCertificate` with invalid certificates (returns `false`, doesn't revert!) * `validateReadPreimage` with correct proofs * Hash verification (rejects wrong certificate hashes) * Chunk extraction at various offsets ### Integration testing with Nitro Test your DA provider connected to a Nitro node: **Setup**: 1. Deploy your validator contract to L1 2. Deploy Nitro contracts (SequencerInbox, OneStepProverHostIo with your validator address) 3. Start your DA provider server 4. Configure the Nitro node to use your provider **Test Scenarios**: * Post batches via Batch Poster (Writer interface) * Recover batches via Reader interface * Validate batches in the validation node * Generate fraud proofs with proof enhancement * Submit fraud proofs to L1 (OSP validation) ### System tests The Nitro repository includes system tests for the DA API with BoLD challenges: **BoLD challenge protocol Tests** (file: `system_tests/bold_challenge_protocol_test.go`): * `TestChallengeProtocolBOLDCustomDA_EvilDataGoodCert`: Corrupted data, valid certificate * `TestChallengeProtocolBOLDCustomDA_EvilDataEvilCert`: Corrupted data, invalid certificate * `TestChallengeProtocolBOLDCustomDA_UntrustedSignerCert`: Certificate signed by untrusted signer * `TestChallengeProtocolBOLDCustomDA_ValidCertClaimedInvalid`: Valid certificate incorrectly claimed invalid **Block Validator Tests**: * `TestBlockValidatorReferenceDAWithProver`: Proof enhancement with prover * `TestBlockValidatorReferenceDAWithJIT`: Proof enhancement with JIT Study these tests to understand expected behavior and edge cases. ### Testing invalid certificate handling **Critical Test**: Verify your system handles invalid certificates correctly: **Reader Behavior**: * Invalid certificate -> error returned * Nitro treats the batch as empty (zero transactions) * Chain continues processing **Validator Behavior**: * Invalid certificate -> returns `claimedValid=0`, **not an error** * Proof enhancement completes successfully * onchain validation returns `false` **Onchain Contract Behavior**: * Invalid certificate -> `validateCertificate` returns `false` * **Must NOT revert** (critical requirement!) * Fraud-proof succeeds, proving the certificate is invalid **Test Case**: ```go // Generate invalid certificate (bad signature, untrusted signer, etc.) invalidCert := generateInvalidCertificate() // Validator should return claimedValid=0, not error result, err := validator.GenerateCertificateValidityProof(invalidCert) assert.NoError(t, err) // No error! assert.Equal(t, 0, result.ClaimedValid) // Claims invalid // onchain validation should return false isValid, err := contract.ValidateCertificate(proof) assert.NoError(t, err) // No revert! assert.False(t, isValid) // Returns false ``` ## 11. Security considerations DA API integrations have unique security requirements. Follow these guidelines to build a secure system. ### Certificate hash as only preimage key The **only** way to retrieve batch data during fraud proofs is via `keccak256(certificate)`. This hash serves as the preimage key. **Implications**: * Deterministic mapping: Same certificate always maps to the same data * No certificate substitution: Can't swap a valid cert for another valid cert * Hash collision resistance: Must use keccak256 (256-bit security) **Your reader must**: * Record preimages using `keccak256(certificate)` as the key * Use `arbutil.DACertificatePreimageType` as the preimage type * Store the full batch payload as the preimage value ### Determinism requirements All components must be fully deterministic: **Reader**: * Same certificate -> always returns same data * Validation rules must be deterministic (no timestamps, no randomness) **Writer**: * Can be non-deterministic (different nodes may generate different certificates for the same data) * But the certificate must deterministically identify the data **Validator**: * Same inputs -> always returns the same proofs * No network randomness, no timestamps in proofs * Proofs must be verifiable onchain with only the proof data **onchain Contract**: * Pure deterministic validation * No external calls (except to immutable addresses) * No block timestamps, no randomness ### Invalid certificate handling **Reader Behavior** (during normal execution): * Invalid certificate -> return error * Nitro treats the batch as empty (zero transactions) * Chain continues without halting **Validator Behavior** (during fraud proofs): * Invalid certificate -> return `claimedValid=0`, **NOT an error** * Fraud-proof system needs to prove "this certificate is invalid" * Errors should only be for transient failures (RPC issues) **Onchain Contract Behavior**: * Invalid certificate -> `validateCertificate` returns `false` * **Must not revert** (would make proving invalidity impossible) * Only revert for truly unexpected conditions ### No trusted external oracles Fraud proofs must be verifiable onchain **without trusting external oracles**: **What is allowed**: * Querying L1 state (trusted signers, contract storage) * Using L1 precompiles (ecrecover, sha256, etc.) * Reading immutable contract addresses **Why?** Fraud proofs must be verifiable onchain without depending on external services that could be unavailable or malicious. ### Certificate hash verification The `OneStepProverHostIo` contract verifies that the `keccak256(certificate)` matches the hash in the machine proof. This verification prevents: **Certificate substitution attack**: * Attacker posts certificate A to L1 * During fraud proof, tries to use certificate B (different data) * OSP rejects: keccak256(B) ≠ keccak256(A) **You don't need to implement this check**—the OSP enforces it. But understand it's critical to security. ### Claim verification For `validateCertificate`, the OSP verifies the prover's `claimedValid` byte matches the validator contract's return value. This verification prevents: **False validity claims**: * Prover claims an invalid certificate is valid -> OSP rejects * Prover claims a valid certificate is invalid -> OSP rejects This preventive measure ensures both honest and malicious provers are held accountable. ### Immutable validator address The `OneStepProverHostIo` is deployed with an immutable `customDAValidator` address. Once deployed: * The validator address is unchangeable * Prevents governance attacks or validator swapping * Ensures consistent validation rules **Implication**: Choose your validator contract carefully at deployment. If you need to update logic, you'll need to deploy a new OSP and update BoLD contracts. ### Data availability guarantees The DA API does **not** enforce data availability—that's your responsibility: **Your DA system must ensure**: * Data is actually available when certificates are issued * Data remains available for at least the requested period, but maintaining the data forever for genesis syncing is recommended. **Nitro only ensures**: * Invalid certificates are challengeable * That valid certificates are verifiable (provable) * No invalid data is executed Data availability itself is your DA system's responsibility. ## Appendix A: Streaming protocol ### Context and protocol overview This section outlines the necessity, role, and activation of the Data Availability (DA) streaming subprotocol within Arbitrum Nitro. #### Rationale and function Experience with AnyTrust deployments demonstrated that the Batch Poster's "one-shot" transmission of large data batches to DA Committee Members can be susceptible to network instability, leading to submission failures or critical latency. To address this, we introduced the Data Streaming Protocol: * It operates as a sub-layer between the Nitro node's Batch Poster and the DA server. * It segments large batches into a sequence of smaller, short messages, which get streamed sequentially. This strategy significantly improves the resilience and reliability of data submission—despite increasing the total message count. #### Opt-in activation This protocol is opt-in. Integrators can activate it to ensure robust data submission when handling large batches in environments with variable network quality. To enable data streaming on your Nitro node, use the following command-line flag: `--node.da.external-provider.use-data-streaming` ### Server-side protocol implementation (integrators) When configuring the Nitro node with the streaming flag (`--node.da.external-provider.use-data-streaming`), it utilizes an internal sender implementation that relies on a server-side JSON-RPC API exposed by the DA provider. Integrators must implement the following three endpoints to enable the streaming protocol. | `daprovider_startChunkedStore` | Initiates the streaming and allocates a batch identifier. | | ------------------------------- | ----------------------------------------------------------- | | `daprovider_sendChunk` | Transmits a single data segment (chunk). | | `daprovider_commitChunkedStore` | Concludes the stream and requests the final DA Certificate. | Integrators can customize these names using the following CLI flags on the Nitro node: * `--node.da.external-provider.data-stream.rpc-methods.start-stream` * `--node.da.external-provider.data-stream.rpc-methods.stream-chunk` * `--node.da.external-provider.data-stream.rpc-methods.finalize-stream` > **NOTE** — Universal Integer Encoding Standard > > All `uint64` integer parameters (e.g., `timestamp`, `nChunks`, `BatchId`) must be encoded as JSON strings prefixed with `0x` (hexadecimal encoding). #### Start stream: (`daprovider_startChunkedStore`) **Arguments**: * timestamp (uint64) * nChunks (uint64) * chunkSize (uint64) * totalSize (uint64) * timeout (uint64) * signature (bytes) **Return**: A JSON object with `BatchId` field (uint64). This field is a unique identifier generated by the server for this specific stream instance. This `BatchId` must be used in all subsequent `StreamChunk` and `FinalizeStream` calls within this execution. #### Stream chunk (`daprovider_sendChunk`) **Arguments**: * batchId (uint64): The identifier generated by the start-stream. * chunkId (uint64): The zero-indexed position of the data segment within the full batch. * chunk (bytes) * signature (bytes) **Return**: A successful operation returns an HTTP 200 status code. #### Finalize stream (`daprovider_commitChunkedStore`) **Arguments**: * `batchId` (uint64): The identifier generated by the start-stream. * `signature` (bytes) **Return**: A JSON object with `SerializedDACert` field (byte array). This field certifies that the batch has been successfully stored and made available by the DA layer. ### Security and configuration notes #### Signature handling The current default client implementation relies on underlying transport encryption (e.g., TLS/JWT) for security. The signature parameter gets used for basic data integrity checking, not cryptographic authentication. * **Client Behavior**: The client populates `signature` with the Keccak256 hash of all other method arguments. * **Server Implementation**: Server integrators may recompute and verify this hash to check data integrity, but this verification is optional and not required for core protocol functionality. #### Operational constraint: Chunk size limit Integrators can limit the maximum allowed size for individual chunk transmissions to manage server load. * **Limit Control:** Use the following flag on the Nitro node: `--node.da.external-provider.data-stream.max-store-chunk-body-size` * **Action:** The client will ensure that all `StreamChunk` requests, including overhead, do not exceed the size specified by this flag. ### **Go implementation helper** For integrators implementing server-side logic in Go, the recommended approach is to reuse the existing `DataStreamReceiver` component in the Nitro repository. This object handles all internal protocol-state management and logic for the receiving side. #### Recommended module and type * **Module:** `github.com/offchainlabs/nitro/daprovider/data_streaming` * **Core Type:** `DataStreamReceiver` #### Example implementation snippets Implementing the required JSON-RPC endpoints can be reduced to simple wrappers around the `DataStreamReceiver` methods, as demonstrated below. > **NOTE** > > We use the `hexutil` types to correctly handle the required `0x`-prefixed integer encoding specified in [3. Implementing reader RPC methods](#3-implementing-reader-rpc-methods). ```go func (s *Server) StartChunkedStore(ctx context.Context, timestamp, nChunks, chunkSize, totalSize, timeout hexutil.Uint64, sig hexutil.Bytes) (*data_streaming.StartStreamingResult, error) { return s.dataReceiver.StartReceiving(ctx, uint64(timestamp), uint64(nChunks), uint64(chunkSize), uint64(totalSize), uint64(timeout), sig) } func (s *Server) SendChunk(ctx context.Context, messageId, chunkId hexutil.Uint64, chunk hexutil.Bytes, sig hexutil.Bytes) error { return s.dataReceiver.ReceiveChunk(ctx, data_streaming.MessageId(messageId), uint64(chunkId), chunk, sig) } func (s *Server) CommitChunkedStore(ctx context.Context, messageId hexutil.Uint64, sig hexutil.Bytes) (*server_api.StoreResult, error) { message, timeout, _, err := s.dataReceiver.FinalizeReceiving(ctx, data_streaming.MessageId(messageId), sig) if err != nil { return nil, err } // do the actual full data store and generate DA certificate return s.Store(ctx, message, hexutil.Uint64(timeout)) } ``` --- > For a complete page index, fetch # How to customize your Arbitrum chain's precompiles Info ### Customizations require expertise Customizing your chain is a core benefit of building with Arbitrum chains. We strongly recommend that teams interested in customizations work alongside a partner with ArbOS and Nitro software expertise, such as a Rollup-as-a-Service team. Working alongside an experienced Arbitrum chain operator can help your team navigate the complex tradeoff space of rollup customizations, which can include performance, security, and cost considerations. Offchain Labs is positioned to train and enable Rollup-as-a-Service in their work with clients to scale support to the Arbitrum chain ecosystem as a whole. As such, Offchain Labs does not necessarily have the capacity to review code changes made by individual Arbitrum chains. We encourage you to leverage your in-house expertise, collaborate with expert partners, and allocate appropriate resources for both an initial implementation (including an audit) and ongoing maintenance and security management of your customization. Customizing your chain's precompiles involves modifying or extending the built-in, system-level smart contract-like functions (precompiles) that provide efficient access to chain-specific operations. These operations include interacting with the parent chain (L1/L2), querying state, or performing computations. Precompiles are hardcoded at specific addresses (e.g., 0x64 for `ArbSys`) and executed outside the EVM bytecode level for performance. They inherit Ethereum's standard precompiles (e.g., for hashing or elliptic curves) while adding Arbitrum-specific ones (e.g., `ArbAddressTable` for address compression). > **CAUTION** > > The guidance in this document will only work if you use `eth_call` to call the new precompiles. If you call them from other contracts or add non-view/pure methods, this approach will break the block validation. > > To support these additional use cases, follow the instructions described in [How to customize your Arbitrum chain's behavior](/launch-arbitrum-chain/extend-the-protocol/stf.md). There are five primary ways to customize your chain's precompiles: 1. Add new methods to an existing [precompile](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/) (local: `/precompiles`) 2. Create a new precompile. 3. Define a new event. 4. Customize gas usage for a specific method. 5. Call and modify state. ## Prerequisites Clone the Nitro repository before you begin: ```shell git clone --branch v3.11.3 cd nitro git submodule update --init --recursive --force ``` ## Option 1: Add new methods to an existing precompile Using your favorite code editor, open an existing precompile from the [precompiles implementation](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/) directory (local `/precompiles`). We'll use `ArbSys.go` as an example. Open the corresponding Go implementation file (`ArbSys.go`) and add a simple `SayHi` method: ```go func (con *ArbSys) SayHi(c ctx, evm mech) (string, error) { return "hi", nil } ``` Then, open the corresponding Solidity interface file (`ArbSys.sol`) from the precompiles interface (`/contracts-local/src/precompiles`) directory, and add the required interface. Ensure that the method name on the interface matches the name of the function you introduced in the previous step, `camelCased`: ```solidity function sayHi() external view returns(string memory); ``` Next, follow the steps in [How to customize your Arbitrum chain's behavior](/launch-arbitrum-chain/extend-the-protocol/stf.md#step-3-run-the-node) to build a modified Arbitrum Nitro node Docker image and run it. Once your node is running, you can call `ArbSys.sol` either directly using `curl`, or through Foundry's `cast call`. ### Call your function directly using `curl` ```shell curl http://localhost:8449 \ -X POST \ -H "Content-Type: application/json" \ --data '{"method":"eth_call","params":[{"from":null,"to":"0x0000000000000000000000000000000000000064","data":"0x0c49c36c"}, "latest"],"id":1,"jsonrpc":"2.0"}' ``` You should see something like this: ```text {"jsonrpc":"2.0","id":1,"result":"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026869000000000000000000000000000000000000000000000000000000000000"} ``` `0x6869` is the hex-encoded UTF-8 representation of `hi`, which you'll see embedded in the `result` hex string. ### Call your function using Foundry's `cast call` ```text cast call 0x0000000000000000000000000000000000000064 "sayHi()(string)” ``` You should see something like this: ```text hi ``` ## Option 2: Create a new precompile First, navigate to the precompiles implementation directory, `/precompiles`, and create a new precompile implementation file called `ArbHi.go`. We'll define a new method, and we'll give it an address: > **IMPORTANT** > > When selecting an address for your custom precompile, precompile addresses must not conflicts with existing Ethereum and ArbOS precompiles and avoid using addresses in reserved ranges as defined by [EIP-7587](https://eips.ethereum.org/EIPS/eip-7587). These ranges are reserved for future Rollup Improvement Proposal (RIP) upgrades and using them may cause conflicts. ```go package precompiles // ArbHi provides a friendly greeting to anyone who calls it. type ArbHi struct { Address addr // 0x11a, for example } func (con *ArbHi) SayHi(c ctx, evm mech) (string, error) { return "hi", nil } ``` Next, navigate to [arbitrum\_signer.go](https://github.com/OffchainLabs/go-ethereum/blob/v1.12.2/core/types/arbitrum_signer.go) and add the new precompile address. ```go var ArbosAddress = common.HexToAddress("0xa4b05") var ArbosStateAddress = common.HexToAddress("0xA4B05FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") var ArbSysAddress = common.HexToAddress("0x64") var ArbInfoAddress = common.HexToAddress("0x65") // Add your new precompile address here var ArbHiAddress = common.HexToAddress("0x11a") // 0x011a here is an example address ``` Then, update [precompile.go](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/precompile.go) to register the new precompile under the `Precompiles()` method: ```go insert(MakePrecompile(pgen.ArbHiMetaData, &ArbHi{Address: types.ArbHiAddress})) // Set the ArbHiAddress here we just added to arbitrum_signer.go ``` Navigate to the precompiles interface directory, `/contracts-local/src/precompiles`, create `ArbHi.sol`, and add the required interface. Ensure that the method name on the interface matches the name of the function you introduced in the previous step, `camelCased`: ```solidity pragma solidity >=0.4.21 <0.9.0; /// @title Say hi. /// @notice just for test /// This custom contract will set on 0x000000000000000000000000000000000000011a since we set it in precompile.go. interface ArbHi { function sayHi() external view returns(string memory); } ``` Next, follow the steps in [How to customize your Arbitrum chain's behavior](/launch-arbitrum-chain/extend-the-protocol/stf.md#step-3-run-the-node) to build a modified Arbitrum Nitro node Docker image and run it. Once your node is running, you can call `ArbHi.sol` either directly using `curl`, or through Foundry's `cast call`. ### Call your function directly using `curl` ```shell curl http://localhost:8449 \ -X POST \ -H "Content-Type: application/json" \ --data '{"method":"eth_call","params":[{"from":null,"to":"0x000000000000000000000000000000000000011a","data":"0x0c49c36c"}, "latest"],"id":1,"jsonrpc":"2.0"}' ``` You should see something like this: ```text {"jsonrpc":"2.0","id":1,"result":"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026869000000000000000000000000000000000000000000000000000000000000"} ``` ### Call your function using Foundry's `cast call` ```text cast call 0x000000000000000000000000000000000000011a "sayHi()(string)” ``` You should see something like this: ```text hi ``` ## Option 3: Define a new event We'll reuse the `Arbsys` precompile from Option 1 above to demonstrate how to emit a simple `Hi` event from the `SayHi` method in `ArbSys.sol`. First, go to the precompiles implementation directory `/precompiles`, find `ArbSys.go`, and edit the `ArbSys` struct: ```go // ArbSys provides system-level functionality for interacting with L1 and understanding the call stack. type ArbSys struct { Address addr // 0x64 L2ToL1Tx func(ctx, mech, addr, addr, huge, huge, huge, huge, huge, huge, []byte) error L2ToL1TxGasCost func(addr, addr, huge, huge, huge, huge, huge, huge, []byte) (uint64, error) SendMerkleUpdate func(ctx, mech, huge, bytes32, huge) error SendMerkleUpdateGasCost func(huge, bytes32, huge) (uint64, error) InvalidBlockNumberError func(huge, huge) error // deprecated event L2ToL1Transaction func(ctx, mech, addr, addr, huge, huge, huge, huge, huge, huge, huge, []byte) error L2ToL1TransactionGasCost func(addr, addr, huge, huge, huge, huge, huge, huge, huge, []byte) (uint64, error) // Add your customize event here: Hi func(ctx, mech, addr) error // This is needed and will tell you how much gas it will cost, the param is the same as your event but without the first two (ctx, mech), the return param is always (uint64, error) HiGasCost func(addr) (uint64, error) } ``` > **NOTE** > > Every precompile event should have a gas cost function. The name of the gas cost function should be the same as the event name, but with the word "GasCost" appended to it. Then add the event to the `SayHi` method: ```go func (con *ArbSys) SayHi(c ctx, evm mech) (string, error) { err := con.Hi(c, evm, c.caller) return "hi", err } ``` Now navigate to the precompiles interface directory (`/contracts-local/src/precompiles`), open `Arbsys.sol`, and add the required interface. Ensure that the event name on the interface matches the name of the function you introduced in `ArbSys` struct in the previous step: ```solidity event Hi(address caller); ``` If you want to [index the parameter](https://docs.soliditylang.org/en/latest/contracts.html#events) of the event (if you want to filter by that parameter in the future, for example), just add `indexed` to the Solidity interface: ```solidity event Hi(address indexed caller); ``` Our function now emits an event, which means that when calling it, the state will change and a gas cost will be incurred. So we have to remove the `view` function behavior: ```solidity function sayHi() external returns(string memory); ``` Next, build Nitro by following the instructions in [How to build Nitro locally](/run-arbitrum-node/nitro/build-nitro-locally.md). Note that if you've already built the Docker image, you still need run the last step to rebuild. Run Nitro with the following command: ```shell docker run --rm -it -v /some/local/dir/arbitrum:/home/user/.arbitrum -p 0.0.0.0:8547:8547 -p 0.0.0.0:8548:8548 offchainlabs/nitro-node:v3.11.3-beb2108 --parent-chain.connection.url= --chain.id= --http.api=net,web3,eth,debug --http.corsdomain=* --http.addr=0.0.0.0 --http.vhosts=* ``` ### Send the transaction and get the transaction receipt To send a transaction to `ArbSys`, we need to include a gas cost, because the function is no longer a `view`/`pure` function: ```text cast send 0x0000000000000000000000000000000000000064 "sayHi()(string)" ``` Call `eth_getTransactionReceipt` with the returned transaction hash result. You should see something like this: ```text {"jsonrpc":"2.0","id":1,"result":{"blockHash":"Your_blockHash","blockNumber":"Your_blockNumber","contractAddress":null,"cumulativeGasUsed":"0x680b","effectiveGasPrice":"0x5f5e100","from":"Your_address","gasUsed":"0x680b","gasUsedForL1":"0xe35","l1BlockNumber":"l1_blockNumber","logs":[{"address":"0x0000000000000000000000000000000000000064","topics":["0xa9378d5bd800fae4d5b8d4c6712b2b64e8ecc86fdc831cb51944000fc7c8ecfa","0x000000000000000000000000{Your_address}"],"data":"0x","blockNumber":"Your_blockNumber","transactionHash":"Your_txHash","transactionIndex":"0x1","blockHash":"Your_blockHash","logIndex":"0x0","removed":false}],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000100000000000000040000000000000080004000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000004000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000","status":"0x1","to":"0x0000000000000000000000000000000000000064","transactionHash":"Your_txHash","transactionIndex":"0x1","type":"0x2"}} ``` Note the `logs` field within the transaction receipt: ```text "logs":[ { "address":"0x0000000000000000000000000000000000000064", "topics":[ "0xa9378d5bd800fae4d5b8d4c6712b2b64e8ecc86fdc831cb51944000fc7c8ecfa", "0x000000000000000000000000{Your_address}" ], "data":"0x", "blockNumber":"0x40", "transactionHash":"{Your_txHash}", "transactionIndex":"0x1", "blockHash":"0x0b367d705002b3575db99354a0964c033f929f26f4442ed347e47ae43a8f28e4", "logIndex":"0x0", "removed":false } ] ``` ## Option 4: Customize gas usage for a specific method The above instructions demonstrate how you can define a new precompile function. However, if this new function is simply defined without performing gas collection within the function, your precompile will be vulnerable to Denial-of-Service (DOS) attacks. These attacks exploit the function by flooding it with excessive requests without bearing the computational cost. To deter this type of attack, you can implement a gas collection mechanism within your precompile. The event itself doesn't need to specify the gas cost; the program will calculate the gas cost when the event's execution is initially triggered. In addition to introducing gas costs where they don't exist, you can also customize gas costs where they're already being incurred. To demonstrate, consider the `GetBalance` method in `ArbInfo.go`: ```text // GetBalance retrieves an account's balance func (con ArbInfo) GetBalance(c ctx, evm mech, account addr) (huge, error) { if err := c.Burn(params.BalanceGasEIP1884); err != nil { return nil, err } return evm.StateDB.GetBalance(account), nil } ``` The purpose of this method is to retrieve the balance of an address. As defined in [EIP-1884](https://eips.ethereum.org/EIPS/eip-1884), the operation code (opcode) for obtaining the address balance has an associated gas cost of 700 gas. The function accounts for this cost by deducting the specified amount of gas, indicated by the protocol constant `BalanceGasEIP1884`, which is set to `700`, through the call to `c.Burn(int64)`. To customize the gas cost, let's implement an alternative to `GetBalance`, called `GetBalanceCustom`: ```text // GetBalance retrieves an account's balance func (con ArbInfo) GetBalanceCustom(c ctx, evm mech, account addr) (huge, error) { gasForBalanceCall := uint64(300) if err := c.Burn(gasForBalanceCall); err != nil { return evm.StateDB.GetBalance(account), err } return balance, nil } ``` To register this new precompile method, refer to Option 1 above. Next, build Nitro by following the instructions in [How to build Nitro locally](/run-arbitrum-node/nitro/build-nitro-locally.md). Note that if you've already built the Docker image, you still need run the last step to rebuild. Run Nitro with the following command: ```shell docker run --rm -it -v /some/local/dir/arbitrum:/home/user/.arbitrum -p 0.0.0.0:8547:8547 -p 0.0.0.0:8548:8548 offchainlabs/nitro-node:v3.11.3-beb2108 --parent-chain.connection.url= --chain.id= --http.api=net,web3,eth,debug --http.corsdomain=* --http.addr=0.0.0.0 --http.vhosts=* ``` ### Send the transaction and get the transaction receipt In order to obtain the gas used, we can use the `eth_sendRawTransaction` RPC method to test execution on the chain. First, call: ```text cast send 0x0000000000000000000000000000000000000065 "GetBalance()({Any_Address})" ``` Then, call: ```text cast send 0x0000000000000000000000000000000000000065 "GetBalanceCustom()({Any_Address})" ``` The two responses will look like this, respectively: #### Result 1: ```text { "jsonrpc":"2.0", "id":1, "result":{ "blockHash":"{Your_blockHash}", "blockNumber":"0x15", "contractAddress":null, "cumulativeGasUsed":"0x638f", "effectiveGasPrice":"0x5f5e100", "from":"{Your_address}", "gasUsed":"0x638f", "gasUsedForL1":"0x9f5", "l1BlockNumber":"0x979a02", "logs":[ ], "logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "status":"0x1", "to":"0x0000000000000000000000000000000000000065", "transactionHash":"{Your_txHash}", "transactionIndex":"0x1", "type":"0x2" } } ``` #### Result 2: ```text { "jsonrpc":"2.0", "id":1, "result":{ "blockHash":"{Your_blockHash}", "blockNumber":"0x16", "contractAddress":null, "cumulativeGasUsed":"0x61ff", "effectiveGasPrice":"0x5f5e100", "from":"{Your_address}", "gasUsed":"0x61ff", "gasUsedForL1":"0x9f5", "l1BlockNumber":"0x979a08", "logs":[ ], "logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "status":"0x1", "to":"0x0000000000000000000000000000000000000065", "transactionHash":"{Your_txHash}", "transactionIndex":"0x1", "type":"0x2" } } ``` Here we can see that the gas cost incurred by the execution of the first transaction is `gasUsed - gasUsedForL1 = 22938`. Similarly, the gas cost incurred by the execution of the second transaction is `22538`. If you subtract the two, the result is `400`, as expected. To learn more about the gas cost model, see [how to estimate gas](/arbitrum-essentials/how-to-estimate-gas.md). ## Option 5: Call and modify state In this example, we'll demonstrate how to *read from* and *write to* a precompile contract's [ArbOS state](https://github.com/OffchainLabs/nitro/blob/v2.2.0/arbos/arbosState/arbosstate.go#L38). First, open the [arbosstate.go](https://github.com/OffchainLabs/nitro/blob/v3.11.3/arbos/arbosState/arbosstate.go) file and locate the [ArbosState](https://github.com/OffchainLabs/nitro/blob/v2.2.0/arbos/arbosState/arbosstate.go#L38) structure. This is where ArbOS state is defined. Define a state key called `myNumber` of type `storage.StorageBackedUint64`. You can find more types in [storage.go](https://github.com/OffchainLabs/nitro/blob/v3.11.3/arbos/storage/storage.go): ```text type ArbosState struct { // Other states infraFeeAccount storage.StorageBackedAddress brotliCompressionLevel storage.StorageBackedUint64 // brotli compression level used for pricing backingStorage *storage.Storage Burner burn.Burner myNumber storage.StorageBackedUint64 // this is what we added } ``` Next, define the offset of your newly added state (tip: add it to the end so it won't affect other states): ```text const ( versionOffset Offset = iota upgradeVersionOffset upgradeTimestampOffset networkFeeAccountOffset chainIdOffset genesisBlockNumOffset infraFeeAccountOffset brotliCompressionLevelOffset myNumberOffset // define the offset of your new state here ) ``` Then, initialize the state under the [`OpenArbosState`](https://github.com/OffchainLabs/nitro/blob/v2.2.0/arbos/arbosState/arbosstate.go#L89) and [`InitializeArbosState`](https://github.com/OffchainLabs/nitro/blob/v2.2.0/arbos/arbosState/arbosstate.go#L212) methods: `OpenArbosState`: ```text return &ArbosState{ // other states backingStorage.OpenStorageBackedAddress(uint64(infraFeeAccountOffset)), backingStorage.OpenStorageBackedUint64(uint64(brotliCompressionLevelOffset)), backingStorage, burner, backingStorage.OpenStorageBackedUint64(uint64(myNumberOffset)), // define your new state here }, nil ``` `InitializeArbosState`: ```text _ = sto.SetUint64ByUint64(uint64(versionOffset), 1) // initialize to version 1; upgrade at end of this func if needed _ = sto.SetUint64ByUint64(uint64(upgradeVersionOffset), 0) _ = sto.SetUint64ByUint64(uint64(upgradeTimestampOffset), 0) _ = sto.SetUint64ByUint64(uint64(myNumberOffset), 0) // initialize your new state around here ``` Next, define your getter and setter:: ```text func (state *ArbosState) SetNewMyNumber( newNumber uint64, ) error { return state.myNumber.Set(newNumber) } func (state *ArbosState) GetMyNumber() (uint64, error) { return state.myNumber.Get() } ``` Next, head back to the [precompiles directory](https://github.com/OffchainLabs/nitro/blob/v3.11.3/precompiles/) and create a new `ArbHi.go` (introduced in Option 2). This time, we'll add two new methods to read and write the ArbOS state: ```text package precompiles // ArbHi provides a friendly greeting to anyone who calls it. type ArbHi struct { Address addr // 0x11a, for example } func (con *ArbHi) SayHi(c ctx, evm mech) (string, error) { return "hi", nil } func (con *ArbHi) GetNumber(c ctx, evm mech) (uint64, error) { return c.State.GetMyNumber() } func (con *ArbHi) SetNumber(c ctx, evm mech, newNumber uint64) error { return c.State.SetNewMyNumber(newNumber) } ``` Follow the procedure detailed in Option 2 in order to add this new precompile contract, and then run your node. Your smart contract interface should look like this: ```text pragma solidity >=0.4.21 <0.9.0; /// @title Say hi. /// @notice just for test /// This custom contract will set on 0x000000000000000000000000000000000000011a since we set it in precompile.go. interface ArbHi { function sayHi() external view returns(string memory); function getNumber() external view returns(uint64); function setNumber(uint64) external; } ``` ### Send the transaction and get the transaction receipt To send a transaction to `ArbSys`, we need to include a gas cost, because the function is no longer a view/pure function: ```text cast send 0x000000000000000000000000000000000000011a "setNumber()" "2" ``` ### Get results from foundry cast ```text cast call 0x000000000000000000000000000000000000011a "getNumber()(uint64)” ``` You should see something like this: ```text 2 ``` ## Incorporate your changes to precompile into the ArbOS upgrade If you do not customize the precompile before launching your Arbitrum chain network, please continue to follow the instructions on [Customize ArbOS](/launch-arbitrum-chain/extend-the-protocol/arbos.md) to perform an ArbOS version control to avoid blockchain reorg. --- > For a complete page index, fetch # How to customize your Arbitrum chain's behavior Info ### Customizations require expertise Customizing your chain is a core benefit of building with Arbitrum chains. We strongly recommend that teams interested in customizations work alongside a partner with ArbOS and Nitro software expertise, such as a Rollup-as-a-Service team. Working alongside an experienced Arbitrum chain operator can help your team navigate the complex tradeoff space of rollup customizations, which can include performance, security, and cost considerations. Offchain Labs is positioned to train and enable Rollup-as-a-Service in their work with clients to scale support to the Arbitrum chain ecosystem as a whole. As such, Offchain Labs does not necessarily have the capacity to review code changes made by individual Arbitrum chains. We encourage you to leverage your in-house expertise, collaborate with expert partners, and allocate appropriate resources for both an initial implementation (including an audit) and ongoing maintenance and security management of your customization. ## Introduction Before customizing your Arbitrum chain, it's important to understand what the State Transition Function (STF) is. The STF defines how new blocks are produced from input messages (i.e., transactions). This guide is only necessary for changes that modify the STF. To customize other node behavior, such as RPC behavior or the sequencer's ordering policy, you can simply [build your own node](/run-arbitrum-node/nitro/build-nitro-locally.md) without worrying about the rest of this guide. However, changes that modify the STF require updating the fraud proving system to recognize the new behavior as correct. Otherwise, the fraud prover would side with unmodified nodes, which would win fraud proofs against your modified node. Here's some examples of modifications that affect the STF: * Adding a new EVM opcode or precompile: * This modifies the STF because a node with this change would disagree about the outcome of EVM execution compared to an unmodified Nitro node when the new opcode or precompile is invoked. * Rewarding the deployer of a smart contract with a portion of gas spent in the smart contract's execution: * This modifies the STF because a node which has this change applied would disagree about the balance of the deployer after transactions. Such changes would lead to disagreements about block hashes compared to unmodified Nitro nodes. Here's some examples of modifications that don't affect the STF: * Adding a new RPC method to query an address's balance across multiple blocks: * This doesn't modify the STF because it doesn't change onchain balances or block hashes. * Changing the sequencer to order blocks by tip: * The sequencer is trusted to order transactions in Arbitrum Nitro, and it can choose any ordering it wants. * Nodes (and the fraud proofs) will simply accept the new transaction ordering as there is no single ordering they think is correct. ### Modification compatibility with Arbitrum Nitro Some potential modifications are incompatible with Arbitrum Nitro and would not result in a functioning blockchain. Here are some requirements for the Arbitrum Nitro State Transition Function: * The STF must be deterministic. For instance, if you gave an address a random balance using the Go randomness library, * Every node would disagree on the correct amount of balance and the blockchain would not function correctly. However, it is acceptable to take a non-deterministic path to a deterministic output. For instance, if you randomly shuffled a list of addresses, and then gave them each one **ETH**, that would be fine, because no matter how the list of addresses is shuffled the result is the same and all addresses are given one **ETH**. * The STF must not reach a new result for old blocks. For instance, if you have been running an Arbitrum Nitro chain for a while, and then you decide to modify the STF to not charge for gas, a new node that syncs the blockchain will reach a different result for historical blocks. It's also important to synchronize between nodes when an upgrade takes effect. A common mechanism for doing this is having an upgrade take effect at a certain timestamp, by which all nodes must be upgraded. * The STF must be "pure" and not use external resources. For instance, it must not use the filesystem, make external network calls, or launch processes. That's because the fraud proving system does not (and for the most part, cannot) support these resources. For instance, it's impossible to fraud prove what the result of an external network call is, because the fraud prover smart contracts on L1 are unable to do networking. * The STF must not carry state between blocks outside of the "global state". In practice this means persistent state must be stored within block headers or the Ethereum state trie. For instance, [ArbOS](/how-arbitrum-works/deep-dives/arbos.md) stores all retryables in contract storage under a special ArbOS address. * The STF must not modify Ethereum state outside of a transaction. This is important to ensure that replaying old blocks reaches the same result, both for tracing and for validation. The ArbOS internal transaction is useful to modify state at the start of blocks. * The STF must reach a result in under a second. This is a rough rule, but for nodes to keep in sync it's highly recommended to keep blocks quick. * It's also important for the fraud proofs that execution reliably finishes in a relatively short amount of time. * A block gas limit of 32 million gas should safely fit within this limit. * The STF must not fail or panic. It's important that the STF always produces a new block, even if user input is malformed. * For instance, if the STF receives an invalid transaction as input, it'll still produce an empty block. ## Building the modified node To modify the State Transition Function, you'll need to build a modified Arbitrum Nitro node Docker image. This guide covers how to build the node and enable fraud proofs by building a new replay binary. ### Step 1. Download the Nitro source code Clone the Nitro repository before you begin: ```shell git clone --branch v3.11.3 https://github.com/OffchainLabs/nitro.git cd nitro git submodule update --init --recursive --force ``` ### Step 2. Apply modifications Next, make your changes to the State Transition Function. For example, you could [add a custom precompile](/launch-arbitrum-chain/extend-the-protocol/precompiles.md). After this step, you should visit [Customize ArbOS version](/launch-arbitrum-chain/extend-the-protocol/arbos.md) to see if your changes need to upgrade the ArbOS version. If so, please continue to follow that document to add an ArbOS upgrade logic to your code. ### Step 3. Run the node To build the Arbitrum Nitro node image, you'll first need to install Docker. You can confirm if it's already setup by running `docker version` in a terminal. If not, try following [Docker's getting started guide](https://www.docker.com/get-started/), or if you're on Linux, install Docker from your distribution's package manager and start the Docker service. Once you have Docker installed, you can simply run `docker build . --tag custom-nitro-node` in the `nitro` folder to build your custom node. Note A chain running a customized State Transition Function will produce blocks that don't match the verified Nitro WASM module root on the parent chain. Coordinate with Offchain Labs before operating a customized STF against a production chain, as the validator configuration required is not covered in this guide. You have two ways of running your node. #### 1. Using the docker-compose file This is the recommended way if you're running your Arbitrum chain locally through the provided [docker-compose file](https://github.com/OffchainLabs/orbit-setup-script/blob/main/docker-compose.yaml#L39). In `docker-compose.yml`, modify the Docker image used for the Nitro container: ```text ... nitro: image: custom-nitro-node ports: ... ``` And run `docker compose up` to run all of your containers. #### 2. Use `docker run` to run your Nitro node only This method will only run the customized Nitro node (i.e., it will not run Blockscout, or the DA server if you're using an AnyTrust chain). Use the following command: ```shell docker run --rm -it -v /path/to/your/node/dir:/home/user/.arbitrum -p 0.0.0.0:8449:8449 custom-nitro-node --conf.file /home/user/.arbitrum/nodeConfig.json ``` Info The instructions provided in [How to run a full node](/run-arbitrum-node/run-full-node.md) **will not** work with your Arbitrum chain node. See [Optional parameters (Arbitrum chain)](/run-arbitrum-node/run-full-node.md#optional-parameters) for Arbitrum chain-specific CLI flags. Once your node is running, you can try out your modifications to the State Transition Function and confirm they work as expected. ### Step 4. Enable fraud proofs To enable fraud proofs, you'll need to build the "replay binary", which defines the State Transition Function for the fraud prover. The replay binary (sometimes called the machine) re-executes the State Transition Function against input messages to determine the correct output block. It has three forms: * The `replay.wasm` binary is the Go replay binary compiled to WASM. It's used by the JIT validator to verify blocks against the fraud prover. * The `machine.v2.wavm.br` binary is a compressed binary containing the Go replay binary and all its dependencies, compiled to WASM, then translated to the Arbitrum fraud proving variant WAVM. * It's used by Arbitrator when actually entering a challenge and performing the fraud proofs, and has identical behavior to `replay.wasm`. * The WASM module root (stored in `module-root.txt`) is a 32 byte hash usually expressed in hexadecimal which is a merkelization of `machine.v2.wavm.br`. * The replay binary is much too large to post onchain, so this hash is set in the L1 Rollup contract to determine the correct replay binary during fraud proofs. To run a validator node with fraud proofs enabled, the validator node's Docker image will need to contain all three of these versions of the replay binary. #### 4.1 Build a dev image The simplest way to build a Docker image with the new replay binary is to build a dev image. These images contain a freshly built replay binary, but note that the replay binary and corresponding WASM module root will generally change when the code is updated, even if the State Transition Function has equivalent behavior. It's important that the validator's WASM module root matches the onchain WASM module root, which is why this approach is harder to maintain. Over the longer term, you'll want to maintain a separate build of the replay binary that matches the one currently onchain, usable by any node image. To build the dev node image and get the WASM module root, run: ```shell docker build . --target nitro-node-dev --tag custom-nitro-node-dev docker run --rm --entrypoint cat custom-nitro-node-dev target/machines/latest/module-root.txt ``` Once you have the WASM module root, you can put it onchain by calling `setWasmModuleRoot(newWasmModuleRoot)` through the upgradeExecutor contract's method `executeCall()` as the owner. To call this method, you need to set `target` as your Rollup contract address. Ensure that `targetCallData` starts with `0x89384960` (this is the signature of `setWasmModuleRoot(byte32)`), and that it's followed by your WASM module root. The `upgradeExecutor` contract address and Rollup contract address can be found in the chain deployment info JSON. You can confirm that the WASM module root was updated by calling `wasmModuleRoot()` on the Rollup contract. Once you have set the new WASM module root onchain, the validator will recognize blocks produced by your customized STF. You can now run your node with fraud proof verification enabled. You have two ways of running your node. #### 1. Using the docker-compose file As mentioned before, this is the recommended way if you're running your Arbitrum chain locally through the provided [docker-compose file](https://github.com/OffchainLabs/orbit-setup-script/blob/main/docker-compose.yaml#L39). In `docker-compose.yml`, modify the Docker image used for the Nitro container. Notice that we'll now use the `custom-nitro-node-dev` you just created: ```text ... nitro: image: custom-nitro-node-dev ports: ... ``` And run `docker compose up` to run all of your containers. #### 2. Use `docker run` to run your Nitro node only This method will only run the customized Nitro node (i.e., it will not run Blockscout, or the DA server if you're using an AnyTrust chain). Use the following command: ```shell docker run --rm -it -v /path/to/your/node/dir:/home/user/.arbitrum -p 0.0.0.0:8449:8449 custom-nitro-node-dev --conf.file /home/user/.arbitrum/nodeConfig.json ``` #### 4.2 Preserving the replay binary The primary issue with simply using a nitro-node-dev build is that, whenever the code changes at all, the replay binary will also change. If the node is missing the replay binary corresponding to the onchain WASM module root, it will be unable to act as a validator. Therefore, when releasing new node Docker images it's important to include the currently onchain WASM module root. To do that, you'll need to first extract the replay binary from the `nitro-node-dev` Docker image built earlier: ```shell docker run --rm --name replay-binary-extractor --entrypoint sleep custom-nitro-node-dev infinity docker cp replay-binary-extractor:/home/user/target/machines/latest extracted-replay-binary docker stop replay-binary-extractor cat extracted-replay-binary/module-root.txt mv extracted-replay-binary "target/machines/$(cat extracted-replay-binary/module-root.txt)" ``` These commands will output the new WASM module root, and create the directory `target/machines/`. There you'll find the three versions of the replay binary mentioned earlier: `replay.wasm`, `machine.v2.wavm.br`, and `module-root.txt`, along with some other optional files. Now that you've extracted the replay binary, there are two ways to add it to future Docker images, including non-dev image builds. You can either keep it locally and copy it in, or host it on the web. ##### Option 1: Store the extracted replay binary locally Now that we've extracted the replay binary, we can modify the Docker file to copy it into new Docker builds. Edit the `Dockerfile` file in the root of the nitro folder, and after all the `RUN ./download-machine.sh ...` lines, add: ```dockerfile COPY target/machines/ RUN ln -sfT latest ``` Replace each `` with the WASM module root you got earlier. ##### Option 2: Host the replay binary on the web To support building the Docker image on other computers without this local machine directory, you'll need to either commit the machine to git, or preferably, host the replay binary on the web. To host the replay binary on the web, you'll need to host the `replay.wasm` and `machine.v2.wavm.br` files somewhere. One good option is GitHub releases, but any hosting service works. Once you have those two files hosted, instead of the `COPY` and `RUN` command mentioned in option 1, you'll need to add these new lines to the `Dockerfile` file in the root of the nitro folder, after all the `RUN ./download-machine.sh ...` lines: ```dockerfile RUN wasm_module_root="" && \ mkdir "$wasm_module_root" && \ wget -O "$wasm_module_root/replay.wasm" && \ wget -O "$wasm_module_root/machine.v2.wavm.br" && \ echo "$wasm_module_root" > "$wasm_module_root/module-root.txt" && \ ln -sfT "$wasm_module_root" latest ``` Replace the `` with the WASM module root you got earlier, the `` with the direct link to the `replay.wasm` file (it must be a direct link to the file and not just a download site), and the `` with the direct link to the `machine.v2.wavm.br` file. ### Step 5. Verify the fraud proofs In theory, fraud proofs should now be working with your newly built Docker images. Make some transactions on your new blockchain, test out your modifications to the State Transition Function, wait for a batch to be posted, and you should be seeing "validation succeeded" log lines! If you see "Error during validation", then the replay binary is likely not up-to-date with your modifications to the State Transition Function. Ensure that the replay binary is freshly built, not missing any modifications, and that the WASM module root set in the Rollup contract matches your replay binary. --- > For a complete page index, fetch # Batch poster: External signing (KMS) Nitro's batch poster (and staker) sign their parent chain transactions through the **DataPoster** component. By default, it signs locally with a private key, but it also supports **generic RPC-based external signing**: instead of holding the key, Nitro sends an unsigned transaction to a remote signer over (m)TLS, gets back a signed transaction, and independently verifies it. > **INFO** > > There is **no native AWS KMS integration** in the Nitro codebase. KMS support is achieved by running a *separate signer service* that talks to KMS and exposes an Ethereum-style `eth_signTransaction` RPC endpoint. Nitro connects to that endpoint. In other words: "KMS support" = external signer pointed at a KMS-backed signing service. ## How it works internally 1. **Connect**—`rpcClient()` dials the signer URL with a TLS config: optional client cert/key for mTLS (`ClientCert`/`ClientPrivateKey`), optional `RootCA` (lets you use self-signed certs), and `InsecureSkipVerify`. 2. **Sign**—`externalSigner()` returns a signer callback that: * Converts the transaction to `apitypes.SendTxArgs` via `TxToSignTxArgs`. * It fully supports EIP-4844 blob transactions (blobs, commitments, proofs), which the batch poster needs. * Calls the configured RPC method: `client.CallContext(ctx, &data, opts.Method, args)`, expecting an RLP-encoded signed transaction back. * **Verifies** the returned transaction: the hash must match the request and the recovered sender must equal the configured `Address`. This means TLS is *not* relied on for authentication—the signature itself is checked at the application layer. ## Configuration The config struct is `ExternalSignerCfg`: | Field | koanf key | Purpose | | -------------------- | ---------------------- | -------------------------------------------------------------------------------------------- | | `URL` | `url` | RPC endpoint of the signer. **Setting this enables external signing** (overrides local key). | | `Address` | `address` | Hex Ethereum address the signer controls; used to verify returned signatures. | | `Method` | `method` | RPC method name, e.g., `eth_signTransaction`. | | `RootCA` | `root-ca` | (Optional) CA cert to trust — enables self-signed server certs. | | `ClientCert` | `client-cert` | (Optional) client cert for mTLS. | | `ClientPrivateKey` | `client-private-key` | (Optional) client key for mTLS (required if `client-cert` set). | | `InsecureSkipVerify` | `insecure-skip-verify` | Skip server TLS verification (not recommended). | This config is nested under both the batch poster and the staker, so the full CLI flag paths are: **Batch poster:** ```text --node.batch-poster.data-poster.external-signer.url --node.batch-poster.data-poster.external-signer.address --node.batch-poster.data-poster.external-signer.method --node.batch-poster.data-poster.external-signer.root-ca --node.batch-poster.data-poster.external-signer.client-cert --node.batch-poster.data-poster.external-signer.client-private-key --node.batch-poster.data-poster.external-signer.insecure-skip-verify ``` **Staker** uses the same fields under `--node.staker.data-poster.external-signer.*`. When `external-signer.url` is empty, the batch poster requires a local key. **AnyTrust chains need a local key even when external signing is enabled**: the external signer covers the batch transactions posted to the parent chain, but the requests sent to the DA Committee are still signed with a local key. See [External signer support](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md#external-signer-support) for how to configure the separate key. ## What the signer service must implement Your signer (whether KMS-backed or otherwise) must expose an HTTPS RPC server with a method matching `Method` that: * Accepts an Ethereum transaction object (`apitypes.SendTxArgs`—the standard `eth_signTransaction` shape, including blob fields for EIP-4844), * Returns the RLP-encoded **signed** transaction as a hex string, * Signs with the key for the address you configured as `address`. If you use mTLS, the server must require and verify the client cert that matches `client-cert`/`client-private-key`. ## Reference implementations Nitro ships two working examples you can model a KMS service on: * **`cmd/mockexternalsigner/mockexternalsigner.go`**—a standalone signer binary. It builds an `rpc.Server`, registers a signing method, serves over HTTPS with `tls.RequireAndVerifyClientCert`. Swap the local-key `txOpts.Signer` for a KMS-backed signer, and you have a KMS integration. * **`arbnode/dataposter/externalsignertest/externalsignertest.go`**—the test harness, showing the server side (`SignerAPI`, method registration, cert setup with `RequireAndVerifyClientCert`). The RPC method (`externalsignertest.go:185`) takes `*apitypes.SendTxArgs` and returns the RLP-encoded signed transaction as `hexutil.Bytes`. --- > For a complete page index, fetch # How to add your Arbitrum chain to Arbitrum's bridge This how-to will walk you through the process of adding your Arbitrum chain to the [Arbitrum bridge](https://bridge.arbitrum.io/). There's one section for mainnet Arbitrum chains, and another for local testnet Arbitrum chains. You can access either section using the links on the right column. ## Request adding a mainnet Arbitrum chain to the Arbitrum bridge Mainnet Arbitrum chains can be added to the Arbitrum Bridge by filling out [this form](https://github.com/OffchainLabs/arbitrum-token-bridge/issues/new/choose). Once receiving your request, our team will review and apply internal criteria to include your chain in the bridge. Here are some of the criteria that a chain must follow to be added to the bridge: * The use case must fall within existing legal and marketing guidelines * The core Rollup / token bridge contracts must not have been modified, except the cases where modifications have been done with a certified partnership and communicated to Offchain Labs * The infrastructure and core contracts are hosted by one of our partnered RaaS teams ## Add a local testnet Arbitrum chain to the Arbitrum bridge To add a testnet Arbitrum chain to the Arbitrum bridge you can submit a request at the same GitHub link and indicate that it is a testnet (). Congratulations! Your chain should now appear in both the network dropdown in the top navigation pane, and as an option in the bridging UI directly. --- > For a complete page index, fetch # How to adopt the bridged USDC standard on your Arbitrum chain Circle’s [Bridged **USDC** Standard](https://www.circle.com/blog/bridged-usdc-standard) is a specification and process for deploying a bridged form of **USDC** on EVM blockchains with optionality for Circle to upgrade to native issuance in the future. ## Why adopt the bridged **USDC** standard? When **USDC** is bridged into an Arbitrum chain, the default path is to use the chain’s [canonical gateway contracts for **ERC-20**'s](/how-arbitrum-works/deep-dives/token-bridging.md). By way of example, when a user bridges **USDC** from Arbitrum One to an Arbitrum chain, their Arbitrum One **USDC** tokens are locked into the Arbitrum chain’s parent side bridge, and a representative **USDC** token is minted to the user’s address on the Arbitrum chain, via the child side bridge. The challenge with this user flow is two-fold: 1. **Native vs. non-Native **USDC**:** The **USDC** tokens issued by Circle (’native USDC’) are locked in the parent side bridge contract. Conversely, the **USDC** tokens on the Arbitrum chain aren’t native **USDC** but are collateralized by the locked tokens in the bridge. As such, Circle will not recognize these tokens across their product suite. 2. **Fragmented UX:** If Circle were to provide native support for **USDC** by deploying a **USDC** contract on the Arbitrum chain, there would be two forms of **USDC** on the chain (native and non-native **USDC**). This leads to a fragmented user experience, and users with non-native USDC would have to withdraw to the parent chain to be able to turn their tokens into native **USDC**. By deploying the bridged **USDC** standard from the start, all **USDC** tokens that are bridged are locked in a gateway contract that can be adopted by Circle should a chain upgrade its **USDC** into native **USDC**. This allows **USDC** adoption on Arbitrum chains today without encountering either of the two problems above. ## How to implement the bridged **USDC** Standard We provide a custom **USDC** gateway implementation (for parent and child chains) that follows the Bridged **USDC** Standard. These contracts can be used by new Arbitrum chains. This solution will **not** be used in existing Arbitrum chains that are governed by the DAO. * On a parent chain the contract `L1USDCGateway` is used in case the child chain uses **ETH** as native currency, or `L1OrbitUSDCGateway` in case the child chain uses a custom fee token. * On a child chain, `L2USDCGateway` is used. * For the **USDC** token contracts, Circle's reference [implementation](https://github.com/circlefin/stablecoin-evm/blob/master/doc/bridged_USDC_standard.md) is used. This page describes how to deploy a **USDC** bridge compatible with both the Arbitrum chain token bridge and Circle’s Bridged **USDC** Standard. Steps for a transition to native **USDC** issuance are also provided. Note that both Circle and the Arbitrum chain owner must agree to transition to native **USDC** issuance. ## Requirements * Recommended token bridge version 1.2.0 * No additional dependencies with Nitro or Nitro contract version Other requirements: * It is assumed there is already a **USDC** token deployed and used on the parent chain. * Also, it is assumed that the standard Arbitrum chain ownership system is used, i.e., `UpgradeExecutor` is the owner of the `ownable` contracts, and there is an EOA or multi-sig that has the executor role on the `UpgradeExecutor`. * Refer to the [token bridge overview page](/launch-arbitrum-chain/deploy/token-bridge.md) for more information about the token bridge design and operational dynamics. You can learn more in our [overview of gateway operating models](/how-arbitrum-works/deep-dives/token-bridging.md#other-flavors-of-gateways). ## Deployment steps Throughout the docs and code, the terms `L1` and `L2` are used interchangeably with `parent chain` and `child chain`. They have the same meaning, i.e., if an Arbitrum chain is deployed on top of Arbitrum One, then Arbitrum One is `L1`/`parent chain`, while Arbitrum chain is `L2`/`child chain`. You can find more details by consulting the [usdc bridge deployment script and its README](https://github.com/OffchainLabs/token-bridge-contracts/tree/v1.2.3/scripts/usdc-bridge-deployment). Checkout target code, install dependencies, and build ```shell cd token-bridge-contracts yarn install yarn build ``` Populate your `.env` file based on `env.example` in the project's root directory ```shell PARENT_RPC= PARENT_DEPLOYER_KEY= CHILD_RPC= CHILD_DEPLOYER_KEY= L1_ROUTER= L2_ROUTER= INBOX= L1_USDC= ## OPTIONAL arg. If set, the script will register the gateway. Otherwise, it will store the transaction's payload in a file ROLLUP_OWNER_KEY= ``` Run the script: ```shell yarn deploy:usdc-token-bridge ``` The script will do the following: * Load deployer wallets for L1 and L2 * Register L1 and L2 networks in SDK * Deploy new L1 and L2 proxy admins * Deploy bridged (L2) **USDC** using the Circle's implementation * Init L2 **USDC** * Deploy L1 **USDC** gateway * Deploy L2 **USDC** gateway * Init both gateways * If `ROLLUP_OWNER_KEY` is provided, register the gateway in the router through the UpgradeExecutor * If `ROLLUP_OWNER_KEY` is not provided, prepare calldata and store it in the `registerUsdcGatewayTx.json` file * Set minter role to L2 **USDC** gateway with max allowance Now, new **USDC** gateways can be used to deposit/withdraw **USDC**. Everything is now in place to support transition to native **USDC** issuance if Circle and the Arbitrum chain owner agree to it. ## Transitioning to native **USDC** Once a transition to native **USDC** is agreed upon, the following steps are required: * L1 gateway owner pauses deposits on the parent chain by calling `pauseDeposits()` * L2 gateway owner pauses withdrawals on the child chain by calling `pauseWithdrawals()` * master minter removes the minter role from the child chain gateway > **NOTE** > > There should be no in-flight deposits when the minter role is revoked. If there are any, they should be finalized first. Anyone can do that by claiming the failed retryable tickets that execute a **USDC** deposit * L1 gateway owner sets Circle's account as burner on the parent chain gateway using `setBurner(address)` * L1 gateway owner reads the total supply of **USDC** on the child chain and then invokes `setBurnAmount(uint256)` on the parent/child gateway where the amount matches the total supply * **USDC** `masterMinter` gives the minter role with `0 `allowance to the L1 gateway so that the burn can be executed * on the child chain, the L2 gateway owner calls the `setUsdcOwnershipTransferrer(address)` to set the account (provided and controlled by Circle), which will be able to transfer the bridged **USDC** ownership and proxy admin * if not already owned by the gateway, the L2 **USDC** owner transfers ownership to the gateway, and proxy admin transfers admin rights to the gateway * Circle uses the `usdcOwnershipTransferrer` account to trigger `transferUSDCRoles(address)`, which will set the caller as **USDC** proxy admin and will transfer **USDC** ownership to the provided address * Circle calls `burnLockedUSDC()` on the L1 gateway using the `burner` account to burn the `burnAmount` of **USDC** * remaining **USDC** will be cleared off when remaining in-flight **USDC** withdrawals are executed, if any * The L1 gateway owner is trusted to not front-run this transaction to modify the burning amount --- > For a complete page index, fetch # Exchange integration checklist: deposit and withdrawal verification This checklist helps centralized exchanges (and other custodial integrators) detect deposits and process withdrawals reliably on Arbitrum One and Dedicated Blockchains. It also provides a repeatable procedure for re-verifying your indexing logic whenever a new Nitro release or an ArbOS upgrade reaches your chain. The two failure modes every exchange must avoid are the same: 1. **False credits**: crediting a user for a transaction that did not actually transfer value to a platform-controlled address (a reverted transaction, a spoofed event, the wrong token, or an internal call that did not succeed). 2. **Missed deposits**: failing to credit a real transfer because it arrived through a path your indexer does not scan (**ETH** moved by an internal call, a token moved without a top-level `transfer()`, or value delivered by an Arbitrum-specific transaction type). The detection flow below is designed to make both failure modes structurally impossible: you examine every transaction in every block, validate each one against its receipt and traces rather than its calldata, and credit only after the block is final on the parent chain. Re-verify on every upgrade ArbOS upgrades are Arbitrum's equivalent of a hard fork and can change trace output, gas accounting, and transaction-type handling. Treat the [Re-verification procedure](#re-verification-procedure) as mandatory before each upgrade activates on your chain, not as optional cleanup afterward. ## Before you start | Requirement | Why it matters | | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | A full node with the `debug` API enabled (`--http.api=eth,debug,net,web3`), or an RPC provider that exposes `debug_traceBlockByHash`. | Catching **ETH** delivered through internal calls requires call traces. The `eth` namespace alone is not sufficient. | | A Nitro version greater than or equal to the release that ships the latest ArbOS used by your chain. | Nitro is backward compatible, but trace output and gas accounting are tied to the active ArbOS version. | | The exact set of platform-controlled deposit addresses, plus supported token contract addresses with their decimals. | Every detection rule below keys off these two sets. | | Confirmation of whether your chain supports the `finalized` and `safe` block tags. | These tags are available on Arbitrum One. On a Dedicated Blockchain, support depends on your parent-chain configuration. | Run an archive or debug-capable node `debug_traceBlockByHash` may not return traces beyond a pruned node's retention window. Index in near real time, or run an archive node when you need to backfill historical blocks. ## How deposit detection works Poll the chain roughly once per second and process every block exactly once, in order, with no gaps: ```text eth_blockNumber → latest sequenced height └─ for each unprocessed height: eth_getBlockByNumber → block, including the ordered "transactions" array debug_traceBlockByHash → per-transaction call traces (tracer: callTracer) └─ for each transaction: eth_getTransactionByHash → transaction detail (to, value, input, type) eth_getTransactionReceipt → status, logs, gasUsed classify → ETH | ERC-20 | internal → add to the READY list └─ eth_getBlockByNumber("finalized") → credit READY deposits at or under the finalized height ``` Three properties make this correct. You examine every transaction in every block, plus internal calls through traces, so no value-bearing path is skipped. You credit only transactions whose receipt `status` is `0x1` and whose movement is confirmed by an event log or a trace, not solely by calldata. And you credit only after a block is finalized on the parent chain, so a parent-chain reorganization cannot reverse a credited deposit. Calldata here means transaction input, not batch data availability Throughout this guide, "calldata" means a transaction's `input` field, the per-transaction data you read over RPC and deliberately validate against receipts and traces. It is unrelated to how your chain posts batches to its parent chain, whether as EIP-4844 blobs or parent-chain calldata. That data-availability choice does not affect deposit detection: you always index transactions through the RPC methods below. ## Step 1: Stay in sync with the chain head 1. Call `eth_blockNumber` to get the latest sequenced height. 2. Compare it against your last scanned height. 3. For each missing height, call `eth_getBlockByNumber(height, true)` to retrieve full transaction objects. The returned `transactions` array is ordered; use that ordering as the index for the trace output in the next step. 4. **Handle reorganizations by rewinding, not patching.** Store each scanned block's hash, keyed by height. On each poll, confirm that every new block's `parentHash` matches the hash you stored for the height below it. A mismatch means the chain reorganized — and because a reorg can both remove a deposit and introduce a new one, re-checking only the records already in `READY` is not enough. Walk back to the most recent height whose stored hash still matches the canonical chain (the common ancestor), discard every `READY` entry and stored hash above it, move your scan cursor back to that height, and re-scan forward from there. You never need to rewind below the finalized height, because finalized blocks cannot reorganize. Arbitrum block timing Arbitrum produces blocks far more frequently than Ethereum, and the Sequencer assigns their ordering. The `latest` tag is the Sequencer tip and is not yet final. Never credit a deposit based on `latest`. See [Step 4](#step-4-confirm-finality-before-crediting). ## Step 2: Pull call traces for the block Call `debug_traceBlockByHash(blockHash, {"tracer": "callTracer"})`. The result is an array whose entries correspond one-to-one, by index, with the block's `transactions` array: `trace[i]` is the trace for `transactions[i]`. Each entry's `result` contains the top-level call plus a nested `calls` array describing internal calls. Only `CALL` frames carry **ETH** value; `STATICCALL` and `DELEGATECALL` frames never do. This is how you detect **ETH** that moved through an internal call rather than a top-level transfer. Attach each trace to its transaction by index before classifying, then confirm against the transaction hash. ## Step 3: Classify and validate every transaction For each transaction, first fetch its details and receipt: * `eth_getTransactionByHash(txHash)` returns `from`, `to`, `value`, `input`, and `type`. * `eth_getTransactionReceipt(txHash)` returns `status`, `logs`, and `gas` or `gasUsed`. **Gate first.** If `status` is not `0x1`, skip the transaction entirely. A reverted transaction never moves value, regardless of what its calldata claims. This single check is your primary defense against false credits. Then apply the checks below in order, A through C. ### A. Native ETH deposit This applies when `to` is one of your platform deposit addresses. Read `value`, convert it from hexadecimal to decimal, and divide by `10^18`. Record `{from, to, amount, coin: "ETH", blockNumber, blockHash, txHash}` in the `READY` list. ### B. Standard ERC-20 token deposit This applies when `to` is one of your supported token contract addresses, `input` begins with `0xa9059cbb` (the `transfer(address,uint256)` selector), and `input` is 138 hexadecimal characters long (`0x` plus 8 selector characters, 64 address characters, and 64 amount characters). Do not trust calldata alone. Confirm the transfer against the receipt: 1. `gas` is greater than or equal to `gasUsed`. 2. `logs` has at least one entry. 3. A log exists where all of the following hold: * `log.address` equals `receipt.to` (the token contract). * `topics[0]` equals `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef`, the `Transfer(address,address,uint256)` event signature. * `0x` followed by `topics[1][26:66]` (the sender) equals the transaction `from`. * `0x` followed by `topics[2][26:66]` (the recipient) equals `0x` followed by `input[34:74]`, and is a platform user's address. * `data[2:66]` (the transferred amount) equals `input[74:138]`. Credit `data[2:66]` as decimal, divided by `10^decimals` for that token. Record `{from, toAddress, amount, coin, blockNumber, blockHash, txHash}`. ### C. Internal token or ETH deposit This is the catch-all reached when neither A nor B matched. Value can still have reached a platform address through an internal call. Require `gas >= gasUsed`, and, for the token sub-case, require more than one log entry. For an internal token transfer, scan every entry in the receipt `logs`. Credit the transfer when `log.address` is one of your supported token contracts (this identifies the coin), `topics[0]` is the `Transfer` signature above, and `0x` followed by `topics[2][26:66]` is a platform user's address. Credit `data[2:66]` divided by `10^decimals`. For an internal **ETH** transfer, recursively scan the `callTracer` frames for the transaction — the top-level call and every entry in its nested `calls` arrays. Credit a frame when `to` is a platform user's address, `value` is not `0x0`, the frame's `type` is `CALL` (only `CALL` frames carry **ETH** value), the frame has no `error`, and the frame's `gasUsed` is less than or equal to its `gas`. Credit `value` divided by `10^18`. Arbitrum-specific deposits to watch Besides ordinary externally owned account transactions, value can arrive through Arbitrum-native transaction types. When a user bridges directly to an exchange-controlled address, the credit appears as an `ArbitrumDepositTx` (type `0x64`), where ArbOS adds balance to the destination after the parent-chain bridge locks the same funds. Retryable-ticket execution (`ArbitrumRetryTx`, `0x68`, and `ArbitrumSubmitRetryableTx`, `0x69`) can also deliver value. System transactions of type `ArbitrumInternalTx` (`0x6A`) are ArbOS-generated bookkeeping and must never be credited. Classify by observed value movement, as in A through C, rather than assuming every credit is a standard transfer. See [Geth at the core](/how-arbitrum-works/reference/geth.md) for the full list of transaction types. ## Step 4: Confirm finality before crediting Run an asynchronous loop that calls `eth_getBlockByNumber("finalized", false)` and reads `result.number` as the finalized height. For each `READY` entry whose block height is at or below the finalized height, credit the mapped user the recorded amount and coin. Because Step 1 rewinds and re-scans whenever a block's hash stops matching the canonical chain, every `READY` entry at or below the finalized height already reflects the canonical chain, and finalized blocks can no longer reorganize — so a match on height is sufficient to credit. The block tags carry different finality guarantees: * `latest` is the Sequencer tip and is not yet posted to the parent chain. Never credit on `latest`. * `safe` means the batch containing this block has been posted and reached finality on the parent chain. It is resistant to reorganizations but can still be reverted by a deep parent-chain reorganization. * `finalized` means the batch is finalized on the parent chain, and reversal is highly improbable. Credit occurs here. Finality on Dedicated Blockchains The `safe` and `finalized` tags are available on Arbitrum One. On a self-managed Dedicated Blockchain, support for these tags depends on your parent-chain configuration and on how your node tracks parent-chain finality. Confirm the behavior on your chain before relying on `finalized`, and document the expected confirmation latency for your integrators. ## Withdrawal flow Distinguish two operations that are both casually called "withdrawals." ### Exchange to the user on the same Dedicated Blockchain This is the common case: an ordinary transaction from your hot wallet to the user's address, either a native **ETH** transfer or a token `transfer()`. Submit the transaction and track it by hash. Mark the withdrawal complete only when `eth_getTransactionReceipt` returns a `status` of `0x1`, and apply the same finality discipline you use for deposits before treating it as irreversible. Use EIP-1559-style fee fields, and account for the parent-chain data-posting fee, which is reflected in the effective gas cost rather than in the gas units. ### Bridging value to the parent chain If the user withdraws to the parent chain (for example, from Arbitrum One to Ethereum), the funds move through the bridge and outbox and are subject to the dispute window before they can be claimed. Do not treat the parent-chain side as settled until the message is confirmed and executed through the outbox. Direct integrators to the bridge documentation for the current dispute-period mechanics rather than hard-coding a duration. ## Test vectors Maintain at least one fixture per detection path so you can assert that your indexer credits the right amount and rejects everything else. Capture each fixture from a live node (Arbitrum Sepolia is preferred because it receives ArbOS upgrades first) and pin it to the stated Nitro and ArbOS versions. | # | Scenario | Expected result | | ---- | ------------------------------------------------------------------------ | -------------------------------------------- | | TV-1 | Native **ETH** transfer to a platform deposit address, `status` `0x1` | Credit **ETH** equal to `value / 10^18` | | TV-2 | **ERC-20** `transfer()` to a supported token with a valid `Transfer` log | Credit token equal to `amount / 10^decimals` | | TV-3 | **ERC-20** `transfer()` that reverts (`status` `0x0`) | No credit | | TV-4 | Token moved through an internal call (no top-level `transfer()`) | Credit through the log scan in C | | TV-5 | **ETH** moved to a user through an internal call frame | Credit through the trace scan in C | | TV-6 | `ArbitrumDepositTx` (`0x64`) to a platform address | Credit **ETH** from the bridge deposit | | TV-7 | `ArbitrumInternalTx` (`0x6A`) system transaction | No credit | | TV-8 | `Transfer` event emitted by an unsupported or spoofed contract | No credit (address not in the set) | Each fixture should record the block hash, the transaction hash, the raw `eth_getTransactionByHash` and `eth_getTransactionReceipt` responses, the relevant slice of `debug_traceBlockByHash`, and the expected credit or rejection. ## Version compatibility ArbOS upgrades can change the details this logic depends on, so confirm versions against the [ArbOS releases overview](/run-arbitrum-node/arbos-releases/overview.md) and the [Nitro releases page](https://github.com/OffchainLabs/nitro/releases) before each integration cycle. Three things can change across an upgrade and affect your indexer: * **Tracer output** — the structure or completeness of `debug_traceBlockByHash` call frames. * **Gas accounting** — how `gasUsed` is computed and how the parent-chain data fee is represented. * **Transaction-type handling** — the addition of, or changes to, Arbitrum-native transaction types. None of the historical upgrades that prompted partner questions changed the core detection contract (poll, fetch the block, trace, validate logs and traces, then gate on finality). Each one still has to be re-verified, because an upgrade could change the byte-level details above. ## Re-verification procedure Run this whenever a new Nitro release or ArbOS upgrade targets your chain, before activation on your production chain rather than after. 1. **Watch the upgrade channels.** Subscribe to the [Arbitrum Node Upgrade Announcement channel on Telegram](https://t.me/arbitrumnodeupgrade) and read the relevant upgrade notice. 2. **Test on Sepolia first.** Arbitrum Sepolia receives ArbOS upgrades ahead of Arbitrum One. Point a staging indexer at a Sepolia node running the new Nitro version. 3. **Replay your test vectors.** Run TV-1 through TV-8 against the upgraded node and assert identical credit and rejection outcomes. 4. **Diff the raw responses.** Compare `debug_traceBlockByHash`, `eth_getTransactionReceipt`, and `eth_getBlockByNumber` payloads before and after the upgrade for the same fixtures, and investigate any structural change. 5. **Verify the two invariants explicitly.** Confirm that no invalid or reverted transaction produces a credit, and that no valid deposit on any path is missed. 6. **Confirm the finality tags.** Check that the `finalized` height advances and that you are not crediting on `latest`. 7. **Upgrade your own node** to the required Nitro version per the [Nitro support policy](/run-arbitrum-node/nitro-support-policy.md), then re-run Steps 3 through 6 on your production chain shortly after activation. 8. **Record the result** — node version, ArbOS version, date, and pass or fail, so the next upgrade has a baseline to diff against. For Dedicated Blockchain owners We recommend waiting at least four weeks after an ArbOS release is live on Arbitrum One before upgrading a self-managed chain, so that any stability issues surface first. ## Related resources * [ArbOS software releases: overview](/run-arbitrum-node/arbos-releases/overview.md) * [Geth at the core](/how-arbitrum-works/reference/geth.md) * [RPC methods: Arbitrum compared with Ethereum](/arbitrum-essentials/arbitrum-vs-ethereum/rpc-methods.md) * [Nitro support policy](/run-arbitrum-node/nitro-support-policy.md) --- > For a complete page index, fetch # Third-party Arbitrum chain infrastructure providers This document provides an overview of third-party Arbitrum chain infrastructure providers that support production-grade Arbitrum chain deployments. > **NOTE** > > This list is not exhaustive, and will be continuously updated as the Arbitrum ecosystem evolves. ## Rollup-as-a-Service (RaaS) providers For most production use-cases, we encourage Arbitrum chain operators to work with one of the following RaaS (Rollup as a Service) providers. These providers manage the infrastructure required to maintain high-performance, secure Arbitrum chain deployments: * [QuickNode](https://www.quicknode.com/) * [Caldera](https://www.caldera.xyz/) * [Conduit](https://conduit.xyz/) * [AltLayer](https://altlayer.io/) * [Gelato](https://www.gelato.network/) * [Asphere](https://www.ankr.com/rollup-as-a-service-raas) * [Alchemy](https://www.alchemy.com/rollups) * [Zeeve](https://www.zeeve.io) ## Chain explorers Chain explorers let you view transactions, blocks, addresses, and network activity associated with your Arbitrum chain. The following explorers support Arbitrum chains, and can be used to monitor and analyze your chain's activity: * [Blockscout](https://www.blockscout.com/) * [Socialscan](https://socialscan.io/) * [Lore](https://www.lorescan.com/) * [Routescan](https://routescan.io/) Additionally, Arbitrum chains leveraging blobs for data availability may use tools like [Blobscan](https://blobscan.com/) to see which blob/block includes a given transaction. ## Bridges You can easily launch an Arbitrum chain with a canonical token bridge, which allows transfers to and from the chain via Arbitrum One, Nova, or the parent chain to which your Arbitrum chain settles transactions. For applications that require the ability to transfer assets to chains outside of the Arbitrum ecosystem or in an expedited manner (without waiting for complete finality), the following third-party bridging providers can be used: * [LayerZero](https://layerzero.network/) * [Connext](https://www.connext.network/) * [Hyperlane](https://www.hyperlane.xyz/) * [Axelar](https://axelar.network/) * [Across](https://across.to/) * [Decent](https://www.decent.xyz/) ## Data availability providers for AnyTrust Chains [AnyTrust protocol](/how-arbitrum-works/deep-dives/anytrust-protocol.md) offers native support data availability. If you are turning on Fast Withdrawals, we recommend having at least three members as part of your Data Availability Committee. Here are some providers we recommend: * [Chainbase](https://chainbase.com/) * [Ankr](https://www.ankr.com/) * [Kiln](https://www.kiln.fi/) * [Chainstack](https://chainstack.com/) * [Nansen](https://www.nansen.ai/) * [Unifra](https://unifra.io/) * [BCW Group](https://bcw.group/) * [Caldera](https://www.caldera.xyz/) ## Indexers Indexers provide a convenient way to retrieve historic or application-specific data without having to interface with your chain through an RPC endpoint. The following third-party providers offer indexing services that can be used with Arbitrum chains: * [Alchemy](https://www.alchemy.com/) * [Chainstack](https://chainstack.com/) * [Goldsky](https://goldsky.com/) * [Ormi](https://www.ormilabs.xyz/) * [The Graph](https://thegraph.com/) * [Traceye](https://traceye.io/) * [QuickNode](https://www.quicknode.com/) * [Sequence](https://sequence.xyz/indexer) ## Oracles The following Oracle providers can be used to integrate offchain data with your Arbitrum chain's smart contracts: * [Chainlink](https://chain.link/) * [Chronicle](https://chroniclelabs.org/) * [Pyth](https://pyth.network/) * [Redstone](https://redstone.finance/) * [Randomizer](http://Randomizer.ai) (VRF only) * [Supra](https://supra.com/) * [RedStone](https://redstone.finance/) ## RPC endpoints RPC endpoints are the primary interface through which users and developers interact with any chain, whether it be for transaction submission, reading state, or indexing historical data. The following third-party providers offer RPC endpoint services compatible with Arbitrum chains: * [Alchemy](https://www.alchemy.com/) * [Ankr](https://www.ankr.com/) * [Chainstack](https://chainstack.com/) * [Grove](https://grove.city/) * [GetBlock](https://getblock.io/) * [QuickNode](https://www.quicknode.com) * [Sequence](https://sequence.xyz/node-gateway) ## Alternative data availability One way to reduce transaction fees for Arbitrum chains is to configure a Data Availability (DA) solution that stores chain data offchain. Although the AnyTrust protocol offers native support for this functionality (and is configurable by default on Arbitrum AnyTrust chains), the following third-party providers give you another way to store data offchain. Note that using these services will limit your chain's ability to leverage AnyTrust protocol improvements as they relate to transaction fee and DA configurability: * [Celestia](https://celestia.org/) * [EigenDA](https://www.eigenlayer.xyz/) * [AvailDA](https://www.availproject.org/) * [EspressoDA](https://docs.espressosys.com/network#data-availability) * [Near](https://docs.near.org/chain-abstraction/data-availability) --- > For a complete page index, fetch # Migrate between RaaSes ## Overview of the provider migration process A migration between RaaS providers is an operational handoff, not a redeployment of the Rollup. With planning, downtime can be kept to minutes, while the total transfer spans several days for infrastructure, partners, and communications. The new operator must stand up shadow infrastructure, replicate data, and pass health checks before the DNS and Batch‑poster handover. Warning This document is provided for informational purposes only and does not constitute an end-to-end guide for executing the migration. An infrastructure migration between RaaS providers will require significant planning between the two providers. Offchain Labs is unable to support a third party in an infrastructure handoff. OCL does not have special knowledge or a private “fast path.” This document provides the extent of our guidance. All questions about handoff planning should be between the two RaaS teams. We can provide [introductions to RaaSes](https://docs.arbitrum.io/launch-arbitrum-chain/third-party-integrations/third-party-providers#rollup-as-a-service-raas-providers). #### Who should read this doc? Chain owners and operators. #### What are the must-transfer items? [Sequencer](/how-arbitrum-works/deep-dives/sequencer.md) runtime, batch poster, bonder/validators, DAC signer set for [AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md), RPC/explorer domains, alerting/monitoring credentials. Your RaaS may have other requirements. #### Do chain ID or L1 contracts change? Normally, no, the chain will stay on the same chain ID and L1 Rollup contracts. #### Who coordinates ecosystem partners? Chain Owner. Typical partners include oracles, indexers, bridges, wallets, analytics, and custody, among others. #### How long is the downtime? With planning, you can keep downtime to minutes. RPC nodes will stop serving write transactions like `eth_sendRawTransaction` a few minutes before downtime. The full transfer (infra spin-up, partner handovers, comms) typically spans several days. #### What should we monitor during handover? Retryables creation, batch posting cadence, [Assertion](/how-arbitrum-works/deep-dives/assertions.md) Confirmation, base fee, sequencer health, DAC signer liveness. Please see '[Monitoring Tools and Considerations](https://docs.arbitrum.io/launch-arbitrum-chain/maintain-your-chain/monitoring-tools-and-considerations)’ for more information. #### What legal/administrative assets need to move? Domains (RPC/explorer), CDN accounts, SSL certificates, and GitHub/S3 buckets that host snapshots. Your RaaS provider may have different requirements. #### What is the process involved in migrating an existing Arbitrum chain from one RaaS operator to another? Chain migration will require close coordination between the old and new chain operators. With proper planning and coordination, a migration can result in only a few minutes of downtime, with the total transfer process occurring over several days. An end-to-end migration will require: 1. The new operator will spin up appropriate nodes & infrastructure (e.g., validators, sequencers) 2. Ownership transfer of any onchain contracts. 3. Ownership transfer of any important community assets / URLs (e.g., RPC or explorer URLs) 4. Updating onchain parameters responsible for identifying nodes (e.g., Validator whitelist) --- > For a complete page index, fetch # Migrate from another stack to an Arbitrum chain ## Overview of the stack migration process Warning This document is provided for informational purposes only and does not constitute an end-to-end guide for executing the migration. Offchain Labs doesn’t have special knowledge or a private “fast path.” This document provides the extent of our guidance. Your current RaaS provider owns and executes the migration. We can provide [introductions to RaaSes](/launch-arbitrum-chain/integrations/infrastructure-providers.md#rollup-as-a-service-raas-providers). ### Why migrate to an Arbitrum chain? Teams typically migrate when they need one or more of the following: * **Lower fees**: Arbitrum chain Rollup or [AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) for low transaction fees. * **Various customization options**: Custom gas token, flexible DA, [Timeboost](/how-arbitrum-works/timeboost/gentle-introduction.md), [BoLD](/how-arbitrum-works/bold/gentle-introduction.md), etc. * **Mature tooling and ecosystem**: Bridges, indexers, wallets, monitoring, and partners with Arbitrum chain-ready integrations. ### Two migration paths Teams coming from other stacks have two viable paths: * **Ecosystem migration (simpler, faster)**: Launch a fresh Arbitrum chain. Users/app Bridge funds and redeploy contracts. This approach is quicker and less risky, but it puts a burden on apps and users. * **Chain-state migration (harder, slower)**: Transplant state/history so apps keep addresses and positions. This method requires more engineering, testing, and downtime planning. The right choice depends on your user requirements, partner dependencies, and tolerance for downtime/engineering effort. Your RaaS owns and executes the migration plan. ### Can we keep the same chain ID? Teams need to decide whether to use the old chain ID or a new one. **Why this is relevant**: Wallets, explorers, and dApps assume that a given chain ID maps to a single, consistent history. * If you keep the same chain ID, you should also preserve historical details: Every historical block/hash and RPC response for the pre-migration range must remain consistent after migration. If not, apps and analytics will break. * If you’re going with a new chain ID, then you don’t need to preserve historical details. But you need to redirect users via UI, domains, and documentation. ### How should canonical bridges and token addresses be handled during migration? Teams should determine the canonical bridge for each asset and the authoritative token addresses before the handover. This decision is important because confusion here could cause liquidity fragmentation and cause users to bridge the “wrong” way. **Recommended actions**: * Publish an official asset list (symbols, decimals, canonical token address, and canonical bridge) before migration. * Enforce a one-way migration from the legacy chain to the new Arbitrum chain (freeze legacy deposits while allowing Arbitrum chain deposits). * Coordinate with third-party bridges/routers to repoint routes. * Deprecate legacy wrappers where needed and communicate exact swap/burn/mint paths. ### How do we preserve data & historical queries? **Problem**: After migration, historical data remains on the old stack, while new data resides on the Arbitrum chain. **Why it matters**: Apps, analytics, and explorers often read historical events. If they must re-integrate two endpoints, things will break. **Recommended action**: Run a dual-RPC gateway that: * Route pre-migration block range to an archive node on the old stack, and * Route post-migration requests to the new Arbitrum chain. Apps can continue to use a single RPC URL and don’t need to be aware of the boundary. ### How can we migrate without downtime on my chain? True, zero-downtime is not realistic for a Rollup stack migration; however, you can minimize downtime that impacts users. ### How long is the downtime for chain-state migration? Expect a freeze window of hours for snapshot integrity. The full project timeline is often set aside 4-8 weeks, including partner cutovers and audits. > **WARNING** — Important > > Pause withdrawals earlier than the main freeze window to ensure no funds get trapped in in-flight bridges (i.e., deposits paused at the start of the freeze and withdrawals paused earlier, as per bridge requirements). ### How can we migrate without losing state? Ideally, we would like to keep the full state You can use the chain-state migration process. The two common approaches are: * State replay: Re-execute legacy blocks into the Arbitrum chain. This process can be heavy, but it can preserve more historical information. **Caution**: If the legacy stack has different opcode semantics, precompiles, or system contracts, replay can diverge, resulting in a different end state even with identical transactions. Validate equivalence early on a private testnet before committing. * Genesis import: Export balances/storage and pre-seed them in the chain’s genesis. This process is lighter and can preserve the state at a chosen block. ### How can I migrate without affecting my Apps/ecosystem on my chain? Minimize surface changes and abstract the rest. Keep external interfaces stable: * Chain ID: Only if you can preserve historical details, otherwise choose a new chain ID * A consistent RPC URL via a dual-RPC gateway * Contract addresses via replay/genesis pre-allocation * Canonical assets/bridges. Coordinate early with wallets, indexers, bridges, and oracles to repoint routes and allow-lists. ### What breaks with zk-specific features? Zero knowledge (zk) pre-compiles don’t carry over. Port logic to Solidity or [Stylus](/stylus/gentle-introduction.md) or remove/replace functionality. Please check with your RaaS regarding this matter. #### What is the process for migrating an existing chain on a different stack to Arbitrum chain? On a high level, there are two paths for migration: * **Ecosystem migration:** Simplifies the chain operator experience by placing the burden on users and apps to self-migrate their assets and contracts to a new chain deployment. * **Chain state migration:** A more complex migration where the chain state gets transplanted into the new Rollup stack/contract system, but the ecosystem and user experience may stay relatively the same. The rest of this answer assumes that the team wants to perform the latter. For chain-state migration, the overall process for chain-state migration from another optimistic stack chain is: #### Contracts Migration: 1. Moving escrowed funds from the previous Rollup bridge into an Arbitrum chain bridge #### Node Migration & Set Up: 1. Freeze legacy chain writes and make RPC read-only: 1. Stop the legacy chain’s proposer/[Sequencer](/how-arbitrum-works/deep-dives/sequencer.md) so no new blocks or transactions get created after the chosen upgrade block. 2. Configure the legacy RPC to reject write methods (e.g., `eth_sendRawTransaction`) and only serve readable requests for historical data. 2. Ensure that you can export required state from your EVM-compatible client at the upgrade block. 3. Expect a freeze window of hours (depends on state size) for snapshot integrity. 4. Run Nitro nodes for post-migration traffic. 5. Run a legacy archive node in parallel. This archive node wouldn’t try to advance beyond the upgrade block. 6. Put a dual-RPC gateway in front that routes pre-migration ranges to the legacy archive node and post-migration ranges to Nitro, so apps keep a single RPC URL. The only example of a chain that has done this is Powerloom, and they have also [documented this in their records](https://docs.powerloom.io/category/chain-migration/). ### Is there a checklist that we can follow? Here is a rough outline of the tasks you need to complete to ensure a smooth cutover, assuming you have a 30-day lead time before the chain migration. Consult your RaaS provider for the most accurate guidance, as the checklist may not cover every detail. #### 2 months before → 4 weeks before migration: planning and thinking * [ ] Choose migration path * [ ] Select Arbitrum chain mode: Rollup (Ethereum DA) vs. AnyTrust (DA committee) * [ ] Chain ID strategy: keep or change. #### 4 weeks before → 3 weeks before migration: alignment * [ ] Open partner threads: Bridges, oracles, indexers, wallets, explorers, etc. * [ ] Publish migration documents (internal/external), asking for input/comments with rationale, rough dates, and contact points. #### 3 weeks before → 2 weeks before migration: build and test * [ ] RaaS: set up private Arbitrum chain * [ ] Testnet deployment: Sequencer, [Batch Poster](/how-arbitrum-works/deep-dives/assertions.md), Bonder, DA config. * [ ] Inventory contracts & state: Addresses, precompiles, proxies, admin keys, governance * [ ] Stand up Dual-RPC gateway prototype: Route pre-migration range to old stack archive; post-migration to Arbitrum chain. * [ ] Prepare official asset list: Canonical bridge(s), authoritative token addresses, symbols, decimals. * [ ] Set up monitoring & alerts: Retryables, batches, assertions, DAC liveness. #### 2 weeks before → 1 week before: cutover preparation * [ ] Announce freeze windows: Date/time, what pauses, expected downtime. * [ ] Lower DNS TTLs for RPC/explorer, provision new SSL certificates. * [ ] Partners confirm readiness. * [ ] Conduct a final dry run with canary transactions/batches in staging and document the process. #### 1 day before starting new chain: * [ ] Pause new deposits on legacy chain; keep withdrawals open if safe. #### The day of launching the new chain: * [ ] Stop old chain block/transaction production: disable proposer/sequencer, set legacy RPC to read-only, block `eth_sendRawTransaction` method, thus no new blocks or transactions. * [ ] Take a snapshot or confirm the last block for state replay * [ ] Bring up the new Arbitrum chain mainnet: import state or launch genesis. * [ ] Start batch poster/bonder and verify assertions on L1. * [ ] Flip DNS for RPC/explorer; enable dual-RPC gateway for historical queries * [ ] Open canonical bridges and publish asset list * [ ] Make announcements after all health checks clear and partner confirmations pass. #### 1 day after launch → 1 week after launch: harden and handoff * [ ] Watch retryables/batches/assertions and base-fee curve * [ ] Confirm indexer backfills and wallet chain-add UX * [ ] Incentivize any remaining users/apps to move: if ecosystem path. * [ ] Decommission legacy infrastructure after ≥ 72 hours of stable activity. --- > For a complete page index, fetch # How to upgrade ArbOS on your Arbitrum chain This how-to provides step-by-step instructions for Arbitrum chain operators who want to upgrade ArbOS on their Arbitrum chain(s). Familiarity with [ArbOS](/how-arbitrum-works/deep-dives/arbos.md), Arbitrum chains, and [chain ownership](/launch-arbitrum-chain/operate/ownership-and-access.md) is expected. Note that Arbitrum chain owners have full discretion over when and whether to upgrade their ArbOS version. The specific upgrade requirements for each ArbOS release are located under each reference page for that specific [ArbOS release](/run-arbitrum-node/arbos-releases/overview.md#list-of-available-arbos-releases). #### Step 1: Update Nitro on nodes and validators Refer to the [requirements for the targeted ArbOS release](/run-arbitrum-node/arbos-releases/overview.md) to identify the specific [Nitro release](https://github.com/OffchainLabs/nitro/releases/) that supports the ArbOS version that you're upgrading to. For example, if your upgrade targets ArbOS 51, you'd use Nitro `v3.9.6` (Docker image: `offchainlabs/nitro-node:v3.9.6-91bf578`) or higher. This is the version of the Nitro stack that needs to be running on each of your Arbitrum chain's nodes. A list of [all Nitro releases can be found on Github](https://github.com/OffchainLabs/nitro/releases). Begin by upgrading your validator node(s) to the specified Nitro version, then update each remaining Arbitrum chain node to match this version. Note that upgrading your node version *must occur* before the deadline established for the target ArbOS upgrade. Refer to the timestamp in the ArbOS upgrade schedule for a precise deadline. #### Step 2: Upgrade the WASM module root & your chain's Nitro contracts While every ArbOS upgrade will require an update to the WASM module root, not every ArbOS upgrade will require an upgrade to the chain's `nitro-contracts` version. If necessary, as defined in the release notes for each ArbOS release ([example of ArbOS 51](/run-arbitrum-node/arbos-releases/arbos51.md)), you may need to deploy new versions of some (or all) of the Nitro contracts to the parent chain of your Arbitrum chain. These contracts include the rollup logic, bridging logic, fraud-proof contracts, and interfaces for interacting with Nitro precompiles. To verify the current version of your Nitro contracts, follow [these instructions](https://github.com/OffchainLabs/chain-actions/blob/main/README.md#check-version-and-upgrade-path) while replacing the inbox contract address and network name with that of your Arbitrum chain. This information will allow you to find the correct upgrade path for your Nitro contracts. To update the WASM module root and deploy your chain's Nitro contracts to the parent chain for the most recent ArbOS release, you will need the following inputs (obtained from the [requirements for the targeted ArbOS release](/run-arbitrum-node/arbos-releases/overview.md)): * The WASM module root, and if necessary, * The required `nitro-contracts` version Once you have the WASM module root and have identified the required `nitro-contracts` version for the target ArbOS release, if any, [please follow the instructions in this guide](https://github.com/OffchainLabs/chain-actions?tab=readme-ov-file#nitro-contracts-upgrades) for specific actions based on the `nitro-contracts` version you are deploying. Note that each ArbOS release will require performing this step with a different WASM module root and may require a different version of `nitro-contracts`. The guide linked above will be kept updated with the instructions for each specific ArbOS release. The `WASM module root` is a 32-byte hash created from the Merkelized Go replay binary and its dependencies. When ArbOS is upgraded, a new WASM module root is generated due to modifications in the State Transition Function (STF). This new WASM module root must be set in the rollup contract on the parent chain. For example, the WASM module root for ArbOS 51 Dia is `0xc2c02df561d4afaf9a1d6785f70098ec3874765c638e3cb6dbe8d3c83333e14c` (consensus-v51.1). To set the WASM module root manually (i.e., not using the above guide), use the `Rollup proxy` contract's [`setWasmModuleRoot`](https://github.com/OffchainLabs/nitro-contracts/blob/38a70a5e14f8b52478eb5db08e7551a82ced14fe/src/rollup/RollupAdminLogic.sol#L321) method. Note that the `upgrade executor` contract on the parent chain is the designated owner of the Rollup contract, so the **chain owner account** needs to initiate a call to the `upgrade executor` contract in order to perform the upgrade. This call should include the correct calldata for setting the new WASM module root. Backward compatibility WASM module roots are backward compatible, so upgrading them before an ArbOS version upgrade will not disrupt your chain's functionality. #### Step 3: Schedule the ArbOS version upgrade To schedule an ArbOS version upgrade for your Arbitrum chain, [follow this guide](https://github.com/OffchainLabs/chain-actions/tree/main/scripts/foundry/arbos-upgrades/at-timestamp). In addition to the upgrade action contract address and the account address for the chain owner account, you will need the following inputs: 1. **`newVersion`**: Specify the ArbOS version you wish to upgrade to (e.g., `51`). 2. **`timestamp`**: Set the exact UNIX timestamp at which you want your Arbitrum chain (Orbit) to transition to the new ArbOS version. If you would prefer to do this manually, simply call the [`scheduleArbOSUpgrade`](https://github.com/OffchainLabs/nitro-precompile-interfaces/blob/fe4121240ca1ee2cbf07d67d0e6c38015d94e704/ArbOwner.sol#L116) function on the `ArbOwner` [precompile](/arbitrum-essentials/precompiles/reference.md) of the Arbitrum chain(s) you're upgrading. Because this is an administrative action (similar to upgrading your Wasm module root), the **chain owner account** must call the target chain's `upgrade executor` contract with the appropriate calldata in order to invoke the `scheduleArbOSUpgrade` function of the ArbOwner precompile. This will schedule the ArbOS upgrade using the specified version and timestamp. Immediate upgrades To upgrade immediately (without scheduling), set the timestamp to `0`. Obtaining the current ArbOS version You can obtain the current ArbOS version of your chain by calling `ArbSys.ArbOSVersion()`. Keep in mind that this function adds `55` to the current ArbOS version. For example, if your chain is running on ArbOS 10, calling this function will return `65`. When scheduling the ArbOS upgrade through `ArbOwner.scheduleArbOSUpgrade` you must use the actual ArbOS version you're upgrading to. For example, if you're upgrading to ArbOS 51, you will pass `51` when calling this function. #### Step 4: Enable ArbOS specific configurations or feature flags (not always required) For some ArbOS upgrades, such as [ArbOS 51 Dia](/run-arbitrum-node/arbos-releases/arbos51.md), there may be additional requirements or steps that need to be satisfied to ensure your Arbitrum chain can use all of the new features and improvements made available in that particular ArbOS release. If there are additional requirements for the targeted ArbOS release you're attempting to upgrade to; the additional requirements will be listed on the reference pages for [the targeted ArbOS release](/run-arbitrum-node/arbos-releases/overview.md#list-of-available-arbos-releases). For example, the additional requirements for Arbitrum Chains upgrading to ArbOS 51 can be found [here on the ArbOS 51 docs](/run-arbitrum-node/arbos-releases/arbos51.md). Congratulations! You've upgraded your Arbitrum chain(s) to the specified ArbOS version. --- > For a complete page index, fetch # Batch poster troubleshooting This guide covers common operational issues that batch poster operators encounter, with guidance on diagnosing root causes and resolving them. For initial setup, see [Run a batch poster](/launch-arbitrum-chain/run-a-node/batch-poster.md). For fee-related tuning, see [Batch poster fee tuning](/launch-arbitrum-chain/chain-config/batch-poster/fee-tuning.md). ## Batch poster balance management The [batch poster](/how-arbitrum-works/deep-dives/assertions.md) account is an Externally Owned Account (EOA) that pays gas fees on the parent chain to submit batch transactions. There is no automatic mechanism to keep this account funded—operators must monitor the balance and replenish it manually or through external automation. ### Symptoms of low balance * Batch posting transactions fail on the parent chain due to insufficient funds * The batch poster logs show transaction submission errors * Unposted batches accumulate, causing a growing backlog (see [Batch posting backlog diagnosis](#batch-posting-backlog-diagnosis)) ### Monitoring guidance The [Monitoring tools and considerations](/launch-arbitrum-chain/operate/monitoring.md) guide recommends monitoring the batch poster balance. In practice, this means: * **Query the balance directly**: Use the parent chain RPC to check the batch poster EOA balance at regular intervals. * **Set up alerting**: Configure external monitoring (e.g., a balance-checking script, onchain monitoring service, or infrastructure alerting tool) to notify you when the balance drops below a threshold. Balance threshold guidance There is no universally recommended balance threshold—the appropriate level depends on your parent chain's gas prices, your chain's batch posting frequency, and batch sizes. As a general approach, estimate your daily batch posting cost (number of batches per day multiplied by average gas cost per batch) and maintain a buffer of several days' worth of posting costs. Monitor actual spending over time and adjust accordingly. ### Funding strategy * **Keep the account overfunded**: The recommendation is to keep the batch poster account overfunded rather than maintaining a tight balance. This provides a buffer against gas price spikes. * **Automate top-ups if possible**: Consider scripting periodic balance checks and transfers from a treasury account to the batch poster EOA. * **Monitor alongside gas prices**: A balance that seems adequate during low gas prices may drain quickly during sustained gas spikes. Factor in parent chain gas price volatility when developing your funding strategy. ## Mempool errors ### "Posting this transaction will exceed max mempool size" This error indicates that the parent chain node's mempool has rejected a transaction from the batch poster because it was at capacity. ### What causes this error * **Too many pending transactions**: The batch poster has submitted multiple transactions that haven't yet been included in a block, and the parent chain node's mempool has reached its limit for pending transactions from a single account or overall. * **Gas price too low for inclusion**: If gas prices have risen since the transactions were submitted, older transactions may remain in the mempool and not be included. New submissions then push against the mempool's size limits. * **RBF not escalating fast enough**: When using DB or Redis storage (which support RBF), the replacement fee schedule may not be aggressive enough to get transactions included during congestion, causing the mempool to fill with stale transactions. ### Resolution 1. **Check the parent chain's current gas prices** and compare them to your data poster's fee cap configuration. If gas prices are above your `--node.batch-poster.data-poster.target-price-gwei`, the data poster can't bid high enough to cover the fee. See [Batch poster fee tuning](/launch-arbitrum-chain/chain-config/batch-poster/fee-tuning.md) for guidance on adjustments. 2. **Review your queued transaction storage configuration**: If you're using DB or Redis storage, pending transactions accumulate and rely on RBF for inclusion. Consider whether your RBF schedule (`--node.batch-poster.data-poster.replacement-times` for non-blob batch poster, `--node.batch-poster.data-poster.blob-tx-replacement-times` for blob batch poster) is escalating fees quickly enough. See Queued transaction database selection for storage option details. 3. **Consider Noop storage for parent chains without mempools**: If your parent chain is an Arbitrum chain or another chain without a traditional mempool, Noop storage avoids this issue entirely by waiting for each transaction receipt before submitting the next one. Mempool size limits The exact mempool weight limits depend on the parent chain node's configuration, not the batch poster itself. The batch poster does not control the parent chain node's mempool size. If you are running your own parent chain node, consult its documentation for mempool configuration. If using a third-party RPC provider, the mempool limits are determined by the provider's infrastructure. #### `ErrExceedsMaxMempoolSize` → "error posting batch" * \[DEBUG→WARN→ERROR over 5 min]—The next batch's nonce would exceed `unconfirmed nonce + max-mempool-transactions`, so it's held back. * \[CAUSE]—L1 is confirming the poster's transactions slowly, and the in-flight window (default: 18) is filled. * \[MEANING]—Normal back-pressure short-term; sustained means posts are stuck. * \[ACTION]—If persistent, check L1 confirmation/fees, consider raising `max-mempool-transactions`. #### `lack of L1 balance prevents posting transaction with desired fee cap` * \[WARN]—The poster's wallet can't cover the target max cost, so the bid is capped at the available balance. * \[CAUSE]—Low parent-chain balance and/or high L1 fees. * \[ACTION]—Fund the L1 wallet (`send-l1`); watch `arb/batchposter/wallet/eth`. #### `a large batch posting backlog exists` * \[INFO/WARN/ERROR]—Unposted messages are piling up; `INFO` if it recently hit L1 bounds, `WARN` at backlog > 10, `ERROR` at > 30. * \[CAUSE]—Posts not keeping up (fees, mempool cap, L1 congestion, or the poster is behind). * \[ACTION]—Investigate why posting is throttled; `ERROR` is an alerting threshold. #### `error fetching batch poster wallet balance` / `...gas refunder balance` * \[WARN]—Balance gauge update failed. * \[CAUSE]—L1 RPC hiccup. * \[MEANING]—Monitoring gap only, not a posting failure. * \[ACTION]—Ignore if transient. ## Batch posting backlog diagnosis A batch posting backlog occurs when the sequencer continues ordering transactions, but the batch poster can't submit them to the parent chain at the same rate. This results in an accumulation of unposted messages. ### Common causes | Cause | Description | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Insufficient batch poster balance | The EOA does not have enough funds to pay parent chain gas fees | | Gas prices exceeding fee cap | Parent chain gas prices are above `--node.batch-poster.data-poster.target-price-gwei`, causing the data poster to pause submissions | | Parent chain congestion | Even with adequate fees, high parent chain congestion can delay transaction inclusion | | Node sync issues | The batch poster node may have fallen behind in syncing with the parent chain, preventing accurate fee estimation or transaction submission | | Reverted transactions (DB/Redis) | When using DB or Redis storage, a reverted batch posting transaction causes the batch poster to halt. Noop storage tolerates reverts and retries automatically | ### How to identify a backlog * **Monitor the `SequencerInbox` contract**: Track the frequency of batch submissions to the parent chain. A sudden drop or stop in batch submissions indicates an issue. * **Check the sequencer's transaction backlog size**: As noted in the [Monitoring tools and considerations](/launch-arbitrum-chain/operate/monitoring.md) guide, a large and growing backlog can be a sign of network health issues. * **Review batch poster logs**: Look for error messages related to transaction submission failures, fee estimation errors, or the data poster pausing. Backlog metrics The specific log messages and metrics that indicate a backlog depend on the Nitro node version and configuration. Operators should familiarize themselves with their node's log output during normal operation so that deviations are easier to spot. Key log prefixes to watch for include `BatchPoster:` and `DataPoster:` entries. ### Resolution approaches 1. **Check the batch poster balance**: ensures the EOA has sufficient funds on the parent chain. 2. **Check the parent chain gas prices**: If prices exceed your fee cap, either wait for prices to drop or increase `--node.batch-poster.data-poster.target-price-gwei` (see [Batch poster fee tuning](/launch-arbitrum-chain/chain-config/batch-poster/fee-tuning.md)). 3. **Verify parent chain connectivity**: Ensure the batch poster node can reach the parent chain RPC endpoint and that the parent chain node is fully synced. 4. **Check for reverted transactions**: If using DB or Redis storage, a reverted transaction halts the batch poster. Check the batch poster logs for revert errors. Restarting the batch poster or clearing the queued transaction storage (with caution) may be necessary. 5. **Review node sync status**: Confirm the batch poster node is synced with both the Arbitrum chain and the parent chain. ## Retry errors ### "Failed to re-send transaction" This error appears when the data poster attempts to resubmit a batch posting transaction (typically as part of RBF), and the resubmission fails. ### What causes this error * **Nonce conflicts**: The original transaction may have already been included onchain, making the replacement attempt invalid because the nonce has already been consumed. * **Storage inconsistencies**: When using DB or Redis for queued transaction tracking, mismatches between the stored transaction state and the onchain state can cause resubmission failures. This can happen after a node restart if the storage wasn't cleanly synced. * **Parent chain rejection**: The parent chain node may reject the replacement transaction for various reasons—the replacement fee isn't high enough (must be at least 10% higher for standard RBF), the transaction format is invalid, or the parent chain node is experiencing issues. Error context The exact phrasing and context of retry errors can vary across Nitro versions. The "failed to re-send transaction" message generally indicates an RBF attempt that didn't succeed. Check the surrounding log lines for more specific error details (for example, "nonce too low", "replacement transaction underpriced", or connection errors). ### Resolution 1. **Check if the original transaction was included**: Query the parent chain for the batch poster's latest nonce and compare it to what the data poster expects. If the original transaction was already included, the retry error is benign—the data poster should recover automatically on the next cycle. 2. **Review the queued transaction storage**: If using DB or Redis, the stored transaction state may be stale. A node restart typically resolves transient storage inconsistencies. 3. **Check parent chain connectivity**: Ensure the batch poster can reliably reach the parent chain RPC. Intermittent connectivity causes retry failures. 4. **Inspect RBF fee escalation**: If the error mentions "replacement transaction underpriced", the fee increase between attempts may be insufficient. The parent chain typically requires at least a 10% fee increase for RBF. Review the `blob-tx-replacement-times` schedule—shorter intervals may not allow enough price movement between attempts. ## Reverts and halting #### `Large gap between last seen and current block number, skipping check for reverts` * \[WARN]—`pollForReverts` fell > 100 blocks behind the chain, so it skipped scanning the intervening blocks for reverted poster transactions and fast-forwarded. * \[CAUSE]—Node lag, a long pause, or a header-subscription stall. * \[MEANING]—Reverts in that skipped window won't be detected. * \[ACTION]—Usually transient; if frequent, investigate node sync/L1 RPC health. #### `Transaction from batch poster reverted` * \[WARN→ERROR]—A batch posting transaction got a failed receipt on L1. * \[ERROR (not WARN)]—When using persistent storage, it sets `batchReverted` and **halts** all further posting. * \[CAUSE]—Contract rejected the batch (e.g., message-count mismatch, bad sequence number, gas/blob issue). * \[ACTION]—Investigate `txErr`; restart clears the halt flag; a count mismatch after force-inclusion may need `allow-posting-first-batch-when-sequencer-message-count-mismatch`. #### `Error checking batch reverts` * \[DEBUG/WARN]—Revert check failed. * \[DEBUG]—If the error contains "not found" (a benign parent-node inconsistency where one node served a header, another lacks it), else WARN. * \[ACTION]—Ignore the DEBUG case; investigate persistent WARNs. ## Nonce/sync #### `failed to update nonce with queue empty; falling back to using a recent block` * \[WARN]—Couldn't get the finalized nonce, so it used a recent block instead. Safe because the queue is empty. * \[CAUSE]—Finality data unavailable/RPC issue. * \[ACTION]—Benign one-off; investigate if constant. #### `Failed to get current nonce` * \[WARN]—Nonce refresh failed, but a previous nonce exists, so it's non-fatal. * \[CAUSE]—L1 RPC. * \[ACTION]—Ignore if transient. #### `Failed to get latest nonce` * \[WARN]—Couldn't fetch the unconfirmed nonce this loop; the iteration backs off 10s (`minWait`). * \[ACTION]—Transient RPC. #### `failed to update tx poster balance` / `failed to update tx poster nonce` * \[WARN]—Periodic state refresh in the data-poster loop failed; on balance failure, the loop backs off 10s. * \[ACTION]—Transient RPC. #### `DataPoster failed to send transaction` * \[WARN]—The RPC `SendTransaction` call errored. * \[CAUSE]—Mempool rejection, underpriced, RPC issue. * \[MEANING]—The transaction stays queued and is retried. * \[ACTION]—Watch for repetition. #### `maybeLogError` family—`failed to replace-by-fee transaction` / `failed to re-send transaction` * \[DEBUG/INFO → WARN/ERROR after 20 consecutive]—Per-nonce send/RBF errors that escalate if they persist. `storage.ErrStorageRace` starts `DEBUG`; `ErrFutureReplacePending`/`ErrNonceTooHigh` start `INFO`; anything else is immediate `ERROR`. * \[ACTION]—The escalated `WARN/ERROR` is the real signal—investigate then. ## Fee/pricing #### `can't meet data poster fee cap obligations with current target max cost` / `can't meet current parent chain fees with current target max cost` * \[INFO]—The computed fee cap can't cover the current L1 base fee/required cost. * \[CAUSE]—L1 fees spiked above the poster's escalation target, or balanced-capped. * \[ACTION]—If posts stall, tune the fee formula (`target-price-gwei`, `urgency-gwei`, `max-fee-bid-multiple-bips`) or fund the wallet. #### `submitting transaction with GasFeeCap less than latest basefee` / `...BlobGasFeeCap less than latest blobfee` * \[INFO]—Posting anyway with a cap below the current fee, expecting it to confirm as fees drop. * \[ACTION]—Normal during fee volatility; concerning only if posts never confirm. #### `unable to fetch suggestedTipCap from l1 client to update arb/batchposter/suggestedtipcap metric` * \[WARN]—Couldn't get a tip suggestion for the metric. * \[MEANING]—Metric gap only. * \[ACTION]—Ignore if transient. ## L1 bounds/reorg #### `Disabling batch posting due to batch being within reorg resistance margin from layer 1 minimum block or timestamp bounds` * \[ERROR]—The batch's first message is within `reorg-resistance-margin` of the L1 minimum bound, so posting is refused this round. * \[CAUSE]—The margin guard (default 10m) protects against reorgs near the lower bound. * \[ACTION]—Expected safety behavior; set margin to `0` only if you accept the reorg risk. #### `disabling L1 bound as batch posting message is close to the maximum delay` * \[ERROR]—Overriding the L1 block bound because messages are near `max-delay`; the `l1-block-bound-bypass` margin kicked in to avoid stalling. * \[ACTION]—Informational; means it chose to post over respecting the bounds. #### `not posting more messages because block number or timestamp exceed L1 bounds` * \[INFO]—Stopped adding messages that fall outside the current L1 bound window. * \[ACTION]—Normal bounding behavior. #### `error getting max time variation on L1 bound block; falling back on latest block` The L1 node couldn't give state at the finalized bound block, so it falls back to the latest block—usually transient. #### `unknown L1 block bound config value; falling back on using finalized` * \[ERROR]—Bound resolution issues; the latter means a bad `l1-block-bound` config value. * \[ACTION]—Fix the config value for the `ERROR`. #### `DataPoster is avoiding creating a mempool nonce gap` * \[INFO]—Held a transaction back rather than create a nonce gap that a reorg could expose (predecessor not yet reorg-resistant). * \[ACTION]—Normal reorg-safety; the transaction is retried. ## DA/fallback (AnyTrust/AltDA) #### `DA writer failed, operator action required` * \[ERROR]—A non-fallback DA writer error; posting stops. * \[ACTION]—**Investigate immediately**—this is an explicit operator-action alert. #### `DA writer explicitly requested fallback` * \[WARN]—A DA backend requested a fallback; the poster moves to the next writer/EthDA. * \[ACTION]—Check DA provider health; sustained fallback means degraded DA. Related info * `DA writer reports message too large, will rebuild batch` * `DA writers exhausted, will rebuild for EthDA` * `EthDA fallback period complete, will retry AltDA` These pair with the `da_success`/`da_failure`/`da_last_success` metrics. ## Lock/coordination and gas estimation #### `Error checking if we could acquire redis lock` * \[WARN]—Redis lock check failed; it optimistically tries anyway. * \[CAUSE]—Redis connectivity. * \[ACTION]—Check Redis if high availability matters. #### `Not posting batches right now because another batch poster has the lock or this node is behind` * \[DEBUG]—Normal on backup posters/when behind. * \[ACTION]—Expected; not an error. #### `Failed to estimate gas for EIP-7623 check 1/2` * \[WARN]—An EIP-7623 calldata-cost estimation probe failed. * \[ACTION]—Usually transient; relevant only on EIP-7623 parent chains. #### `error estimating gas for batch` * \[escalates via ephemeral handler]—Gas estimation failed (`ErrNormalGasEstimationFailed`); DEBUG→WARN→ERROR over 5 min. * \[CAUSE]—L1 state lag, reverting estimation, inbox not caught up. * \[ACTION]—Investigate if it reaches `ERROR`. ## Config/startup #### `max-size is deprecated; use max-calldata-batch-size...` * \[ERROR]—Deprecated flag in use. * \[ACTION]—Migrate to `max-calldata-batch-size`. #### `Disabling data poster storage, as parent chain appears to be an Arbitrum chain without a mempool` * \[INFO]—Auto-switched to no-op storage on an Arbitrum parent. * \[ACTION]—Expected for L3s. #### `messagesPerBatch is somehow zero` * \[WARN]—Defensive guard against a should-be-impossible state; defaults to `1`. * \[ACTION]—Benign unless recurring. ## The escalation rule to remember Many of these (mempool size, storage race, gas estimation, nonce-too-high, accumulator-not-found) are logged quietly at first and only escalate to `WARN/ERROR` if they persist past \~1–5 minutes, and `batchPosterFailureCounter` increments **only** at `ERROR` level. So a single `WARN` is usually noise; a *sustained* one that reaches `ERROR` is the real signal. Pair these with the metrics: `estimated_batch_backlog`, `wallet/eth`, the `dataposter/nonce/*` gap, and `da_failure`. ## Quick reference | Symptom | Likely cause | Resolution | | --------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------- | | Batch posting stopped, no error in logs | Batch poster balance depleted | Fund the batch poster EOA on the parent chain | | "Mempool weight limit exceeded" | Too many pending transactions in parent chain mempool | Review fee cap settings; check RBF escalation schedule | | Batches not posting during gas spike | Gas prices exceed `--node.batch-poster.data-poster.target-price-gwei` | Increase fee cap or wait for prices to normalize | | "Failed to re-send transaction" | Nonce conflict or RBF underpriced | Check if original transaction was included; review fee escalation | | Growing sequencer backlog | Multiple possible causes | Check balance, gas prices, parent chain connectivity, and node sync | | Batch poster halted after revert | Using DB/Redis storage with a reverted transaction | Review revert cause; restart batch poster or clear storage | | Blob transactions pending for hours | Blob tip cap too low | Increase `max-blob-tx-tip-cap-gwei`; review replacement schedule | --- > For a complete page index, fetch # BoLD upgrade playbook for multisig and security council chains This guide covers the operational sequencing of a BoLD upgrade for chains where the `execute` call is performed by a multisig or a security council rather than a single chain owner key. It continues from the step-by-step upgrade in [BoLD for Arbitrum chains](/launch-arbitrum-chain/chain-config/validation/bold.md) and assumes you have read it. Concepts, the full parameter table, and the base upgrade steps are not repeated here. If your chain owner is a single key that can send the upgrade transaction immediately, you do not need this guide—follow the [base steps](/launch-arbitrum-chain/chain-config/validation/bold.md) instead. ## The problem this guide solves The [base upgrade](/launch-arbitrum-chain/chain-config/validation/bold.md) sequence contains two steps that appear to conflict when a security council is involved: * **Step 4** runs `bold-populate-lookup`, which reads the last confirmed assertion so the new Rollup contract can be initialized from it. * **Step 5** executes the upgrade, and reverts if a new assertion has been confirmed since Step 4. The [base guide](/launch-arbitrum-chain/chain-config/validation/bold.md) therefore recommends stopping all validators at [Step 4](/launch-arbitrum-chain/chain-config/validation/bold.md#step-4-gather-the-information-of-the-current-state-of-the-chain) to prevent a new confirmation from blocking [Step 5](/launch-arbitrum-chain/chain-config/validation/bold.md#step-5-run-the-upgrade-script). For a production chain, Step 5 is not a transaction you can send on demand. Signers must review a simulation before signing, and collecting a security council's signatures can take days. Read literally, the base sequence implies you must keep every validator stopped for that entire period—a multi-day halt to assertion posting and confirmation on a production chain, which is unacceptable and carries its own risks. *You do not have to do this.* The following sections explain why. ## Why the signed payload is independent of the assertion The upgrade action's entry point takes no assertion data: ```solidity function perform(address[] memory validators) external ``` The genesis state for the new Rollup contract is not passed in as an argument. Instead, `perform` reads it at execution time from a separate helper contract, `StateHashPreImageLookup`: ```solidity bytes32 latestConfirmedStateHash = OLD_ROLLUP.getNode(OLD_ROLLUP.latestConfirmed()).stateHash; (ExecutionState memory genesisExecState, uint256 inboxMaxCount) = PREIMAGE_LOOKUP.get(latestConfirmedStateHash); ``` That helper stores its entries in a mapping keyed by state hash, and its setter is permissionless with no access control beyond a hash-consistency check: ```solidity mapping(bytes32 => bytes) internal preImages; function set(bytes32 h, ExecutionState calldata executionState, uint256 inboxMaxCount) public { require(h == stateHash(executionState, inboxMaxCount), "Invalid hash"); preImages[h] = abi.encode(executionState, inboxMaxCount); emit HashSet(h, executionState, inboxMaxCount); } ``` See [`BOLDUpgradeAction.sol`](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/BOLDUpgradeAction.sol) for both contracts. Three consequences follow, and together they resolve the conflict: 1. **The calldata your signers approve never changes.** What the council signs is an upgrade executor `execute` call wrapping `perform(validators)`. Because no assertion data appears in that calldata, running `bold-populate-lookup` again does not alter the payload, and therefore does not invalidate signatures already collected. 2. **Populating the lookup is additive, not destructive.** `set` writes to a new mapping key per state hash. Repeated runs accumulate entries; they do not overwrite or clear earlier ones. A lookup entry for an assertion that has since been superseded is harmless. 3. **Anyone can populate the lookup at any time.** `set` is `public` and requires no privileged key, so refreshing the lookup is not itself a governance action and needs no signatures. What `perform` actually requires is narrower than the [base guide](/launch-arbitrum-chain/chain-config/validation/bold.md) implies. It is not that no assertion may be confirmed after Step 4. It is that the lookup must contain an entry for whichever assertion is latest-confirmed **at the moment of execution**. Confirm this sequence with the Offchain Labs team before executing it on a production chain. The reasoning above is drawn from the contract source, but a mainnet BoLD upgrade is a high-consequence, one-way operation, and your chain may use customized contracts for which it does not hold. ## Recommended sequence This sequence keeps validators running throughout signature collection. ### Step 1: Prepare and deploy the upgrade action Follow Steps 0 through 3 of the [base upgrade guide](/launch-arbitrum-chain/chain-config/validation/bold.md#step-0-pre-requisites) without changes. Validators keep running. This deploys the action contract with your chain's configuration and fixes the address that the `execute` payload will target. ### Step 2: Populate the lookup once, to produce the payload and simulation Run `bold-populate-lookup` and then `bold-local-execute` with a non-owner key, which prints the payload rather than sending it: ```shell $ L1_PRIV_KEY=xxx yarn script:bold-populate-lookup --network {mainnet|arb1|nova|base|sepolia|arbSepolia} $ L1_PRIV_KEY=xxx yarn script:bold-local-execute --network {mainnet|arb1|nova|base|sepolia|arbSepolia} ``` Use the printed `execute(...)` calldata as the payload for your multisig transaction, and use the populated state to produce the simulation your signers will review. ### Step 3: Collect signatures with validators running Circulate the payload and simulation. Validators continue posting and confirming assertions normally during this period. Assertions confirmed now will not match the lookup entry from Step 2, which is expected and is corrected in the next step. Note Distinguish two things that behave differently as the chain advances. A **signature** covers the transaction calldata, which is fixed, so it stays valid. A **simulation** reflects onchain state at the time it was produced, so it goes stale as new assertions confirm. If your signing process requires signers to review a fresh simulation, re-run the simulation against current state and re-populate the lookup beforehand—but you do not need to re-collect signatures that were already given for the same calldata. ### Step 4: Re-populate the lookup immediately before execution Once you have the signatures and are ready to execute, run `bold-populate-lookup` again: ```shell $ L1_PRIV_KEY=xxx yarn script:bold-populate-lookup --network {mainnet|arb1|nova|base|sepolia|arbSepolia} ``` This writes an entry for the assertion that is latest-confirmed right now. Any key can send it. Info The script finds the last confirmed assertion by searching for its `NodeCreated` event in the most recent 100,000 parent-chain blocks. If your chain confirms assertions infrequently enough that the event falls outside that window, the script cannot find it. Take this into account before pausing assertion creation for any length of time. ### Step 5: Execute the upgrade Submit the signed multisig transaction. Then continue with Steps 6 and 7 of the [base guide](/launch-arbitrum-chain/chain-config/validation/bold.md) to update node configuration, restart nodes, and monitor the new Rollup contract. ## Closing the residual race One narrow race remains: an assertion could be confirmed in the interval between your final `bold-populate-lookup` in Step 4 and your `execute` transaction landing. If that happens, `perform` looks up a state hash that has no entry and the transaction reverts with: ```shell Hash not yet set ``` This is a benign, recoverable failure. The upgrade did not partially apply—it reverted. Choose whichever mitigation fits your operations, in rough order of preference: * **Submit both in one bundle or block.** If your tooling can batch, sending `set` and `execute` together removes the gap entirely. This is the cleanest option and requires no validator downtime at all. * **Retry.** Because `set` is permissionless and inexpensive, simply re-running Step 4 and resubmitting is often the pragmatic answer, particularly on chains that confirm assertions hours apart. * **Freeze validators briefly, at execution time only.** Stop assertion-confirming validators shortly before you submit, rather than for the whole signature window. This is the base guide's recommendation, narrowed from days to minutes. If you choose this, read the following section first, because a freeze has a side effect on the validator allowlist. ## Keeping validation permissioned across the upgrade Two configuration parameters govern whether your chain stays permissioned. They are often assumed to compete; they do not, and setting only the first is not sufficient. | Parameter | Role | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `disableValidatorWhitelist` | When `false` (the recommended value), only addresses in the `validators[]` array may post assertions. | | `validatorAfkBlocks` | A liveness escape hatch. If assertions stop being confirmed for this many parent-chain blocks, **anyone can permissionlessly disable the allowlist**, regardless of `disableValidatorWhitelist`. | `validatorAfkBlocks` is not overridden by `disableValidatorWhitelist`. It is the mechanism that removes the allowlist, and it applies precisely when the allowlist is in force. The parameter table's note that `validatorAfkBlocks` is ignored when `disableValidatorWhitelist` is `true` is a statement that the escape hatch is redundant on a chain that is already permissionless—not that setting `disableValidatorWhitelist` to `false` protects you from it. This matters directly to the upgrade, because **any validator freeze is a period of assertion inactivity** and counts toward the window. It is the main reason not to hold a multi-day freeze while collecting signatures. To keep validation permissioned: * Set `disableValidatorWhitelist` to `false` and populate `validators[]`. * Set `validatorAfkBlocks` to `0`, which disables the escape hatch entirely. This is preferable to setting an artificially large value, which achieves the same intent less clearly and still leaves a finite window. * If you keep the escape hatch enabled, alert on assertion inactivity well before the window elapses. See the validator section of [Monitoring tools and considerations](/launch-arbitrum-chain/operate/monitoring.md) for the metrics to watch. Disabling the escape hatch with `validatorAfkBlocks: 0` is a deliberate trade-off. It removes the protection that lets a chain recover permissionlessly if every allowlisted validator becomes unavailable. Make this choice knowingly, and pair it with monitoring and an on-call rotation for your validator set. To weigh permissioned against permissionless validation, see [BoLD for Arbitrum chains](/launch-arbitrum-chain/chain-config/validation/bold.md#caveats-that-come-with-adopting-arbitrum-bold-for-permissionless-validation). It covers bond sizing and resource exhaustion risk. ## Preparing the bond token BoLD changes what validators bond with. The new bond token must be in place before the upgrade, not after. **BoLD requires an ERC-20 bond token.** In the BoLD Rollup contract, bonding runs entirely through **ERC-20** transfers: ```solidity function newStake(uint256 tokenAmount, address _withdrawalAddress) external whenNotPaused function receiveTokens(uint256 tokenAmount) private { IERC20(stakeToken).safeTransferFrom(msg.sender, address(this), tokenAmount); } ``` No bonding function in the BoLD `RollupUserLogic` is `payable`, so there is no native-currency path. Whether this is a migration for your chain depends on which pre-BoLD variant you are upgrading from. Legacy `nitro-contracts` shipped two Rollup user logic contracts, and your chain uses exactly one of them: | Pre-BoLD contract | Bonding | Effect of the upgrade | | ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `RollupUserLogic` | Native parent chain currency, via `payable` functions calling `_newStake(msg.value)` | The bond asset changes. By default the node handles this automatically: it wraps native currency into **WETH** (`--node.bold.auto-deposit`) and approves it for bonding (`--node.bold.auto-increase-allowance`), so validator wallets only need sufficient native currency. Manual acquisition and approval are only needed if these flags are set to `false`, or if the `stakeToken` is not **WETH**-compatible (auto-deposit cannot acquire other tokens). | | `ERC20RollupUserLogic` | An **ERC-20** token already | The bond asset does not necessarily change. Confirm whether your configured `stakeToken` is staying the same. | Determine which one your chain runs before planning validator preparation. Do not assume the native-currency case, even though it is the more common one. Why operators think BoLD chains bond in ETH On Arbitrum One and Arbitrum Nova the `stakeToken` is **WETH**, the **ERC-20** wrapper around **ETH**. Explorers and dashboards often display this as **ETH**, which makes it look as though BoLD supports native bonding. It does not—the contract holds **WETH**. If you are checking your own chain's configuration, read `stakeToken` from the Rollup contract rather than relying on a display label. Before you execute the upgrade: * Confirm the `stakeToken` address in your upgrade configuration is a compliant **ERC-20** on the parent chain. **WETH** is the recommended choice, and Offchain Labs does not recommend custom tokens as the bond asset. * Ensure each validator wallet holds enough of that token to meet `stakeAmt`, in addition to parent-chain native currency for gas. * Plan for first-time bonding on the new contract. Validators bond only once, but existing bonds do not carry over from the old Rollup contract, so every validator must bond again after the upgrade. For guidance on choosing `stakeToken` and `stakeAmt` values, see [Bond and validator configurations](/launch-arbitrum-chain/chain-config/validation/bond-and-validator.md) and [Economics of disputes](/how-arbitrum-works/bold/bold-economics-of-disputes.md). ## After the upgrade Once the upgrade executes, watch for `AssertionCreated` and `AssertionConfirmed` events on the new Rollup contract, as described in the base guide. If assertions do not appear, or validators fail to start or bond, see [Validator troubleshooting](/launch-arbitrum-chain/operate/validator-troubleshooting.md). --- > For a complete page index, fetch # Batch poster recovery This section covers how the batch poster recovers state after a crash, restart, or bad start—including DB restore from a batch-poster checkpoint, storage backends, revert and halt recovery, Redis failover, nonce sync, and reorg handling. ## The batch poster is mostly stateless—its checkpoint lives on L1 The batch poster does **not** primarily checkpoint its position in its own database. On every posting attempt it reconstructs where to resume from authoritative sources: the **SequencerInbox contract on L1** plus the local **inbox tracker DB**. Its own data-poster DB holds only the **in-flight transaction queue** (transactions sent but not yet confirmed), for replace-by-fee (RBF). This is why recovery is robust: lose the data-poster DB and the poster still knows exactly where to resume. ## Recovery state machine flow ![Recovery state machine flow](/img/bp-recovery.png) Recovery state machine flow ### The position "checkpoint" This is wired as the data poster's `MetadataRetriever`. So the "checkpoint" (`batchPosterPosition`: message count, delayed count, next sequence number) is RLP-encoded into each transaction's metadata, but the **source of truth** is L1 + the inbox tracker, not a snapshot. ```go func (b *BatchPoster) getBatchPosterPosition(ctx context.Context, blockNum *big.Int) ([]byte, error) { bigInboxBatchCount, err := b.seqInbox.BatchCount(...) // <-- read from L1 contract ... prevBatchMeta, err = b.batchMetaFetcher.GetBatchMetadata(inboxBatchCount - 1) // <-- inbox tracker DB return rlp.EncodeToBytes(batchPosterPosition{ MessageCount: prevBatchMeta.MessageCount, DelayedMessageCount: prevBatchMeta.DelayedMessageCount, NextSeqNum: inboxBatchCount, }) } ``` ### How resume actually works on restart 1. `FetchLast()` the data-poster queue. **If non-empty** (transactions survived restart), resume from `lastQueueItem.Nonce()+1` with its stored metadata—continues exactly where it left off, RBF-ing pending transactions. 2. **If empty**, call `updateNonce` to sync the nonce from L1, then fetch position metadata via `getBatchPosterPosition`. If `updateNonce` fails, a non-persistent queue that's still waiting for L1 finality returns an error and retries next round; otherwise the poster reads the nonce from a recent block (the "failed to update nonce with queue empty; falling back to using a recent block" warning). In short: *queue intact → resume in-flight; queue gone → rebuild cleanly from L1*. ## Storage backends and what survives a restart | Backend | Persists? | Recovery behavior | | ---------------- | --------------------------------------------- | ---------------------------------------------------------------------------------- | | **dbstorage** | Yes (consensus DB, `BatchPosterPrefix` table) | Queue rehydrated from keyed entries; `FetchContents`/`FetchLast` reload on startup | | **redisstorage** | Yes (shared) | Sorted-set keyed by nonce; **HMAC-signed** entries; enables failover | | **noop** | No | Stores nothing; post-and-forget; used when parent chain is Arbitrum (no mempool) | Notably, when the parent chain itself is an Arbitrum chain, the data poster **forces no-op storage**—there's no L1 mempool to RBF into, so there's nothing to persist or recover. ## Recovery mechanism 1: `dangerous.clear-dbstorage` When the persisted queue gets into a bad state, set `--node.batch-poster.data-poster.dangerous.clear-dbstorage`. At construction, with DB storage active, it calls `PruneAll` before starting: ```go func (s *Storage) PruneAll(ctx context.Context) error { idx, err := s.lastItemIdx(ctx) // dbstorage/storage.go:94 ... return s.Prune(ctx, until+1) // delete every entry through the last } ``` `Prune` iterates and batch-deletes all keys below the bound and rewrites the count. After clearing, the empty-queue path above rebuilds nonce and position from L1. **Use once, then unset**—it discards in-flight transaction tracking, so clearing while transactions are genuinely pending risks nonce conflicts or double-posting. It's a no-op unless `use-db-storage` is the active backend. ## Recovery mechanism 2: revert → halt, and force-inclusion recovery **Halt on revert**. `pollForReverts` watches L1; `checkReverts` finds a failed receipt from the poster's sender and returns `shouldHalt := !UsingNoOpStorage()`. On a confirmed revert it sets `b.batchReverted.Store(true)`; `MaybePostSequencerBatch` then refuses to post: `"batch was reverted, not posting any more batches"`. This is deliberate—a revert means something is wrong; the poster stops rather than burn funds. Recovery requires operator investigation and a restart (which clears the in-memory `batchReverted` flag). **Force-inclusion mismatch** `dangerous.allow-posting-first-batch-when-sequencer-message-count-mismatch` handles the case where the poster's DB message count drifts from the chain's `sequencerReportedSubMessageCount`. The scenario: poster down >24h, someone force-includes a delayed message via the parent contract (which doesn't bump `sequencerReportedSubMessageCount`), so on restart the inbox reader's count diverges. The fix: ```go prevMessageCount := batchPosition.MessageCount if b.config().Dangerous.AllowPostingFirstBatchWhenSequencerMessageCountMismatch && !b.postedFirstBatch { ... prevMessageCount = 0 // contract skips the prevMessageCount equality check when it's 0 } ``` Setting `prevMessageCount = 0` tells the SequencerInbox to skip the equality check, so the first post goes through and re-aligns the onchain count. It applies **only to the first batch after startup** (`!b.postedFirstBatch`) — once posted, the mismatch resolves itself. ## Recovery mechanism 3: Redis failover (high availability) Multiple posters coordinate via `redislock`, default `Enable: true`, `LockoutDuration: 1m`, `RefreshDuration: 10s`: 1. Primary holds the lock: the loop checks `CouldAcquireLock` and logs `"Not posting batches right now because another batch poster has the lock or this node is behind"` on backups. 2. If the primary crashes, the lock **expires after `LockoutDuration`**; a backup with `background-lock` acquires it. 3. The backup recovers shared state from **Redis queue storage**, which is **HMAC-signed** so a tampered queue is rejected. Nonce continuity comes from `updateNonce` against L1. So failover state-sharing rides on persistent, signed Redis storage plus L1-derived nonce and position. ## Recovery mechanism 4: nonce/sync on restart `updateNonce` queries the finalized (or latest) L1 nonce; when it advances past `s.Nonce` it logs `"Data poster transactions confirmed"` and **prunes confirmed txs** from the queue (`Prune(ctx, nonce-1)`). On a failed fetch with a prior nonce it's non-fatal (`"Failed to get current nonce"` warning). `wait-for-l1-finality` (default true) governs whether it tracks finalized vs latest — trading confirmation latency for reorg safety. ## Reorg handling * **Parent-chain reorg, revert polling**: if the chain went backward (`nextRevertCheckBlock > blockNum`) it resets to re-check; the >100-block gap warning fast-forwards and skips. * **Mempool nonce-gap avoidance**: before sending a transaction of a different type than its predecessor (or whose predecessor isn't yet reorg-resistant), it checks the sender nonce one block back (latest − 1) and, if the nonce exceeds the reorg-resistant count, leaves the transaction queued—`"DataPoster is avoiding creating a mempool nonce gap"`—rather than risk a gap a reorg would expose. * **`l1-block-bound`** (`safe`/`finalized`/`latest`/`ignore`) and **`reorg-resistance-margin`** (default 10m) keep batches from referencing L1 blocks that could reorg out (see also [Batch poster troubleshooting](/launch-arbitrum-chain/operate/batch-poster-troubleshooting.md)). ## Where state physically lives The data-poster DB is a table within the consensus DB: `rawdb.NewTable(consensusDB, storage.BatchPosterPrefix)`. The inbox tracker independently persists `SequencerBatchCountKey`/`DelayedMessageCountKey` (initialized to `0` if absent). A full DB restore therefore restores both the in-flight queue **and** the inbox-tracker counts—but even a wiped data-poster table self-heals from L1. ## Operator recovery cheat-sheet * *Corrupt or stuck queue* → restart with `data-poster.dangerous.clear-dbstorage` (once), then remove it. * *Batch reverted or poster halted* → investigate the revert, then restart to clear `batchReverted`. * *Message-count mismatch after force-inclusion or long downtime* → restart with `dangerous.allow-posting-first-batch-when-sequencer-message-count-mismatch` (first batch only). * *Primary died, HA configured* → automatic failover after `lockout-duration`; backup resumes from signed Redis state + L1 nonce. * *Lost data-poster DB entirely* → no manual action needed (just make sure there is no pending parent chain batch posting transaction); position rebuilds from L1 + inbox tracker, queue starts empty. --- > For a complete page index, fetch # Arbitrum Node Key Rotation Guide This guide covers how to rotate keys for different roles in your Arbitrum Orbit chain: batch poster, validator/bonder, and Data Availability Servers (DAS). Important Prerequisites * **Backup**: Always back up your current configuration and private keys before starting * **Testing**: Test key rotation on a testnet environment first, if possible * **Downtime**: Plan for potential brief service interruptions during the rotation process * **Permissions**: Ensure you have the necessary permissions to call the required smart contract functions ## 1. Batch poster key rotation Critical warning For sequencer nodes running both the sequencer and batch poster on the same server, you **must** first split them and then configure Redis for message synchronization. Failure to do so may cause chain reorganizations. Refer to the [High Availability Sequencer documentation](/launch-arbitrum-chain/run-a-node/high-availability-sequencer.md) for setup instructions. ### Prerequisites * A new batch poster account funded with sufficient **ETH** for gas fees * Access to call functions on the `SequencerInbox` contract * Gracefully stop the current batch poster ### Generate new batch poster keys #### 1. Enable the new batch poster Call the following function on the `SequencerInbox` contract. The caller must be the **rollup owner** or the **batch poster manager**. ```solidity setIsBatchPoster(, true) ``` If you also want to transfer the batch poster manager role to a new address, the **rollup owner** must call: ```solidity setBatchPosterManager() ``` #### 2. Update the node configuration * Update your node configuration to use the new private key * Restart the batch poster service with the new configuration #### 3. Verify operation * Monitor logs to confirm the new batch poster is successfully submitting batches * Check that transactions are processing normally #### 4. Disable the old batch poster ```solidity setIsBatchPoster(, false) ``` ## 2. Validator/bonder key rotation ### Prerequisites * A new validator account funded with sufficient **ETH** * The required bond amount available for the new validator * Access to call functions on the `Rollup` contract ### Activate new keys #### 1. Enable new validator Call the following function on the `RollupAdminLogic` contract. The caller must be the **rollup owner**. Note that `setValidator` accepts arrays, allowing you to enable or disable multiple validators in a single call. ```solidity setValidator([], [true]) ``` #### 2. Update the node configuration * Update your validator node configuration to use the new private key * Restart the validator service #### 3. Verify the new validator operation * Monitor that the new validator successfully posts bond transactions * Confirm that assertion and confirmation transactions are submitted successfully #### 4. Disable the old validator ```solidity setValidator([], [false]) ``` #### 6. Recover old validator bond (if applicable) * Wait for your old validator's latest bond assertion to be confirmed * Call `reduceDeposit(0)` on the `RollupUserLogic` contract to reduce the bond to zero * Call `withdrawStakerFunds()` on the `RollupUserLogic` contract to withdraw the released funds ## 3. AnyTrust Data Availability Committee (DAC) rotation ### Prerequisites * A new DAC keyset generated * Access to call functions on the `SequencerInbox` contract ### Generate, deploy, and verify new DAC keys #### 1. Generate a new keyset * Follow the [Generate Keyset documentation](/launch-arbitrum-chain/chain-config/data-availability/configure-dac.md#step-1-generate-the-keyset-and-keyset-hash-with-all-the-information-from-the-servers) with your new group of DAS servers * Ensure that all new DASs are properly configured and accessible #### 2. Register the new keyset onchain Call the following function on the `SequencerInbox` contract. The caller must be the **rollup owner**. ```solidity setValidKeyset() ``` Note The `setValidKeyset` function takes the raw keyset bytes as its only parameter. The assumed-honest count is encoded within the keyset bytes themselves during keyset generation — it is not a separate contract parameter. #### 3. Deploy new DAS servers * Start the new DAS with the updated configuration * Verify they are accessible and responding to health checks #### 4. Verify integration * Monitor that the batch poster can successfully write batches to the new DAS servers * Check DAS server logs for successful data storage operations * Confirm data availability requests are getting handled successfully #### 5. Invalidate the old keyset (optional) Once the new DAS servers and keyset are fully verified, you can optionally invalidate the old keyset to prevent it from being used. Call the following function on the `SequencerInbox` contract. The caller must be the **rollup owner**. ```solidity invalidateKeysetHash() ``` Note After invalidating the old keyset, you **must** stop and restart the batch poster so it picks up the new keyset. If you skip this step, the batch poster will continue attempting to use the old (now invalid) keyset and fail to submit batches. * You can obtain the old keyset hash from the `SetValidKeyset` event that was emitted when the old keyset was originally registered * Do **not** invalidate the old keyset until the new keyset is fully verified and the batch poster is successfully posting with the new DAS servers — invalidating prematurely can disrupt data availability for your chain * After invalidation, the old keyset can no longer be used to certify data availability certificates --- > For a complete page index, fetch # Monitoring tools and considerations When deploying and maintaining an Arbitrum chain, there are several key elements that need to be monitored. This page has two parts: 1. **Monitoring tools** — scripts and services available to chain maintainers. 2. **What to monitor, by component** — a reference of the specific metrics, onchain signals, and thresholds to watch for each part of the stack (sequencer, batch poster, validator, and chain fees), plus deployment and hardware guidance. The metric names in this page are the internal Nitro metric paths (for example, `arb/sequencer/backlog`). On the Prometheus scrape endpoint, each `/` is replaced with `_` (so `arb/sequencer/backlog` is scraped as `arb_sequencer_backlog`). See [Enabling Nitro metrics](#enabling-nitro-metrics) for how to expose them. ## Arbitrum chain verification script The [Arbitrum chain verification script](https://github.com/OffchainLabs/arbitrum-chain-sdk/tree/feat-add-verification-scripts/examples/verify-rollup) retrieves information from an Arbitrum chain and its parent chain to verify that all parameters are configured correctly. After gathering the data, it generates a comprehensive report and issues warnings for any discrepancies detected. This tool is particularly useful after deploying and configuring an Arbitrum chain, to make sure that the onchain information has been correctly set. Info The Arbitrum chain verification script is currently under active development and is considered a work-in-progress (WIP). Consequently, its findings should be approached with caution, as there is a potential for false positives. ## The `arbitrum-monitoring` alerting suite The [`arbitrum-monitoring`](https://github.com/OffchainLabs/arbitrum-monitoring) repository provides five ready-to-run monitoring scripts that watch the critical liveness signals of an Arbitrum chain and can send alerts to Slack, with more monitors under active development. All monitors read your chain's parameters (RPC endpoints and core contract addresses) from a shared `config.json` file, and can be run once (for example, on a schedule) or continuously. ### Retryable monitor [Retryable tickets](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md) are messages sent from a parent chain and executed on the Arbitrum chain. Due to their asynchronous nature (they are executed several minutes after being created), if insufficient funds are provided at the time of creation, they might not automatically redeem (execute) upon arrival at the Arbitrum chain. When this occurs, a manual redemption of the ticket is required. The [retryable monitor](https://github.com/OffchainLabs/arbitrum-monitoring/tree/main/packages/retryable-monitor) tracks these tickets from creation through execution: it watches for ticket creation events on the parent chain, then checks each ticket's status on the Arbitrum chain — automatically redeemed, manually redeemed, still pending, or failed. It alerts on tickets that failed to redeem automatically and on tickets approaching their seven-day expiration window, so that no cross-chain message expires unexecuted. ### Batch poster monitor The [batch poster monitor](https://github.com/OffchainLabs/arbitrum-monitoring/tree/main/packages/batch-poster-monitor) tracks batch posting activity and data availability. It alerts when no batches have been posted within the chain's expected time bounds while user transactions are pending, when the batch poster account balance falls below an estimate of about three days of posting costs, and when a backlog of unposted blocks accumulates. For [AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) chains, it also detects when the chain has fallen back to posting full calldata on the parent chain instead of DACerts, which indicates a problem with the Data Availability Committee. ### Assertion monitor The [assertion monitor](https://github.com/OffchainLabs/arbitrum-monitoring/tree/main/packages/assertion-monitor) tracks the creation and confirmation of assertions on the chain's Rollup contract, supporting both [BoLD](/how-arbitrum-works/bold/gentle-introduction.md) and legacy (pre-BoLD) chains. It alerts when no assertions are being created despite chain activity, when assertions remain unconfirmed after the challenge period has elapsed, and on validator-related risks such as low validator participation, a base stake below the expected threshold on BoLD chains, or a disabled validator allowlist on legacy chains. ### ArbOS version monitor The [ArbOS version monitor](https://github.com/OffchainLabs/arbitrum-monitoring/tree/main/packages/arbos-version-monitor) checks that every monitored chain is running at least a minimum ArbOS version, by querying the `ArbSys` precompile on each chain. It alerts when a chain reports a version below the configured minimum, and when the chain's RPC endpoint is unavailable or returns an invalid value, since such a chain cannot be monitored. ### Node sync monitor The [node sync monitor](https://github.com/OffchainLabs/arbitrum-monitoring/tree/main/packages/node-sync-monitor) checks the health of your operator-run nodes by comparing each node against a trusted reference RPC endpoint for the same chain, such as the chain's public gateway. A node is considered healthy when it reports fully synced and its head is within a configurable number of blocks of the reference. Comparing against an independent reference correctly handles chains that produce blocks on demand: a stuck node shows growing lag against the reference, while an idle chain stays equal. Note that the comparison is only as reliable as the reference: if the reference RPC itself fails or falls behind, the monitor's results may be inaccurate, so point it at infrastructure independent of the monitored nodes. ## Data Availability Server (DAS) health checks If you've deployed an [AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) chain with a Data Availability Committee, it is recommended to actively monitor the endpoints of the different configured DA servers. The [How to deploy a DAS](/launch-arbitrum-chain/chain-config/data-availability/deploy-das.md) guide contains a section for testing both the RPC and REST endpoints of any given DAS, by using the `anytrusttool` available in Nitro. ## Enabling Nitro metrics Most of the signals below are exposed as Prometheus-compatible metrics by the Nitro node. Start the node with the `--metrics` flag to enable the metrics server, then scrape the endpoint at `http://:/debug/metrics/prometheus` (default port `6070`): ```shell nitro --metrics \ --metrics-server.addr 0.0.0.0 \ --metrics-server.port 6070 ``` For metrics-server flags, memory-related metrics, health-check patterns, and Kubernetes `ServiceMonitor` examples, see [Node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md). The signals that are not Nitro metrics — onchain events and account balances on the parent chain — are called out explicitly in each table below. ## What to monitor, by component The sections below group the elements to monitor by the component they belong to. Each table lists the signal, where it comes from, what it means, and a suggested condition to alert on. Suggested alert thresholds are starting points; tune them to your chain's activity and settlement layer. ### Sequencer The sequencer accepts transactions, orders them, and produces blocks. Two different "backlogs" are often confused: the **sequencer's transaction backlog** (transactions received but not yet sequenced into blocks) and the **batcher backlog** (sequenced messages not yet posted to the parent chain). They are separate metrics and live in separate components — the batcher backlog is covered under [Batch poster](#batch-poster). | Signal | Source | What it means | Suggested alert | | --------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | `arb/sequencer/backlog` | Nitro metric (sequencer) | The sequencer's transaction backlog: transactions accepted but not yet included in a block. A sign of network health. | Sustained growth over several minutes | | `arb/sequencer/active` | Nitro metric (sequencer) | Whether this node is currently the active sequencer. Useful when running a sequencer coordinator with failover. | Value flaps, or no node reports active | | `arb/feed/backlog/messages` | Nitro metric (nodes with feed output enabled — the sequencer or a relay) | Messages retained by the broadcast server until it sees them covered by batches on the parent chain. Growth means batches aren't landing — not that a follower is lagging. | Sustained growth | | `eth_syncing` fields | RPC (any node) | While a node is syncing, `eth_syncing` returns fields such as `batchSeen`, `batchProcessed`, `msgCount`, and `blockNum`. See caveats below. | `batchSeen` − `batchProcessed` keeps growing | `eth_syncing` returns `false` once a node is fully synced, so it is useful for tracking a node that is catching up, not for steady-state health. Its out-of-sync output is **not a stable API** and can change between Nitro versions, so do not build production alerting on the exact field shape. For a fully synced node, use the metrics above and an RPC liveness check (an `eth_chainId` call returning HTTP 200) instead. See [`eth_syncing`](/arbitrum-essentials/arbitrum-vs-ethereum/rpc-methods.md#eth_syncing) for the full field reference. ### Batch poster The batch poster (the "batcher") posts sequenced transactions to the `SequencerInbox` contract on the parent chain as batches. It needs a funded account and must keep landing batches, or the chain's data stops being published to the parent chain. | Signal | Source | What it means | Suggested alert | | ----------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------- | | `arb/batchposter/estimated_batch_backlog` | Nitro metric (batch poster) | The batcher backlog: sequenced batches waiting to be posted to the parent chain. | Sustained growth over several minutes | | `arb/batchposter/wallet/eth` | Nitro metric (batch poster) | The batch poster account balance, in ether. Monitor fund usage directly from this metric. | Below your funding threshold | | `arb/inbox/latest/batch` | Nitro metric (any node) | The latest batch number the node has seen in the `SequencerInbox`. Stalls if batches stop landing. | No increase while the chain has activity | | `SequencerBatchDelivered` event | Parent chain (`SequencerInbox`) | Emitted each time a batch is delivered onchain. The onchain confirmation that batches are landing. | No event within your expected posting interval | Tip Monitor the batch poster account balance directly through `arb/batchposter/wallet/eth` (and the validator balance through `arb/staker/balance`, below) rather than estimating monthly spend from configs and gas multipliers. Direct balance monitoring reflects real usage; config-based estimation drifts from reality as parent-chain fees move. There is no automatic mechanism to refund these accounts, so keep them overfunded and alert well before they run dry. If batches stop being posted, see [Batch poster troubleshooting](/launch-arbitrum-chain/operate/batch-poster-troubleshooting.md) for common causes and tuning flags. ### Validator Validators post and confirm assertions (historically called "RBlocks" or "nodes") of the chain's state to the Rollup contract on the parent chain. The metrics differ depending on whether your chain runs the legacy staker or the BoLD staker. Note If you are relying on `arb/staker/action/last_success` and seeing it go stale for long periods even while assertions are being created, this is expected on two counts. First, that gauge updates each time the staker successfully acts — during periods when the staker has no action to take, it stays flat even though the validator is healthy and polling. Second, it is a **legacy-staker** metric; on BoLD chains, assertion activity is tracked by the `arb/validator/poster/*` and `arb/validator/scanner/*` metrics instead. Prefer the assertion-progress and failure signals below over `last_success` as your primary health check, and corroborate with the onchain assertion events. | Signal | Source | What it means | Suggested alert | | --------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | | `arb/staker/staked_node` | Nitro metric (legacy staker) | The latest assertion this validator has staked on. Should advance as the chain progresses. | No advance while the chain has activity | | `arb/staker/confirmed_node` | Nitro metric (legacy staker) | The latest confirmed assertion this validator sees. | Stalls unexpectedly | | `arb/staker/action/failure` | Nitro metric (legacy staker) | Counter of failed staker actions. A better failure signal than a stale `last_success` gauge. | Any sustained increase | | `arb/staker/balance` | Nitro metric (staker) | The validator account balance, in ether. Monitor fund usage directly. | Below your funding threshold | | `arb/validator/poster/assertion_posted` | Nitro metric (BoLD staker) | Counter of assertions posted by this validator. The primary "assertions are being made" signal on BoLD chains. | No increase while the chain has activity | | `arb/validator/poster/error_posting_assertion` | Nitro metric (BoLD staker) | Counter of errors while posting assertions. | Any sustained increase | | `arb/validator/scanner/latest_confirmed_assertion_block_number` | Nitro metric (BoLD staker) | Parent-chain block number of the latest confirmed assertion. | No advance over an extended period | | `arb/validator/validations/failed` | Nitro metric (any validator) | Counter of failed block validations. | Any sustained increase | | `NodeCreated` or `AssertionCreated` events | Parent chain (Rollup contract) | The onchain confirmation that assertions are being created. Watch alongside the metrics above. | No event over an extended period | **Long periods of inactivity can disable the validator allowlist.** If the latest confirmed assertion (or its first child, once one exists) is older than `validatorAfkBlocks` parent-chain blocks, the Rollup contract's validator allowlist can be [permissionlessly disabled](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/RollupUserLogic.sol#L62). `validatorAfkBlocks` is configured per chain — read it from your Rollup contract; a value of `0` disables this mechanism entirely. Alert on assertion inactivity well before this window elapses. For example, on Arbitrum One `validatorAfkBlocks` is set to `201600` (\~28 days of parent-chain blocks). Note that this applies regardless of `disableValidatorWhitelist`. Setting `disableValidatorWhitelist` to `false` keeps your chain permissioned in normal operation but does not protect the allowlist from this escape hatch. To learn how the two parameters interact, and why any deliberate validator pause counts toward this window, see [Keeping validation permissioned across the upgrade](/launch-arbitrum-chain/operate/bold-upgrade-playbook.md#keeping-validation-permissioned-across-the-upgrade). If assertions stop being posted or confirmed, see [Validator troubleshooting](/launch-arbitrum-chain/operate/validator-troubleshooting.md) for common causes and tuning flags. ### Chain fees and load The chain's base fee is a useful signal of sustained demand. An Arbitrum chain has a `gas target` (also called the speed limit), measured in gas per second, that acts as a threshold for pricing. When cumulative usage exceeds it, a backlog of excess gas accumulates and the `L2 base fee` rises using an approach similar to [Ethereum's EIP-1559 pricing algorithm](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md). The default gas target is `7,000,000` gas per second as of ArbOS 6 (it was `1,000,000` in ArbOS 0). A sustained base-fee spike therefore indicates demand is exceeding the speed limit — as a rough guide, 7M gas per second is on the order of 330 simple transfers per second, though the exact transactions-per-second figure depends on the gas cost of the transactions being run. | Signal | Source | What it means | Suggested alert | | ------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------- | | `arb/block/basefee` | Nitro metric (any node) | The current L2 base fee. Spikes above the minimum indicate the chain is congested. | Sustained elevation above baseline | | `ArbGasInfo.getGasBacklog()` | Precompile (RPC) | The backlogged amount of gas burnt in excess of the speed limit. Drives the base fee up. | Sustained growth | | `ArbGasInfo.getGasAccountingParams()` | Precompile (RPC) | Returns the chain's speed limit, pool size, and block gas limit. Use to confirm configuration. | Values differ from expected configuration | For guidance on choosing and changing the gas target, see [Manage gas target](/launch-arbitrum-chain/chain-config/costs/gas-target.md). ## Deployment guidance **Do not run the sequencer and a validator on the same node.** They have different resource profiles and different failure modes, and co-locating them means a single machine failure takes down both liveness (sequencing) and safety (validation) at once. Running them separately also keeps their funding and monitoring independent: * The **batch poster** account (`arb/batchposter/wallet/eth`) pays to post batches to the parent chain. * Each **validator** account (`arb/staker/balance`) pays to post and confirm assertions. Fund and alert on each account separately, and keep each overfunded — there is no automatic top-up mechanism for either. ## Appendix: reference hardware specifications The following are the canonical hardware specifications used by Offchain Labs for Arbitrum One nodes. Treat them as a reference point for sizing your own chain; requirements scale with your chain's activity and history. | Role | vCPU / cores | RAM | Storage | Reference instance | | --------- | ------------ | ----- | ----------- | ------------------ | | Sequencer | 10 cores | 90 GB | 6.6 TB NVMe | `i7ie.3xlarge` | | Archive | 2 cores | 40 GB | 16 TB | — | | Validator | 3 cores | 32 GB | 4 TB | — | | RPC | 4 cores | 32 GB | — | — | Fast, locally attached NVMe SSD storage is strongly recommended, especially for the sequencer and archive nodes, because disk latency directly affects performance as read volume grows. See [State growth](/launch-arbitrum-chain/operate/state-growth.md) for more on storage sizing over time. --- > For a complete page index, fetch # Ownership structure and access control A **chain owner** of an Arbitrum chain is an entity that can carry out critical upgrades to the chain's core protocol; this includes upgrading protocol contracts, setting core system parameters, and adding and removing other chain owners. An Arbitrum chain's initial chain owner is set by the chain's creator when the chain is deployed. The chain-ownership architecture is designed to give Arbitrum chain creators flexibility in deciding how upgrades to their chain occur. ## Architecture Chain ownership affordance is handled via [**Upgrade Executor**](https://github.com/OffchainLabs/upgrade-executor) contracts. Each Arbitrum chain is deployed with two Upgrade Executors — one on the Arbitrum chain itself, and one on its parent chain. At deployment, the chain's critical affordances are given to the Upgrade Executor contracts. Some examples: * The parent chain's core protocol contracts are upgradeable proxies that are controlled by a proxy admin; the proxy admin is owned by the Upgrade Executor on the parent chain. * The core Rollup contract's admin role is given to the Upgrade Executor on the parent chain. * The affordance to call setters on the `ArbOwner` precompile—which allows for setting system gas parameters and scheduling ArbOS upgrades (among other things)—is given to the Upgrade Executor on the Arbitrum chain. Calls to an Upgrade Executor can only be made by chain owners; e.g., entities granted the `EXECUTOR_ROLE` affordance on the Upgrade Executor. Upgrade Executors also have the `ADMIN_ROLE` affordance granted to themselves, which lets chain owners add or remove chain owners. With this architecture, the Upgrade Executor represents a single source of truth for affordances over critical upgradability of the chain. Precompile reference The [`ArbOwner` precompile reference](/arbitrum-essentials/precompiles/reference.md#arbowner) can be found on the [Precompiles reference page](/arbitrum-essentials/precompiles/reference.md). ## Upgrades Upgrades occur via a chain owner initiating a call to an Upgrade Executor, which in turn calls some chain-owned contract. Chain owners can either call [`UpgradeExecutor.executeCall`](https://github.com/OffchainLabs/upgrade-executor/blob/a8d3020c2771d164ebd323b1d99249049fe749f9/src/UpgradeExecutor.sol#L73), which will in turn call the target contract directly, or [`UpgradeExecutor.execute`](https://github.com/OffchainLabs/upgrade-executor/blob/a8d3020c2771d164ebd323b1d99249049fe749f9/src/UpgradeExecutor.sol#L57), which will delegate-call to an "action contract" and use its code to call the target contract. ## Per-function permissions The tables below list the privileged functions on the `SequencerInbox`, `Rollup`/`RollupAdminLogic`, and `Bridge` contracts, who is allowed to call each one, and the [Arbitrum Chain SDK](https://github.com/OffchainLabs/arbitrum-chain-sdk) (formerly the Orbit SDK) helper that performs the call (where one exists). This answers the recurring question of which calls the Rollup owner makes and which must be routed through the Upgrade Executor. **How to read the *Required caller* column.** The source of truth is the access-control modifier on each function in the [`nitro-contracts`](https://github.com/OffchainLabs/nitro-contracts) source: * A function gated to the **Rollup owner or admin** — an `onlyRollupOwner`-style modifier, or any `RollupAdminLogic` admin function — is, in a standard deployment, triggered by the owner **through the Upgrade Executor**, because the Upgrade Executor contract *is* the owner/admin of these contracts. See [Upgrades](#upgrades) above for the conceptual explanation of that flow. * A function callable **directly** by a dedicated role (for example the batch poster manager) does **not** need to go through the Upgrade Executor; that role's address calls it directly. The Chain SDK's `sequencerInboxPrepareTransactionRequest` and `rollupAdminLogicPrepareTransactionRequest` helpers accept an `upgradeExecutor` argument and wrap the call in `UpgradeExecutor.executeCall` automatically, so the owner path is handled for you. ### `SequencerInbox` Access is enforced by the `onlyRollupOwner` and `onlyRollupOwnerOrBatchPosterManager` modifiers. | Function | Required caller | Chain SDK helper | | --------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | `setValidKeyset(bytes)` | Rollup owner — via Upgrade Executor | `setValidKeyset` / `buildSetValidKeyset` | | `invalidateKeysetHash(bytes32)` | Rollup owner — via Upgrade Executor | `buildInvalidateKeysetHash` | | `setIsBatchPoster(address, bool)` | Rollup owner (via Upgrade Executor) **or** batch poster manager (directly) | `buildSetIsBatchPoster` (also `buildEnableBatchPoster` / `buildDisableBatchPoster`) | | `setIsSequencer(address, bool)` | Rollup owner (via Upgrade Executor) **or** batch poster manager (directly) | `sequencerInboxPrepareTransactionRequest` | | `setMaxTimeVariation(MaxTimeVariation)` | Rollup owner — via Upgrade Executor | `buildSetMaxTimeVariation` | | `setBatchPosterManager(address)` | Rollup owner — via Upgrade Executor | `sequencerInboxPrepareTransactionRequest` | | `setFeeTokenPricer(IFeeTokenPricer)` | Rollup owner — via Upgrade Executor | `sequencerInboxPrepareTransactionRequest` | | `setBufferConfig(BufferConfig)` | Rollup owner — via Upgrade Executor | `sequencerInboxPrepareTransactionRequest` | | `updateRollupAddress()` | Rollup owner (inline check) — via Upgrade Executor | `sequencerInboxPrepareTransactionRequest` | Info `setAllowListEnabled` is **not** a `SequencerInbox` function, despite being commonly asked about in that context. It lives on the `Inbox` (the delayed inbox/`AbsInbox`)—see the [Inbox](#inbox-delayed-inbox) row below. ### Rollup/`RollupAdminLogic` Every function on `RollupAdminLogic` is reachable only through the Rollup proxy's admin, which in a standard deployment is the Upgrade Executor. There is no per-function modifier—the proxy routes admin calls to this logic contract only when the caller is the admin—so **all of these require the Upgrade Executor** and none have a dedicated Chain SDK helper. They are all issued through the generic `rollupAdminLogicPrepareTransactionRequest({ functionName, args, upgradeExecutor, rollup, account })` dispatcher. | Function | Required caller | Chain SDK helper | | -------------------------------------------------- | ----------------------------------- | ------------------------------------------- | | `setValidator(address[], bool[])` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `setOwner(address)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `setConfirmPeriodBlocks(uint64)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `setMinimumAssertionPeriod(uint256)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `setValidatorAfkBlocks(uint64)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `setValidatorWhitelistDisabled(bool)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `increaseBaseStake(uint256)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `decreaseBaseStake(uint256, uint64)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `setWasmModuleRoot(bytes32)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `setLoserStakeEscrow(address)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `setInbox(IInboxBase)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `setSequencerInbox(address)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `setDelayedInbox(address, bool)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `setOutbox(IOutbox)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `removeOldOutbox(address)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `setAnyTrustFastConfirmer(address)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `setChallengeManager(address)` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `pause()` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `resume()` | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `forceRefundStaker(address[])` (only `whenPaused`) | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `forceCreateAssertion(...)` (only `whenPaused`) | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | | `forceConfirmAssertion(...)` (only `whenPaused`) | Rollup admin — via Upgrade Executor | `rollupAdminLogicPrepareTransactionRequest` | Note The function set above reflects the BoLD-era `RollupAdminLogic`. On pre-BoLD chains some names differ (for example, `setBaseStake` in place of `increaseBaseStake`/`decreaseBaseStake`, and `forceCreateNode`/`forceConfirmNode` in place of the `*Assertion` variants). Always confirm against the `nitro-contracts` version your chain runs. ### Bridge Access is enforced by the `onlyRollupOrOwner` modifier. The caller must be the Rollup contract itself or the Rollup owner. The owner path runs through the Upgrade Executor. The Chain SDK has no dedicated Bridge helpers; these are configured during deployment or via the `Rollup` admin forwarders above. | Function | Required caller | Chain SDK helper | | ---------------------------------------------- | ------------------------------------------------------------------ | ---------------- | | `setSequencerInbox(address)` | Rollup contract, or Rollup owner — owner path via Upgrade Executor | — | | `setDelayedInbox(address, bool)` | Rollup contract, or Rollup owner — owner path via Upgrade Executor | — | | `setOutbox(address, bool)` | Rollup contract, or Rollup owner — owner path via Upgrade Executor | — | | `setSequencerReportedSubMessageCount(uint256)` | Rollup contract, or Rollup owner — owner path via Upgrade Executor | — | | `updateRollupAddress(IOwnable)` | Rollup contract, or Rollup owner — owner path via Upgrade Executor | — | ### Inbox (delayed inbox) `setAllowListEnabled` and the allowlist setter live on the `Inbox` (`AbsInbox`), not on `SequencerInbox`. Access is enforced by `onlyRollupOrOwner`. | Function | Required caller | Chain SDK helper | | --------------------------------- | ------------------------------------------------------------------ | -------------------------- | | `setAllowListEnabled(bool)` | Rollup contract, or Rollup owner — owner path via Upgrade Executor | `buildSetAllowListEnabled` | | `setAllowList(address[], bool[])` | Rollup contract, or Rollup owner — owner path via Upgrade Executor | `buildSetAllowList` | ## Ownership flexibility A chain owner is simply an address; it is set by the Arbitrum chain's deployer and can represent any sort of governance scheme (i.e., it could be an EOA (as is set via the [Chain SDK Rollup creation example](https://github.com/OffchainLabs/arbitrum-chain-sdk/tree/main/examples/create-rollup-eth)), a Multisig, a governance token system, etc.) The Arbitrum DAO governed chains, while not Arbitrum chains themselves, use a similar architecture and upgrade pattern as Arbitrum chains, with both a governance token and a Multisig (aka, the "Security Council") as chain owners. For more info and best practices on action contracts, see ["DAO Governance Action Contracts"](https://github.com/ArbitrumFoundation/governance/blob/main/src/gov-action-contracts/README.md). Note The DAO-governed chains' Upgrade Executor contracts don't have the `.executeCall` method; only the `.execute` method. --- > For a complete page index, fetch # Post-launch deployment of deterministic contracts Some forms of contract deployment rely on pre-made transactions with hardcoded gas limits designed for non-Arbitrum chains that only cover execution gas. Since Arbitrum also charges for data posting, these limits may be too low, causing transactions to fail. This page describes a safe, permissionless post-launch deployment method for such cases. Using this approach, you can deploy contracts like `Multicall3`, the `EntryPoint` contract, or a `create2` factory to a live Arbitrum chain without needing to modify chain configuration. It leverages Arbitrum's cross-chain messaging infrastructure via the `Inbox.sendL2Message` function, which submits a pre-signed child chain transaction from the parent chain. Unlike retryable tickets, these messages incur parent chain data fees only at submission and aren’t subject to the 100,000 gas cap imposed on deterministic deployment flows. ## When to use this method This approach is useful when: * Your Arbitrum chain is already live. * You want to deploy a contract at a specific address (e.g., using `create2`). * The chain's genesis configuration did not include the contract. Common examples include deployments of contracts like `Multicall3`, the `ERC-4337 EntryPoint` contract, or a `create2` factory/deterministic deployment proxy. Note that if `deployFactoriesToL2` is set to `true` when calling `createRollup()`, the following contracts are deployed to the Arbitrum chain by default: * [`Multicall3`](https://github.com/mds1/multicall3) * [`DeterministicDeploymentProxy`](https://github.com/Zoltu/deterministic-deployment-proxy) * [`ERC-4337 EntryPoint`](https://github.com/eth-infinitism/account-abstraction) * [`CREATE3Deployer`](https://github.com/0xSequence/create3) ## Overview To deploy a contract post-launch: #### 1. Fund the deployer address on the child chain: Send **ETH** to the child chain address that will send the deployment transaction. Funding is possible using a retryable ticket or Parent-to-Child chain deposit. #### 2. Create the signed child chain transaction: Construct and sign a raw child chain transaction that performs the contract deployment (e.g., using `create2`). [`DeployHelper.sol`](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/DeployHelper.sol) can be used as a reference for formatting the transaction. #### 3. Send the message from the parent chain: Call `Inbox.sendL2Message(bytes calldata message)` on the Arbitrum chain’s inbox contract. Provide the signed transaction as the input. #### 4. Wait for the transaction to be executed on the child chain: The child chain will process and execute the message as a regular transaction. This flow does not use retryables, so if the transaction fails, it is not auto-retried and the nonce is consumed. This means the same transaction cannot be replayed, and a new transaction with a higher nonce must be created and submitted. > **WARNING** > > Make sure that the deployer address is sufficiently funded on the child chain before sending the transaction. If the transaction runs out of gas or fails for any reason, the associated nonce will be burned. This means that the contract cannot be deployed at the predetermined deterministic address, potentially breaking tooling and integrations that rely on that specific address. ## Advantages and considerations This approach avoids parent chain data fees and bypasses the 100,000 gas limit associated with retryable tickets. It also requires no chain upgrade or restart and works on any standard Arbitrum or Arbitrum chain setup. However, failed transactions are not automatically retried since the flow bypasses retryable safety mechanisms. The sender must have enough **ETH** on child chain to cover gas costs, and the signed transaction must be valid and use the correct nonce. --- > For a complete page index, fetch # Managing state growth and corresponding issues As the Arbitrum ecosystem grows, more and more teams are choosing to build on the Arbitrum tech stack. Arbitrum chains offer a feature-rich and scalable Rollup stack that allows teams to focus on their ecosystem growth and build great products. As a result, many Arbitrum chains are seeing usage and throughput increase rapidly, often with sustained transaction load at the chain’s throughput limit for months on end. This page aims to educate Arbitrum chain operators and owners on safely operating a high throughput chain. Amongst all the factors, the primary consideration is **state growth rate** and **state size**. We’ll discuss how increased state size affects the performance of different components in the Arbitrum chain stack and how certain metrics can be used to indicate a need to upgrade various infrastructure components. ## Understanding state size and state growth rate When we say ‘state size’, we mean the total amount of data recorded on the blockchain; state size is a critical metric for node performance as a larger state creates higher infrastructure requirements on nodes for storage and searching through existing states. The state growth rate is simply the rate at which state size increases. A high state growth rate creates higher requirements for nodes to process state transitions and perform operations needed to keep up with the tip of the chain. The critical Nitro stack parameter affecting state growth and state growth rate is the **gas target**. Offchain Labs has built an algorithm to ensuring a safe, sustained operating limit for Arbitrum chains. You can read more about the nuances of the gas target [here](/how-arbitrum-works/deep-dives/gas-and-fees.md#the-gas-target). The default gas target is designed to ensure Arbitrum chains operate performantly and sustainably. ## Behaviour at ultra-high throughput At high state growth rates, especially in cases where a chain is pushing past prescribed limits, an Arbitrum chain may display certain behaviors that either indicate or result from the chain load being higher than its infrastructure can support. The following is a list of such behaviors. ### 1. Read Operation Bottleneck at High Disk Latency As the number of read requests on a chain grows, the impact of disk latency on performance becomes more pronounced. The performance impact can be considered the total amount of read requests made as a multiple of the disk latency. High read request volumes may necessitate switching to using low-latency local NVMe drives. ### 2. Increased single-core CPU and RAM Utilization Observing high utilization on single-core CPU and RAM indicates that you may require more performant hardware. As this trend continues, hardware investments become prohibitively expensive for the ecosystem or require increasingly custom solutions, which decreases accessibility for node runners. ### 3. Increased Total State Database Size The accelerated state database growth rate, on the order of multiple terabytes of data per month, indicates that your chain may require increasing drive sizes. Played out over time, this may force node runners on the chain to adopt prohibitively expensive or hard-to-procure drives (e.g., those notes available on major cloud providers). ### 4. Increased Disk Write Operations per Second The number of write operations per second directly correlates to state size growth. As the state growth rate increases, ecosystem nodes that aren’t properly resourced may fall out of sync with the chain. ### 5. Sync from Genesis Time As state size increases, the time a new node needs to catch up to the chain also increases. A large state size and state growth rate can result in new nodes catching up to the chain in the worst case. ## Summary of symptoms, mitigations, risks The general trend with any issue in the table below is as follows: * The simple resolutions involve moving to more expensive infrastructure. * When simple resolutions are exhausted, infrastructure becomes both expensive and bespoke (options that available cloud providers do not support) * The long-term risk (and point of no return) is when infrastructure requirements are too expensive or too inaccessible for node runners. | Behaviour | Risk | Mitigations & Considerations | | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Performance degradation due to storage reads at high disk latency | An increase in read operations causes nodes to spend more accessing disk state. As the number of read operations increases, these delays can degrade chain performance. | Upgrade drives to local NVMe (PCIe Gen4/Gen5, not configured with RAID) with higher speeds. In the short term, NVMe usage will greatly increase the cost of node runners. In the extreme, you may run out of usable drive specifications on available infrastructure vendors. | | Growing or constant sequencer backlog (using `arb_sequencer_backlog`) over a sustained period. | Seeing a growing or persistent backlog implies that nodes cannot keep up with the transaction load accepted by the sequencer. | | | Large state database size and high growth rate of the state database | A large state database size will require that nodes run more expensive disks. This reduces the economic feasibility for node runners. In extreme cases, the required disk size may be unsupported by accessible cloud service providers. | The primary resolution is to upgrade the disk size requirement for nodes on your chain. | | High utilization of single-core CPU and RAM | As with the cases above, this symptom implies a need to upgrade hardware. The main risk is the economic feasibility and long-term accessibility of new hardware options. | The only resolution is to upgrade your node’s CPU and RAM. | --- > For a complete page index, fetch # Arbitrum chain upgrade runbook This runbook is the operational wrapper around an Arbitrum chain upgrade: what to validate before you start, what order to execute in, how to confirm the upgrade succeeded, and what you can undo if it doesn't. It complements [ArbOS upgrade](/launch-arbitrum-chain/operate/arbos-upgrade.md), which remains the canonical reference for how each individual step works. This page does not restate those mechanics—it links to them at each step and focuses on the coordination, verification, and failure-recovery work around them. An upgrade touches five things that must stay mutually compatible: 1. The Nitro version running on every node and validator. 2. The `nitro-contracts` deployed on the parent chain. 3. The WASM module root set in the Rollup contract. 4. The WAVM machines on each validator's disk—one per root the validator must validate (current and pending). A new Nitro version alone doesn't guarantee they are present. 5. The ArbOS version active on the child chain. Most failed upgrades are a mismatch between two of these five, and the majority are mismatches between the onchain root (3) and the machines on validators (4). [Troubleshoot WASM module root failures](#troubleshoot-wasm-module-root-failures) covers those in detail. ArbOS upgrades are one-way Once an ArbOS upgrade activates, you cannot downgrade the ArbOS version. Plan on rolling forward, not back. Read [Rollback options](#rollback-options) **before** you schedule an upgrade, so you know which parts of the process are reversible and which aren't. **The Nitro version and the ArbOS version are independent**: Nitro is per-node software you can deploy and roll back; the ArbOS version is chain-wide state, moved only by `scheduleArbOSUpgrade`. Upgrading Nitro does not change the chain's ArbOS version. ## Record your target versions Every version-specific value comes from the reference page for the ArbOS release you're targeting. Record all five before you begin, because later steps consume them as inputs: | Input | Where to get it | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Target ArbOS version | The [list of available ArbOS releases](/run-arbitrum-node/arbos-releases/overview.md#list-of-available-arbos-releases) | | Minimum Nitro version | The requirements section of the targeted ArbOS release page | | `nitro-contracts` version | The targeted ArbOS release page. Not every ArbOS upgrade requires a contracts upgrade | | WASM module root | The targeted ArbOS release page | | Consensus tag | The consensus version paired with that WASM module root (for example, `consensus-v51`). Cross-check the pairing against the [`Dockerfile`](https://github.com/OffchainLabs/nitro/blob/master/Dockerfile) in the Nitro repository | Why the consensus tag matters The consensus tag and the WASM module root are two names for the same artifact, and `scripts/download-machine.sh` requires **both** as arguments. Release pages sometimes reference point releases (for example, `consensus-v51.1`) that carry a different module root than the base tag. Record the exact pair you intend to run, and confirm it against the `Dockerfile` in the Nitro repository, which is the source of truth for which tag maps to which root. ## Pre-upgrade validation Complete every check in this section before scheduling anything onchain. ### Confirm your current ArbOS version Call `ArbSys.ArbOSVersion()` on your chain. This function returns the ArbOS version **plus 55**—a chain running ArbOS 40 returns `95`. When you later schedule the upgrade, you pass the real version number, not the offset one. See [Obtaining the current ArbOS version](/launch-arbitrum-chain/operate/arbos-upgrade.md#step-3-schedule-the-arbos-version-upgrade) for details. ### Confirm your current contracts version and upgrade path Determine which `nitro-contracts` version your chain currently runs and whether a direct upgrade path to the target exists. Follow [Check version and upgrade path](https://github.com/OffchainLabs/chain-actions/blob/main/README.md#check-version-and-upgrade-path) in the `chain-actions` repository, substituting your chain's inbox address and network name. Contract upgrades are not always a single hop. Confirm the full path now rather than discovering a required intermediate version mid-upgrade. ### Confirm chain owner access Every administrative action in this runbook is executed by the upgrade executor contract, not directly by your chain owner account. Before you begin, verify that: * You control an account with the executor role on the parent chain upgrade executor. * That account is funded on the parent chain. * You can reach the child chain upgrade executor, which owns the `ArbOwner` precompile actions. See [Ownership and access](/launch-arbitrum-chain/operate/ownership-and-access.md) to confirm which account holds which role. ### Confirm the target machine is present on every validator This is the check that most often gets skipped, and it's the one that causes the failures in [Troubleshoot WASM module root failures](#troubleshoot-wasm-module-root-failures). Verify that each validator has the machine for the **target** WASM module root on disk *before* you set that root onchain—not after the upgrade activates. Machines are discovered by directory layout, so inspect the directory directly: ```shell ls -l /home/user/.arbitrum/machines/ cat /home/user/.arbitrum/machines/latest/module-root.txt ``` A correctly populated machines directory looks like this: ```text machines/ ├── latest -> 0x8a7513bf... # symlink to the newest machine directory └── 0x8a7513bf.../ # directory name must equal the module root ├── module-root.txt # contains 0x8a7513bf... ├── machine.v2.wavm.br └── replay.wasm # only present if the release publishes it ``` Staging the machine is only half the check. The block validator reads its machine list and resolves its pending root once, at startup, so restart each validator after staging and confirm the startup log before you set the root onchain: ```text INFO BlockValidator initialized current=0x8a7513bf... pending=0xc2c02df5... ``` `pending` must equal your target root. If it doesn't, the validator will refuse the onchain root change in step 3. Don't defer this to [post-upgrade verification](#verify-the-wasm-module-root-matches-everywhere)—by then the root is already set. WASM module roots are backward compatible You can stage the machines on your validators and set the new WASM module root well before the ArbOS upgrade activates. Doing so does not disrupt the chain, so there's no reason to leave it until the last moment. Order matters within that window, though: stage the machine on each validator and restart it **first**, then set the root onchain. A running validator only follows an onchain root change if its pending root already resolves to the new root, and it resolves that root at startup. ## Determine whether you need `download-machine.sh` `scripts/download-machine.sh` in the Nitro repository fetches a prebuilt WAVM machine for a given consensus tag. Whether you need to run it depends entirely on how you obtain your node binary: | Your setup | Do you need `download-machine.sh`? | | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Official Docker image, no STF customization | **No.** Machines are already baked into the image | | Building the Docker image from source | **Usually yes.** The `Dockerfile` only downloads the machines on its *uncommented* `RUN ./download-machine.sh` lines | | Custom STF build | **Yes**, and you must also rebuild the replay binary so the resulting module root matches what you set onchain | The Dockerfile only downloads recent machines In the Nitro repository's `Dockerfile`, the `RUN ./download-machine.sh` lines for older consensus versions are **commented out**—only the most recent few are active. If your chain's Rollup contract still points at an older module root, a from-source build produces an image with no machine for that root, and your validator fails to start. This is the single most common cause of the startup failures described below. Check the `Dockerfile` for a line matching your consensus version before assuming the image contains it. To fetch a machine manually, run the script from your machines directory with both the consensus tag and its module root: ```shell cd /home/user/.arbitrum/machines ./download-machine.sh consensus-v51.1 0xc2c02df561d4afaf9a1d6785f70098ec3874765c638e3cb6dbe8d3c83333e14c ``` The script creates a directory named after the module root, writes `module-root.txt` inside it, repoints the `latest` symlink at it, and downloads `machine.v2.wavm.br` (plus `replay.wasm`, if that release published one). If you maintain a custom STF, downloading a published machine is not sufficient—see [Customize your chain's behavior](/launch-arbitrum-chain/extend-the-protocol/stf.md) to learn how to build and register your own. ## Execution order Order matters. Each step assumes the previous one completed successfully. | Step | Action | Notes | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | [Update Nitro on nodes and validators](/launch-arbitrum-chain/operate/arbos-upgrade.md#step-1-update-nitro-on-nodes-and-validators) | Validators first, then remaining nodes. **Must** complete before the ArbOS upgrade deadline | | 2 | Stage the target WAVM machine on every validator, then restart the validator | **Must** precede step 3. The block validator resolves its pending root at startup, so stage the machine before the step 1 restart and one restart covers both | | 3 | [Set the new WASM module root](/launch-arbitrum-chain/operate/arbos-upgrade.md#step-2-upgrade-the-wasm-module-root--your-chains-nitro-contracts) | Safe to do early, as long as every validator has been restarted with the target machine and its startup log shows `pending=` | | 4 | Upgrade `nitro-contracts`, if the release requires it | Follow [Nitro contracts upgrades](https://github.com/OffchainLabs/chain-actions?tab=readme-ov-file#nitro-contracts-upgrades) | | 5 | [Schedule the ArbOS version upgrade](/launch-arbitrum-chain/operate/arbos-upgrade.md#step-3-schedule-the-arbos-version-upgrade) | Pass the real version number and a UNIX timestamp. A timestamp of `0` upgrades immediately | | 6 | [Enable release-specific configuration or feature flags](/launch-arbitrum-chain/operate/arbos-upgrade.md#step-4-enable-arbos-specific-configurations-or-feature-flags-not-always-required) | Not always required; check the release page | Stage machines and restart before you set the root onchain A running validator adopts a new onchain root only if that root equals its **pending** root; anything else is refused with `unexpected wasmModuleRoot! cannot validate!`. The legacy staker then stops acting entirely, while BoLD only warns. Both the pending root and the machine list are read **once, at startup**, so staging a machine under a running validator does nothing—the validator must be restarted before step 3. See [Understand which roots your validator is required to have](#understand-which-roots-your-validator-is-required-to-have). Upgrade nodes before the activation timestamp A node that is too old to process the newly active ArbOS version stops with a fatal `please upgrade to the latest version of the node software` error. Because the ArbOS upgrade activates in consensus at the scheduled timestamp, every node must already be running a compatible Nitro version when that timestamp passes. Leave yourself margin. ## Post-upgrade verification Work through all four checks. A chain can look healthy on the surface while its validator is quietly unable to bond. ### Verify the active ArbOS version Call `ArbSys.ArbOSVersion()` again and confirm it advanced to your target, remembering the +55 offset. If the scheduled timestamp has passed and the version has not changed, the upgrade did not activate—check that you scheduled the correct version and that nodes are actually producing blocks past the timestamp. ### Verify the WASM module root matches everywhere Compare the root set in the Rollup contract against the root your validator resolved. Read the onchain value from the parent chain: ```shell cast call "wasmModuleRoot()(bytes32)" --rpc-url ``` Then confirm your validator agrees. On startup, the block validator logs both roots it will validate against: ```text INFO BlockValidator initialized current=0x8a7513bf... pending=0x8a7513bf... INFO validator chosen WasmModuleRoot=0x8a7513bf... chosen=jit maxWorkers=8 ``` The `current` value should match the onchain `wasmModuleRoot()`. A `validator chosen` line appears for every root listed in `BlockValidator initialized`; if a machine for any root is missing, startup fails with `cannot validate WasmModuleRoot`. ### Verify the validator is bonding Confirm the validator is still advancing assertions and posting bonds after the upgrade. A validator that started cleanly can still be unable to act if the Rollup contract's root doesn't match what the validator actually validated with—that produces the runtime mismatch described in [The validator starts but can't advance its bond](#the-validator-starts-but-cant-advance-its-bond). If your chain has fast withdrawals enabled, re-check them specifically after the upgrade—see [Fast withdrawals](/launch-arbitrum-chain/chain-config/validation/fast-withdrawals.md). ### Verify the batch poster is still posting Confirm the batch poster is submitting batches to the sequencer inbox at its usual cadence, and that no backlog is accumulating. See [Monitoring tools and considerations](/launch-arbitrum-chain/operate/monitoring.md) for what to watch. Batch poster log noise after an upgrade New Nitro versions probe for contract features that older `nitro-contracts` deployments don't implement, so a version-skewed window between steps 1 and 4 can produce `eth_call` errors in the batch poster log that are benign. Confirm batches are still landing onchain before treating any of them as an incident. To diagnose messages that persist after the upgrade completes, see [Batch poster troubleshooting](/launch-arbitrum-chain/operate/batch-poster-troubleshooting.md) and [Batch poster recovery](/launch-arbitrum-chain/operate/bp-recovery.md). ## Troubleshoot WASM module root failures These failures share a single root cause—the validator does not have, or hasn't loaded, the machine for a module root it's required to handle—but they surface at three different severities: fatal at startup, non-fatal at startup, and at runtime. Identify which one you have before changing any configuration. | Error message | Node starts? | What it means | | --------------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `latestWasmModuleRoot not set` | No | No machine directory was found **at all** | | `cannot validate WasmModuleRoot ` | No | Machines were found, but none provides this specific required root | | `unable to find validator machine directory for the on-chain WASM module root` | Yes | Startup compatibility pre-check failed. Logged at `ERROR`, but **does not stop the node** | | `on-chain WASM module root did not match with any of the allowed WASM module roots` | Yes | Same pre-check, when `--validation.wasm.allowed-wasm-module-roots` is set and nothing matched | | `wasmroot doesn't match rollup : , valid: []` | Yes | Runtime mismatch. The validator runs but cannot advance its bond | | `unexpected wasmModuleRoot! cannot validate! found , current , pending ` | Yes | The onchain root changed to a root that is neither the validator's current nor its pending root, so the validator refuses to adopt it. The legacy staker then stops acting entirely; BoLD logs a warning and continues | Operators upgrading Nitro have reported the two fatal cases wrapped in a `failed to create node` startup error (`failed to create consensus node` on newer versions); the specific message quoted in that error tells you which one you're looking at. ### The node won't start For either fatal case, work through these in order: 1. **Check the `Dockerfile` for your consensus version.** If you built the image from source, confirm there's an *uncommented* `RUN ./download-machine.sh` line for the consensus tag your chain needs. This is the most common cause. 2. **Run `download-machine.sh` with the correct version.** Fetch the machine for the exact consensus tag and module root pair you recorded earlier. 3. **Rebuild the replay binary.** Required if you maintain a custom STF, where no published machine will ever match your root. Then confirm the machines directory is actually discoverable, because two layout rules are enforced silently: * **A directory is ignored unless its name is either `latest` or exactly its own module root.** The comparison is against the lowercase, `0x`-prefixed form. A directory named with a truncated, uppercase, or otherwise reformatted root is skipped without an error, even though `module-root.txt` inside it is correct. * **Discovery stops at the first search path that yields any machine.** One stale directory in an earlier path masks a correct one later in the search order. If `--validation.wasm.root-path` is set, it is the *only* path searched. When `--validation.wasm.root-path` is unset, the search order is: 1. `/target/machines` (source builds) 2. `./machines`, relative to the working directory 3. `./target/machines` 4. `machines/` in the grandparent directory of the binary—for example, `/usr/local/machines` for a binary at `/usr/local/bin/nitro` Setting `--validation.wasm.root-path` explicitly is the reliable fix when discovery picks the wrong directory. ### Understand which roots your validator is required to have A validator must have machines for **both** roots the block validator resolves, not just the current one: * `--node.block-validator.current-module-root` (default `current`, read from the chain) * `--node.block-validator.pending-upgrade-module-root` (default `latest`) When these differ, both are required, and a missing machine for **either** produces `cannot validate WasmModuleRoot`. This is why a validator that ran fine yesterday can fail to start today: the chain's root changed, and the current root—read from the chain—now names a root with no machine in the image. Pending, by contrast, is resolved from what's on disk (via the machines/latest symlink), so it can't point at a machine you don't have; a pinned pending root that's missing fails startup with the same cannot validate WasmModuleRoot error. By default, `pending` resolves through the `machines/latest` symlink, which `download-machine.sh` repoints to whatever it fetched last. If you'd rather not depend on that, pin it: `--node.block-validator.pending-upgrade-module-root=`. Either way the machine must be on disk, or startup fails with `cannot validate WasmModuleRoot`—pinning changes which root is required, not whether one is. Clearing \`pending-upgrade-module-root\` is not a fix for a Rollup mismatch Setting `--node.block-validator.pending-upgrade-module-root=` to an empty value only shrinks the set of roots required **at startup**. It has no effect on the runtime `wasmroot doesn't match rollup` error, which compares against the Rollup contract rather than against this flag. So if your node starts but can't advance its bond, clearing this flag won't help—you need the correct machine on disk. Conversely, if you cleared it to work around a startup failure, you've disabled validation against the pending upgrade root, which means you won't discover a missing machine until the upgrade activates. Clearing it also means the validator has no pending root to match against, so it can't adopt a new onchain root at runtime either. Restore it once the correct machine is staged. ### The validator starts but can't advance its bond This error only occurs on chains still running the legacy staker—the BoLD staker doesn't perform this check. It means the node is running, but the root in the Rollup contract isn't among the roots the block validator actually validated with: ```text error advancing stake from node 18 (hash 0x4d42...): error generating node action: wasmroot doesn't match rollup : 0x260f5fa5..., valid: [0x8b104a2e...] ``` Read it as: **the Rollup contract expects the first root; your validator only has the second.** Both roots map to consensus versions you can look up in the Nitro repository's `Dockerfile`—which usually reveals that the expected root corresponds to a consensus version your image never downloaded. The fix is to stage the machine for the root the Rollup contract expects, then restart the validator. Confirm success by checking that a `validator chosen` line appears for that root. A related message, `block validation is still pending`, is not a mismatch—it means the block validator hasn't validated anything yet and has no roots to compare. Wait for validation to progress. Don't suppress the mismatch check `--node.staker.dangerous.ignore-rollup-wasm-module-root` downgrades this error to a warning and lets the validator make assertions anyway. Its own help text describes it as dangerous, and for good reason: asserting against a module root the Rollup contract doesn't recognize risks producing assertions that can be challenged. Fix the machine, don't silence the check. Likewise, `--validation.wasm.enable-wasmroots-check=false` disables only the non-fatal startup pre-check. It hides an early warning without addressing the cause. ## Rollback options How much you can undo depends entirely on whether the ArbOS upgrade has activated. The ArbOS upgrade applies in consensus once the chain's block timestamp reaches the scheduled timestamp, and the upgrade logic only ever moves the version **upward**. | What you changed | Reversible? | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | Scheduled ArbOS upgrade, before the timestamp | **Yes.** Call `scheduleArbOSUpgrade` again to push the timestamp out or to set a version at or below the current one | | WASM module root | **Yes.** Call `setWasmModuleRoot` with the previous root. Roots are backward compatible | | Nitro node version | **Partially.** You can downgrade, but not below a version that supports the chain's *active* ArbOS version | | `nitro-contracts` | **Difficult.** Technically possible by re-pointing implementations through the upgrade executor, but treat it as a last resort | | Activated ArbOS version | **No.** There is no downgrade path | ### Before the activation timestamp A scheduled upgrade is just two stored values—a target version and a timestamp—and the upgrade only fires when the chain's timestamp reaches the target timestamp *and* the target version is higher than the current one. Until then, you can freely cancel or reschedule by calling `scheduleArbOSUpgrade` again through the upgrade executor. This is the safe window; if you have any doubt about validator readiness, postpone from here rather than pushing forward. ### After the activation timestamp The ArbOS version cannot be lowered. Recovery means rolling forward: 1. **Restore node availability first.** If nodes are failing to start, resolve that with the WASM root troubleshooting above. Getting nodes running on the new ArbOS version is almost always faster than any attempt to unwind. 2. **Don't downgrade Nitro below the active ArbOS version.** A node too old for the active ArbOS version stops with a fatal out-of-date error, so downgrading to "get back to a working state" makes the outage worse. 3. **Escalate rather than improvise.** If the chain is producing blocks but validation is broken, the chain is still live and withdrawals are the pressure point. Contact Offchain Labs support before attempting contract-level changes. Reverting contracts after assertions exist Rolling `nitro-contracts` back is only remotely viable if no assertions have been posted that depend on the new contracts' behavior. Once they have, a revert can invalidate chain history or strand in-flight withdrawals. Treat a contracts rollback as an incident requiring support involvement, not a routine operation. ## Related documents * [ArbOS upgrade](/launch-arbitrum-chain/operate/arbos-upgrade.md)—step-by-step mechanics for each upgrade action * [ArbOS releases](/run-arbitrum-node/arbos-releases/overview.md)—per-release requirements, WASM module roots, and Nitro versions * [Ownership and access](/launch-arbitrum-chain/operate/ownership-and-access.md)—which account holds which administrative role * [Monitoring tools and considerations](/launch-arbitrum-chain/operate/monitoring.md)—what to watch during and after an upgrade * [Batch poster troubleshooting](/launch-arbitrum-chain/operate/batch-poster-troubleshooting.md)—diagnosing batch poster errors * [Customize your chain's behavior](/launch-arbitrum-chain/extend-the-protocol/stf.md)—building a custom STF and its replay binary * [Nitro CLI flags reference](/run-arbitrum-node/nitro/cli-flags-reference.md)—reference for the Nitro configuration flags named here (`dangerous` flags are not listed) --- > For a complete page index, fetch # Validator troubleshooting This guide covers operational issues that validator operators encounter after setup, with guidance on diagnosing root causes and resolving them. For initial setup, see [How to run a validator](/run-arbitrum-node/more-types/run-validator-node.md). For the metrics and alerts to watch, see the validator section of [Monitoring tools and considerations](/launch-arbitrum-chain/operate/monitoring.md). For sequencing a BoLD upgrade, see the [BoLD upgrade playbook](/launch-arbitrum-chain/operate/bold-upgrade-playbook.md). Most of this guide applies to both the legacy staker and the BoLD staker. Where behavior differs, this guide calls it out. ## Validation strategies in operation The [validator setup guide](/run-arbitrum-node/more-types/run-validator-node.md#validation-strategies) describes what each strategy does. This section covers the operational consequences that are easy to get wrong. | Strategy | Operational notes | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Watchtower` | Default. Takes no onchain action and needs no wallet or bond. Logs: `found incorrect assertion in watchtower mode` (legacy) or `Detected invalid assertion, but not configured to post a rival stake` (BoLD) on disagreement. | | `Defensive` | Bonds and challenges only when it finds a bad assertion. Idle in normal operation, so an absence of onchain activity is not a fault signal. | | `StakeLatest` | Pre-BoLD chains only. Not available on BoLD chains. | | `ResolveNodes` | Bonds and resolves assertions **that already exist**. It never creates new ones, so a chain whose validators are all `ResolveNodes` will stop advancing. At least one `MakeNodes` validator is required to post new assertions. | | `MakeNodes` | Creates assertions, resolves unconfirmed ones, and challenges bad ones. | Switching strategy does not re-bond A validator bonds once. Changing strategy—for example from `MakeNodes` to `Defensive`—does not place a new bond, and does not require one. If you are troubleshooting a validator that will not act after a strategy change, the bond is not the thing to check; look at wallet gas balance, allowlist membership, and parent-chain connectivity instead. Multiple MakeNodes validators produce expected reverts If more than one `MakeNodes` validator runs on the same chain, they may attempt to create the same assertion simultaneously. Only one succeeds; the others revert. These reverts are normal contention, not a malfunction. Do not alert on them individually—alert on assertions failing to appear at all. See [`arb/validator/poster/error_posting_assertion`](/launch-arbitrum-chain/operate/monitoring.md) for the counter to watch, and expect a nonzero baseline when you run redundant proposers. ## Assertion timing flags Four flags control assertion timing, and they are frequently confused with one another. None of them takes a `-duration` suffix, despite that form circulating in support threads. | Flag | Default | Applies to | What it controls | | --------------------------------------------- | -------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--node.bold.assertion-posting-interval` | `15m0s` | BoLD staker | How often this validator posts a new assertion. | | `--node.staker.make-assertion-interval` | `1h0m0s` | Legacy staker | How often a `MakeNodes` validator creates assertions. Bypassed during a dispute. Has no effect on BoLD chains. | | `--node.bold.minimum-gap-to-parent-assertion` | `1m0s` | BoLD staker | Minimum time to wait after the parent assertion was created before posting a child. A floor between consecutive assertions, distinct from the posting cadence above. | | `--node.bold.assertion-confirming-interval` | `1m0s` | BoLD staker | How often the validator checks whether a pending assertion can be confirmed. This is a polling interval, not a delay before confirmation is permitted. | Two rules to check when tuning these: * **The posting interval must exceed the Rollup contract's `minimumAssertionPeriod`.** That is an onchain constraint measured in parent chain blocks; posting faster than it results in reverted transactions. Read `minimumAssertionPeriod` from your Rollup contract rather than assuming the default. * **Do not tune the confirming interval to make confirmations happen sooner.** Confirmation timing is governed onchain by `confirmPeriodBlocks` and, where a challenge occurred, `challengeGracePeriodBlocks`. Lowering `--node.bold.assertion-confirming-interval` only increases how often the validator checks, which adds parent-chain RPC load without advancing anything. For the full flag list, see the [Nitro CLI flags reference](/run-arbitrum-node/nitro/cli-flags-reference.md). For the onchain parameters, see the parameter table in [BoLD for Arbitrum chains](/launch-arbitrum-chain/chain-config/validation/bold.md#table-of-arbitrum-bold-parameters). ## Parent-chain read consistency `--node.bold.rpc-block-number` determines which block the BoLD staker reads onchain data from. It accepts `finalized` (the default), `safe`, or `latest`. | Value | Trade-off | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `finalized` | Default and safest. The validator only acts on data that cannot be reorganized, at the cost of lagging roughly two epochs behind on an Ethereum parent chain. | | `safe` | Shorter lag, small reorg exposure. | | `latest` | No lag, full reorg exposure. The validator may act on data that later disappears. | ### `error initializing staker: could not create assertion chain: no contract code at given address` This is the most common symptom of the default `finalized` setting, and it usually appears immediately after a BoLD upgrade. **Cause:** the validator is reading at the finalized block, and the block containing the new Rollup contract deployment has not finalized yet. From the validator's point of view, no contract exists at the configured address. **Resolution:** wait for the upgrade transactions to finalize, then start the node again. This is expected behavior and not a misconfiguration. Do not switch to `latest` to work around it. That permanently exposes the validator to acting on data that a reorg can undo, to solve a problem that resolves itself in minutes. Check as well that `rollup.rollup` in `--chain.info-json` points at the new Rollup address, and that `rollup.stake-token` is set. A stale `rollup.rollup` produces the same error and does not resolve on its own. ## Assertion errors ### `ASSERTION_NOT_EXIST` A revert from the Rollup contract. It originates in `requireExists`, which rejects any assertion whose status is `NoAssertion`: ```solidity require(self.status != AssertionStatus.NoAssertion, "ASSERTION_NOT_EXIST"); ``` In other words, the validator referenced an assertion hash that the Rollup contract has no record of. **This is almost never a protocol fault.** The usual causes are environmental: * **Unsynced node.** The validator computed state from a chain it has not fully caught up on, and referenced an assertion that does not exist onchain. * **RPC inconsistency.** A parent-chain endpoint behind a load balancer can serve reads from nodes at different heights, so the validator sees an assertion in one request and not in the next. Point the validator at a single consistent endpoint. * **Reorg exposure.** With `--node.bold.rpc-block-number=latest`, an assertion the validator observed may have been reorganized away. **Resolution:** confirm the node is fully synced, verify the parent-chain endpoint returns consistent results, and confirm the Rollup address in `--chain.info-json` is correct. Move to `finalized` if you are running `latest`. ### `Detected invalid assertion, but not configured to post a rival stake` The node's locally computed state disagrees with an assertion posted onchain. On a healthy chain this warrants immediate investigation—it is the signal watchtower mode exists to produce. Before escalating, rule out local causes: an unsynced or partially synced node, a mismatched Nitro version, or a wrong `--chain.info-json` will all produce local state that legitimately disagrees with a correct onchain assertion. Confirm the disagreement against a second, independently operated node before treating it as a real dispute. ## Stuck validator transactions If a validator transaction has been pending for more than roughly 10 minutes, it is stuck rather than slow. The staker queues only one transaction by default `--node.staker.data-poster.max-mempool-transactions` defaults to **`1`**, compared to `18` for the batch poster. A single stuck transaction therefore blocks every subsequent validator action until it clears. This is the first thing to check, and the difference from batch poster behavior surprises most operators. Work through these in order: 1. **Raise the queue depth.** Increase `--node.staker.data-poster.max-mempool-transactions` so that one pending transaction does not stall the whole validator. Increase it deliberately rather than setting `0` (unlimited), which removes the backpressure that prevents runaway submission. 2. **Check the fee cap against current parent-chain gas prices.** If parent-chain gas has risen above what the data poster will bid, the transaction cannot be included. Raise the relevant fee cap. 3. **Accelerate the transaction manually.** Resubmit at a higher gas price from the same account and nonce to replace it. Keep the nonce identical—submitting a new nonce leaves the original stuck and creates a gap. 4. **Confirm the wallet is funded.** The validator needs parent-chain native currency for gas, separately from its ERC-20 bond token. Watch `arb/staker/balance`. ## Confirming an assertion manually If assertions are pending well past the confirmation period and no validator is confirming them, a validator can call `confirmAssertion` on the Rollup contract directly: ```solidity function confirmAssertion( bytes32 assertionHash, bytes32 prevAssertionHash, AssertionState calldata confirmState, bytes32 winningEdgeId, ConfigData calldata prevConfig, bytes32 inboxAcc ) external onlyValidator(msg.sender) whenNotPaused ``` Treat this as a recovery tool, not a routine operation. A healthy validator confirms assertions on its own, so needing this manually points at an underlying problem—usually a stopped validator, an unfunded wallet, or a strategy set that contains no validator willing to resolve. Before attempting it, note the constraints: * **It is not permissionless.** `onlyValidator` restricts the caller to the allowlist on a permissioned chain. If your allowlist has been misconfigured, fixing the allowlist comes first. * **`prevAssertionHash` must be the current latest confirmed assertion.** Assertions confirm in order; you cannot skip ahead. * **`confirmPeriodBlocks` must have elapsed** since the assertion was created. * **If the parent assertion has more than one child**, a challenge occurred. The winning edge must be confirmed, and `challengeGracePeriodBlocks` must have elapsed since that edge was confirmed. The grace period exists to give the chain owner or security council a window to intervene—do not attempt to route around it. ## Related monitoring Assertion-progress and failure signals, including which metrics apply to the legacy staker versus the BoLD staker and the `validatorAfkBlocks` allowlist caution, are documented in the validator section of [Monitoring tools and considerations](/launch-arbitrum-chain/operate/monitoring.md). --- > For a complete page index, fetch # Arbitrum chain FAQ ### Can I use the Chain SDK to deploy a mainnet chain? Yes! The Arbitrum Chain SDK's core technology has undergone a comprehensive audit and is now capable of supporting deployments to mainnet. You can read more about it in our [preview expectations notice](https://docs.arbitrum.io/launch-orbit-chain/concepts/public-preview-expectations#arbitrum-orbit-is-mainnet-ready-but-deploy-to-testnet-first). Learn more: [Chain SDK introduction](https://docs.arbitrum.io/launch-arbitrum-chain/arbitrum-chain-sdk-introduction). ### Do I need permission/license to launch an Arbitrum chain? You can launch any Arbitrum chain permissionlessly. Nitro's license is under a [Business Source license](https://github.com/OffchainLabs/nitro?tab=License-1-ov-file), similar to DeFi protocols like Uniswap and Aave, among others. This license contains an Additional Use Grant that permits the permissionless deployment of Nitro software on blockchains that settle to Arbitrum One or Nova. However, Arbitrum chains that settle to a parent chain other than Arbitrum One or Nova are subject to additional licensing guidelines under the [AEP](https://docs.arbitrum.foundation/aep/ArbitrumExpansionProgramTerms.pdf). Learn more: [AEP license](https://docs.arbitrum.io/launch-arbitrum-chain/aep-license). ### Does Arbitrum officially deploy and/or maintain L3s for external teams? No. Teams are required to deploy and maintain their Arbitrum chains. There are, however, several RaaS (Rollup as a Service) providers that can deploy and maintain your Arbitrum chain on your behalf. Learn more: [Third-party providers](https://docs.arbitrum.io/launch-arbitrum-chain/06-third-party-integrations/02-third-party-providers). ### Can I modify the underlying technology to customize my Arbitrum chain? Yes, you can make any changes you require to the underlying Nitro code base. Learn more: [Customize the State Transition Function](https://docs.arbitrum.io/launch-arbitrum-chain/05-customize-your-chain/customize-stf). ### What Data Availability (DA) solutions are currently available for Arbitrum chains? Arbitrum chains currently support three different DA solutions: * Rollup, posting data to the parent chain, which ultimately posts the data to Ethereum. * AnyTrust, posting data to a Data Availability Committee, selected by the chain owner. * Celestia, posting data to the [Celestia network](https://blog.celestia.org/celestia-is-first-modular-data-availability-network-to-integrate-with-arbitrum-orbit/). Note that using AnyTrust provides the chain owner with the most flexibility and the most cost-effective fees. Learn more: [Configure data availability](https://docs.arbitrum.io/launch-arbitrum-chain/02-configure-your-chain/common/data-availability/config-data-availability). ### What token is used to pay gas fees on Arbitrum chains? By default, Arbitrum chains pay gas in **ETH**. However, Arbitrum chains that use AnyTrust are configurable to use any **ERC-20** token for the gas fee token of the chain. Learn more: [Choose a custom gas token](https://docs.arbitrum.io/launch-arbitrum-chain/features/common/gas-and-fees/choose-custom-gas-token). ### Can I use Ethereum toolkits to develop on my Arbitrum chain? Arbitrum chains are fully EVM-compatible. Most tools that support Ethereum should be able to support an Arbitrum chain. There are, however, specific differences that developers need to consider when building on an Arbitrum chain. You can find them in our [overview of the differences between Arbitrum and Ethereum](https://docs.arbitrum.io/for-devs/concepts/differences-between-arbitrum-ethereum/overview). ### Do Arbitrum chains have any built-in AA solution? No, but ZeroDev is heavily tested, easy to use and to integrate. Learn more: [ZeroDev smart account integration](https://docs.arbitrum.io/for-devs/third-party-docs/ZeroDev/zero-dev). ### Is there any cross-chain bridging solution between two Arbitrum chains? There is currently no native Arbitrum-to-Arbitrum chain bridging solution, except for going through the parent chain (even if they share the same parent chain). However, many third-party bridges have expressed interest in supporting Arbitrum chains. Learn more: [Cross-chain messaging](/arbitrum-essentials/bridging/cross-chain-messaging.md). ### Is there an official block explorer for Arbitrum chains? Arbitrum chains deployments usually come with an open-source Blockscout explorer by default, but there are many third-party solutions that have expressed interest in supporting Arbitrum chains. Learn more: [Monitoring tools and block explorers](https://docs.arbitrum.io/build-decentralized-apps/reference/06-monitoring-tools-block-explorers). ### Is there any indexing solution that supports Arbitrum chains? Similar to bridges and block explorers, there are many third-party indexing solutions that have expressed interest in supporting Arbitrum chains. Learn more: [The Graph](https://docs.arbitrum.io/for-devs/third-party-docs/TheGraph/thegraph). ### Can I increase the maximum contract size for my Arbitrum chain? Yes, Arbitrum chains support an increased smart contract size limit of up to 96kB. You can use our [Chain SDK](https://github.com/OffchainLabs/arbitrum-orbit-sdk) and configure the parameters [`MaxCodeSize`](https://github.com/OffchainLabs/arbitrum-orbit-sdk/blob/main/src/prepareChainConfig.ts#L29)[ and ](https://github.com/OffchainLabs/arbitrum-orbit-sdk/blob/main/src/prepareChainConfig.ts#L29)[`MaxInitCodeSize`](https://github.com/OffchainLabs/arbitrum-orbit-sdk/blob/main/src/prepareChainConfig.ts#L29) when calling [`prepareNodeConfig`](https://github.com/OffchainLabs/arbitrum-orbit-sdk/blob/main/examples/prepare-node-config/index.ts#L43). Once deployed, you cannot change the parameters for the smart contract size limit through an upgrade. For more information refer to the [Smart contract size limit page](https://docs.arbitrum.io/launch-arbitrum-chain/02-configure-your-chain/common/validation-and-security/config-smart-contract-size-limit) ### How can I modify Nitro to force posting an invalid assertion and test the fraud proof mechanism? Forcing an invalid assertion in the chain is not currently supported. However, if you're building Nitro locally, you can run the following test that goes through the whole rollup/challenge mechanism: ```shell go test ./system_tests/ -tags=challengetest -run=TestChallenge ``` ### What fee collectors can be configured on my chain? Four fee types are configurable on an Arbitrum chain: * **L2 base fee**: L2 execution fees corresponding to the minimum base price of the chain. This fee is deposited into the `infraFeeAccount`, which can be set by calling `ArbOwner.setInfraFeeAccount().` * **L2 surplus fee**: L2 execution fees above the minimum base price (in the case of congestion). This fee goes to the `networkFeeAccount`, which can be set by calling `ArbOwner.setNetworkFeeAccount().` * **L1 base fee**: Relative fees for posting a transaction on the parent chain. This fee is paid ultimately to the fee collector of the active batch poster. A call to `SequencerInbox.setIsBatchPoster()` on the parent chain will set the batch poster. Delegating a different fee collector for that batch poster can be specified by calling `ArbAggregator.setFeeCollector()`. * **L1 surplus fee**: Any extra fees rewarded to the batch poster. This is paid to a specific `L1RewardRecipient`, which can be set by calling `ArbOwner.setL1PricingRewardRecipient()`. For more detailed information about fees, please refer to the [L1 Fees](https://docs.arbitrum.io/arbos/l1-pricing) and [L2 Fees](https://docs.arbitrum.io/arbos/gas) pages. To learn more about precompiles, refer to the [Precompiles reference page](https://docs.arbitrum.io/build-decentralized-apps/precompiles/reference). Learn more: [Fee management](https://docs.arbitrum.io/launch-arbitrum-chain/02-configure-your-chain/common/fees/07-fee-management). ### What is the lowest you can set the base fee to? You can set the base fee to any amount to charge users less. You can even set it to `0` (however, this would open the chain to DOS attacks). If the Arbitrum chain base fee is `0`, users are then only paying for the cost of DA. Learn more: [Gas and fees deep dive](https://docs.arbitrum.io/how-arbitrum-works/deep-dives/gas-and-fees). ### How does fee collection work in Nitro? Four fee types are configurable on Nitro: * **L2 base fee**: L2 execution fees corresponding to the minimum base price of the chain. This is paid to the `infraFeeAccount`, and you can set it by calling `ArbOwner.setInfraFeeAccount()`. * **L2 surplus fee**: L2 execution fees above the minimum base price (in the case of congestion). This is paid to the `networkFeeAccount`, and you can set it by calling `ArbOwner.setNetworkFeeAccount()`. * **L1 base fee**: Relative fees for posting this transaction on the parent chain. This is paid ultimately to the fee collector of the active batch poster. You can set the batch poster by calling `SequencerInbox.setIsBatchPoster()`, on the parent chain, and specify a different fee collector for that batch poster by calling `ArbAggregator.setFeeCollector()`. * **L1 surplus fee**: Any extra fees rewarded to the batch poster. This is paid to a specific `L1RewardRecipient`, and you can set it by calling `ArbOwner.setL1PricingRewardRecipient()` You can find more detailed information about fees in these pages: * [L1 fees](https://docs.arbitrum.io/arbos/l1-pricing) * [L2 fees](https://docs.arbitrum.io/arbos/gas) And information about the precompiles methods in the [Precompile References](https://docs.arbitrum.io/build-decentralized-apps/precompiles/reference). Learn more: [Gas and fees deep dive](https://docs.arbitrum.io/how-arbitrum-works/deep-dives/gas-and-fees). ### Can you upgrade the smart contract size limit once deployed? No. There's no way to version it so that old blocks are executed under the past limit and new blocks are executed for the new limit. ### Can you set the block speed below 100ms? The implications of reducing the block speed below 100ms are that an increased block count will put more strain on third-party providers, node runners, indexers, etc. Learn more: [Sequencer timing adjustments](https://docs.arbitrum.io/launch-arbitrum-chain/features/advanced/sequencer-timing-adjustments). ### Can I set fast deposits for an L3? Yes, we have documented fast deposits in [how to configure delayed inbox finality](https://docs.arbitrum.io/launch-arbitrum-chain/02-configure-your-chain/common/validation-and-security/arbitrum-chain-finality). There is a flag that can be changed, which allows an Arbitrum chain to wait for finality on the parent chain before depositing a transaction. For an L3 on Arb One, for example, we consider this a feature because they can instantly register a deposit on the child chain, as Arbitrum One has an ultra-low re-org risk (having never re-organized). For an L2, however, we recommend waiting \~12 minutes for L1 finality, as there is a greater risk of reorganization, for example, the deposit transaction not being included immediately in the fork-choice. ### How do I increase the max transaction data size? **Warning: Always test first!!!** Test this thoroughly on a testnet first. 1. This is a hard limit in the codebase to prevent DoS attacks. If you want to modify this, you'll need to follow this [procedure](https://docs.arbitrum.io/launch-arbitrum-chain/05-customize-your-chain/customize-stf) to create a new WASM module root with an updated max L2 message size. Other limits might also need to be updated, such as the maximum decompressed batch size. Be sure to modify everything starting with the latest version, which includes a couple of extra safety checks that may help prevent issues. 2. You could disable this limit if you were confident you would stick to AnyTrust and not need to fall back to posting data onchain. 3. Defaults are in place for non-AnyTrust batch posting, which would need to fit the L3 user's transaction into a batch posted as an L2 transaction, thus respecting L2 transaction size limits. ### What is the max theoretical TPS for an Arbitrum Chain? Max TPS is a challenging metric to measure, as it relies on network activity and the type of submitted transactions. We can, however, calculate the max throughput using default Arbitrum chain parameters. The actual maximum throughput depends on the configurable execution parameters: **Using standard Arbitrum chain defaults** * Block time: 250ms * Block gas limit: 32M L2 gas * Max L2 gas per second: 128M gas/sec * These parameters are entirely configurable, for example, by dropping the block time to 100ms or by increasing the block gas limit (which comes at the cost of faster state bloat). * Dropping to 100ms and doubling the block gas limit to 64m L2 gas would achieve 640m L2 gas per second. **The actual TPS varies depending on the gas cost per transaction:** * A simple transfer (\~21,000 gas) could approximately achieve around 6,000 TPS. * A more complex transaction (\~200,000 gas) would enable approximately 640 TPS. ### Why is the WETH Gateway not necessary for custom gas token chains? The **WETH** gateway used in the token bridge is a special, custom gateway that unwraps the \*\*WETH \*\*deposits and sends them to the Arbitrum chain, then wraps them again on the Arbitrum chain. Since **ETH** is the gas token in the Arbitrum chain, there's no need to perform this operation, so you can use a standard **ERC-20** for **WETH** (this is the default case of the token bridge so that you wouldn't need a special **WETH** gateway). If you want to enable extra custom operations with **WETH**, you can create a custom token and a custom gateway to handle this case. Learn more: [Choose a custom gas token](https://docs.arbitrum.io/launch-arbitrum-chain/features/common/gas-and-fees/choose-custom-gas-token). ### How do we verify if an ERC-20 was bridged using the native bridge? The following applies to a parent-side bridge deployed on Arbitrum One: If the token was bridged using the native token bridge, you can go to [parent\_side\_bridge\_contract](https://github.com/OffchainLabs/token-bridge-contracts/blob/5bdf33259d2d9ae52ddc69bc5a9cbc558c4c40c7/contracts/tokenbridge/arbitrum/gateway/L2ERC20Gateway.sol#L47) Call the `calculateL2TokenAddress` address with the address of the token on the parent chain. For example, in the case of plugging in the **ERC-20** address on Arbitrum One `0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9`, you get `0xB2C565cd7e807e6A1C38bFE32D8FAb9c96ffCeCa`. ### What are the estimated infrastructure costs to run the Sequencer? The estimated cost is approximately $600 per sequencer replica per month for a low-to-medium activity chain. With networking and storage included, the estimated monthly cost would be $1,500 for the Sequencer. ### What are the different validator modes? * **Defensive (allowlist required)**:- Post bonds and create a challenge if local state disagrees with the onchain assertion (wallet required, will only post bonds onchain if a bad assertion is found. * **StakeLatest (allowlist required)**:- Stay bonded on the latest assertion and challenge any bad assertions found (wallet required, always bonded, uses some gas every time new assertion created) * **ResolveNodes (allowlist required)**:- Stay bonded on the latest assertion, resolve any unconfirmed assertions, and challenge any bad assertions found (wallet required, always bonded, uses some gas every time an unconfirmed assertion is resolved or a new assertion is created) * **MakeNodes (allowlist required)**:- Continuously create new assertions, challenging any bad assertions found (wallet required, always bonded, most expensive node to run) * Note that if there is more than one MakeNodes validator running, they might all try to create a new assertion at the same time. In that case, only one will be successful, and the others will have still spent gas on reverted calls that did nothing. * **Watchtower**:- A node in Watchtower mode will immediately log an error if an onchain assertion deviates from the locally computed chain state. It doesn't require a wallet, as it never takes any action onchain. This mode is the default-enabled strategy for all nodes (full and archive). Learn more: [Run a validator node](https://docs.arbitrum.io/run-arbitrum-node/more-types/02-run-validator-node). ### What are the costs to run a validator? The estimated monthly cost for Watchtower validators will be approximately $500. All other validators will cost $800 to $900 per month. ### What is the amount of funds that a validator needs in its wallet to participate in fraud proofs? To participate in a fraud-proof game, your validator will need funds (can be any **ERC-20**) for two things: 1. Gas costs to post assertions- Depends on your parent chain and is estimated to cost 163109 gas per assertion. If you assume one assertion per hour, then you can multiply your gas cost per hour to calculate the annual cost. This assumption holds under normal operation, but during a challenge, you may post more assertions (these will scale with the number of challenges), so it might be one assertion every 10 minutes, depending on how many challenges are ongoing. 2. Bonds to participate- Depends on your config. You can set the bond amounts to be any amount you want. For Arbitrum One, this is 3600 **ETH** initially and then 555+79 **ETH** per each subsequent challenge. These amounts were carefully selected and designed, with extensive research behind them, specifically for Arbitrum One. There could be one or multiple challenges, and so there will always be a possibility where more funds are needed. Learn more: [BoLD economics of disputes](https://docs.arbitrum.io/how-arbitrum-works/bold/bold-economics-of-disputes). ### How do I export snapshots for nitro-based chains? Is there any tooling for this? We currently don't have any toolkits available for this. The best way to make a snapshot is to gracefully stop the node and make a copy of the database. Learn more: [Nitro database snapshots](https://docs.arbitrum.io/run-arbitrum-node/nitro/03-nitro-database-snapshots). ### What happens if I don't post an assertion for a long period of time? Over time, without creating assertions, the validator whitelist in the Rollup contract can become disabled. If you look at the [`removeWhitelistAfterValidatorAfk`](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/RollupUserLogic.sol#L62) method, it allows you to disable the whitelist after `confirmPeriodBlocks + VALIDATOR_AFK_BLOCKS` L1 blocks have passed since the last assertion created. As a default, that's 7200 + 45818 blocks (a bit less than eight days). Disabling the whitelist is not a significant issue if you continue to monitor the chain and run a validator. It can be enabled using [`setValidatorWhitelistDisabled()`](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/RollupAdminLogic.sol#L378) by the chain owner. But it's always better to avoid reaching that point. ### Is there a way to increase idle timeout on RPC nodes for websocket connections? There is no way to increase the idle timeout for WebSocket connections via configuration options (geth doesn't provide this, and neither do we). The default value in Geth for the idle timeout is, as noted in [the query](https://github.com/OffchainLabs/go-ethereum/blob/85dc1b7ed058bea72a2707e59f5878815dd00485/rpc/websocket.go#L38), 30 seconds. ### How do I run a split validator? Using Nitro's split validation, it can be run as a separate container using: ```text node.block-validator.validation-server-configs-list ``` It's possible to run it in the same container. That is the default way, and then Nitro uses a loopback connection. You can read more about this in [running a split-validator](https://docs.arbitrum.io/launch-arbitrum-chain/07-arbitrum-node-runners/04-run-split-validator-node). --- > For a complete page index, fetch # Overview of Arbitrum chains Arbitrum chains give you flexibility and control without the constraint of running your own Layer 1 blockchain. Instead of bootstrapping and subsidizing your own validator set, your chain [anchors its security and finality to Ethereum](/how-arbitrum-works/deep-dives/transaction-lifecycle.md), turning security into a variable, usage-based expense. That economic model is the core reason to choose Arbitrum: your business captures the fee revenue and priority-access value that L1s hand off to validators, because costs scale with usage ([gas targets](/launch-arbitrum-chain/chain-config/costs/dynamic-pricing.md#how-to-set-multiple-gas-targets)) rather than set (fixed) fees. You also control the economics directly—[custom gas tokens](/launch-arbitrum-chain/chain-config/costs/custom-gas-token-rollup.md), [fee policy](/launch-arbitrum-chain/chain-config/costs/fee-management.md), and revenue capture—while getting institution-grade settlement: sub-second soft finality for a responsive user experience, [configurable hard finality on Ethereum in minutes](/launch-arbitrum-chain/chain-config/sequencer/chain-finality.md), and [native withdrawals in as little as 15 minutes](/launch-arbitrum-chain/chain-config/validation/fast-withdrawals.md), backed by a clean, externally legible counterparty-risk story for your risk, audit, and compliance stakeholders. Beyond economics, an Arbitrum chain lets you launch fast and customize deeply. You get day-one configuration over high, fully tunable throughput and [block times (as low as 100ms)](/launch-arbitrum-chain/chain-config/sequencer/sequencer-timing-adjustments.md), [data availability (Rollup, AnyTrust, or external DA)](/launch-arbitrum-chain/chain-config/data-availability/config-data-availability.md), [sequencing and MEV rules](/how-arbitrum-works/timeboost/gentle-introduction.md), KYC/AML and permissioning, privacy, [precompiles](/launch-arbitrum-chain/extend-the-protocol/precompiles.md), governance, and multi-prover settlement—and you automatically inherit every future Ethereum and Arbitrum upgrade, including [Stylus](/stylus/gentle-introduction.md), without custom engineering. Critically, your chain isn't siloed: it plugs directly into Ethereum's deep liquidity and the broader Arbitrum ecosystem. And you can de-risk your go-to-market with a phased ["launch-and-migrate" path](/launch-arbitrum-chain/migrate/from-another-stack.md)—prove your product on the shared, liquid Arbitrum One, then graduate to your own dedicated Arbitrum chain as a seamless continuation on the same stack, not a costly replatforming. ![Arbitrum chain settlement layers](/img/orbit-settlement-layers.svg) ## Customization ### Speed and finality Set confirmation speed and settlement behavior for products where timing and certainty matter—payments, trading, treasury, and internal asset movement. | Feature | Your benefit | User benefit | Guide | | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Tune block time | You own the speed-versus-cost tradeoff. Faster blocks let you position the chain as a premium, low-latency venue (trading, gaming, payments) without waiting on a third party to change infrastructure. | Near-instant confirmations make the app feel like a familiar web experience rather than a slow onchain one, reducing the “did my transaction go through?” hesitation. | - [Configure chain finality](/launch-arbitrum-chain/chain-config/sequencer/chain-finality.md)
- [Sequencer timing adjustments](/launch-arbitrum-chain/chain-config/sequencer/sequencer-timing-adjustments.md) | | Configure deposit finality (delayed inbox) | You match certainty guarantees to your actual risk profile to avoid paying for stronger finality than your product needs. Adjust the time before the chain processes a deposit. | Predictable, well-defined settlement means users know exactly when funds and actions are final—important for anyone moving real value. | [Configure chain finality](/launch-arbitrum-chain/chain-config/sequencer/chain-finality.md) | | Enable fast withdrawals to reduce withdrawal finality time | Fewer support tickets and complaints about locked-up capital, and a more competitive bridging story when users compare your chain to alternatives. | Users get their funds in minutes instead of waiting the full challenge window, dramatically lowering the friction of exiting the chain. | [Fast withdrawals](/launch-arbitrum-chain/chain-config/validation/fast-withdrawals.md) | Note The 100ms figure is an optional lower bound on block time, not the default. Most chains run at the 250ms default; 100ms is available when you opt into it. By default, fast withdrawals is not enabled—the default withdrawal time is 6.4 days. ### Transaction pricing model Align costs with your business model, including the option to use a custom gas token that fits customer experience, treasury strategy, or internal accounting needs. | Feature | Your benefit | User benefit | Guide | | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Use a custom ERC-20 as the native gas token | You can drive utility to your own token, align fee revenue with your treasury strategy, and simplify internal accounting by denominating gas in the unit you already track. | Users pay fees in a familiar or branded token rather than acquiring a separate asset, removing a common onboarding hurdle. | - [Custom gas token (Rollup)](/launch-arbitrum-chain/chain-config/costs/custom-gas-token-rollup.md)
- [Custom gas token (AnyTrust)](/launch-arbitrum-chain/chain-config/costs/custom-gas-token-anytrust.md) | | Manage the fee parameters that govern what users pay and fee distribution | Direct control over cost recovery and revenue, so you can tune the chain's economics to be self-sustaining or subsidized as your business model requires. | Transparent, deliberately set fees rather than opaque or volatile costs, which builds trust in the pricing. | [Fee management](/launch-arbitrum-chain/chain-config/costs/fee-management.md) | | Configure native mint/burn behavior for the gas token | You can use a cross-chain-native token (such as a stablecoin) as your gas token, so it moves in and out of your chain through canonical interop protocols instead of lock-and-mint wrappers—keeping your treasury and accounting denominated in the real asset. | Users hold and pay fees in the canonical token rather than a wrapped derivative, so their gas balance stays fungible and redeemable across chains. | [Native mint and burn](/launch-arbitrum-chain/chain-config/costs/configure-native-mint-burn.md) | | Dynamic pricing | You smooth out pricing (gas fee) volatility by setting multiple gas targets that react to short-term spikes and long-term load separately, so brief demand bursts don't turn into severe, sustained gas-price spikes. | More stable, predictable fees during periods of high demand instead of sudden, sharp cost increases exactly when the chain is busiest. | [Dynamic pricing](/launch-arbitrum-chain/chain-config/costs/dynamic-pricing.md) | ### Throughput and latency Handle higher volumes and faster response times for products that cannot degrade during periods of peak demand. | Feature | Your benefit | User benefit | Guide | | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Dedicated throughput so your chain does not compete for computation and storage resources | Guaranteed capacity means you can make performance commitments (effectively an SLA) to partners and customers without worrying about noisy-neighbor contention. | Consistent performance during peak events—launches, drops, market volatility—instead of degraded speed exactly when demand is highest. | [Manage gas target](/launch-arbitrum-chain/chain-config/costs/gas-target.md) | | Set the gas target and block gas limit to match your expected load | You provision capacity to match your expected transaction volume—scaling up for high-throughput workloads or holding it lean to manage operating costs—so throughput planning becomes a controllable lever rather than a fixed constraint you inherit. | Capacity provisioned to real demand means orders and settlements continue to clear promptly even during peak volume, rather than facing delays or failed transactions when the network is congested. | - [Gas target guidance](/launch-arbitrum-chain/chain-config/costs/gas-target.md)
- [Gas optimization](/launch-arbitrum-chain/chain-config/costs/gas-optimization.md#block-gas-limit) | ### Privacy and access controls Create participation rules and data access controls that align with regulated workflows and protect sensitive information. | Feature | Your benefit | User benefit | Guide | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | Run permissioned validators to vet and restrict who participates in validation—useful for enterprise or regulated environments (for example, KYC for validators) | You can meet regulatory and legal requirements, reduce compliance risk, and make the chain viable for enterprise and institutional partners who can't operate on a fully open infrastructure. | Assurance that the chain operates within a vetted, accountable set of participants — a prerequisite for many regulated financial and enterprise products. | [Validation and BoLD](/launch-arbitrum-chain/chain-config/validation/bold.md) | | Restrict who can read chain data by keeping data off the public L1 with an AnyTrust data availability committee | You keep sensitive business and user data out of a fully public ledger, satisfying privacy obligations and protecting competitive information. | Greater confidentiality around their activity and data than a fully public chain would offer. | [Configure data availability](/launch-arbitrum-chain/chain-config/data-availability/config-data-availability.md) | | Move to permissionless validation later via BoLD when you are ready to decentralize | You can launch with tight control and decentralize on your own timeline — no re-platforming required as your product and risk tolerance mature. | A credible path to stronger trustlessness and censorship resistance over time, rather than being locked into a permissioned model forever. | [Validation and BoLD](/launch-arbitrum-chain/chain-config/validation/bold.md) | Note These controls apply at the validator and data-availability layers—who validates the chain and who can read its data. They are not a per-user transaction allowlist; screening which end users may submit transactions is an application-layer concern, not a chain-config setting. ### Transaction sequencing Customize transaction ordering to match your product, whether the priority is wider access, lower MEV exposure, or tighter handling of transaction flow. | Feature | Your benefit | User benefit | Guide | | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | Keep the default FCFS ordering for intuitive, simple ordering that the world's largest exchanges use for trading | Fair, simple ordering keeps fast block times and avoids the reputational cost of a chain seen as hostile to ordinary users. | Built-in protection from front-running and sandwich attacks, so users aren't quietly taxed by MEV extractors on every trade. | [How the Sequencer works](/how-arbitrum-works/deep-dives/sequencer.md) | | Enable Timeboost to auction an express lane, letting the chain owner capture MEV while preserving fair ordering for everyone else | You capture MEV as a revenue stream for the chain rather than leaking it to external searchers. | Non-express transactions keep their fair-ordering protections, while users who genuinely need priority have a transparent way to pay for it. | [Timeboost configuration](/launch-arbitrum-chain/chain-config/sequencer/timeboost.md) | ### Governance and data availability Choose how the chain is upgraded, administered, and backed by data availability based on the balance of cost, transparency, and resilience you need. | Feature | Your benefit | User benefit | Guide | | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | Define the chain-owner role and access controls that govern upgrades and administration, and plan a path toward progressive decentralization | You retain the control needed for upgrades and incident response early on, then deliberately decentralize as the chain matures—balancing agility with credibility. | Clear accountability for who can change the chain, plus a visible path toward stronger, more decentralized guarantees. | [Ownership and access control](/launch-arbitrum-chain/operate/ownership-and-access.md) | | Choose your data availability model—Rollup, AnyTrust, Alt-DA—to trade off cost against transparency and resilience | You dial the cost-versus-security balance directly; AnyTrust can cut data costs substantially, while Rollup maximizes security and transparency. | Lower fees when you choose AnyTrust, or maximum security and Ethereum-grade transparency when you choose Rollup | [Configure data availability](/launch-arbitrum-chain/chain-config/data-availability/config-data-availability.md) | ## Performance On an Arbitrum chain, performance isn’t a single parameter—it’s the combined result of a few independent levers. Each trades one property against another (speed vs. infrastructure load, throughput vs. node stability, settlement speed vs. security), and you tune them to fit your product’s risk tolerance and demand profile. There are four primary parameters to configure, and language support capability is automatically included (no configuration needed). ### Finality and settlement How quickly transactions become final and withdrawable. Set the **delayed inbox finality**, the **challenge period**, and **fast withdrawals** to balance settlement speed against re-org and security risk. | Feature | Your benefit | User benefit | Guides | | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Delayed inbox, fast withdrawals, challenge period | Delayed Inbox finality, challenge period, and fast withdrawals — to balance settlement speed against re-org and security risk for your risk, audit, and compliance stakeholders. | Faster, more predictable settlement: sub-second soft finality for responsiveness and quicker withdrawals back to the parent chain. | - [Delayed inbox](/launch-arbitrum-chain/chain-config/sequencer/chain-finality.md)
- [Fast withdrawals](/launch-arbitrum-chain/chain-config/validation/fast-withdrawals.md)
- [Challenge period](/launch-arbitrum-chain/chain-config/validation/challenge-period.md) | ### Cost and fee stability How predictable prices stay under load. Use **Dynamic Pricing**, **fee management** (base/surplus minimums), a **gas price floor**, and **batch-poster fee tuning** to smooth spikes and capture revenue. | Feature | Your benefit | User benefit | Guides | | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | - Dynamic pricing
- Fee management
- Gas price floor
- Batch poster fee tuning | Shape fee economics directly — Dynamic Pricing (multiple gas targets), base/surplus fee minimums, a gas price floor, and batch-poster fee tuning — to smooth price spikes and capture revenue while costs scale with usage. | More stable, predictable transaction costs under load, avoiding sudden fee spikes during large payout runs or volatile periods. | - [Dynamic pricing](/launch-arbitrum-chain/chain-config/costs/dynamic-pricing.md)
- [Fee management](/launch-arbitrum-chain/chain-config/costs/fee-management.md)
- [Gas optimization](/launch-arbitrum-chain/chain-config/costs/gas-optimization.md)
- [Batch poster fee tuning](/launch-arbitrum-chain/chain-config/batch-poster/fee-tuning.md) | ### Language support Stylus is inherited by every Arbitrum chain. It lets you develop smart contracts in languages your team may already know, instead of learning Solidity. Out of the box, it lets you write performant contracts in Rust, C, and C++ (plus community-tier AssemblyScript) alongside Solidity, reusing mature libraries while keeping full EVM compatibility. * [Stylus gentle intro](/stylus/gentle-introduction.md) * [Deploy non-Rust WASM contracts](/stylus/how-tos/deploying-non-rust-wasm-contracts.md) ## Compliance ### Sanctioned-address screening at the protocol level Protocol-level transaction filtering added to the Sequencer and State Transition Function (STF), at the chain owner's discretion. | Owner benefit | User benefit | Guide | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | Enables filtering as a first-class protocol capability to opt into, rather than building and maintaining bespoke tooling, while retaining full discretion over whether to enable it. | Transactions are screened by the network itself, so protection doesn’t depend on any single app behaving correctly. | [Compliance filtering](/launch-arbitrum-chain/chain-config/sequencer/compliance-filtering.md) | | Can reuse an existing provider relationship and risk methodology rather than build screening in-house. | Screening reflects industry-standard sanctions data rather than ad hoc lists. | [Compliance filtering](/launch-arbitrum-chain/chain-config/sequencer/compliance-filtering.md) | | Can tailor enforcement granularity to policy—addresses, events, or both. | Only the activity that the policy actually targets is blocked, reducing false positives. | [Compliance filtering](/launch-arbitrum-chain/chain-config/sequencer/compliance-filtering.md) | | A ready-made menu of enforcement actions covering transfers, tokens, and opcodes. | Clearly scoped restrictions keep permitted activity available. | [Compliance filtering](/launch-arbitrum-chain/chain-config/sequencer/compliance-filtering.md) | | The restricted set remains up to date without manual redeployment. | Newly sanctioned addresses are enforced promptly, improving protection. | [Compliance filtering](/launch-arbitrum-chain/chain-config/sequencer/compliance-filtering.md) | ### Continuous monitoring (synchronization pipeline) | Feature | Owner benefit | User benefit | Guide | | --------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------- | | CLI flags | Can tune update frequency and storage to their operational needs. | Timely list updates mean less exposure to recently restricted addresses. | [CLI flags reference](/run-arbitrum-node/nitro/cli-flags-reference.md) | ## What problem do Arbitrum chains solve? Arbitrum chains are dedicated chains built with Arbitrum technology. Teams can configure execution, fee models, governance, data availability, validation, and other chain parameters for their application or business requirements. The Ethereum ecosystem is supported by a **decentralized network of nodes** that each run Ethereum's Layer 1 (L1) client software. Ethereum's block space is in high demand, so users are often stuck waiting for the network to become less congested (and thus, less expensive). Arbitrum's protocols address this challenge by offloading some of the Ethereum network's heavy lifting to **another decentralized network of nodes** that support the Arbitrum stack (Arbitrum chains). ## How do Arbitrum chains help the Ethereum ecosystem? Arbitrum helps Ethereum move towards a **multi-chain future**. This is valuable for the following reasons: | Value add | Description | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Scalability** | Multiple chains help overcome scaling bottlenecks by dividing activity into opt-in environments with separate resource management. | | **Flexible security models** | Different chains can experiment with different security models, allowing for tradeoffs. For example: Arbitrum One and Arbitrum Nova are both L2 chains, with Arbitrum Nova giving developers the ability to optimize for lower fees. With Arbitrum chains, extending the technology and experimenting is easier than ever. | | **Flexible execution environments** | Different chains can experiment with more-or-less restrictive execution environments. For example, although Arbitrum chains are fully EVM compatible, Arbitrum chains can restrict smart contract functionality to optimize for your project's needs. | | **Flexible governance** | Arbitrum chains let you define your own governance protocols. | ## Are Arbitrum chains the same thing as "app chains"? It depends on your definition of "app chain". Arbitrum chains can be used as application-specific chains (often referred to as "app chains" or "appchains"). But **they aren't just for apps**. They're for **hosting EVM-compatible smart contracts using self-managed infrastructure that isolates compute resources away from Arbitrum's public L2 chains** based on your unique needs. * You can use your Arbitrum chain to host the smart contracts that support one app, two apps, an ecosystem of apps, or no apps at all. * Ethereum-grade security and interoperability, so your assets, users, and builders flow to/from Ethereum seamlessly and safely. * You can use your Arbitrum chain to host a private, centralized service. * Your Arbitrum chain can be special-purpose, general-purpose, and everything in-between. * Customize/tune your chain to your specific use case however you want. * You could even build an app that uses multiple Arbitrum chains to support strange new forms of redundancy, high availability, and trustlessness. --- > For a complete page index, fetch # Arbitrum chain licensing ## What do I need to know about the Arbitrum chain license? Nitro is currently licensed under a [Business Source License](https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md), similar to DeFi protocols like Uniswap and Aave, among others, with an “Additional Use Grant” to ensure that everyone can have full comfort using and running nodes on all public Arbitrum chains. The Additional Use Grant also permits deployment of the Nitro software in a permissionless, zero-cost fashion, as a new blockchain provided that the chain settles to either Arbitrum One or Arbitrum Nova. L3s that settle to Arbitrum One or Nova have no obligation to share revenue with the Arbitrum DAO and remain first class members of the Arbitrum ecosystem. As an expansion of this license, the [Arbitrum Expansion Program](https://docs.arbitrum.foundation/aep/ArbitrumExpansionProgramTerms.pdf) (AEP) is a self-service licensing model that makes it easy for developers to build and customize L2s/L3s using Arbitrum’s technology alongside different parent chains. ### Benefits * Leverage battle-tested technology to permissionlessly deploy L2s/L3s that settle to any supported parent chain. * Governance freedom: Arbitrum chains are not required to be governed by the Arbitrum DAO. * Flexible licensing allows developers to modify chain configurations. Arbitrum chains are free to modify any part of the stack, including implementation of custom gas tokens, alternative DA integrations, novel sequencing mechanisms, account abstraction, altVMs, etc. * L3s that settle to parent chains other than Arbitrum One and Nova must contribute [net chain revenue](https://docs.arbitrum.foundation/calculate-aep-fees), where 8% flows to the DAO and 2% to the developer guild. --- > For a complete page index, fetch # Public preview: What to expect Arbitrum chains are currently a **public preview** offering. This concept document explains what "public preview" means, what to expect from Arbitrum chain's public preview capabilities, and how to engage with our team as you tinker. ## Arbitrum chains are Mainnet ready, but deploy to Testnet first Arbitrum chain's core technology has undergone a comprehensive audit and is now able to support deployments to Mainnet. It's important to note that Arbitrum chains are a new technology and as such, **there are risks involved**. To mitigate these risks, you're strongly encouraged to **deploy your Arbitrum chain on Testnet first**. If you don't launch on Testnet first, you significantly increase risk. Refer to the [Chain SDK Rollup creation example](https://github.com/OffchainLabs/arbitrum-chain-sdk/tree/main/examples/create-rollup-eth) for instructions that walk you through the process of deploying your Arbitrum chain to Testnet. ## How products like Arbitrum chains developed at Offchain Labs Offchain Labs builds products in a way that aligns loosely with the spirit of "building in public". We like to release things **early and often** so that we can capture feedback and iterate in service of your needs, as empirically as possible. To do this, some of our product offerings are documented with **public preview** disclaimers that look like this: This banner's purpose is to set expectations while inviting readers like you to express your needs so that we can incorporate them into the way that we iterate on product. ## What to expect when using public preview offerings As you tinker and provide feedback, we'll be listening. Sometimes, we'll learn something non-obvious that will result in a significant change. More commonly, you'll experience incremental improvements to the developer experience as the offering grows out of its **public preview** status, towards **stable** status. Public preview offerings are evolving rapidly, so don't expect the degree of release notes discipline that you'd expect from a stable offering. Keep your eyes open for notifications regarding patch, minor, and major changes, along with corresponding relnotes that highlight breaking changes and new capabilities. ## How to provide feedback Our product team primarily uses three feedback channels while iterating on public preview capabilities: 1. **Docs**: Click on the **Request an update** button located in the top-right corner of any document to provide feedback on the docs and/or developer experience. This will lead you to a prefilled Github issue that members of our product team periodically review. 2. **Discord**: [Join the Arbitrum Discord](https://discord.gg/arbitrum) to engage with members of the Arbitrum community and product team. 3. **Google form**: Complete [this form](http://bit.ly/3yy6EUK) to ask for support. ## What to expect when providing feedback Our ability to respond to feedback is determined by our ever-evolving capacity and priorities. We can't guarantee responses to all feedback submissions, but our team is listening, and we'll try our best to acknowledge and respond to your feedback. No guarantees though! > **INFO** > > [Our team is hiring](https://jobs.lever.co/offchainlabs). ## Thank you! Thanks for helping us build things that meet your needs! We're excited to engage with OGs and newcomers alike; please don't hesitate to reach out. --- > For a complete page index, fetch # Run an L3 rollup from scratch > **INFO** — RaaS providers > > It is highly recommended that you work with a Rollup-as-a-Service (RaaS) provider to deploy a production chain. You can find a list of [RaaS providers](/launch-arbitrum-chain/integrations/infrastructure-providers.md#rollup-as-a-service-raas-providers) in our integrations directory. This how-to is where you should start if you have not deployed a chain on Arbitrum before. You will walk through how to deploy a chain with the default settings—the intent is to familiarize you with the process before you begin adding modifications to a custom chain. Here we will create an Arbitrum L3 chain that settles to Arbitrum Sepolia chain. ### What you'll do 1. **Set up**: Create a folder, add your wallet key, and install the tools 2. **Deploy**: Put your chain's contracts on Arbitrum Sepolia (costs a small amount of **ETH**) 3. **Generate config**: Create the file your node needs to run 4. **Fund wallets**: Send **ETH** to the batch poster and validator (they need it for gas) 5. **Run the node**: Start your chain (one Docker command) 6. **Deploy token bridge**: Enable bridging tokens between your chain and Arbitrum Sepolia ### Before you start | Requirement | What you need | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Node.js** | v20 or later. [Install Node.js](https://nodejs.org/) | | **Docker** | [Install Docker](https://docs.docker.com/get-docker/) | | **ETH on Arbitrum Sepolia** | Your deployer wallet needs at least \~0.1 **ETH** for gas. Use a [faucet](https://arbitrum.faucet.dev/) or [bridge from Sepolia](https://bridge.arbitrum.io/) | | **A wallet private key** | From MetaMask or any wallet. Export it (it starts with `0x`) | ## Step 1: Set up the project * Create a folder and install the packages: ```bash mkdir my-l3-rollup && cd my-l3-rollup npm init -y npm install @arbitrum/chain-sdk viem dotenv ``` * Create a file named `.env` in that folder. Start with this line (replace with your real private key): ```text DEPLOYER_PRIVATE_KEY=0xYourPrivateKeyHere ``` * If you fail to add this key, or do not enter it correctly, it will fail with: `Error: private key must be 32 bytes, hex or bigint, not string` * You **will need this file** in future steps. ### Environment variables reference | Variable | Required? | When | Description | | ----------------------------------- | ---------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DEPLOYER_PRIVATE_KEY` | **Yes** | Step 1 | Your wallet's private key (starts with `0x`, 66 chars). Used to deploy the chain and token bridge. Never share or commit it. | | `PARENT_CHAIN_RPC` | Optional | Step 2, 3, 6 | Arbitrum Sepolia RPC URL. If omitted, uses the default public RPC (can be slow or time out). Use a [node provider](/arbitrum-essentials/reference/node-providers.md) for reliability. | | `BATCH_POSTER_PRIVATE_KEY` | Optional | Step 2 | Private key for the batch poster. If omitted, the deploy script generates one and prints it—copy it into `.env` for the next steps. | | `VALIDATOR_PRIVATE_KEY` | Optional | Step 2 | Private key for the validator. If omitted, the deploy script generates one and prints it—copy it into `.env` for the next steps. | | `CHAIN_DEPLOYMENT_TRANSACTION_HASH` | **Yes** (after Step 2) | Step 3, 6 | Transaction hash from the deploy script. Copy it from Step 2's output into `.env`. | | `CHAIN_RPC` | Optional | Step 6 | Your chain's RPC URL. Defaults to `http://localhost:8449` when running locally. Override if your node is elsewhere. | > **CAUTION** — Private key format > > Your key must start with `0x` and be 64 hex characters long (66 characters total). **Never** share it or commit it to Git. ## Step 2: Deploy the chain In this step you will deploy your chain's contracts to Arbitrum Sepolia. It will cost a small amount of **ETH** in gas fees. * Create a new file named `deploy.mjs` in your project folder. Then copy and paste this entire script: deploy.mjs ```javascript import { createPublicClient, http } from 'viem'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { arbitrumSepolia } from 'viem/chains'; import { prepareChainConfig, createRollupPrepareDeploymentParamsConfig, createRollup } from '@arbitrum/chain-sdk'; import { sanitizePrivateKey, generateChainId } from '@arbitrum/chain-sdk/utils'; import { config } from 'dotenv'; config(); const batchPosterPrivateKey = process.env.BATCH_POSTER_PRIVATE_KEY || generatePrivateKey(); const validatorPrivateKey = process.env.VALIDATOR_PRIVATE_KEY || generatePrivateKey(); const batchPoster = privateKeyToAccount(batchPosterPrivateKey).address; const validator = privateKeyToAccount(validatorPrivateKey).address; const parentChain = arbitrumSepolia; const parentChainPublicClient = createPublicClient({ chain: parentChain, transport: http(process.env.PARENT_CHAIN_RPC), }); const deployer = privateKeyToAccount(sanitizePrivateKey(process.env.DEPLOYER_PRIVATE_KEY)); async function main() { const chainId = generateChainId(); const chainConfig = prepareChainConfig({ chainId, arbitrum: { InitialChainOwner: deployer.address, DataAvailabilityCommittee: false, // Rollup (not AnyTrust) }, }); const createRollupConfig = createRollupPrepareDeploymentParamsConfig(parentChainPublicClient, { chainId: BigInt(chainId), owner: deployer.address, chainConfig, }); const result = await createRollup({ params: { config: createRollupConfig, batchPosters: [batchPoster], validators: [validator], }, account: deployer, parentChainPublicClient, }); console.log('Deployment successful!'); console.log('Transaction hash:', result.transactionReceipt.transactionHash); console.log('Save this for the next step: CHAIN_DEPLOYMENT_TRANSACTION_HASH=' + result.transactionReceipt.transactionHash); console.log('Save these keys (they were used for batch poster and validator):'); console.log('BATCH_POSTER_PRIVATE_KEY=' + batchPosterPrivateKey); console.log('VALIDATOR_PRIVATE_KEY=' + validatorPrivateKey); console.log('Fund these addresses with ETH on Arbitrum Sepolia (for Step 4):'); console.log('BATCH_POSTER_ADDRESS=' + batchPoster); console.log('VALIDATOR_ADDRESS=' + validator); } main().catch(console.error); ``` * Run the deployment script: ```bash node deploy.mjs ``` * When it is completed, the script prints lines starting with `CHAIN_DEPLOYMENT_TRANSACTION_HASH=`, `BATCH_POSTER_PRIVATE_KEY=`, and `VALIDATOR_PRIVATE_KEY=`. * **Copy those three lines into your `.env` file** (created in Step 2). Add them below `DEPLOYER_PRIVATE_KEY`. * Your `.env` should then have four lines. The script also prints `BATCH_POSTER_ADDRESS=` and `VALIDATOR_ADDRESS=`—you will fund these in Step 4. Save the file before continuing. ## Step 3: Generate the node config In this step you will create the `node-config.json`—the file your node needs to know how to connect to your chain and Arbitrum Sepolia. * Create a new file named `prepare-node-config.mjs`. Copy and paste this entire script: prepare-node-config.mjs ```javascript import { writeFile } from 'fs/promises'; import { createPublicClient, http } from 'viem'; import { arbitrumSepolia } from 'viem/chains'; import { createRollupPrepareTransaction, createRollupPrepareTransactionReceipt, prepareNodeConfig } from '@arbitrum/chain-sdk'; import { config } from 'dotenv'; config(); const parentChain = arbitrumSepolia; const parentChainPublicClient = createPublicClient({ chain: parentChain, transport: http(process.env.PARENT_CHAIN_RPC), }); async function main() { const txHash = process.env.CHAIN_DEPLOYMENT_TRANSACTION_HASH; if (!txHash) throw new Error('Set CHAIN_DEPLOYMENT_TRANSACTION_HASH in .env'); const tx = createRollupPrepareTransaction(await parentChainPublicClient.getTransaction({ hash: txHash })); const txReceipt = createRollupPrepareTransactionReceipt(await parentChainPublicClient.getTransactionReceipt({ hash: txHash })); const config = tx.getInputs()[0].config; const chainConfig = JSON.parse(config.chainConfig); const coreContracts = txReceipt.getCoreContracts(); const nodeConfig = prepareNodeConfig({ chainName: 'My L3 Rollup', chainConfig, coreContracts, batchPosterPrivateKey: process.env.BATCH_POSTER_PRIVATE_KEY, validatorPrivateKey: process.env.VALIDATOR_PRIVATE_KEY, stakeToken: config.stakeToken, parentChainId: parentChain.id, parentChainRpcUrl: process.env.PARENT_CHAIN_RPC || parentChain.rpcUrls.default.http[0], }); // This quickstart doesn't require stake; if you leave it as true it will crash the node if (nodeConfig.node?.staker) { nodeConfig.node.staker.enable = false; } await writeFile('node-config.json', JSON.stringify(nodeConfig, null, 2)); console.log('Node config written to node-config.json'); main().catch(console.error); ``` * Now run the script: ```bash node prepare-node-config.mjs ``` * You should see a confirmation in the console `Node config written to node-config.json`. That file is ready for the next step. ## Step 4: Fund the batch poster and validator The batch poster and validator need **ETH** on Arbitrum Sepolia to pay for gas when posting [batches and assertions](/how-arbitrum-works/deep-dives/assertions.md). Send **ETH** to both addresses from your deployer wallet or a [faucet](https://arbitrum.faucet.dev/). * Get the addresses from your Step 2 output (the lines starting with `BATCH_POSTER_ADDRESS=` and `VALIDATOR_ADDRESS=`). * Send at least 0.01 **ETH** to each address from your deployer wallet or a [faucet](https://arbitrum.faucet.dev/). Without this, the node will not produce blocks or finalize. ## Step 5: Run the node In this step you will start your chain. One node runs everything: it sequences blocks, posts them to Arbitrum Sepolia, and validates. Keep this terminal open—the node runs in the foreground. * Run these two commands: ```bash mkdir -p ./arbitrum-data cp node-config.json ./arbitrum-data/node-config.json ``` ```bash docker run --rm -it \ -v $(pwd)/arbitrum-data:/home/user/.arbitrum \ -p 8547:8547 -p 8548:8548 \ offchainlabs/nitro-node:v3.9.4-7f582c3 \ --conf.file /home/user/.arbitrum/node-config.json ``` The node will start and begin syncing. Wait until you see it producing blocks (logs will show block numbers). Your chain's RPC is then available at `http://localhost:8449`. ## Step 6: Deploy the token bridge In this step you'll enable the bridging tokens between your chain and Arbitrum Sepolia. **Your node must be running** (Step 5). * Open a **new terminal** in the same project folder—leave the node running in the first one. * Create a new file named `deploy-token-bridge.mjs`. Copy and paste this entire script: deploy-token-bridge.mjs ```javascript import { createPublicClient, http, defineChain } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { arbitrumSepolia } from 'viem/chains'; import { createRollupPrepareTransaction, createRollupPrepareTransactionReceipt, createTokenBridgePrepareTransactionRequest, createTokenBridgePrepareTransactionReceipt, createTokenBridgePrepareSetWethGatewayTransactionRequest, createTokenBridgePrepareSetWethGatewayTransactionReceipt } from '@arbitrum/chain-sdk'; import { sanitizePrivateKey } from '@arbitrum/chain-sdk/utils'; import { config } from 'dotenv'; config(); const parentChain = arbitrumSepolia; const parentChainPublicClient = createPublicClient({ chain: parentChain, transport: http(process.env.PARENT_CHAIN_RPC), }); const rollupOwner = privateKeyToAccount(sanitizePrivateKey(process.env.DEPLOYER_PRIVATE_KEY)); async function main() { const txHash = process.env.CHAIN_DEPLOYMENT_TRANSACTION_HASH; if (!txHash) throw new Error('Set CHAIN_DEPLOYMENT_TRANSACTION_HASH in .env'); const tx = createRollupPrepareTransaction(await parentChainPublicClient.getTransaction({ hash: txHash })); const txReceipt = createRollupPrepareTransactionReceipt(await parentChainPublicClient.getTransactionReceipt({ hash: txHash })); const coreContracts = txReceipt.getCoreContracts(); const chainConfig = JSON.parse(tx.getInputs()[0].config.chainConfig); const chainId = chainConfig.chainId; const chainRpc = process.env.CHAIN_RPC || 'http://localhost:8449'; const chain = defineChain({ id: chainId, network: 'Arbitrum chain', name: 'arbitrum-chain', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: [chainRpc] } }, testnet: true, }); const chainPublicClient = createPublicClient({ chain, transport: http() }); const txRequest = await createTokenBridgePrepareTransactionRequest({ params: { rollup: coreContracts.rollup, rollupOwner: rollupOwner.address, }, parentChainPublicClient, orbitChainPublicClient: chainPublicClient, // <-- ADD account: rollupOwner.address, }); console.log('Deploying token bridge...'); const bridgeTxHash = await parentChainPublicClient.sendRawTransaction({ serializedTransaction: await rollupOwner.signTransaction(txRequest), }); const bridgeTxReceipt = createTokenBridgePrepareTransactionReceipt(await parentChainPublicClient.waitForTransactionReceipt({ hash: bridgeTxHash })); console.log('Token bridge deployed on parent chain'); console.log('Waiting for retryables on your chain...'); const retryableReceipts = await bridgeTxReceipt.waitForRetryables({ orbitPublicClient: chainPublicClient, }); if (retryableReceipts[0].status !== 'success' || retryableReceipts[1].status !== 'success') { throw new Error('Retryables failed'); } console.log('Token bridge contracts created on your chain'); const setWethTxRequest = await createTokenBridgePrepareSetWethGatewayTransactionRequest({ rollup: coreContracts.rollup, parentChainPublicClient, orbitChainPublicClient: chainPublicClient, // <-- ADD account: rollupOwner.address, }); const setWethTxHash = await parentChainPublicClient.sendRawTransaction({ serializedTransaction: await rollupOwner.signTransaction(setWethTxRequest), }); const setWethTxReceipt = createTokenBridgePrepareSetWethGatewayTransactionReceipt(await parentChainPublicClient.waitForTransactionReceipt({ hash: setWethTxHash })); const wethRetryableReceipts = await setWethTxReceipt.waitForRetryables({ orbitPublicClient: chainPublicClient, }); try { const wethRetryableReceipts = await setWethTxReceipt.waitForRetryables({ orbitPublicClient: chainPublicClient, }); if (wethRetryableReceipts[0].status !== 'success') throw new Error('WETH gateway retryable failed'); console.log('WETH gateway configured. Token bridge ready.'); } catch (e) { console.warn('WETH gateway retryable was not auto-redeemed:', e.message); console.warn('Redeem it manually with redeem-ticket.mjs using the ticket id above (TICKET).'); } } main().catch(console.error); ``` * This is an optional that will redeem a retryable: Optional: redeem-ticket.mjs ```javascript import { createPublicClient, createWalletClient, http } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { sanitizePrivateKey } from '@arbitrum/chain-sdk/utils'; import { config } from 'dotenv'; config(); // The ticket id printed by deploy-token-bridge.mjs ("Unexpected status for retryable ticket: 0x...") const TICKET = process.env.WETH_TICKET_ID; const ARB_RETRYABLE_TX = '0x000000000000000000000000000000000000006E'; const L3_RPC = process.env.CHAIN_RPC || 'http://localhost:8547'; const L3_CHAIN_ID = Number(process.env.CHAIN_ID); // your chain id, e.g. 36173670529 const abi = [ { type: 'function', name: 'getTimeout', stateMutability: 'view', inputs: [{ name: 'ticketId', type: 'bytes32' }], outputs: [{ type: 'uint256' }] }, { type: 'function', name: 'redeem', stateMutability: 'nonpayable', inputs: [{ name: 'ticketId', type: 'bytes32' }], outputs: [{ type: 'bytes32' }] }, ]; const acct = privateKeyToAccount(sanitizePrivateKey(process.env.DEPLOYER_PRIVATE_KEY)); const chain = { id: L3_CHAIN_ID, name: 'l3', nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: [L3_RPC] } } }; const pub = createPublicClient({ chain, transport: http() }); const wallet = createWalletClient({ account: acct, chain, transport: http() }); const hash = await wallet.writeContract({ address: ARB_RETRYABLE_TX, abi, functionName: 'redeem', args: [TICKET] }); const rec = await pub.waitForTransactionReceipt({ hash }); console.log('redeem tx', hash, 'status', rec.status); // getTimeout reverts with NoTicketWithID once the ticket is consumed -> success ``` * Run the script (it reads from your `.env` file): ```bash node deploy-token-bridge.mjs ``` * It will take a minute or two. When you see `Token bridge ready.`, the process is complete. ## A running chain Congratulations! You now have a fully running L3 chain that settles to Arbitrum Sepolia, with a sequencer, validator, and token bridge. Your chain's RPC is at `http://localhost:8449`. #### What "done/success" looks like: ```text L3 router.getGateway(parentWETH) == L3 wethGateway -> WETH gateway registered eth_chainId -> 0x86c1e6881 (your chain id) eth_blockNumber advances; RPC reachable at http://localhost:8449 ``` ### If something failed | Problem | What to check | | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | Step 2 fails with "insufficient funds" | Your deployer wallet needs **ETH** on Arbitrum Sepolia. Use a [faucet](https://arbitrum.faucet.dev/). | | Step 2 fails with "invalid private key" | Your `DEPLOYER_PRIVATE_KEY` must start with `0x` and be 66 characters. | | Step 3 fails with "CHAIN\_DEPLOYMENT\_TRANSACTION\_HASH" | You didn't add the three lines from Step 2's output to `.env`. Copy them exactly. | | Step 5: node runs but no blocks | Fund the batch poster and validator (Step 4). They need **ETH** on Arbitrum Sepolia. | | Step 6 fails or hangs | Make sure the node from Step 5 is still running and has finished syncing. | | Step 5: "docker: command not found" | Install [Docker](https://docs.docker.com/get-docker/) and ensure it's running. | ### Next steps * **Product-level testnet:** To run your chain with production infrastructure (HA sequencers, separate nodes, public RPC) instead of locally, complete Steps 1–4 above, then follow [Run L3 rollup infrastructure (product-level testnet)](/launch-arbitrum-chain/quickstart/l3-rollup-testnet.md). * For production chains, customization, and more details, see the [full deployment guide](/launch-arbitrum-chain/deploy/deploy-chain.md), [node config guide](/launch-arbitrum-chain/deploy/configure-node.md), and [token bridge guide](/launch-arbitrum-chain/deploy/token-bridge.md). --- > For a complete page index, fetch # Run testnet infrastructure on your first rollup (product-level testnet) > **INFO** — RaaS providers > > It is highly recommended that you work with a Rollup-as-a-Service (RaaS) provider to deploy a production chain. You can find a list of [RaaS providers](/launch-arbitrum-chain/integrations/infrastructure-providers.md#rollup-as-a-service-raas-providers) in our integrations directory. This page provides step-by-step instructions for running your chain's full infrastructure as a production-level testnet with high availability (HA). The setup uses multiple sequencers with automatic failover, Redis for coordination, relays for feed distribution, and separate full nodes, batch poster, and validator—the same architecture used in production. ## Steps at a glance 1. Extract chain info from `node-config.json` 2. Add Helm repo and create a namespace 3. Set up Redis 4. Deploy sequencers (3 replicas) 5. Deploy sequencer relays 6. Deploy external relays 7. Deploy full nodes 8. Deploy batch poster 9. Deploy validator 10. Set up Sequencer Coordinator Manager (optional) 11. Expose RPC 12. Verify 13. Deploy the token bridge > **TIP** — Run all steps in the same terminal > > The commands use shell variables (`$CHAIN_ID`, `$PARENT_CHAIN_ID`, `$PARENT_RPC`, `$REDIS_URL`). Run Steps 1–11 in the **same terminal session** so these variables persist, or re-export them if you open a new terminal. ## Prerequisites | Requirement | What you need | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Deployed chain** | Complete [Deploy your first rollup](/launch-arbitrum-chain/quickstart/l3-rollup-from-scratch.md), Steps 1–4 (deploy, generate config, fund batch poster and validator), **do not complete Step 5** | | **Kubernetes cluster** | Access to a cluster with multiple availability zones (e.g., EKS, GKE, AKS) | | **Helm** | [Install Helm](https://helm.sh/docs/intro/install/) | | **kubectl** | Configured to access your cluster | | **jq** | [Install jq](https://jqlang.github.io/jq/download/) for parsing `node-config.json` | | **Go** | [Install Go](https://go.dev/dl/) (for Step 10, building SQM) | | **Redis** | In-cluster (Step 3) or managed (e.g., AWS ElastiCache) | ## Step 1: Extract chain info from `node-config.json` * On your local machine, in the folder that contains `node-config.json`, run: node-config.json ```bash # Export so variables persist for all steps below export CHAIN_ID=$(jq -r '.chain["info-json"]' node-config.json | jq -r '.[0]["chain-id"]') export PARENT_CHAIN_ID=$(jq -r '.chain["info-json"]' node-config.json | jq -r '.[0]["parent-chain-id"]') export PARENT_RPC=$(jq -r '.["parent-chain"].connection.url' node-config.json) # Save chain info to file (used by Helm --set-file) # Save chain info to file (used by Helm --set-file) jq -r '.chain["info-json"]' node-config.json > chain-info.json echo "CHAIN_ID=$CHAIN_ID" echo "PARENT_CHAIN_ID=$PARENT_CHAIN_ID" echo "PARENT_RPC=$PARENT_RPC" echo "chain-info.json saved" ``` * In Step 8, you will extract the batch poster private key—never commit it to Git or expose it in logs. ## Step 2: Add Helm repo and create namespace * Run the following commands: ```bash helm repo add offchainlabs https://charts.arbitrum.io helm repo update kubectl create namespace my-l3-chain kubectl config set-context --current --namespace=my-l3-chain ``` * Replace `my-l3-chain` with your preferred namespace in all commands below. ## Step 3: Set up Redis Redis coordinates which sequencer is active and shares state between components. #### Option A: In-cluster Redis (simplest for testnet) * Run the following commands: ```bash helm repo add bitnami https://charts.bitnami.com/bitnami helm install redis bitnami/redis \ --namespace my-l3-chain \ --set auth.enabled=false \ --set replica.replicaCount=1 export REDIS_URL="redis://redis-master.my-l3-chain.svc.cluster.local:6379" ``` #### Option B: Managed Redis (e.g., AWS ElastiCache) * Create a Redis cluster on your cloud provider. Note the endpoint, then: ```bash export REDIS_URL="redis://YOUR_REDIS_ENDPOINT:6379" ``` * Ensure Redis is reachable from your Kubernetes cluster (in the same VPC or network). ## Step 4: Deploy sequencers * Deploy 3 sequencer replicas with the sequencer coordinator enabled. Replace placeholders with your values. Use `--set-file` for the chain info JSON to avoid shell escaping issues: Deploy sequencers: ```shell configmap.data.parent-chain.connection.url=$PARENT_RPC \ --set configmap.data.chain.id=$CHAIN_ID \ --set-file configmap.data.chain.info-json=chain-info.json \ --set configmap.data.node.sequencer=true \ --set configmap.data.node.delayed-sequencer.enable=true \ --set configmap.data.node.seq-coordinator.enable=true \ --set configmap.data.node.seq-coordinator.redis-url=$REDIS_URL \ --set configmap.data.node.feed.output.enable=true \ --set configmap.data.node.feed.output.port=9642 \ --set configmap.data.execution.sequencer.enable=true \ --set configmap.data.init.empty=true \ --set perReplicaHeadlessService.enabled=true ``` * If sequencers fail to coordinate, each needs a unique URL.: Create sequencer-extra-env.yaml ```yaml extraEnv: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: NITRO_NODE_SEQ__COORDINATOR_MY__URL value: 'http://$(POD_NAME).sequencer-nitro-headless.my-l3-chain.svc.cluster.local:8547/rpc' ``` * Then add `-f sequencer-extra-env.yaml` to the `helm install` command above. * Wait for the sequencer pods to be ready: ```bash kubectl get pods -l app.kubernetes.io/name=nitro -w ``` > **WARNING** — Set sequencer priority after deployment > > After deploying sequencers, you must set at least one sequencer in the priority list. Without this, no sequencer will be active, and your chain won’t produce blocks. Complete Step 10 to set priorities using the Sequencer Coordinator Manager, or set them directly in Redis. ## Step 5: Deploy sequencer relays Sequencer relays combine feeds from all sequencer replicas. Other components connect to these relays rather than directly to the sequencers. Run the following commands: ```shell replicaCount=2 \ --set configmap.data.chain.id=$CHAIN_ID \ --set configmap.data.node.feed.input.url=ws://sequencer-nitro-0.sequencer-nitro-headless:9642\,ws://sequencer-nitro-1.sequencer-nitro-headless:9642\,ws://sequencer-nitro-2.sequencer-nitro-headless:9642 ``` * The relay service names (`sequencer-nitro-0`, etc.) depend on your Helm release name. If you used a different release name for the sequencer, adjust accordingly (format: `-nitro-.-nitro-headless`). ## Step 6: Deploy external relays External relays connect to sequencer relays and serve the public feed. They add a layer of isolation between the sequencers and public traffic. * Run the following command: ```bash helm install external-relay offchainlabs/relay \ --namespace my-l3-chain \ --set replicaCount=2 \ --set configmap.data.chain.id=$CHAIN_ID \ --set configmap.data.node.feed.input.url=ws://sequencer-relay:9642 ``` ## Step 7: Deploy full nodes Full nodes serve RPC requests and forward transactions to the active sequencer. They use Redis to find the active sequencer automatically. Run the following commands: ```shell \ --set configmap.data.parent-chain.id=$PARENT_CHAIN_ID \ --set configmap.data.parent-chain.connection.url=$PARENT_RPC \ --set configmap.data.chain.id=$CHAIN_ID \ --set-file configmap.data.chain.info-json=chain-info.json \ --set configmap.data.execution.forwarder.redis-url=$REDIS_URL \ --set configmap.data.node.feed.input.url=ws://external-relay:9642 \ --set configmap.data.init.empty=true \ --set configmap.data.execution.forwarding-target=http://sequencer-nitro:8547 ``` * If your chain already has blocks, use `--set configmap.data.init.latest=pruned` instead of `init.empty=true`. ## Step 8: Deploy batch poster The batch poster posts transaction batches to the parent chain. * Save the private key to a file (do not commit this file to Git): ```bash printf '%s' "$(jq -r '.node["batch-poster"]["parent-chain-wallet"]["private-key"]' node-config.json)" > batch-poster-key.txt ``` Deploy the batch poster: ```shell configmap.data.parent-chain.id=$PARENT_CHAIN_ID \ --set configmap.data.parent-chain.connection.url=$PARENT_RPC \ --set configmap.data.chain.id=$CHAIN_ID \ --set-file configmap.data.chain.info-json=chain-info.json \ --set-string configmap.data.execution.forwarding-target=null \ --set configmap.data.node.seq-coordinator.enable=true \ --set configmap.data.node.seq-coordinator.redis-url=$REDIS_URL \ --set configmap.data.node.batch-poster.enable=true \ --set configmap.data.node.feed.input.url=ws://sequencer-relay:9642 \ --set-file configmap.data.node.batch-poster.parent-chain-wallet.private-key=batch-poster-key.txt ``` * Remove the key file after deployment: ```bash rm batch-poster-key.txt ``` > **WARNING** — Do not add batch poster to sequencer priority > > Do **not** add the batch poster to the Sequencer Coordinator Manager's sequencer priority list. Otherwise, it could become the active sequencer unintentionally. ## Step 9: Deploy validator The validator validates blocks and posts [assertions](/how-arbitrum-works/deep-dives/assertions.md) to the parent chain. It is required for chain security. The validator wallet needs **ETH** on the parent chain (Arbitrum Sepolia) for gas. * Save the validator private key to a file (do not commit to Git): ```bash printf '%s' "$(jq -r '.node["staker"]["parent-chain-wallet"]["private-key"]' node-config.json)" > validator-key.txt ``` Deploy the validator: ```bash configmap.data.parent-chain.id=$PARENT_CHAIN_ID \ --set configmap.data.parent-chain.connection.url=$PARENT_RPC \ --set configmap.data.chain.id=$CHAIN_ID \ --set-file configmap.data.chain.info-json=chain-info.json \ --set configmap.data.node.sequencer=false \ --set configmap.data.node.batch-poster.enable=false \ --set configmap.data.node.staker.enable=true \ --set configmap.data.node.staker.strategy=MakeNodes \ --set configmap.data.node.feed.input.url=ws://external-relay:9642 \ --set configmap.data.execution.forwarding-target=http://sequencer-nitro:8547 \ --set-file configmap.data.node.staker.parent-chain-wallet.private-key=validator-key.txt \ --set configmap.data.init.empty=true ``` * Remove the key file after deployment: ```bash rm validator-key.txt ``` ## Step 10: Set up Sequencer Coordinator Manager and set priority > **WARNING** — Required for sequencer activation > > This step is **required**, not optional. You **must** set sequencer priority for your chain to work. Without setting a priority, no sequencer will be active. The Sequencer Coordinator Manager (SQM) provides a UI to manage the sequencer priority list. You can also set priority directly in Redis (advanced users). #### 1. Port-forward Redis (if using in-cluster Redis from Step 3): * Run the following: ```bash kubectl port-forward svc/redis-master 6379:6379 -n my-l3-chain ``` * Keep this running. In a **new terminal**: #### 2. Build and run SQM Requires [Go](https://go.dev/dl/) and build tools. If the build fails, the Nitro repo may need additional dependencies—the chain runs fine without SQM. * Run the following: ```bash git clone --branch v3.9.4 https://github.com/OffchainLabs/nitro.git cd nitro make target/bin/seq-coordinator-manager ./target/bin/seq-coordinator-manager redis://127.0.0.1:6379 ``` * If Redis is external (e.g., ElastiCache), use its URL instead of `redis://127.0.0.1:6379`. #### 3. Use the SQM to add sequencers to priority list When you first run SQM, all sequencers will be in the `--Not in priority list but online--` section. You must add at least one to the priority list: 1. **Select a sequencer** from the non-priority list using the arrow keys and press Enter 2. **Choose position 1** from the dropdown menu 3. **Click/press `Update`** to add it to the priority list at position 1 4. **Repeat** for other sequencers if you want multiple sequencers with failover (e.g., add at positions 2, 3) 5. **Press `s`** to save changes to Redis (this makes them permanent) 6. **Verify** one sequencer is marked with a `chosen` indicator (this is the active sequencer) 7. **Press `q`** to quit > **TIP** — Add all sequencers to priority list > > For proper high availability with automatic failover, add all 3 sequencers to the priority list at different positions (1, 2, 3). The sequencer at position 1 becomes active. If it fails, position 2 takes over automatically. > **WARNING** > > Do **not** add the batch poster to the priority list. The batch poster should never become the active sequencer. Alternatively, you can press `a` to manually add a new sequencer by entering its URL. The URL must match the `my-url` configured for each sequencer (e.g., `http://sequencer-nitro-0.sequencer-nitro-headless.my-l3-chain.svc.cluster.local:8547/rpc`). ## Step 11: Expose RPC Expose the full node RPC so users can connect. #### Kubernetes LoadBalancer * Start the LoadBalancer: ```bash kubectl patch svc fullnode-nitro -n my-l3-chain -p '{"spec": {"type": "LoadBalancer"}}' kubectl get svc fullnode-nitro -n my-l3-chain ``` * Use the external IP as your chain RPC URL (e.g., `http://EXTERNAL_IP:8547/rpc`). #### Optional: CDN (Cloudflare) For production, put a CDN in front: 1. Add a DNS A record pointing to your LoadBalancer IP 2. Enable Cloudflare proxy (orange cloud) for DDoS protection 3. Use the Cloudflare hostname as your RPC URL ## Step 12: Verify #### Option A: Port-forward (quick test before exposing) * Run the following: ```bash kubectl port-forward svc/fullnode-nitro 8547:8547 -n my-l3-chain ``` * Then in another terminal: ```bash curl -X POST -H "Content-Type: application/json" \ --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ http://localhost:8547/rpc ``` #### Option B: After exposing via LoadBalancer * Run the following: ```bash # Replace with your LoadBalancer IP or hostname RPC_URL="http://YOUR_FULLNODE_IP:8547/rpc" curl -X POST -H "Content-Type: application/json" \ --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ $RPC_URL ``` * You should get a JSON response with a block number (e.g., `{"jsonrpc":"2.0","id":1,"result":"0x..."}`). Wait until blocks are producing before proceeding to Step 13. ## Step 13: Deploy token bridge This enables bridging tokens between your chain and Arbitrum Sepolia. Run this from your project folder (the one with `node-config.json` and `.env` from the from-scratch page). The chain must be producing blocks (Step 12). * Create `deploy-token-bridge.mjs` in your project folder: deploy-token-bridge.mjs ```javascript import { createPublicClient, http, defineChain } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { arbitrumSepolia } from 'viem/chains'; import { createRollupPrepareTransaction, createRollupPrepareTransactionReceipt, createTokenBridgePrepareTransactionRequest, createTokenBridgePrepareTransactionReceipt, createTokenBridgePrepareSetWethGatewayTransactionRequest, createTokenBridgePrepareSetWethGatewayTransactionReceipt } from '@arbitrum/chain-sdk'; import { sanitizePrivateKey } from '@arbitrum/chain-sdk/utils'; import { config } from 'dotenv'; config(); const parentChain = arbitrumSepolia; const parentChainPublicClient = createPublicClient({ chain: parentChain, transport: http(process.env.PARENT_CHAIN_RPC), }); const rollupOwner = privateKeyToAccount(sanitizePrivateKey(process.env.DEPLOYER_PRIVATE_KEY)); async function main() { const txHash = process.env.CHAIN_DEPLOYMENT_TRANSACTION_HASH; if (!txHash) throw new Error('Set CHAIN_DEPLOYMENT_TRANSACTION_HASH in .env'); const tx = createRollupPrepareTransaction(await parentChainPublicClient.getTransaction({ hash: txHash })); const txReceipt = createRollupPrepareTransactionReceipt(await parentChainPublicClient.getTransactionReceipt({ hash: txHash })); const coreContracts = txReceipt.getCoreContracts(); const chainConfig = JSON.parse(tx.getInputs()[0].config.chainConfig); const chainId = chainConfig.chainId; const chainRpc = process.env.CHAIN_RPC || 'http://localhost:8547'; const chain = defineChain({ id: chainId, network: 'Arbitrum chain', name: 'arbitrum-chain', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: [chainRpc] } }, testnet: true, }); const chainPublicClient = createPublicClient({ chain, transport: http() }); const txRequest = await createTokenBridgePrepareTransactionRequest({ params: { rollup: coreContracts.rollup, rollupOwner: rollupOwner.address }, parentChainPublicClient, account: rollupOwner.address, }); console.log('Deploying token bridge...'); const bridgeTxHash = await parentChainPublicClient.sendRawTransaction({ serializedTransaction: await rollupOwner.signTransaction(txRequest), }); const bridgeTxReceipt = createTokenBridgePrepareTransactionReceipt(await parentChainPublicClient.waitForTransactionReceipt({ hash: bridgeTxHash })); console.log('Token bridge deployed on parent chain'); console.log('Waiting for retryables on your chain...'); const retryableReceipts = await bridgeTxReceipt.waitForRetryables({ orbitPublicClient: chainPublicClient, }); if (retryableReceipts[0].status !== 'success' || retryableReceipts[1].status !== 'success') { throw new Error('Retryables failed'); } console.log('Token bridge contracts created on your chain'); const setWethTxRequest = await createTokenBridgePrepareSetWethGatewayTransactionRequest({ rollup: coreContracts.rollup, parentChainPublicClient, account: rollupOwner.address, }); const setWethTxHash = await parentChainPublicClient.sendRawTransaction({ serializedTransaction: await rollupOwner.signTransaction(setWethTxRequest), }); const setWethTxReceipt = createTokenBridgePrepareSetWethGatewayTransactionReceipt(await parentChainPublicClient.waitForTransactionReceipt({ hash: setWethTxHash })); const wethRetryableReceipts = await setWethTxReceipt.waitForRetryables({ orbitPublicClient: chainPublicClient, }); if (wethRetryableReceipts[0].status !== 'success') { throw new Error('WETH gateway retryable failed'); } console.log('WETH gateway configured. Token bridge ready.'); } main().catch(console.error); ``` * Run it with your chain's RPC URL (the LoadBalancer IP from Step 11): ```bash CHAIN_RPC=http://YOUR_LOADBALANCER_IP:8547/rpc node deploy-token-bridge.mjs ``` * Replace `YOUR_LOADBALANCER_IP` with your external IP. The script reads from your `.env` file. When you see `Token bridge ready.`, you now have the testnet infrastructure running. ### Troubleshooting * Run `kubectl get pods -n my-l3-chain` to check pod status * Run `kubectl logs -n my-l3-chain` to inspect logs * Run `kubectl get svc -n my-l3-chain` to verify service names (if relay connection fails, the service name may differ) * **No blocks producing**: Ensure batch poster and validator were funded (from-scratch Step 4) * **Token bridge retryables fail**: Wait for blocks to be producing (Step 12) before running Step 13 ## Summary | Component | Purpose | | -------------------- | --------------------------------------------------------------- | | **Sequencers (3)** | Redundant transaction queueing; one active, others standby | | **Redis** | Coordinates active sequencer selection | | **Sequencer relays** | Combine feeds from all sequencers | | **External relays** | Public-facing feed endpoints | | **Full nodes** | Serve RPC and forward transactions to active sequencer | | **Batch poster** | Posts batches to the parent chain | | **Validator** | Validates blocks and posts assertions to the parent chain | | **SQM** (optional) | Manual failover and sequencer management | | **Token bridge** | Enables bridging tokens between your chain and Arbitrum Sepolia | Your chain RPC is at your full node URL (load balancer or CDN). Users and apps can connect from anywhere. ### Next steps For running production mainnet, consider a RaaS provider. See the [list of RaaSes on the Third-party providers page](/launch-arbitrum-chain/integrations/infrastructure-providers.md#rollup-as-a-service-raas-providers). --- > For a complete page index, fetch # Deploy a production chain: an overview > **INFO** — RaaS providers > > It is highly recommended that you work with a Rollup-as-a-Service (RaaS) provider to deploy a production chain. You can find a list of [RaaS providers](/launch-arbitrum-chain/integrations/infrastructure-providers.md#rollup-as-a-service-raas-providers) in our integrations directory. Deploying new Arbitrum chains is done through a [`RollupCreator`](/launch-arbitrum-chain/deploy/canonical-factory-contracts.md) contract that processes the creation of the needed contracts and sends the initialization messages from the parent chain to the newly created Arbitrum chain. To assist with these operations, the Arbitrum chain SDK contains a series of tools and scripts that help create and manage your chain(s). Its capabilities include: * Configuration and deployment of your Arbitrum chain's core contracts * Configuration and deployment of the chain's TokenBridge contracts * Initialization of your chain and management of its configuration post-deployment This overview describes the process for creating a new Arbitrum chain, with each step linking to the appropriate guide to follow. You'll find guides to use the Arbitrum chain SDK for deploying a new chain, configuring your node, initializing your chain's configuration, and creating a token bridge. > **INFO** — The Arbitrum chain SDK > > It is recommended to use the Arbitrum chain SDK when deploying new chains and performing chain owner actions. ## 1. Select a chain type There are two main types of Arbitrum chains. Review the following table to determine which type best fits your needs: | Chain type | Description | Use case | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | **Rollup** | Offers Ethereum-grade security by batching, compressing, and posting data to the parent chain, similarly to Arbitrum One. | Ideal for applications that require high security guarantees. | | **AnyTrust** | Implements the [AnyTrust protocol](/how-arbitrum-works/deep-dives/anytrust-protocol.md), relying on an external Data Availability Committee (DAC) to store data and provide it on-demand, effectively using it as its Data Availability (DA) layer. | Suitable for applications that require lower transaction fees. | Additionally, Arbitrum chains can be configured to use **ETH** or any standard **ERC-20** token as the gas token. To understand the implications of using a custom gas token, see [Configure a custom gas token](/launch-arbitrum-chain/chain-config/costs/custom-gas-token-rollup.md). ## 2. Deploy your chain After selecting a chain type, follow the [chain deployment guide](/launch-arbitrum-chain/deploy/deploy-chain.md) to deploy your chain using the Chain SDK. ## 3. Configure your Arbitrum chain's node Once the chain is deployed, you'll need to generate the configuration to run its node. To learn how, visit [Configure your Arbitrum chain's node](/launch-arbitrum-chain/deploy/configure-node.md). ## 4. Deploy your Arbitrum chain's token bridge Your Arbitrum chain's token bridge contracts allow **ERC-20** tokens to move between your Arbitrum chain and its underlying parent chain. Read [Deploy your Arbitrum chain's token bridge](/launch-arbitrum-chain/deploy/token-bridge.md) to learn how to set up your bridge. --- > For a complete page index, fetch # Run a batch poster To learn how the batch-poster role relates to the other Nitro node roles, including its wallet requirements and the safety rules for running redundant posters, see [How to assign roles to a Nitro node](/run-arbitrum-node/assign-node-roles.md). The [batch poster](/how-arbitrum-works/deep-dives/assertions.md) uses the following default configuration flags and values: | Flag | Default value | Description | | ----------------------------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--node.batch-poster.enable` | false | Enables posting batches to L1 | | `--node.batch-poster.max-calldata-batch-size` | 100000 | Maximum calldata batch size; replaces the deprecated `--node.batch-poster.max-size`. Default value is overwritten if it's an L3 (90000). For AnyTrust batches, use `--node.da.anytrust.max-batch-size` (default 1,000,000) | | `--node.batch-poster.poll-interval` | 10 seconds | Period to wait after no batches are ready to be posted before checking again | | `--node.batch-poster.error-delay` | 10 seconds | Delay time after error posting batch | | `--node.batch-poster.compression-level` | 11 | Batch compression level (Arbitrum uses Brotli compression which has compression levels from 1-11) | | `--node.batch-poster.parent-chain-wallet.private-key` | none | Sets the private key of the parent chains wallet | If you created an L3 Arbitrum chain and generated your node config file for a full node, the only values for the batch poster that will be set are: * `--node.batch-poster.enable = true` * `--node.batch-poster.max-calldata-batch-size = 90000` * `--node.batch-poster.parent-chain-wallet.private-key = YOUR_PRIVATE_KEY` If you're the chain owner, using the Arbitrum Chain SDK to generate your config file is the best option. To add a new batch poster, call the `setIsBatchPoster(address,bool)` method of the `SequencerInbox` contract on the parent chain: ```shell cast send --rpc-url $PARENT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY $SEQUENCER_INBOX_ADDRESS "setIsBatchPoster(address,bool)()" $NEW_BATCH_POSTER_ADDRESS true ``` ## Queued transaction database selection `queuedTxs` are transactions that the sequencer has ordered and are ready to be posted: * **Noop**: `--node.batch-poster.data-poster.use-noop-storage` * **Redis**: `--node.batch-poster.redis-url` * **DB**: `--node.batch-poster.data-poster.use-db-storage` You can use only one database at a time, so you can't have both a Redis server and a DB in use simultaneously. > **NOTE** > > If your parent chain is an Arbitrum chain or doesn't have a mempool, you can ignore this section. Noop storage is automatically chosen because batches post sequentially -- a new batch only posts after the previous transaction goes through. No database tracking for queued transactions is required. ### Noop Noop storage does not store any `queuedTxs`. This is beneficial when the parent chain is an Arbitrum chain or one without a mempool, since the sequencer processes every transaction immediately and is never stuck in mempool limbo due to low gas. When Noop is enabled, the batch poster will wait for a confirmation that the transaction has gone through. If the transaction reverts, the batch poster will try again without halting the operation. There is no Replace By Fee (RBF) logic, since that applies only to chains that use a mempool. ### DB DB will store data locally on the node and support persistent `queuedTxs`. Only a single batch poster can use the DB. The DB supports RBF, so transactions will not get stuck in the parent chain's mempool. If a transaction is reverted, then the batch poster must halt. ### Redis Redis is a fast local/external database that runs separately from the Nitro software so that node restarts preserve `queuedTxs`. Storing transactions also enables RBF, which can prevent transactions from being stuck in the mempool due to insufficient gas. However, using Redis means the batch poster will halt if a transaction reverts. To configure Redis, you can also specify a `redis-signer` value with the flag `--node.batch-poster.data-poster.redis-signer.signing-key`. #### Key differences > **INFO** > > The default values are DB on by default, and Noop on by default if and only if the parent chain does not have a mempool (any L3 whose parent is an Arbitrum chain). | Feature | Noop | DB | Redis | | ------------------ | ---- | ---------- | -------------- | | Persistence | None | Local disk | External Redis | | Survives restarts | No | Yes | Yes | | Replace-by-fee | No | Yes | Yes | | Tolerates reverts | Yes | No | No | | Waits for receipts | Yes | No | No | ## Enable blob posting This section explains how to configure an Arbitrum node to post [`EIP-4844`](https://eips.ethereum.org/EIPS/eip-4844) blob transactions to the parent chain, which can significantly reduce data availability costs. ### Prerequisites Before enabling blob transactions, verify that your setup meets these requirements: 1. Chain configuration Your Arbitrum chain must be running in **Rollup mode**. 2. Parent chain compatibility Your parent chain (typically Ethereum mainnet or a testnet) must support [`EIP-4844`](https://eips.ethereum.org/EIPS/eip-4844). You can verify this by checking that recent block headers contain: * `ExcessBlobGas` field * `BlobGasUsed` field 3. ArbOS version * If your version is below 20, upgrade by following the [ArbOS upgrade guide](/launch-arbitrum-chain/operate/arbos-upgrade.md). #### Method 1: Smart contract call Call the `arbOSVersion()` function on the [`ArbSys` precompile](/arbitrum-essentials/precompiles/reference.md#arbsys) contract: * **Contract address**: `0x0000000000000000000000000000000000000064` * **Function**: `arbOSVersion()` returns `uint256` * You can call this using any Ethereum client or block explorer on your Arbitrum chain #### Method 2: Using `cast` (if you have Foundry installed) ```shell cast call 0x0000000000000000000000000000000000000064 "arbOSVersion()" --rpc-url YOUR_ARBITRUM_RPC_URL ``` If your version is below ArbOS 20, upgrade by following the [ArbOS upgrade guide](/launch-arbitrum-chain/operate/arbos-upgrade.md). ##### Configuration 1. To enable blob transaction posting, add the following configuration to your node: ```json { "node": { "batch-poster": { "post-4844-blobs": true } }, "parent-chain": { "blob-client": { "beacon-url": "YOUR_BEACON_URL" } } } ``` 2. After updating your configuration: * Save the configuration file. * Restart your Arbitrum node. * Monitor the logs to confirm blob posting is active. ##### Verification 1. Once restarted, you can verify that blob transactions are being posted successfully by monitoring your node logs. ##### Log message to look for 1. When a blob transaction is successfully posted, you'll see a log entry similar to: ```shell INFO [05-23|00:49:16.160] BatchPoster: batch sent sequenceNumber=6 from=24 to=28 prevDelayed=13 currentDelayed=14 totalSegments=9 numBlobs=1 ``` 2. **Key indicator**: The `numBlobs` field shows the number of blobs included in the transaction. * `numBlobs=0`: Traditional calldata transaction was posted * `numbBlobs>0`: Blob transaction was successfully posted (in the example above, a single blob was sent) ## Troubleshooting #### Why is my node still posting calldata instead of blobs? 1. Your node may continue using calldata in these scenarios: * **Cost optimization**: When blob gas prices are high, calldata posting may be more economical; you can set `--node.batch-poster.ignore-blob-price` flag to `true` to force the batch poster to use blobs. * **Batch type switching protection**: After a non-blob transaction is posted, the following 16 transactions will also use calldata to prevent frequent switching. 2. Check your node logs for blob-related error messages and verify that your parent chain is accessible and fully synced. ## Optional parameters 1. You can also set the following optional parameters to control blob posting behavior: | Flag | Description | | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--node.batch-poster.ignore-blob-price` | Boolean. Default: `false`. If the parent chain supports `EIP-4844` blobs and `ignore-blob-price` is set to `true`, the batch poster will use `EIP-4844` blobs even if using calldata is cheaper. Can be `true` or `false`. | | `--parent-chain.blob-client.authorization` | String. Default: `""`. Value to send with the HTTP Authorization: header for Beacon REST requests, must include both scheme and scheme parameters. | | `--parent-chain.blob-client.secondary-beacon-url` | String. Default: `""`. A secondary Beacon REST endpoint URL to use as a fallback. | | `--node.batch-poster.data-poster.blob-tx-replacement-times` | durationSlice. Default: `[5m0s, 10m0s, 30m0s,1h0m0s,4h0m0s,8h0m0s,16h0m0s,22h0m0s]`. Comma-separated list of durations since first posting a blob transaction to attempt a replace-by-fee. | | `--node.batch-poster.data-poster.max-blob-tx-tip-cap-gwei` | Float. Default: `1`. The maximum tip cap to post `EIP-4844` blob-carrying transactions at. | | `--node.batch-poster.data-poster.min-blob-tx-tip-cap-gwei` | Float. Default: `1`. The minimum tip cap to post `EIP-4844` blob-carrying transactions at. | ## Batch poster revenue config To change revenue configurations for a batch poster, first check the list of current registered batch posters through the [`ArbAggregator` precompile](/arbitrum-essentials/precompiles/reference.md#arbaggregator) by calling `getBatchPosters()(address[])`: ```shell cast call --rpc-url $ARB_CHAIN_RPC 0x000000000000000000000000000000000000006D "getBatchPosters() (address[])" ``` While there are other ways to get the list of batch posters for an Arbitrum chain, this method only lists batch posters who are registered by `ArbAggregator.addBatchPoster()` or have posted at least a single batch, which is better for revenue reasons. Once you have the batch poster address, you can obtain the fee collector address for that batch poster using the `getFeeCollector(address)(address)` from the [`ArbAggregator` precompile](/arbitrum-essentials/precompiles/reference.md#arbaggregator). ```shell cast call --rpc-url $ORBIT_CHAIN_RPC 0x000000000000000000000000000000000000006D "getFeeCollector(address) (address)" $BATCH_POSTER_ADDRESS ``` You can also use the Arbitrum Chain SDK: ```typescript const orbitChainClient = createPublicClient({ chain: , transport: http(), }).extend(arbAggregatorActions); const networkFeeAccount = await orbitChainClient.arbAggregatorReadContract({ functionName: 'getFeeCollector', args: [], }); ``` > **NOTE** > > Before setting a fee collector for a batch poster, ensure the batch poster is registered in the `BatchPostersTable`. This can be achieved by: > > * Manually calling `ArbAggregator.addBatchPoster()` for the address, or > * The address has been successfully posted for at least one batch > > To set a new fee collector for a specific batch poster, use the method `setFeeCollector(address, address)` of the [`ArbAggregator` precompile](/arbitrum-essentials/precompiles/reference.md#arbaggregator): > > ```shell > cast send --rpc-url $ORBIT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x000000000000000000000000000000000000006D "setFeeCollector(address,address) ()" $BATCH_POSTER_ADDRESS $NEW_FEECOLLECTOR_ADDRESS > ``` > > You can also do this with the [Arbitrum Chain SDK](https://github.com/OffchainLabs/arbitrum-chain-sdk): > > ```typescript > const owner = privateKeyToAccount(); > > const orbitChainClient = createPublicClient({ > > chain: , > > transport: http(), > > }).extend(arbAggregatorActions); > > > > const transactionRequest = await orbitChainClient.arbAggregatorPrepareTransactionRequest({ > > functionName: 'setFeeCollector', > > args: [, ], > > upgradeExecutor: false, > > account: owner.address, > > }); > > > > await orbitChainClient.sendRawTransaction({ > > serializedTransaction: await owner.signTransaction(transactionRequest), > > }); > ``` To add a new batch poster, call the `setIsBatchPoster(address,bool)` method of the [`SequencerInbox` contract](https://github.com/OffchainLabs/nitro/blob/6aa06038ef34f9838be5952f4a66fa99807a2e6e/arbnode/sequencer_inbox.go#L59) on the parent chain: ```shell cast send --rpc-url $PARENT_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY $SEQUENCER_INBOX_ADDRESS "setIsBatchPoster(address,bool) ()" $NEW_BATCH_POSTER_ADDRESS true ``` ### Setting revenue values > **NOTE** > > These values are only editable by the chain owner(s). There are onchain values in ArbOS that set how much ArbOS changes per L1 gas spent on transaction data. This can be set by communicating with the [`ArbOwner` precompile](/arbitrum-essentials/precompiles/reference.md#arbowner). ```shell cast send --rpc-url $ARB_CHAIN_RPC --private-key $OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setL1PricingRewardRate(uint64) ()" NEW_L1_PRICING_REWARD ``` Along with `setPerBatchGasCharge()`, which sets the base charge (in L1 gas) attributed to each data batch in the calldata pricer. This can be called with: ```shell cast send --rpc-url ARB_CHAIN_RPC --private-key OWNER_PRIVATE_KEY 0x0000000000000000000000000000000000000070 "setPerBatchGasCharge(int64) ()" NEW_BATCH_GAS_CHARGE ``` ### Batch poster interval config The batch poster has a max delay, primarily set by `--node.batch-poster.max-delay` parameter in the [Nitro node configuration](https://github.com/OffchainLabs/nitro/blob/master/arbnode/batch_poster.go) (set via the JSON config file or command-line flags when deploying an Arbitrum chain). It defines the maximum amount of time the batch poster will wait after receiving a transaction before posting a batch that includes it. The default value is one hour (3600 seconds): * **Configuration options**: In the `node.batch-poster` section of the config, e.g., `"max-delay": "30m"` for a 30-minute maximum wait. Lower values increase batch posting frequency, but at the cost of potentially smaller, less efficient batches during periods of low activity, which increases gas costs on the parent chain. If there are no transactions in a batch, then this setting does not apply. * **Prevention of issues**: A shorter max delay reduces the opportunity for transaction reordering in the sequencer by requiring shorter waits. It also limits exposure to chain reorgs, since batches post sooner, which anchors them to the parent chain before potential fluctuations can invalidate sequencing. Extremely low posting times aren't recommended, as spamming the parent chain with batches increases costs without providing any benefit. * **Recommended settings**: For high-throughput chains, set to 5-15 minutes to balance latency and efficiency. For low-activity chains, keep the value of one hour. The batch poster also has a `--node.batch-poster.max-calldata-batch-size` parameter (which replaces the deprecated `--node.batch-poster.max-size`) represented in bytes. It is the maximum size a calldata batch can be. If the total queued transactions compression estimate exceeds the max size, the batch poster will post the max size of transactions to the L1. The default value is 100000 bytes. On AnyTrust chains, the maximum size of batches sent to the DAC is controlled separately by `--node.da.anytrust.max-batch-size` (default 1,000,000 bytes). Lower values will result in increased frequency of batch posting during high activity. And because Brotli compression is lossy, smaller batch files almost always result in suboptimal compression compared to larger files. which means the gas price will be higher overall for two smaller batches than for one larger batch, assuming the two smaller batches contain the same transactions as the one larger batch. Calldata and blob posting have an upper limit, so raising this value too high can cause issues, while lowering it can lead to inefficient compression and batch spamming on high-activity chains. The recommended value is 100,000 bytes. --- > For a complete page index, fetch # How to set up a high-availability sequencer > **NOTE** > > This documentation is for production sequencer deployments. If you want to set up a sequencer for testing or on a testnet, please refer to [How to run a testnet sequencer node](/run-arbitrum-node/sequencer/run-sequencer-node.md). ## Introduction The [sequencer](/how-arbitrum-works/deep-dives/sequencer.md) is a critical component of your Arbitrum chain and is responsible for queuing transactions submitted to the network. It serves as the transaction ordering engine, accepting transactions forwarded from full nodes, queuing them, and returning feed messages to those full nodes. It subsequently sends queued transactions to batch posters for data posting. If your sequencer goes offline, the network cannot process new transactions arriving at the child chain's RPC nodes, impacting the user experience. This guide provides detailed instructions for setting up a high availability (HA) sequencer architecture to minimize downtime and ensure your Arbitrum chain remains operational even if individual components fail. ## Prerequisites Before you begin, ensure you have: * Experience with Kubernetes and container orchestration * Access to a Kubernetes cluster with multiple availability zones * Understanding of Redis and cloud infrastructure * A properly configured parent chain node or RPC node endpoint * Sequencer keys and permissions * Sufficient storage and compute resources ## High-availability sequencer architecture A high-availability sequencer deployment consists of seven key components: 1. **CDN/Load Balancer**: Manages traffic routing and bot detection 2. **Nitro Fullnodes**: Process read requests and forward write transactions 3. **External Relays**: Handle public feed traffic 4. **Sequencer Relays**: Combine feeds from all sequencers 5. **Sequencers**: Multiple redundant transaction queueing machines 6. **Redis**: Coordinates active sequencer selection and sequencer-related information sharing 7. **Batch Poster**: Posts transaction batches to the parent chain ### Architecture diagrams The architecture varies slightly depending on whether your full nodes use Redis to identify the active sequencer or forward transactions to a predefined endpoint. #### Send to active enabled In this configuration, full nodes query Redis to determine the active sequencer and forward transactions directly to it: ![send to active enable](/img/ha-sequencer-sendto-active.png) #### Send to active disabled In this configuration, full nodes forward transactions to a predefined endpoint without checking which sequencer is active: ![send to active disable](/img/ha-sequencer-send-to-nonactive.png) ## Using helm charts for deployment We recommend using the [Offchain Labs community Helm charts](https://github.com/OffchainLabs/community-helm-charts) to deploy your high-availability sequencer setup. These charts provide pre-configured templates for all the necessary components and make it easier to maintain your deployment. Base configuration values are provided in the examples below. However, you should adjust them to fit your needs and use values files for production deployments. ## Detailed component setup ### 1. Load balancing (CDN) We strongly recommend using a CDN for managing traffic and security concerns: * **Recommendation**: Use Cloudflare or a similar CDN service * **Configuration**: * Direct RPC traffic to the Nitro full node fleet * Direct feed traffic to public-facing relays * Implement rate limiting and bot detection as needed * **Benefits**: Distributes load, improves security, and enhances availability ### 2. Relays setup The requirement is for two types of relays in the architecture: #### Sequencer relays * Deployment should have multiple replicas * Configure to listen to feed outputs from all sequencers * All other components connect to these relays instead of directly to the sequencers * Minimize direct load on sequencer nodes Deploy sequencer relays using the relay helm chart: ```shell helm install sequencer-relay offchainlabs/relay \ --set replicaCount=2 \ --set configmap.data.chain.id= \ --set configmap.data.node.feed.input.url=ws://sequencer-nitro-0.sequencer-nitro:9642,ws://sequencer-nitro-1.sequencer-nitro:9642,ws://sequencer-nitro-2.sequencer-nitro:9642 ``` Key configuration parameters: * `replicaCount`: Number of relay replicas to deploy (recommend at least two for high availability) * `configmap.data.chain.id`: Your chain ID * `configmap.data.node.feed.input.url`: Comma-separated list of WebSocket URLs for all sequencer feed outputs. This relies on the `perReplicaHeadlessService.enabled=true` parameter in the sequencer deployment to create individual services for each sequencer replica. #### External relays * Connect to Sequencer Relays (not directly to sequencers) * Handle all public feed requests * Provide an additional layer of isolation for production sequencers * Since these are public facing, ensure they scale appropriately based on your traffic needs: ```shell helm install external-relay offchainlabs/relay \ --set replicaCount=2 \ --set configmap.data.chain.id= \ --set configmap.data.node.feed.input.url=ws://sequencer-relay:9642 ``` You can check [run a feed relay](/run-arbitrum-node/run-feed-relay.md) to see how to set up a relay node. ### 3. Nitro full node setup ```console helm install fullnode offchainlabs/nitro \ --set replicaCount=2 \ --set configmap.data.parent-chain.id= \ --set configmap.data.parent-chain.connection.url= \ --set configmap.data.chain.id= \ --set configmap.data.execution.forwarding-target=http://sequencer-nitro:8547 ``` #### Send to active configuration (optional) To enable Redis-based active sequencer discovery: * Monitor Redis to identify the active Sequencer * Enable with: `-execution.forwarder.redis-url=redis://:6379` * Ensure connectivity to individual sequencer services and Redis * Test failover scenarios before production deployment ```shell helm install fullnode offchainlabs/nitro \ --set replicaCount=2 \ --set configmap.data.parent-chain.id= \ --set configmap.data.parent-chain.connection.url= \ --set configmap.data.chain.id= \ --set configmap.data.execution.forwarder.redis-url=redis://:6379 ``` #### Mutating-only endpoint (optional) For high availability, it is recommended to route mutating transactions to a fleet of `precheckers`, which will forward them to the sequencer. This configuration can be done by setting up a separate endpoint for mutating transactions and only allowing calls such as `eth_sendRawTransaction` to be routed to the `precheckers`, which then route to the sequencer. This insulates the sequencer from unnecessary load and enables it to focus on transaction ordering. However, , this configuration requires custom load-balancing logic and is out of the scope of this guide. ### 4. Redis setup Set up a highly available Redis cluster for sequencer coordination: #### Deployment options * Use a managed service like AWS ElastiCache (recommended) * Deploy within Kubernetes using a StatefulSet with PersistentVolumeClaims #### Requirements * Minimum of three replicas across different availability zones (recommended) * Secured access (only accessible within the Kubernetes cluster) * Backups enabled * **Configuration**: * Use a Redis cluster or Redis Sentinel for high-availability * Secure the endpoint with proper network policies * Monitor Redis health as part of your overall monitoring strategy ### 5. Sequencer setup Deploy multiple sequencer replicas with availability zone spread using the nitro helm chart (availability zone spread is not demonstrated in the example below): ```shell helm install sequencer offchainlabs/nitro \ --set replicaCount=3 \ --set configmap.data.parent-chain.id= \ --set configmap.data.parent-chain.connection.url= \ --set configmap.data.chain.id= \ --set configmap.data.node.sequencer.enable=true \ --set configmap.data.node.delayed-sequencer.enable=true \ --set configmap.data.node.seq-coordinator.enable=true \ --set configmap.data.node.seq-coordinator.redis-url= \ --set configmap.data.node.feed.output.enable=true \ --set configmap.data.node.feed.output.port=9642 \ --set configmap.data.execution.sequencer.enable=true \ --set perReplicaHeadlessService.enabled=true ``` #### Critical sequencer coordinator parameters The sequencer coordinator is the key component for high availability. These parameters are essential: | Parameter | Description | Recommended Value | | -------------------------------- | ---------------------------- | -------------------- | | `node.seq-coordinator.enable` | Enable sequencer coordinator | `true` | | `node.seq-coordinator.redis-url` | Redis URL for coordination | Your Redis URL | | `node.seq-coordinator.my-url` | URL for this sequencer | Unique per sequencer | #### Setting the sequencer's self URL A critical configuration for the sequencer coordinator is setting a unique URL for each sequencer instance. This configuration can be adjusted using Kubernetes environment variables: ```yaml extraEnv: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: NITRO_NODE_SEQ__COORDINATOR_MY__URL value: 'http://$(POD_NAME)..svc.cluster.local:8547/rpc' ``` This configuration: 1. Gets the pod name from Kubernetes metadata 2. Uses it to create a unique URL for each sequencer instance 3. Sets this URL as the `node.seq-coordinator.my-url` parameter Adjust the domain name (`.svc.cluster.local`) to match your Kubernetes cluster's DNS configuration. This configuration requires Nitro to be configured to read environment variables beginning with `NITRO_`. #### Individual sequencer services You can enable the automatic creation of headless services for each sequencer replica by setting the `perReplicaHeadlessService.enabled=true` parameter in the Helm chart (as shown in the installation command above). This configuration will create individual services named `-nitro-` that allow direct access to each sequencer replica. These individual services are critical for a proper high-availability setup because they allow components like sequencer relays to connect directly to specific sequencer instances. This direct connection is essential for: * **Proper failover**: When the active sequencer changes, other components can address the new active sequencer directly * **Feed aggregation**: Sequencer relays need to collect feeds from all sequencer instances to ensure no messages get lost during transitions ### 6. Sequencer coordinator manager To manage active sequencer selection, use the built-in sequencer coordinator UI: * Follow detailed instructions at: [Running a Sequencer Coordinator Manager](https://docs.arbitrum.io/node-running/how-tos/running-a-sequencer-coordinator-manager) * Use this interface to switch between sequencer replicas when needed manually * Configure permissions and access controls appropriately ### 7. `Batchposter` setup Deploy the batch poster using the Nitro helm chart: ```shell helm install batchposter offchainlabs/nitro \ --set configmap.data.parent-chain.id= \ --set configmap.data.parent-chain.connection.url= \ --set configmap.data.chain.id= \ --set configmap.data.execution.forwarding-target=null \ --set configmap.data.node.seq-coordinator.enable=true \ --set configmap.data.node.seq-coordinator.redis-url= \ --set configmap.data.node.batch-poster.enable=true \ --set "configmap.data.node.batch-poster.parent-chain-wallet.private-key=" ``` > **WARNING** — Important > > Do not add the batch poster to the sequencer priority list in the Sequencer Coordinator Manager (SQM) to prevent it from becoming the active sequencer unintentionally. ## Monitoring and maintenance ### Health checks Implement comprehensive health checks for all components: * **Sequencer health**: Monitor the sequencer's logs and metrics * **Redis connectivity**: Ensure all components can access Redis * **Feed availability**: Verify feed connectivity between components * **Transaction processing**: Monitor end-to-end transaction flow ### Troubleshooting If you run into any issues, visit the [node-running troubleshooting guide](/run-arbitrum-node/troubleshooting.md). ## References * [How to Run a Fullnode](https://docs.arbitrum.io/node-running/how-tos/running-a-node) * [Running a Sequencer Coordinator Manager](https://docs.arbitrum.io/node-running/how-tos/running-a-sequencer-coordinator-manager) * [Kubernetes Documentation](https://kubernetes.io/docs/) * [Offchain Labs Community Helm Charts](https://github.com/OffchainLabs/community-helm-charts) --- > For a complete page index, fetch # How to run a full node with Helm on Kubernetes This guide shows how to deploy a full node for an Arbitrum chain on Kubernetes using the [community Helm chart](https://github.com/OffchainLabs/community-helm-charts/tree/main/charts/nitro). It applies to any Arbitrum chain—[Arbitrum One](https://arbitrum.io), Nova, Sepolia, and your own Arbitrum chain—and covers installation, memory configuration, first sync from a snapshot, monitoring, the log signals that distinguish a healthy node from a broken one, and the outbound endpoints to allow through a firewall. The chart's defaults target Arbitrum One, so Arbitrum One is used as the worked example throughout. Each step notes what changes for other chains. Info If you want to run a node with Docker instead of Kubernetes, see [How to run a full node for an Arbitrum chain](/run-arbitrum-node/run-full-node.md). This page assumes you've reviewed the [Start here](/run-arbitrum-node/start-here.md) page, which explains the RPC endpoints, Nitro version, and database snapshots required to run a node. ## Prerequisites * **Parent chain access:** an execution RPC endpoint for the chain's parent. For Arbitrum One, Nova, and Sepolia, the parent is Ethereum, so you also need an **L1 beacon/blob** endpoint (see [L1 Ethereum RPC providers](/run-arbitrum-node/l1-ethereum-beacon-chain-rpc-providers.md)). For an Arbitrum chain whose parent is another chain, use the RPC of that parent chain; a beacon endpoint is required only when the chain posts blobs to its parent chain. * **Cluster:** Kubernetes with Helm installed. * **Disk:** size storage for the chain you're running. A node's database size varies widely from chain to chain and grows over time, so check the expected size with your chain operator before provisioning (for Arbitrum One, it runs to multiple TB). Raise the chart's default `persistence.size` (`500Gi`) accordingly, and allow roughly 2× the snapshot size for an additional temporary disk for extraction on the first run. ## Step 1: Deploy with Helm First, add the chart repo: ```shell helm repo add offchainlabs https://charts.arbitrum.io helm repo update ``` Then install, selecting the configuration for your chain:
Arbitrum One, Nova, Sepolia The chart defaults to Arbitrum One (`chain.id` `42161`, `parent-chain.id` `1`), so an Arbitrum One node needs only three values: ```shell # Arbitrum One helm install arb1-fullnode offchainlabs/nitro \ --set configmap.data.parent-chain.connection.url= \ --set configmap.data.parent-chain.blob-client.beacon-url= \ --set configmap.data.init.latest=pruned # pruned snapshot, recommended for Arbitrum One ``` For Nova or Sepolia, override the chain and parent-chain IDs. For example, Arbitrum Sepolia: ```shell # Arbitrum Sepolia (parent chain is Ethereum Sepolia, 11155111) helm install arbsepolia-fullnode offchainlabs/nitro \ --set configmap.data.parent-chain.id=11155111 \ --set configmap.data.parent-chain.connection.url= \ --set configmap.data.parent-chain.blob-client.beacon-url= \ --set configmap.data.chain.id=421614 ```
Your Arbitrum chain A node for your own Arbitrum chain needs its chain info, sequencer endpoint, and feed URL. Because `chain.info-json` is a large JSON string, supply these through a values file rather than `--set`: ```yaml # values-mychain.yaml configmap: data: parent-chain: id: connection: url: chain: id: name: info-json: '' execution: forwarding-target: node: feed: input: url: ``` ```shell helm install mychain-fullnode offchainlabs/nitro -f values-mychain.yaml ``` If the parent chain is Ethereum, also set `configmap.data.parent-chain.blob-client.beacon-url`. AnyTrust chains additionally need a Data Availability configuration (`node.da.anytrust.*`, including a `rest-aggregator`); see [Data Availability](/run-arbitrum-node/data-availability.md). The older `node.data-availability.*` flags still work but are deprecated in Nitro and will be removed in a future release.
Defaults that commonly trip up operators * **Don't copy the chart README's init example verbatim.** It uses `nitro-genesis.tar`, which syncs from genesis — a huge, slow process on a chain with a long history, such as Arbitrum One. Where a pruned snapshot exists, prefer `configmap.data.init.latest=pruned` (default base `https://snapshot.arbitrum.foundation/`). * **The chart changes the RPC path prefix.** Defaults are `http.rpcprefix=/rpc` and `ws.rpcprefix=/ws`, so RPC is served at `http://host:8547/rpc`, not vanilla Nitro's `/`. Clients that omit `/rpc` get a 404. * **Metrics are off by default.** Enable `configmap.data.metrics=true` and `serviceMonitor.enabled=true` to scrape them (see [Step 4](#step-4-enable-monitoring)). ## Step 2: Configure memory management (optional) For a containerized node, set a memory limit so the chart can size the Go runtime. This is optional but recommended, and applies to every chain: * **`resources.limits.memory`** set this to activate the chart's automatic `GOMEMLIMIT`. When a memory limit is present, the chart derives `GOMEMLIMIT` from it (`env.nitro.goMemLimit`, on by default—it subtracts an estimate of non-Go memory and applies a `0.9` multiplier). * **`MALLOC_ARENA_MAX=2`** already set by the chart by default (`env.nitro.mallocArenaMax`, `enabled: true`, `value: 2`), so you don't normally need to configure it. Adjust or disable it under `env.nitro.mallocArenaMax` if needed. * **`node.resource-mgmt.mem-free-limit`** (optional) an RPC memory throttle that's disabled by default. Recommended if you expose this node as a public RPC. To set any other environment variable, use `extraEnv`, whose entries are spliced directly into the pod's `env`: ```yaml extraEnv: - name: SOME_VAR value: 'value' ``` For the allocator details, the `GOMEMLIMIT` formula, and `MALLOC_ARENA_MAX`, see the [memory management deep-dive](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md) and the [memory management section](/run-arbitrum-node/run-full-node.md#memory-management) of the Docker full-node guide. ## Step 3: First sync from a snapshot Whether a snapshot is required depends on the chain: * **Arbitrum One** requires a snapshot during the first run due to its Classic-era history. Use `configmap.data.init.latest=pruned`. * **Nova and Sepolia** have published snapshots that speed up the initial sync but aren't strictly required. * **Your Arbitrum chain** typically syncs from genesis with no snapshot, unless you provide one via `init.url`. When using a snapshot, the default base is `https://snapshot.arbitrum.foundation/`. Initial sync takes a while. The init flag is ignored once a database already exists, so it's safe to leave it in place across restarts. Confirm the current snapshot type and download details in the [Nitro database snapshots](/run-arbitrum-node/nitro/nitro-database-snapshots.md) guide. ## Step 4: Enable monitoring Turn on metrics and a `ServiceMonitor` so Prometheus can scrape the node: ```shell helm upgrade offchainlabs/nitro \ --reuse-values \ --set configmap.data.metrics=true \ --set serviceMonitor.enabled=true ``` Once enabled, the node exposes Prometheus metrics at `http://:6070/debug/metrics/prometheus`—the metrics server binds to `0.0.0.0:6070` by default (`configmap.data.metrics-server.port`), and the `ServiceMonitor` scrapes that port and path (`serviceMonitor.path` defaults to `/debug/metrics/prometheus`). ### Grafana dashboard The repository’s README points to the Grafana dashboard's [Releases page](https://github.com/OffchainLabs/community-helm-charts/releases). If a release doesn't attach the dashboard JSON, pull the last published copy from the repository's git history and import it: ```shell git clone https://github.com/OffchainLabs/community-helm-charts git -C community-helm-charts show 430a2cc~1:operations/grafana/dashboards/overview.json > overview.json ``` Then in Grafana, go to **Dashboards → Import** and paste `overview.json`. This exported dashboard has no `__inputs` datasource prompt, and its `Source` variable is hard-pinned to an internal Mimir UID. After importing, you **must** repoint the `Source` variable to your own Prometheus instance, or every panel will read "No data." Then set `nitronodejob` to this node's scrape job, and leave `sequencerjob`, `validatorjob`, and `relayjob` empty so only the full-node rows render. ### Probes The chart wires a built-in **startup probe** by default, but **liveness and readiness probes are off** unless you set `livenessProbe` and `readinessProbe`. The startup probe's long failure window protects the initial sync (so a slow sync won't trigger a restart), but it doesn't catch a hang after the node is up. **Configure a liveness probe yourself** for ongoing hang detection. ## Log signals to watch The chart sets `log-type=json`. The following INFO/WARN/ERROR strings distinguish a healthy node from a broken one on any Arbitrum chain (the sequencer hostnames in the examples below are Arbitrum One's; your chain's will differ): | Event | Healthy signal | Broken / warning signal | | ---------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **New block created** | `INFO created block` with `l2Block` / `l2BlockHash`, advancing continuously while producing | No advancing `created block` line → block production stalled | | **User tx forwarded to sequencer** | Success is not logged | `WARN error forwarding transaction trying different target`; `ERROR Failed to publish transaction to any of the forwarding targets` | | **Sequencer feed message** | `INFO Feed connected`; `DEBUG received batch item` | `WARN failed connect to sequencer broadcast, waiting and retrying`; `ERROR Server connection timed out without receiving data` | | **Inbox messages (parent chain)** | `INFO InboxTracker` with `sequencerBatchCount` / `messageCount` / `l1Block` advancing | `WARN error reading inbox`. **Note**: `backwards reorg of delayed messages` is logged at INFO level—it's normal on parent-chain reorgs, not an alert. | ### Why a full node can't forward transactions to the sequencer A full node forwards user transactions to the sequencer. When forwarding fails, the cause falls into one of three buckets. Forwarding failures are **log-based, not metric-based**—the forwarder emits no metrics—so alert by grepping the log strings below. | Cause | Signal | | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | **All forwarding targets unreachable** (egress blocked, or sequencer + all fallbacks down) | `ERROR Failed to publish transaction to any of the forwarding targets`, preceded by per-target `WARN` lines | | **Sequencer returns a business error** (non-connection, e.g. `nonce too low`) | `WARN error forwarding transaction trying different target` with the sequencer's `err`—not escalated; the error is returned to the caller | | **Memory 429 throttling** (rejected before forwarding) | The client receives `HTTP 429 Too many requests` and the metric `arb/rpc/limitcheck/failure` increments. There is **no per-request log line**. | Representative log lines (hostnames shown are Arbitrum One's): ```text # (a) ALL TARGETS UNREACHABLE — egress blocked, or sequencer + all fallbacks down WARN error forwarding transaction trying different target current target=https://arb1-sequencer.arbitrum.io/rpc err="dial tcp: lookup ...: no such host" WARN error forwarding transaction to a backup target target=https://arb1-sequencer-fallback-1.arbitrum.io/rpc pos=1 total targets=6 err="timeout exceeded" ERROR Failed to publish transaction to any of the forwarding targets numTargets=6 # (b) SEQUENCER RETURNS A BUSINESS ERROR (non-connection) — logged once, NOT escalated, returned to caller WARN error forwarding transaction trying different target current target=https://arb1-sequencer.arbitrum.io/rpc err="nonce too low: address 0x..., tx: 5 state: 7" # (no "Failed to publish..." line follows — the error is handed straight back to the RPC client) # (c) MEMORY 429 THROTTLING — rejected BEFORE it reaches the forwarder; no per-request log line. # The signals are an HTTP 429 to the client and an increment of arb/rpc/limitcheck/failure. INFO Cgroups v2 detected, enabling memory limit RPC throttling # startup, confirms throttling is active ERROR Error checking memory limit err=... checker=CgroupsMemoryLimitChecker # only if the cgroup read fails ``` ## Network egress allowlist A full node reaches out to a fixed set of endpoints. If you run on a cloud provider, outbound traffic is often restricted by default, so you'll likely need to allow these destinations explicitly in your cloud security group or firewall rules (for example, AWS security groups, GCP firewall rules, or an egress `NetworkPolicy`). The specific hostnames depend on the chain: for DAO-governed networks, they come from the chain's built-in chain info, and for your own Arbitrum chain, they come from the `chain.info-json`, `execution.forwarding-target`, and feed URLs you configure. The categories are the same across chains: | Purpose | Where the endpoint comes from | When | | ----------------------------- | ------------------------------------------------------------------- | ------------------------------------------- | | **Sequencer** (tx forwarding) | chain info `sequencer-url` / `execution.forwarding-target` | Always—the node forwards user txs here | | Sequencer fallbacks | chain info `secondary-forwarding-target` | Failover | | **Sequencer feed** | chain info `feed-url` / `node.feed.input.url` | Always | | Feed fallbacks / delayed | chain info `secondary-feed-url` / `node.feed.input.secondary-url` | Failover | | Block metadata | chain info `block-metadata-url` | Only if tracking block metadata / Timeboost | | DB snapshot | the `init` source host | Initial sync only | | **Parent chain** | your parent-chain RPC + beacon (beacon only for an Ethereum parent) | Always | | DA / REST endpoints | `rest-aggregator` URL list | AnyTrust chains only (e.g. Nova) | For **Arbitrum One**, those resolve to: | Purpose | Endpoint(s) | | ------------------------- | ---------------------------------------------------------------------------------------------- | | Sequencer (tx forwarding) | `https://arb1-sequencer.arbitrum.io/rpc` | | Sequencer fallbacks | `https://arb1-sequencer-fallback-{1..5}.arbitrum.io/rpc` | | Sequencer feed (primary) | `wss://arb1-feed.arbitrum.io/feed` | | Feed fallbacks / delayed | `wss://arb1-delayed-feed.arbitrum.io/feed`, `wss://arb1-feed-fallback-{1..5}.arbitrum.io/feed` | | Block metadata | `https://arb1.arbitrum.io/rpc` | | DB snapshot | `https://snapshot.arbitrum.foundation/` | | Parent chain | Your Ethereum L1 execution RPC + L1 beacon/blob endpoint | Firewall egress For a default Arbitrum One full node, allow `*.arbitrum.io` (443 HTTPS + WSS), `snapshot.arbitrum.foundation` (443, init only), and your own L1 RPC + beacon hosts. For other chains, allow that chain's sequencer, feed, and snapshot hosts, plus your parent-chain endpoints. **Additional outbound paths exist only if you enable them:** the version alerter (off by default, queries an operator-set endpoint), the classic redirect, or DA/REST endpoints (AnyTrust chains such as Nova). --- > For a complete page index, fetch # Run a split validator node ## Running split validators for Arbitrum chains Split validators separate the validation work to a stateless validation node, which provides several key benefits: * **Resource management**: Easier to scale and manage compute resources independently * **Fault isolation**: Prevents database corruption if the validation node crashes (e.g., due to OOM errors) * **Flexibility**: Allows running multiple validation nodes for horizontal scalability This guide explains how to set up a split validator configuration for Arbitrum chains by running a Nitro node (bonder) and a validation node separately. Before you read this doc, please ensure you have already walked through [run a validator](/run-arbitrum-node/more-types/run-validator-node.md) docs to understand the basics of a validator node. For an overview of how split validation fits among the other Nitro node roles, see [How to assign roles to a Nitro node](/run-arbitrum-node/assign-node-roles.md). ### What is AUTH-RPC and the validation API Nitro nodes can expose two separate WebSocket RPC interfaces: * **The public RPC** (configured under `--http.*` and `--ws.*`) serves the standard `eth_`, `net_`, and other namespaces that wallets and dApps use. * **AUTH-RPC** (configured under `--auth.*`) is a second, JWT-authenticated WebSocket interface intended for trusted intra-component communication. It listens on its own address and port and by default exposes only the `validation` namespace. The `validation_*` namespace is how the bonder delegates WASM execution to the validation node—it carries `Validate` (run the WASM machine on a prepared input and return the resulting global state) plus `CreateExecutionRun`, `GetStepAt`, and `GetProofAt` (used during challenges to fetch step hashes and one-step proofs). The full method list is in the [`validation_*` API reference](#reference-the-validation-api) below. The rest of this guide walks through the deployment; the API reference and the option to expose additional namespaces over AUTH-RPC are at the end. ### Prerequisites * Docker or Kubernetes with Helm installed * Bonder private key * Chain information JSON for your Arbitrum chain ### Docker deployment guide #### Step 1: Set up the validation node First, generate a JWT secret for secure communication: ```bash xxd -l 32 -ps -c 40 /dev/urandom > /tmp/nitro-val.jwt ``` Start the validation node with the JWT secret: ```bash docker run --rm -it \ --entrypoint nitro-val \ -p 0.0.0.0:5200:5200 \ offchainlabs/nitro-node:v3.11.3-beb2108 \ --auth.addr 127.0.0.1 \ --auth.origins 0.0.0.0 \ --auth.jwtsecret /tmp/nitro-val.jwt \ --auth.port 5200 --metrics \ --metrics-server.addr=0.0.0.0 \ --metrics-server.port=6070 ``` The validation node will listen on port 5200, which will also enable the metrics server on port 6070. #### Step 2: Set up the bonder node Copy the JWT secret to your mount directory: ```bash cp /tmp/nitro-val.jwt /some/local/dir/arbitrum ``` Start the bonder node with the following command, connecting it to your validation node: ```bash docker run --rm -it \ -v /some/local/dir/arbitrum:/home/user/.arbitrum \ offchainlabs/nitro-node:v3.11.3-beb2108 \ --parent-chain.connection.url= \ --node.staker.enable=true \ --node.staker.strategy=MakeNodes \ --node.staker.parent-chain-wallet.private-key= \ --chain.info-json= \ --execution.forwarding-target= \ --node.block-validator.validation-server-configs-list="[{\"jwtsecret\":\"/home/user/.arbitrum/nitro-val.jwt\",\"url\":\"ws://Your_validation_address\"}]" ``` Replace the placeholders with your specific values: * ``: Your parent chain RPC endpoint * ``: Your bonder's private key * ``: Chain information JSON * ``: Your forwarding node URL (usually is the sequencer endpoint) * `Your_validation_address`: Address of your validation node (including port) ### Kubernetes deployment with Helm Arbitrum provides a [community Helm chart](https://github.com/OffchainLabs/community-helm-charts) for Kubernetes deployment. #### Step 1: Create validation node configuration Create a file named `validation_values.yaml`: ```yaml configmap: data: parent-chain: id: 1 # Use appropriate parent chain ID connection: url: 'https://your-parent-chain-rpc' execution: forwarding-target: 'https://your-forwarding-node' node: staker: enable: true strategy: 'MakeNodes' parent-chain-wallet: private-key: 'your-staker-private-key' chain: name: 'Your Chain Name' id: 42161 # Your chain ID info-json: '[Your chain info JSON]' jwtSecret: enabled: true value: 'Your 32 bytes hex jwt' validator: enabled: true splitvalidator: deployments: - name: 'current' ``` #### Step 2: Deploy the validation node ```bash helm install nitro-validator offchainlabs/nitro --values validation_values.yaml ``` ### Monitoring and maintenance * Monitor your bonding and validation node logs regularly: ```bash # For Docker docker logs -f # For Kubernetes kubectl logs -f ``` * Check the status of both the bond and the validation node through the Arbitrum dashboard or API ### Additional configurations for validation nodes To get a full list of parameters for the validation node, you can run the following command: ```bash docker run --rm -it --entrypoint nitro-val offchainlabs/nitro-node:v3.11.3-beb2108 --help ``` ### Additional configurations for helm charts For more advanced helm chart configurations, please refer to the Arbitrum community Helm Chart [README](https://github.com/OffchainLabs/community-helm-charts/blob/main/charts/nitro/README.md) ### Monitoring To check if your validation node is running correctly, you can check the `rpc_duration_validation_validate_success_count` metric in your metrics server. This metric is a counter of the number of successful validation calls; if it is increasing, it means your validation node is running correctly. You can also check where this log (`validation node started`) appears: * If this log appears on the validation node, it means your validation node is running correctly. * If this log appears on the bond node, it means your bond node is still validating itself. It indicates that your bond node is not connecting to the validation node correctly; you need to double-check your configuration. ### Reference: the `validation_*` API When AUTH-RPC is enabled, the validation node registers the `validation` namespace, exposing the following methods (all prefixed `validation_` in RPC requests): | Method | Purpose | | ----------------------------------------- | -------------------------------------------------------------------- | | `validation_name` | Returns the validation node identifier. | | `validation_capacity` | Returns the configured concurrent-execution capacity. | | `validation_room` | Returns the number of currently available execution slots. | | `validation_validate` | Runs one validation against a given input and WASM module root. | | `validation_wasmModuleRoots` | Lists the WASM module roots this node can validate. | | `validation_stylusArchs` | Lists the Stylus target architectures the node supports. | | `validation_createExecutionRun` | Starts a long-running execution and returns an `execid` handle. | | `validation_getStepAt` | Returns the machine hash and global state at a given step. | | `validation_getMachineHashesWithStepSize` | Batch step-hash lookup, used during challenge resolution. | | `validation_getProofAt` | Returns the one-step proof at a given step. | | `validation_prepareRange` | Pre-loads execution state for a step range. | | `validation_execKeepAlive` | Heartbeats an execution run to extend its retention. | | `validation_checkAlive` | Probes that an execution run is still loaded on the validation node. | | `validation_closeExec` | Releases an execution run's resources. | These methods are internal protocol calls between the bond and the validation node. You normally don't invoke them directly—the bonder calls them automatically when it needs validation work. This reference is most useful for reading logs or debugging connectivity between the two components. ### Advanced: exposing additional namespaces over AUTH-RPC `--auth.api` accepts a list of namespaces, not just `validation`. You can add others—for example, `eth` or `net` — if you want to grant a trusted client read access to those APIs without exposing them on the public RPC: ```text --auth.api validation,eth,net ``` Anything listed here is served on the AUTH-RPC endpoint and gated by the JWT, so the caller must hold the same JWT secret to access it. The endpoint still listens only on `--auth.addr` (default `127.0.0.1`), so keep it on loopback or a private network — JWT authentication alone is not a substitute for network isolation. This is rarely needed for bonders; the main use case is internal services (a custom monitor, an indexer behind your own bastion) that need authenticated read access to the node. * If this log appears on the bonder node (validator node), it means your bonder node is still validating itself. It indicates that your bonder node is not connecting to the validation node correctly; you need to double-check your configuration. --- > For a complete page index, fetch # Arbitrum FAQ ### Why do I need ETH to use the Arbitrum network? **ETH** is the currency used to pay gas fees on Arbitrum, and it powers all transactions on Arbitrum. You can bridge **ETH** (and other tokens) from Ethereum to Arbitrum through [Arbitrum's bridge](https://bridge.arbitrum.io/). ### Do I need to pay a tip or priority fee for my Arbitrum transactions? Transaction processing occurs in the order that the Sequencer receives them; no priority fee is necessary for Arbitrum transactions. If a transaction includes a priority fee, the origin address of the transaction will be refunded at the end of execution. ### How can I see the balance of ETH and other tokens in my wallet on Arbitrum? Most wallets are "connected" to one given network at a time. To view your **ETH** or token balances, ensure that you have an established connection to the appropriate Arbitrum chain. In MetaMask and OKX Wallet, you can switch networks via the **Networks** drop-down. In this drop-down, please select the desired network (either Arbitrum One or Arbitrum Nova for our mainnet networks). If your preferred network hasn't been added to your wallet yet, you can add it via the [Arbitrum Bridge](https://bridge.arbitrum.io/). ### What happens if I send my funds to an exchange that doesn't support Arbitrum? If you send the funds and the receiving wallet/exchange doesn't support the Arbitrum network you are sending funds through, unfortunately, there is nothing we can do to recover your funds. You would need to contact the wallet/exchange support and see if they can assist you in retrieving the funds. ### Does Arbitrum have a mempool? The Arbitrum Sequencer orders transactions on a first-come, first-served basis. The Sequencer inserts transactions into a queue based on the order in which they are received and executes them accordingly, which eliminates the need for a mempool. The Sequencer's queue has no space limit; transactions on the queue will eventually timeout and get discarded if not executed within a reasonable timeframe. Under normal conditions, the queue is empty, as transaction execution occurs nearly instantaneously. ### What's the difference between Arbitrum Rollup and Arbitrum AnyTrust? Arbitrum Rollup is an Optimistic Rollup protocol; it is trustless and permissionless. Part of how these properties are achieved is by requiring all chain data to be posted on layer 1. This means the availability of this data follows directly from the security properties of Ethereum itself, and, in turn, that any party can participate in validating the chain and ensuring its safety. For more information, see [Inside Arbitrum Nitro](https://docs.arbitrum.io/inside-arbitrum-nitro/). By contrast, Arbitrum AnyTrust introduces a trust assumption in exchange for lower fees; data availability is managed by a Data Availability Committee (DAC), a fixed, permissioned set of entities. We introduce some threshold, `K`, with the assumption that at least `K` members of the committee are honest. For simplicity, we'll hereby assume a committee of size 20 and a `K` value of 2: If 19 out of the 20 committee members *and* the Sequencer are malicious and colluding together, they can break the chain's safety (and, e.g., steal users' funds); this is the new trust assumption. If anywhere between 2 and 18 of the committee members are well behaved, the AnyTrust chain operates in "Rollup mode"; i.e., data gets posted on L1. In what should be the common and happy case, however, in which at least 19 of the 20 committee members are well behaved, the system operates without posting the L2 chain's data on L1, and thus, users pay significantly lower fees. This is the core upside of AnyTrust chains over rollups. Variants of the AnyTrust model in which the new trust assumption is minimized are under consideration; stay tuned. For more, see [Inside AnyTrust](https://developer.arbitrum.io/inside-anytrust). ### How can I check the status of my cross chain message? You can check the status of \*any \*Arbitrum cross chain message at  (you will also be able to execute the cross chain message there, if applicable). You'll need the transaction hash of the "initiating transaction": the L1 transaction hash for an L1-to-L2 message (e.g., a deposit), or the L2 transaction hash for an L2-to-L1 message (e.g., a withdrawal). If you cross-chain message was initiated from , you can also check its status / execute it at that site in the transaction history tab. ### If there is a dispute, can my L2 transaction get reorged / thrown out / "yeeted"? Nope; once an Arbitrum transaction is included on L1, there is no way it can be reorged (unless the L1 itself reorgs, of course). A "dispute" involves Validators disagreeing over execution, i.e., the outputted state of a chain. The inputs, however, can't be disputed; they are determined by the Inbox on L1. (See [Transaction Lifecycle](https://developer.arbitrum.io/tx-lifecycle)) ### ...okay but if there's a dispute, will my transaction get delayed? The only thing that a dispute can add delay to is the confirmation of L2-to-L1 messages. All other transactions continue to be processed, even while a dispute is still ongoing. (Additionally, in practice, most L2-to-L1 messages represent withdrawals of fungible assets; these can be trustlessly completed *even during a dispute* via trustless fast "liquidity exit" applications. See [L2-to-L1 Messages](https://developer.arbitrum.io/arbos/l2-to-l1-messaging)). ### Are "Sequencers" the same entities as "Validators"? Can a centralized Sequencer act maliciously (e.g., steal all my money)? No and no! An Arbitrum Chain's Sequencer(s) and Validators and completely distinct entities, with their own distinct roles. The [Sequencer](https://developer.arbitrum.io/sequencer) is the entity granted specific privileges over ordering transactions; once the Sequencer commits to an ordering (by posting a batch on Ethereum), it has no say over what happens next (i.e., execution). A malicious/faulty Sequencer can do things like reordering transactions or *temporarily* delaying a transaction's inclusion — things which could be, to be sure, annoying and bad — but can do nothing to compromise the chain's safety. The *Validators* are the ones responsible for the safety of the chain; i.e., making bonded claims about the chain state, disputing each other, etc. Currently, on Arbitrum One, the Sequencer is a centralized entity maintained by Offchain Labs. Eventually, we expect the single Sequencer to be replaced by a distributed committee of Sequencers who come to consensus on transaction ordering. This upgrade will be an improvement; we don't want you to have to trust us not to reorder your transactions. However, it also isn't *strictly* necessary for Arbitrum One to achieve its most fundamental properties. In other words: ***An Arbitrum Rollup chain with a centralized Sequencer could theoretically still be trustless!*** Which is to say — the more important thing than decentralizing the Sequencer, i.e., the thing you ought to care more about — is decentralizing the *Validators*. Arbitrum One's validator set is currently allowlisted; over time, we expect [governance](https://docs.arbitrum.foundation/) to expand the allowlist and eventually be removed entirely. For more info see ["State of Progressive Decentralization"](https://docs.arbitrum.foundation/state-of-progressive-decentralization). ### Why was "one week" chosen for Arbitrum One's dispute window? The expectation is that one week should be sufficient for validators to conduct an interactive dispute, provided they do not face challenges in getting their transactions included on Layer 1. The one-week decision occurred following a consensus among the Ethereum research community and other Layer 2 projects. The intention is to allow the community sufficient time to coordinate socially in the event of a coordinated Ethereum bonder censorship attack. ### What's the state of Arbitrum One's decentralization? See [**"State of Progressive Decentralization"**](https://docs.arbitrum.foundation/state-of-progressive-decentralization), or check out the work of our friends at [**L2BEAT**](https://l2beat.com/scaling/risk/)**.** ### Are there any Fiat on-ramps that support Arbitrum? Yes, you can find a list of Fiat on-ramps that support Arbitrum [on our portal](https://portal.arbitrum.io/one?categories=fiat-on-ramp). ### How many blocks are needed for a transaction to be confirmed/finalized in Arbitrum? There are two levels of finality in a [transaction lifecycle](https://developer.arbitrum.io/tx-lifecycle): * **Soft finality**: Once the Sequencer receives and processes a transaction, it emits a receipt through the Sequencer's feed. At this point, if the Sequencer is trusted, the transaction will not undergo reordering, and after processing, the chain state can be determined. * **Hard finality**: At this stage, assuming there's at least one well-behaved active Arbitrum validator, the client can treat their transaction's finality as equivalent to that of an ordinary Ethereum transaction. ### Where can I find stats for Arbitrum? Although we currently don't maintain any stats dashboard for Arbitrum, you can find many [community created dashboards](https://dune.com/browse/dashboards?q=arbitrum) in Dune. ### Will transactions with a higher "gas price bid" be confirmed first? There is no notion of a mempool on Arbitrum; transactions are processed on a first-come, first-served basis by the Sequencer. Thus, the gas price bid parameter does not affect the order in which transactions get processed. ### Where can I find the current Data Availability Committee members? The Arbitrum Nova chain has a 7-party DAC, whose members can be seen in the [Arbitrum Foundation's state of progressive decentralization status document](https://docs.arbitrum.foundation/state-of-progressive-decentralization#data-availability-committee-members). Governance has the ability to remove or add members to the committee. ### Can I withdraw my funds from Arbitrum back to Ethereum without going through the Sequencer? What about funds that are in a contract? Yes, it is possible to send a message from Ethereum to be executed on Arbitrum permissionlessly, while bypassing the Sequencer. You can achieve this by using the `DelayedInbox` contract and forcing the inclusion of the message after a certain amount of time has passed (currently \~24 hours). For more information about this behavior, refer to [How Arbitrum Works](https://docs.arbitrum.io/sequencer#unhappyuncommon-case-sequencer-isnt-doing-its-job). Keep in mind that you can execute any message in this way, be it a withdrawal of funds back to Ethereum or a call to a contract. You can also find an example of force-inclusion in [this tutorial](https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/delayedInbox-l2msg). ### Are there any plans to reduce the time a transaction needs to wait before being able to be force-included from Ethereum into the Arbitrum chain, bypassing the sequencer? (Currently 24 hours) The mechanism that allows force-including transactions from Ethereum (bypassing the sequencer) is intended to be used in very rare cases, especially when it is expected that the sequencer will not be operational again, so that users have a way of interacting with Arbitrum in a trustless way. When using this mechanism, if the sequencer is down for longer than the time window for force-including transactions from Ethereum, the moment it is online again, it can lead to a reorganization of blocks in Arbitrum (it would have received transactions timestamped before the force-included one). 24 hours was chosen because it provides a comfortable period of time for the team running the sequencer infrastructure to fix any bugs that may cause the sequencer to not work. While there aren't any active initiatives to lower that time, the decision ultimately falls in the hands of the Arbitrum DAO, who has discussed the topic in their governance forum ([see here for more information](https://forum.arbitrum.foundation/t/proposal-decrease-censorship-delay-from-24-hours-to-4-hours/13047)). In any case, we could also analyze why would someone use this mechanism having an honest and functional sequencer. For instance, if the reason is a distrust of the sequencer, a centralised agent as of now, one potential solution could be to [decentralize the sequencer](https://medium.com/@espressosys/offchain-labs-partnership-improving-transaction-ordering-for-arbitrum-technology-chains-beyond-de2b6018acb2) instead of reducing the force-inclusion delay time. ### What is the difference between an child chain block and an assertion? A child chain block is very similar to the concept of an parent chain block. These blocks are generated by validator nodes of Arbitrum by executing the state transition function on sequenced transactions. The structure of a child chain block is similar to that of an Ethereum block, with a few differences that you can [see here](https://docs.arbitrum.io/for-devs/concepts/differences-between-arbitrum-ethereum/rpc-methods#blocks). On the other hand, an assertion is a distinctive block that is transmitted back to the parent chain to serve as a fingerprint of the most recent state of the Arbitrum chain. It comprises an assertion of the present state root of the Arbitrum chain and other essential information pertaining to withdrawals and challenges. The structure of assertions can be viewed [here](https://github.com/OffchainLabs/nitro/blob/2436da3fbf339ce72b02f761254aff5b86efafac/contracts/src/rollup/Node.sol#L7). These assertions are also generated by validators, but they are appended to the parent chain. Other validators can [challenge them](https://docs.arbitrum.io/inside-arbitrum-nitro/#resolving-disputes-using-interactive-fraud-proofs) during a specific time frame of approximately one week if they discover that the current state hash of the chain varies from the one that was initially claimed. Once the challenge period elapses, the assertion is confirmed on the parent chain. ### Why do Arbitrum chains enforce a target (for pricing)? Isn't it better that the target grows without limits? The transaction lifecycle sets a target that we have to take into account: validators have to execute each transaction, get the status of the chain, and post an assertion to Ethereum every certain amount of time. If the target of the chain increases too much, there is a risk that validators won't have enough computation power to process all transactions in a timely manner, and will fall behind on validating them, which would cause the chain to delay confirmations of its state. --- > For a complete page index, fetch # Frequently asked questions: Run a node ### How do I run a node? See instructions [in our guide about running a full node](https://developer.arbitrum.io/node-running/how-tos/running-a-full-node)! ### How to verify the integrity of the Nitro database I currently have? We use an accumulator hash for all messages, ensuring that a new message doesn't get added to the database without the preceding message being valid. To verify that everything is functioning correctly, you can check if it is [syncing](https://docs.arbitrum.io/node-running/faq#how-can-i-verify-that-my-node-is-fully-synced) and confirm that the latest block is consistent with other Arbitrum nodes. For instance, you might compare it with information on [Arbiscan](https://arbiscan.io/) (please note that the search function on Arbiscan does not support searches by block hash). ### How can I check if the node is running properly and diagnose the issue if it is not? We have trace-level logging RPC request implemented on our node. You could use it to log all requests and responses at the trace level. (The performance impact of this should be negligible compared to the network overhead of an RPC request in the first place, especially considering that the request/response will only be serialized for logging if that log level is enabled.) ### Why do I need an L1 node to run an Arbitrum node? During the node syncing stage, Arbitrum nodes read transactions from batches that have already been posted and executed on Layer 1. They connect to the Sequencer feed to receive new incoming batched transactions that have not been posted to L1 yet. When fully synced, the Arbitrum node uses the State Transition Function (STF) to consume transactions from the Sequencer feed and update the state accordingly. It also waits for the L1 batch to post. If the finalized L1 batch differs from what the Sequencer published, the node will update its state based on the L1 batched transactions. ### Can I run an Arbitrum node in p2p mode? Arbitrum doesn't have a consensus mechanism, so "p2p mode" doesn't apply. For nodes to sync to the latest chain state, they connect to an L1 node to sync the chain's history that's been posted in calldata and connect to the Sequencer feed for the transactions that have yet to be posted in batches. In no case do nodes need to peer up and sync with each other. ### How do I read messages from the Sequencer feed? Running an Arbitrum relay locally as a [Feed Relay](https://docs.arbitrum.io/node-running/how-tos/running-a-feed-relay) lets you subscribe to the Sequencer feed for real-time data as the Sequencer accepts and orders transactions offchain. Refer to [How to read the sequencer feed](https://docs.arbitrum.io/node-running/how-tos/read-sequencer-feed) for a detailed guide. ### How do I run a node locally for development? See instructions in our  [our guide about running a local devnet node](https://developer.arbitrum.io/node-running/how-tos/local-dev-node). We recommend running Nitro nodes via Docker; to compile directly / run without Docker, you can follow the steps in [How to build Nitro locally](https://docs.arbitrum.io/node-running/how-tos/build-nitro-locally). ### Is there any way to retrieve pre-Nitro archive data from a Nitro node? The pre-Nitro stack is also called the "classic" stack. Full Nitro nodes start with a database that contains the information from the "classic" era. However, a Nitro node can't query archive information contained in "classic" blocks right away. To do that, you also need to run a classic node ([instructions in our guide about running a classic node](https://developer.arbitrum.io/node-running/how-tos/running-a-classic-node)) and set the parameter `—node.rpc.classic-redirect=your-classic-node-RPC`. Please note that this information only applies to Arbitrum One nodes. Arbitrum Nova and Sepolia nodes started with a Nitro stack, so they don't have "classic" data. ### How can I verify that my node is syncing at a desirable speed? Syncing speed can vary depending on multiple factors. You can find the minimum hardware requirements to run your node [on this page](https://developer.arbitrum.io/node-running/how-tos/running-a-full-node#minimum-hardware-configuration). You should also verify your network and disk speeds and ensure that the L1 node is running correctly. ### How can I verify that my node is fully synced? You can make an `eth_syncing` RPC call to your node. Once a Nitro node is fully synced, `eth_syncing` returns the value `false` (just like a normal Geth node). When a Nitro node is still syncing, `eth_syncing` returns a map of values to help understand why the node is not syncing. Nitro execution and bottlenecks differ from those of a normal Geth node, so the `eth_syncing` output is unique to Nitro. You can find information to understand the output of `eth_syncing` in the [RPC methods](https://docs.arbitrum.io/for-devs/concepts/differences-between-arbitrum-ethereum/rpc-methods#eth_syncing) page. ### Is there an alternative to Docker when running a node? We recommend running Nitro nodes using Docker, following [the guide](https://docs.arbitrum.io/run-arbitrum-node/run-full-node) provided in our documentation. However, you can compile the code directly by following the steps described in [this guide](https://developer.arbitrum.io/node-running/how-tos/build-nitro-locally). ### What are the minimum hardware requirements to run a full node? The minimum hardware requirements are available [in this section](https://developer.arbitrum.io/node-running/how-tos/running-a-full-node#minimum-hardware-configuration). ### How can I migrate the date of one synced node to a new one? From a fully synced node, you can copy its database (the `.arbitrum` directory in a default setup) to the same database folder of the new node, and it will start from the same state. Keep in mind that this must be done after a clean shutdown, while the node is not running. ### When querying Classic transactions from a Nitro node, I sometimes get incorrect data, like the zero address as the sender. Why is that? Some old Nitro genesis database snapshots didn't properly set the retry sender for Classic blocks and contained this error. If you need to access that information, you can either resync your Nitro node with one of the [current snapshots](https://snapshot.arbitrum.foundation/index.html) or [run a Classic node](https://docs.arbitrum.io/node-running/how-tos/running-a-classic-node) alongside your Nitro node and configure a redirection for requests to Classic blocks. Please note that this only happens on Arbitrum One. --- > For a complete page index, fetch # Sequencer Keep your node in sync with the sequencer. ### [Run a feed relay](/run-arbitrum-node/sequencer/run-feed-relay) [Installation and configuration.](/run-arbitrum-node/sequencer/run-feed-relay) ### [Read the sequencer feed](/run-arbitrum-node/sequencer/read-sequencer-feed.md) [Installation and configuration.](/run-arbitrum-node/sequencer/read-sequencer-feed.md) ### [Run a Sequencer Coordination Manager (SQM)](/run-arbitrum-node/sequencer/run-sequencer-coordination-manager.md) [Installation and configuration.](/run-arbitrum-node/sequencer/run-sequencer-coordination-manager.md) --- > For a complete page index, fetch # Upgrade notice for ArbOS 51 [ArbOS 51 "Dia"](/run-arbitrum-node/arbos-releases/arbos51.md) will be activated on the Arbitrum Sepolia, Arbitrum One, and Arbitrum Nova chains. ## Actions required for node operators > **WARNING** — Action required > > Arbitrum node operators **must upgrade** to Nitro **`v3.9.6`** ahead of ArbOS 51 activation to continue syncing the chain. > > * **Docker image:** `offchainlabs/nitro-node:v3.9.6-91bf578` > * **Release notes:** ## Important dates The following dates are relevant for Arbitrum chain operators. | Date | Network upgrade | Affected audience | | ----------------------------- | ------------------------------------- | ------------------------------------ | | `Dec 1st 2025, 17:00:00 UTC` | Arbitrum Sepolia upgrade to ArbOS 51 | Node operators for Arbitrum Sepolia | | `Jan 8th, 2026, 17:00:00 UTC` | Arbitrum One/Nova upgrade to ArbOS 51 | Node operators for Arbitrum One/Nova | ## Context [ArbOS 51 "Dia"](/run-arbitrum-node/arbos-releases/arbos51.md) builds upon [ArbOS 40 "Callisto"](/run-arbitrum-node/arbos-releases/arbos40.md) with support for the relevant EVM changes that are a part of Ethereum's [Fusaka upgrade](https://ethereum.org/en/roadmap/fusaka/), as well as additional improvements to the gas pricing algorithm, a change to the min L2 base fee, `MaxTxGasLimit` allowing full block utilization, changes to instrument Nitro’s State Transition Function (STF) to price gas based on specific resource usage, native token mint/burn capabilities, and a few bug fixes. Upstream governance items ([ArbOS 50](https://snapshot.box/#/s:arbitrumfoundation.eth/proposal/0x33754da4006d0ef38666ec5d5e85fd0966a891a594ab9dc21f23beedea2d330b) + [Gas Target & Pricing Framework](https://snapshot.box/#/s:arbitrumfoundation.eth/proposal/0x4a96a91d162975de0d402b83ca8b8a24e808ca357150120fc0d44ae0bf1cc4a5)) were bundled into a single [ArbOS 51 onchain vote](https://www.tally.xyz/gov/arbitrum/proposal/53154361738756237993090798888616593723057470462495169047773178676976253908001?govId=eip155:42161:0xf07DeD9dC292157749B6Fd268E37DF6EA38395B9). Both components have passed Snapshot temperature checks, and the bundled Tally vote was passed **December 18, 2025**. --- > For a complete page index, fetch # Upgrade notice for ArbOS 60 ArbOS 60 "Elara" will be activated on the Arbitrum Sepolia, Arbitrum One, and Arbitrum Nova chains. ## Actions required for node operators ### Sepolia network On Monday, May 18, 2026 at 17:00 UTC, ArbOS 60 is scheduled to activate on the Arbitrum Sepolia chain. Arbitrum Sepolia node operators must upgrade to Nitro v3.10 ahead of this activation to continue syncing the chain. * **Docker image:** [offchainlabs/nitro-node](https://hub.docker.com/layers/offchainlabs/nitro-node/v3.10.0-b1cf6db/images/sha256-9a15d6c581eadbfd440cc74f26fda9c27a1cddd0b77f93e395e4d29678414284) [:v3](https://hub.docker.com/layers/offchainlabs/nitro-node/v3.10.0-b1cf6db/images/sha256-9a15d6c581eadbfd440cc74f26fda9c27a1cddd0b77f93e395e4d29678414284) [.10.0-b1cf6db](https://hub.docker.com/layers/offchainlabs/nitro-node/v3.10.0-b1cf6db/images/sha256-9a15d6c581eadbfd440cc74f26fda9c27a1cddd0b77f93e395e4d29678414284) * **Release notes:** ## Important dates The following dates are relevant for Arbitrum chain operators. | Date | Network upgrade | Affected audience | | --------------------------------- | ------------------------------------ | --------------------------------------- | | Monday, May 18, 2026 at 17:00 UTC | Arbitrum Sepolia upgrade to ArbOS 60 | Node operators running Arbitrum Sepolia | ## Context ArbOS 60 "Elara" builds upon [ArbOS 51 "Dia"](/run-arbitrum-node/arbos-releases/arbos51.md), it introduces a set of protocol upgrades to Arbitrum One and Nova, including support for Dynamic Pricing (multidimensional gas pricing), increased Stylus contract size limits, and new mechanisms for managing minimum base fees. While Dynamic Pricing is included in this release, it will remain disabled at the time of activation. This is because ongoing analyses - led by Offchain Labs in collaboration with Entropy Advisors- have not yet finalized the appropriate gas target parameters required for safe and effective enablement. The ArbOS 60 governance temperature check has already passed successfully. Upstream governance for ArbOS 60 follows the standard process, including Snapshot temperature checks and an onchain vote prior to activation on Arbitrum One and Nova. --- > For a complete page index, fetch # Upgrade notice for ArbOS 61 ArbOS 61 "Elara" is active on Arbitrum Sepolia. The ArbitrumDAO approved it for Arbitrum One and Arbitrum Nova, where it activates on Thursday, August 20, 2026, at 17:00 UTC. Action required Node operators **must upgrade** to Nitro **`v3.11`** or higher to continue syncing these chains. * **Docker image:** [offchainlabs/nitro-node](https://hub.docker.com/layers/offchainlabs/nitro-node/v3.11.0-a618155/images/sha256-a34457bb9461acf12ea01cbf23a9850e15b20a44b499c64cf441051230231fe1) [:v3](https://hub.docker.com/layers/offchainlabs/nitro-node/v3.11.0-a618155/images/sha256-a34457bb9461acf12ea01cbf23a9850e15b20a44b499c64cf441051230231fe1) [.11.0-a618155](https://hub.docker.com/layers/offchainlabs/nitro-node/v3.11.0-a618155/images/sha256-a34457bb9461acf12ea01cbf23a9850e15b20a44b499c64cf441051230231fe1) * **Release notes:** ## Actions required for node operators ### Sepolia network ArbOS 61 activated on the Arbitrum Sepolia chain on Monday, June 29, 2026, at 15:00 UTC. ### Arbitrum One and Arbitrum Nova The ArbitrumDAO approved ArbOS 61 for Arbitrum One and Arbitrum Nova in a Constitutional onchain vote. After the waiting periods and phases set out in the ArbitrumDAO Constitution, ArbOS 61 activates on both chains on Thursday, August 20, 2026, at 17:00 UTC. Complete the upgrade before that date. The [Arbitrum DAO network upgrades table](https://docs.arbitrum.foundation/network-upgrades) records the exact activation time after the upgrade executes. ## Important dates The following dates are relevant for Arbitrum chain operators. | Date | Network upgrade | Affected audience | | -------------------------------------- | ------------------------------------- | ---------------------------------------- | | Monday, June 29, 2026 at 15:00 UTC | Arbitrum Sepolia upgrade to ArbOS 61 | Node operators running Arbitrum Sepolia | | Thursday, August 20, 2026 at 17:00 UTC | Arbitrum One/Nova upgrade to ArbOS 61 | Node operators running Arbitrum One/Nova | ## Context ArbOS 61 "Elara" builds upon [ArbOS 51 "Dia"](/run-arbitrum-node/arbos-releases/arbos51.md). It makes two changes to Arbitrum One and Nova: it raises the Stylus contract code size limit to 96 KB, and it adds a `BaseFeeManager` contract that manages the minimum L2 base fee. ArbOS 61 also ships two features that stay disabled on Arbitrum One and Nova, for the convenience of other Arbitrum chains: an Alternative Data Availability (AltDA) Layer API, and optional compliance transaction filtering. A chain owner must enable each one explicitly. To learn how to build a custom DA provider against the new API, refer to [How to integrate with the DA API](/launch-arbitrum-chain/extend-the-protocol/da-api-guide.md). To learn how transaction filtering works, refer to [Compliance filtering](/launch-arbitrum-chain/chain-config/sequencer/compliance-filtering.md). ArbOS 61 corrects two interacting bugs in the gas refund logic that were found while testing ArbOS 60 on Arbitrum Sepolia. The fixes change the State Transition Function (STF), so they required a new ArbOS version. ArbOS 60 never activated on Arbitrum One or Nova, so neither chain was affected. ArbOS 61 passed a Snapshot temperature check and then a Constitutional onchain vote, which closed on August 1, 2026. To learn more about the proposal, refer to the [ArbOS 61 Elara AIP](https://forum.arbitrum.foundation/t/constitutional-aip-arbos-61-elara/30601). --- > For a complete page index, fetch # Fusaka Compatibility Notice > **DANGER** — 🛑 IMPORTANT 🛑 > > Arbitrum chain operators must ensure that their nodes are properly configured before or shortly after the parent chain upgrades to Fusaka or the child chain upgrades to [ArbOS 51](/run-arbitrum-node/arbos-releases/arbos51.md). This document explains what you need to do if you operate a chain that settles on a Fusaka-enabled parent chain. This includes actions for Arbitrum chains as well as actions for Arbitrum One and Arbitrum Nova node operators. ## What's included in Fusaka The Fusaka upgrade introduces several breaking changes. Review [EIP-7607](https://eips.ethereum.org/EIPS/eip-7607) for detailed information about the EIPs included in the Fusaka hard fork. ## Important dates The following dates are relevant for Arbitrum chain operators. | Date | Network upgrade | Affected audience | | ----------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `Nov 20th 2025, 17:00:00 UTC` | Arbitrum Sepolia upgrade to ArbOS 50 | Node operators for Arbitrum Sepolia | | `Dec 1st 2025, 17:00:00 UTC` | Arbitrum Sepolia upgrade to [ArbOS 51](/run-arbitrum-node/arbos-releases/arbos51.md) | Node operators for Arbitrum Sepolia | | `Jan 8th, 2026, 17:00:00 UTC` | Arbitrum One/Nova upgrade to [ArbOS 51](/run-arbitrum-node/arbos-releases/arbos51.md) | Node operators for Arbitrum One/Nova | | `Oct 14th 2025, 07:36:00 UTC` | Ethereum Sepolia Fusaka hard fork | Node operators for Arbitrum chains settling on Ethereum Sepolia (Arbitrum L2s, Arbitrum Sepolia) | | `Dec 3rd 2025, 21:49:11 UTC` | Ethereum Mainnet Fusaka hard fork | Node operators for Arbitrum chains settling on Ethereum Mainnet (Arbitrum L2s, Arbitrum One/Nova) | ## For node operators Outlined below are different types of Arbitrum chains, along with node software required for operators of chains in those configurations. | Layer | Data Availability | Fallback to blobs enabled? | Required Nitro node version | Configurations for the Ethereum Consensus Layer client | | ------ | -------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | **L2** | **Rollup** | N/A | Update to [`3.9.6`](https://github.com/OffchainLabs/nitro/releases/tag/v3.9.6) (ArbOS 51); [`3.8.0`](https://github.com/OffchainLabs/nitro/releases/tag/v3.8.0) or [`3.7.6`](https://github.com/OffchainLabs/nitro/releases/tag/v3.7.6) (ArbOS 40 or older) | Requires all [historical blob data](#how-to-ensure-your-node-has-access-to-all-historical-blob-data-and-blob-data-from-all-subnets) | | **L2** | **AnyTrust / AltDA** | Enabled | Update to [`3.9.6`](https://github.com/OffchainLabs/nitro/releases/tag/v3.9.6) (ArbOS 51); [`3.8.0`](https://github.com/OffchainLabs/nitro/releases/tag/v3.8.0) or [`3.7.6`](https://github.com/OffchainLabs/nitro/releases/tag/v3.7.6) (ArbOS 40 or older) | Requires all [historical blob data](#how-to-ensure-your-node-has-access-to-all-historical-blob-data-and-blob-data-from-all-subnets) | | **L2** | **AnyTrust / AltDA** | Disabled | No nitro update required | Doesn't require historical blob data | | **L3** | **Rollup** | N/A | No nitro update required | Doesn't require historical blob data | | **L3** | **AnyTrust / AltDA** | Enabled or Disabled | No nitro update required | Doesn't require historical blob data | > **WARNING** — Exceptions > > For batch posters settling to mainnet Ethereum, Nitro >=3.8.0 is required. ### How to ensure your node has access to all historical blob data and blob data from all subnets #### If you run a Nitro node and use an external L1 Ethereum beacon chain RPC: Confirm that your external L1 beacon chain RPC provider has configured their L1 beacon chain node to subscribe to all subnets before the Ethereum Fusaka hard fork and have all historical blob data to ensure your Nitro node can sync from periods beyond the blob retention period (or that your RPC provider has backfilled the blob data properly). #### If you run a Nitro node and operate your own L1 Ethereum beacon chain node: Ensure that your consensus layer client & Nitro node is configured as per the table below. | Client | Compatible with Nitro | Required Nitro Flag | Required flag for subscribing to all subnets | Required flag to serve historical blobs | | -------------------- | --------------------- | ------------------- | --------------------------------------------- | ---------------------------------------------------------------- | | Prysm 7.1.0 or newer | ✅ | None | | `--blob-retention-epochs` `--semi-supernode` `--enable-backfill` | | Lighthouse | ✅ | None | `--supernode` | `--prune-blobs false` or `--blob-prune-margin-epochs` | | Teku | ✅ | None | `--p2p-subscribe-all-custody-subnets-enabled` | None exists | | Lodestar | ✅ | None | `--supernode` | `--chain.archiveDataEpochs` | For additional information regarding specific client flags visit their docs: [Prysm](https://prysm.offchainlabs.com/docs/learn/concepts/blobs), [Lighthouse](https://lighthouse-book.sigmaprime.io/advanced_blobs.html), [Teku](https://docs.teku.consensys.io/concepts/proto-danksharding#what-are-blobs), and [Lodestar](https://chainsafe.github.io/lodestar/run/beacon-management/beacon-cli/). We recommend using Prysm 7.1.0 or newer with the flags `--semi-supernode`, `--enable-backfill`, and removing `--subscribe-all-data-subnets`. To read more about Fusaka PeerDAS changes and why Layer 2 network operators must connect to an Ethereum beacon chain node with historical blob data, see the [historical blobs docs](/run-arbitrum-node/beacon-nodes-historical-blobs.md). --- > For a complete page index, fetch # Offchain Pattern guide This document provides guidance on how to write a document that complies with Offchain's editorial standards ## Content types Choose the right content type based on your audience and purpose: | Content Type | Purpose | When to Use | Example | | ----------------------- | ----------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------- | | **Gentle Introduction** | Day 1 onboarding for newcomers | Multiple audiences need foundational knowledge | [Arbitrum Intro](https://developer.arbitrum.io/intro/) | | **Quickstart** | Fast onboarding with hands-on steps | Single audience needs immediate activation | [Solidity Quickstart](https://docs.arbitrum.io/build-decentralized-apps/quickstart-solidity-remix) | | **How-to** | Step-by-step task completion | Users need to accomplish a specific task | [Running an Archive Node](https://developer.arbitrum.io/node-running/running-an-archive-node) | | **Tutorial** | Comprehensive learning experience | Users need to learn through guided practice | Integration guides | | **Concept** | Explain ideas and relationships | Users need to understand how something works | [Security Council](https://docs.arbitrum.foundation/concepts/security-council) | | **Reference** | Quick lookup of technical details | Users need specific technical information | API documentation | | **Troubleshooting** | Problem-solution mapping | Users are encountering specific issues | [Node Troubleshooting](https://developer.arbitrum.io/node-running/troubleshooting-running-nodes) | ## Writing principles | Principle | Description | Good Example | Avoid | | ----------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | **Use sentence case** | First letter capitalized, rest lowercase | "Deploy your smart contract" | "Deploy Your Smart Contract" | | **Write descriptive links** | Links describe their destination | "See our \[deployment tutorial]" | "Find more \[here]" | | **Minimize technical jargon** | Use plain language when possible | "How to reuse contract methods" | "How to leverage trait-based composition" | | **Lead with what matters** | Put important information first | Start with the outcome or benefit | Bury the key point | | **Write concisely** | Use short, clear sentences | Break up complex ideas | Write three-line sentences | | Use Quicklooks | Use Quicklooks with terms found in docs/partials/glossary | In the past, Arbitrum chains ordered incoming transactions on a "First-Come, First-Serve (FCFS)" basis. | In the past, Arbitrum chains ordered incoming transactions on "First-Come, First-Serve (FCFS)" basis. | ## Plain language Plain language means the reader finds what they need, understands it the first time, and can act on it. This section applies the ISO 24495-1 principles through the concrete rules of the [Federal Plain Language Guidelines](https://www.plainlanguage.gov/guidelines/), plus two rules borrowed from ASD-STE100 Simplified Technical English. Every rule below is testable in review. ### Sentence-level rules | Rule | Correct | Incorrect | | --------------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------- | | **Address the reader as "you"** | "You must fund the batch poster account." | "Users must ensure their batch poster account is funded." | | **Use active voice and name the actor** | "The batch poster compresses transactions and posts them to the parent chain." | "Transactions are compressed and posted to the parent chain." | | **Use present tense** | "The sequencer orders incoming transactions." | "The sequencer will order incoming transactions." | | **One idea per sentence** | Two sentences of about 15 words each. | One sentence of 40 words with three clauses. | | **Use verbs, not nominalizations** | "Configure the sequencer." | "Perform configuration of the sequencer." | | **Put the condition before the action** | "If you run an AnyTrust chain, enable the DA server." | "Enable the DA server if you run an AnyTrust chain." | | **Use the imperative for steps** | "Run `yarn build`." | "You should now proceed to run `yarn build`." | | **State things positively** | "Wait until the assertion is confirmed." | "Do not continue before the assertion is no longer unconfirmed." | | **Give concrete numbers** | "The challenge period is 6.4 days." | "The challenge period takes a while." | | **Reserve must, should, and can** | must = required, should = recommended, can = optional | "should" for a step the reader has no choice about | ### One term, one meaning Pick one name for each concept and use it for the whole page. Alternating between synonyms makes the reader ask whether you mean two different things. | Concept | Pick one and keep it | Don't mix on one page | | ---------------------------------- | -------------------- | --------------------------------------- | | The chain your app runs on | child chain | L2, child chain, Arbitrum chain, rollup | | The node that orders transactions | sequencer | sequencer, sequencing node, the orderer | | The party that proposes assertions | proposer | proposer, staker, validator | Expand every acronym on first use. Wrap that first mention in a Quicklook when a glossary partial exists for the term. ### Words to replace | Don't write | Write | | ----------------------- | ------------------------------- | | utilize, leverage | use | | in order to | to | | prior to, subsequent to | before, after | | facilitate | help | | terminate | end | | sufficient | enough | | additional | more | | approximately | about | | commence, initiate | start | | in the event that | if | | at this point in time | now | | e.g., i.e., etc. | for example, that is, and so on | ### Phrases to cut Delete these openers and keep the sentence that follows: "It is important to note that", "Please note that", "As previously mentioned", "In the context of", "It should be pointed out that". Never write "simply", "just", "easy", "obvious", or "of course". When the step does not work, these words tell the reader the fault is theirs. ### Paragraph and page rules * One topic per paragraph, five lines at most. * Convert any sentence with three or more conditions into a bulleted list or a table. * Lead each section with the outcome, then the detail. * Write headings a reader can scan to find their task. ### Authoring conventions * Use `` instead of Docusaurus `:::info` / `:::note` for callouts in MDX. The component is registered globally via `src/theme/MDXComponents.js`, so no import is needed. ## Terminology guide | Term | Correct | Incorrect | | ------------------------------ | ------------------------------------------------------------------------ | -------------------------------------- | | JavaScript | JavaScript | js, javascript, Javascript | | app | first mention on page → decentralized app
subsequent mentions → app | dapp, dApp | | Smart contract | smart contract, contract | smartcontract | | Cross-chain | cross-chain | cross chain, crosschain | | Allowlist/Denylist | allowlist, denylist | whitelist, blacklist | | ERC-XX (ERC-20, ERC-721, …) | ERC-20, ERC-721, ERC-1155 | ERC20, erc721, … | | Sequencer Coordination Manager | Sequencer Coordination Manager (SQM) | sequencer coordinator manager | | AnyTrust | AnyTrust | anytrust, Anytrust | | Ethereum currency | ETH, Ether, ether | eth, Eth, `ETH` | | onchain | onchain | on-chain, on chain | | Arbitrum chains | "Your Arbitrum chain" | "L3 Orbit chain", "blockchain" | | Challenge period | 6.4 days to challenge an assertion | confirmation period (a different term) | | Bond | bond, bonded funds for proposing | stake, staked funds | | Rollup | Rollup | rollup | ## Diagrams and visual content ### Preferred format * Use SVG for scalability and code-friendliness * Avoid PNG unless necessary ### Recommended tools * excalidraw for creating diagrams * Focus on illustrating concepts, data structures, and flows * **Third-party content guide** - if you’re not sure how to incorporate third-party content and tooling into our docs * See --- > For a complete page index, fetch # ArbOS 11 ArbOS 11 is shipped via Nitro v2.2.0, which is available on Docker hub with the image tag: `offchainlabs/nitro-node:v2.2.0-f7dc9de`. This release of Nitro is a mandatory upgrade for Arbitrum One and Nova validators. For Arbitrum One and Nova, the ArbOS 11 upgrade requires a governance vote to activate. Formal release notes can be found [here](https://github.com/OffchainLabs/nitro/releases/tag/v2.2.0). For an index of all ArbOS releases and upgrade expectations, see the [ArbOS Software Releases Overview](/run-arbitrum-node/arbos-releases/overview.md). ## Requirements: * [Nitro v2.2.0](https://github.com/OffchainLabs/nitro/releases/tag/v2.2.0) or higher * [nitro-contracts v1.1.0](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v1.1.0) or higher * Wasm module root: `0x6b94a7fc388fd8ef3def759297828dc311761e88d8179c7ee8d3887dc554f3c3` ## High-level description of ArbOS 11 changes * Addition of all EVM changes made on the parent chain Ethereum as part of the [Shanghai upgrade](https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/shanghai.md#included-eips). This includes: * [EIP-3651: Warm COINBASE](https://eips.ethereum.org/EIPS/eip-3651) * [EIP-3855: PUSH0 instruction](https://eips.ethereum.org/EIPS/eip-3855) * [EIP-3860: Limit and meter initcode](https://eips.ethereum.org/EIPS/eip-3860) * [EIP-6049: Deprecate SELFDESTRUCT](https://eips.ethereum.org/EIPS/eip-6049) * Improvements and fixes for [retryable tickets](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md) to ensure that the fee calculation to redeem retryable tickets will take into account both the infrastructure fee and the network fee. The infrastructure fee is the minimum child chain base fee only, while the network fee collects child chain congestion charges. This is important for [AnyTrust chains](/how-arbitrum-works/deep-dives/anytrust-protocol.md) like Arbitrum Nova because members of the Data Availability Committee (DAC) gets paid a percentage of the infrastructure fee but not the network fee. Previously, the calculations to determine the fee for redeeming retryable tickets did not consider the infrastructure fee. * Fixes an issue where the [`ArbOwnerPublic` precompile](/arbitrum-essentials/precompiles/reference.md#arbownerpublic) returned the incorrect list of chain owners. This does not change the parties who are able to perform chain owner actions. As intended, only the Arbitrum DAO is able to take chain owner actions for Arbitrum One and Nova. * Resolves an issue where the [`arbBlockHash` method](/arbitrum-essentials/precompiles/reference.md#arbsys) would take up all the gas when reverting. The previous incorrect behavior meant that if a transaction calls `arbBlockHash` with an out-of-range block number, then the transaction would consume all the gas when reverting. * Addition of the [`L1RewardReceipient`](/arbitrum-essentials/precompiles/reference.md#arbgasinfo) and [`L1RewardRate`](/arbitrum-essentials/precompiles/reference.md#arbgasinfo) precompile methods to view the parent chain pricing parameters and make it easier to view the current chain configuration. * Fix the `ArbOwner` precompile to disallow emitting logs in `STATICCALL` contexts, bringing this in line with how the EVM is expected to behave as `STATICCALL` invocations should never be able to emit logs. The previous incorrect behavior would mean that a log was emitted when a chain owner made a `STATICCALL` on the `ArbOwner` precompile. ## Reference links for ArbOS 11 * [Nitro v2.2.0 Release details on Github](https://github.com/OffchainLabs/nitro/releases/tag/v2.2.0) * Original DAO proposal: [AIP: ArbOS Version 11](https://forum.arbitrum.foundation/t/aip-arbos-version-11/19696) * [AIP: ArbOS Version 11 Snapshot Vote](https://snapshot.org/#/arbitrumfoundation.eth/proposal/0xa635e39a2c527f7a1eabf5ea22bdec6f4a265d6c69a06076e65fde0ae0a5941b) * [Formal Tally (onchain) vote for AIP: ArbOS Version 11](https://www.tally.xyz/gov/arbitrum/proposal/77069694702187027448745871790562515795432836429094222862498991082283032976814) * [ArbOS 11 Audit Report by Trail of Bits](https://drive.google.com/file/d/1N3197Z7DuqBpu9qdt-GWPewe8HQakfLY/view) --- > For a complete page index, fetch # ArbOS 20 Atlas ArbOS 20 Atlas is shipped via Nitro v2.3.1, which is available on Docker hub with the image tag: `offchainlabs/nitro-node:v2.3.1-26fad6f`. This release of Nitro is a mandatory upgrade for Arbitrum One and Nova validators. For Arbitrum One and Nova, the ArbOS 20 upgrade requires a governance vote to activate. For an index of all ArbOS releases and upgrade expectations, see the [ArbOS Software Releases Overview](/run-arbitrum-node/arbos-releases/overview.md). ArbOS 20 Atlas builds upon [ArbOS 11](/run-arbitrum-node/arbos-releases/arbos11.md). ## Requirements: * [Nitro v2.3.1](https://github.com/OffchainLabs/nitro/releases/tag/v2.3.1) or higher * [nitro-contracts v1.2.1](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v1.2.1) or higher * Wasm module root: `0x8b104a2e80ac6165dc58b9048de12f301d70b02a0ab51396c22b4b4b802a16a4` * Access to the [Ethereum Beacon Chain APIs](https://ethereum.github.io/beacon-APIs/#/), either from your own self-managed parent chain Ethereum node or from a 3rd party provider like [those on this list](/run-arbitrum-node/l1-ethereum-beacon-chain-rpc-providers.md). ## High-level description of ArbOS 20 changes ArbOS 20 is an upgrade to enable Arbitrum's support for the parent chain Ethereum's [Dencun upgrade](https://eips.ethereum.org/EIPS/eip-7569) scheduled for March 2024. As a result, all of the ArbOS specific changes revolve around implementing the majority of the [Cancun EIPs](https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/cancun.md) on Arbitrum: * Enable Arbitrum chains to batch and post transaction data in the form of Blobs to the parent chain Ethereum, to support [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844). This includes updates to the Sequencer Inbox contract to support posting transactions in the form of blobs, updating Nitro's fraud prover to support proving additional hashes (KZG and SHA256 preimages), and updates to the core Nitro node software to handle parsing data from EIP-4844 blobs. * Addition of the `TSTORE` and `TLOAD` EVM opcodes introduced in [EIP-1153](https://eips.ethereum.org/EIPS/eip-1153) offering a cheaper option than storage for data that’s discarded at the end of a transaction. * Addition of the `MCOPY` EVM opcode introduced in [EIP-5656](https://eips.ethereum.org/EIPS/eip-5656) for cheaper memory copying. * Changes to the `SELFDESTRUCT` EVM opcode to reflect the behavior on the parent chain Ethereum, as outlined in [EIP-6780](https://eips.ethereum.org/EIPS/eip-6780). * Addition of a batch poster manager role that will have the ability to grant and revoke batch-posting affordances. This role is assigned to the operator of the sequencer to allow the batch poster manager perform key rotations for the batch posters. The DAO will continue to have the ability to revoke the seqauencer role, meaning there is no change to the current system's trust model since the DAO ca update the batch poster manager at any time (along with any batch posters). * Increasing the max block height that a batch can be posted, relative to the current block, to 64 bringing this in line with Ethereum's finality guarantees. The current value of 12 was set prior to the Ethereum merge and could mean that a small parent chain reorg can cause an otherwise valid batch to revert. * Fix Sequencer Inbox bug: when posting a batch, the Sequencer provides the "newMessageCount” value as a parameter; if the Sequencer is malicious, it can provide the max uint256 value which in turn would make subsequent calls to forceInclusion revert with an overflow error. Atlas’s upgrade to the Sequencer inbox includes a [change](https://github.com/OffchainLabs/nitro-contracts/blob/dcc51066b26b84cb157cbeba2f9f492ab33f9093/src/bridge/SequencerInbox.sol#L327)) in which forceInclusion does not modify the message count, fixing this bug. This bug had been disclosed to Arbitrum RaaS providers and to the Arbitrum DAO Security Council. ## Special notes on ArbOS 20: Atlas support for EIP-4844 * Upgrading to **the Atlas ArbOS release will require access to the parent chain's Ethereum beacon chain endpoints to retrieve blob data. For nodes of a chain that come online 18 days after Atlas gets activated on their chain will need access to historical data to sync up to the latest state.** If you are not operating your own Ethereum consensus client, [please visit this page to view a list of beacon chain RPC providers](/run-arbitrum-node/l1-ethereum-beacon-chain-rpc-providers.md) where you can access blob data. * Applications on Arbitrum will not have to be modified or take any explicit action to get the benefits of using EIP-4844 (i.e., the whole chain opts-in with ArbOS 20 “Atlas”). * ArbOS 20 “Atlas” adds support for Arbitrum chains to send data in a blob storage format to data availability layers, like the parent chain Ethereum, that support the blob transaction type. This includes Arbitrum One and Arbitrum Nova. ArbOS 20 “Atlas” does not add support for Arbitrum chains to receive data in a blob storage format. This means that an L3 Arbitrum chain on top of an Arbitrum L2 will use calldata when posting L3 transaction data to the underlying L2. The child chain (L2) Arbitrum chain will then be able to post data to a parent chain data availability layer like Ethereum using blobs. * There currently aren’t estimates on what the end-user gas savings of using blob data will be. This topic is something being actively worked on and monitored. Without Mainnet data, the estimates for blob gas prices will not be accurate enough to reliably predict the cost reductions that users will experience—and even with Mainnet data, the savings will vary by use case (i.e., no current way to predict the price impacts from all blob gas market participants yet). In general, however, the use of blobs will reduce the cost of using Arbitrum L2s. To learn more about what EIP-4844 will mean for the child chain users, please checkout this [blog post on Medium by Offchain Lab's Co-foudner and Chief Scientist Ed Felten](https://medium.com/offchainlabs/eip-4844-what-does-it-mean-for-l2-users-5e86ebc4c028). ## Block explorers Below is a non-comprehensive list of explorers that support querying and viewing blob data on Ethereum that get posted by Arbitrum child chain chains. * [Blockscout](https://www.blockscout.com/). For self-deployment, blobs are supported as of blockscout v6.2.0 and blockscout-frontend v1.2.6. * [Arbiscan](https://arbiscan.io/) * [Blobscan](https://blobscan.com/) * [Beaconcha.in](https://beaconcha.in/) ## Additional requirement for Arbitrum L2 chain operators: enabling blob batch posting This section maps to [Step 4 in the guide on *How to upgrade ArbOS on your Arbitrum L2 chain*](/launch-arbitrum-chain/operate/arbos-upgrade.md#step-4-enable-arbos-specific-configurations-or-feature-flags-not-always-required) and contains additional instructions for Arbitrum L2 chain operators for ArbOS 20 Atlas. Specifically, the details below are meant to help Arbitrum L2 chain operators enable blob batch posting to L1 Ethereum following their successful upgrade to the ArbOS 20 Atlas release. > **CAUTION** > > Before proceeding, make sure you have successfully completed Steps 1 through 3 of the guide on [How to upgrade ArbOS on your Arbitrum chain](/launch-arbitrum-chain/operate/arbos-upgrade.md). > > To enable the posting of transaction data in Blobs to L1 Ethereum, please refer to the [Enable post-4844 blobs](/launch-arbitrum-chain/chain-config/batch-poster/enable-4844-blobs.md) section of the Arbitrum chain configuration guide. > > ## Reference links for ArbOS 20 Atlas > > * [Nitro v2.3.1 Release details on Github](https://github.com/OffchainLabs/nitro/releases/tag/v2.3.1) > * Original DAO proposal: [AIP: ArbOS Version 20 "Atlas"](https://forum.arbitrum.foundation/t/aip-arbos-version-20-atlas/20957) > * [AIP: ArbOS Version 20 "Atlas" Snapshot Vote](https://snapshot.org/#/arbitrumfoundation.eth/proposal/0x813a366e287a872ada13d4f8348e771c7aa2d8c3cb00b2be31539ceab5627513) > * [Formal Tally (onchain) vote for AIP: ArbOS Version 20](https://www.tally.xyz/gov/arbitrum/proposal/46905320292877192134536823079608810426433248493109520384601548724615383601450) > * [ArbOS 20 Atlas Audit Report by Trail of Bits](https://github.com/trailofbits/publications/blob/master/reviews/2024-02-offchainlabsarbos-securityreview.pdf) --- > For a complete page index, fetch # ArbOS 32 Bianca > **CAUTION** > > Please upgrade directly to ArbOS 32 from ArbOS 20 and not to ArbOS 30 or ArbOS 31. The ArbOS 32 release builds upon ArbOS 30 and ArBbOS 31 and includes critical fixes & optimizations coming out of rigorous testing and feedback from Stylus teams. ArbOS 32 “Bianca” will be the canonical ArbOS version for the “Bianca” family of releases. > > Future versions of Nitro may remove support for Arbitrum chains which have historically upgraded to, and remain on, ArbOS 30 or ArbOS 31. Due to this, we highly recommend upgrading immediately and directly to ArbOS 32. The minimum Nitro version that supports ArbOS 32 "Bianca" is [Nitro v3.3.1](https://github.com/OffchainLabs/nitro/releases/tag/v3.3.1), which is available on Docker hub with the image tag: `offchainlabs/nitro-node:v3.3.1-e326369`. This release of Nitro is a mandatory upgrade for Arbitrum One and Nova validators. For Arbitrum One and Nova, the ArbOS 32 "Bianca" upgrade required a governance vote to activate. Please note that it is important that you only run the Nitro v3.3.1 against trusted databases. If you want to use an untrusted database, you can first remove the `wasm` directory if it exists (it might be inside the `nitro` folder). Otherwise, the database may have malicious, unvalidated code that can result in remote code execution. This is also mitigated by ensuring you run the Arbitrum Nitro node inside Docker. The Arbitrum docs will remain the canonical home for information regarding ArbOS releases, with more details found on the [ArbOS Software Releases Overview page](/run-arbitrum-node/arbos-releases/overview.md). ## Requirements: * [Nitro v3.3.1](https://github.com/OffchainLabs/nitro/releases/tag/v3.3.1) or higher * [nitro-contracts v2.1.0](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v2.1.0) or higher * WASM module root: `0x184884e1eb9fefdc158f6c8ac912bb183bf3cf83f0090317e0bc4ac5860baa39` ## High-level description of ArbOS 32 changes ArbOS 32 Bianca is a major upgrade for Arbitrum chains. As a refresher, ArbOS upgrades can be treated as Arbitrum’s equivalent of a hard fork - more can be read about this subject over in [Arbitrum ArbOS upgrades](https://forum.arbitrum.foundation/t/arbitrum-arbos-upgrades/19695). Please note that ArbOS 21 Bianca is an upgrade that builds upon [ArbOS 20 Atlas](/run-arbitrum-node/arbos-releases/arbos20.md). ArbOS 32 Bianca brings many features, improvements, and bug fixes to Arbitrum chains. A full list of changes can be found in the Nitro release notes for [Nitro v3.3.1](https://github.com/OffchainLabs/nitro/releases/tag/v3.3.1) or higher (as Nitro 3.3.1 is the endorsed Nitro node version for ArbOS 32 Bianca). Highlighted below are a few of the most impactful and critical features that are introduced with ArbOS 32 Bianca: * Addition and subsequent activation of [Stylus](/stylus/gentle-introduction.md) on Arbitrum chains through the addition of a new WebAssembly-based (WASM) virtual machine that runs alongside the EVM. Stylus enables developers to write smart contracts in new programming languages that compile to WASM, like Rust, that are more efficient and safer than Solidity smart contracts while retaining complete interoperability. * Adding support for [RIP-7212](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md) decreases the costs of verifying the secp256r1 curve onchain [by 99% when compared to current implementations](https://www.alchemy.com/blog/what-is-rip-7212), making secp256r1 verification more feasible for everyday use and enabling application developers and protocols to offer their users improved UX on Arbitrum One and Arbitrum Nova. Without this precompile, verifying this signature onchain is extremely expensive. Passkey-based wallets offer better security than a typical externally owned account (EOA) and cross-device support. Many wallets, notably apps using embedded wallets, have been requesting this feature for over a year. * \[Only relevant to Arbitrum Nova] Updated the transaction fee router contracts on Arbitrum Nova to allow for fees collected to be automatically sent to the ArbitrumDAO Treasury on Arbitrum One. Currently, the ArbitrumDAO receives Arbitrum Nova transaction fees that are sent to an ArbitrumDAO-controlled address that requires a constitutional proposal to move, which is less efficient. This change is specific to Arbitrum Nova and is not expected to impact Arbitrum chains. * Introduction of a new Fast Withdrawals feature for Arbitrum chains to achieve fast finality. This feature allows for transactions processed by a committee of validators to be unanimously confirmed as quickly as 15 minutes, as opposed to the default 6.4-day challenge period. While any Arbitrum chain can adopt Fast Withdrawals, we only recommend Fast Withdrawals for AnyTrust chains. Note that to enable this feature, separate steps must be followed (below). ## Additional requirement for Arbitrum chains who wish to take advantage of the Stylus Cache Manager > **TIP** — Stylus Cache Manager > > It is strongly recommended that teams upgrading to ArbOS 32 also spend the time following the instructions described below to deploy and enable the Stylus Cache Manager. Even if your team does not intend to build with Stylus in the immediate term, enabling the Cache Manager ensures that future usage of Arbitrum Stylus on your chain is smooth and provides a consistent UX with the developer experience of building with Arbitrum Stylus on Arbitrum One. Specific to Stylus and ArbOS 32 "Bianca", we have developed a caching strategy that stores frequently accessed contracts in memory to reduce the costs and time associated with contract execution from repeated initializations. Check out the [Stylus caching strategy docs](/stylus/how-tos/caching-contracts.md) to learn more. In order to take advantage of this caching strategy, an additional step is required to deploy and enable it's use on your Arbitrum chain. ## Additional requirement for Arbitrum chains who wish to enable Fast Withdrawals After you have upgraded your Arbitrum chain to ArbOS 32 "Bianca" (i.e., you have fully completed [Step 3 in the "How to upgrade ArbOS on your Arbitrum chain" guide](/launch-arbitrum-chain/operate/arbos-upgrade.md#step-3-schedule-the-arbos-version-upgrade) for your Arbitrum chain), please follow [these additional instructions](https://github.com/OffchainLabs/chain-actions/tree/main/scripts/foundry/fast-confirm) in the `chain-actions` repository to deploy the Safe contract for the fast confirmation committee and set the Safe contract to be both the validator and fast confirmer on your rollup. Note that Fast Withdrawals is disabled by default unless explicitly set up and enabled by the Arbitrum chain owner/maintainer. ## Reference links for ArbOS 32 Bianca * [Nitro v3.3.1](https://github.com/OffchainLabs/nitro/releases/tag/v3.3.1) * [ArbOS 32 "Bianca" onchain Tally vote](https://www.tally.xyz/gov/arbitrum/proposal/108288822474129076868455956066667369439381709547570289793612729242368710728616) * [AIP: Activate Stylus and Enable Next-Gen WebAssembly Smart Contracts (ArbOS 32)](https://forum.arbitrum.foundation/t/aip-activate-stylus-and-enable-next-gen-webassembly-smart-contracts-arbos-30/22970) * [AIP: Support RIP-7212 for Account Abstraction Wallets (ArbOS 32)](https://forum.arbitrum.foundation/t/aip-support-rip-7212-for-account-abstraction-wallets-arbos-30/23298) * [AIP: Nova Fee Router Proposal (ArbOS 32)](https://forum.arbitrum.foundation/t/aip-nova-fee-router-proposal-arbos-30/23310) * [Arbitrum Stylus Audit Report by Trail of Bits](/audit-reports.md) --- > For a complete page index, fetch # ArbOS 40 Callisto > **CAUTION** > > [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537) is not enabled in ArbOS 40 Callisto. This means that the precompiled contracts for certain operations on the BLS12-381 elliptic curve are not supported. However, these precompiles will be proposed for inclusion in the next ArbOS release. The minimum Nitro version that supports ArbOS 40 "Callisto" is [Nitro v3.6.5](https://github.com/OffchainLabs/nitro/releases/tag/v3.6.5), which is available on Docker Hub with the image tag `offchainlabs/nitro-node:v3.6.5-89cef87`. This release of Nitro is a mandatory upgrade for Arbitrum One and Nova validators. For Arbitrum One and Nova, the ArbOS 40 "Callisto" upgrade required a governance vote to activate. Please note that it is important to run Nitro v3.6.5 only against trusted databases. If you want to use an untrusted database, you can first remove the `wasm` directory if it exists (potentially inside the `nitro` folder). Otherwise, the database may have malicious, unvalidated code that can result in remote code execution. Avoiding unvalidated code is also mitigated by ensuring you run the Arbitrum Nitro node inside Docker. The Arbitrum docs will remain the canonical home for information regarding ArbOS releases, with more details found on the [ArbOS Software Releases Overview page](/run-arbitrum-node/arbos-releases/overview.md). As a refresher, ArbOS upgrades get treated as Arbitrum's equivalent of a hard fork. To learn more, refer to the [Arbitrum ArbOS upgrades](https://forum.arbitrum.foundation/t/arbitrum-arbos-upgrades/19695). Please note that ArbOS 40 Callisto is an upgrade that builds upon [ArbOS 32 Bianca](/run-arbitrum-node/arbos-releases/arbos32.md). ## Requirements: * [Nitro v3.6.5](https://github.com/OffchainLabs/nitro/releases/tag/v3.6.5) or higher * [nitro-contracts v3.1.0](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v3.1.0) or higher * WASM module root: `0xdb698a2576298f25448bc092e52cf13b1e24141c997135d70f217d674bbeb69a` > **CAUTION** > > If your chain is not ready to activate BoLD, please use [nitro-contracts v2.1.3](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v2.1.3) instead; v3.0.0 or higher cannot be used without activating BoLD. ## High-level description of ArbOS 40 changes ArbOS 40 Callisto is an upgrade to enable Arbitrum's support for the parent chain Ethereum's [Pectra upgrade](https://ethereum.org/en/roadmap/pectra/) scheduled for [May 7, 2025 at epoch `364032`](https://blog.ethereum.org/en/2025/04/23/pectra-mainnet). As a result, the majority of the ArbOS-specific changes revolve around implementing the relevant [Prague EIPs](https://eips.ethereum.org/EIPS/eip-7600) on Arbitrum chains. Please see below for the list of all changes included in ArbOS 40 Callisto: ### [EIP-7702: Set EOA Account code](https://eips.ethereum.org/EIPS/eip-7702) EIP-7702 introduces a new transaction type that allows Externally Owned Accounts (EOAs) to set executable code, adding account-abstraction functionality to EOAs such as delegation, batching, sponsorship, and privilege de-escalation. In terms of batching, multiple operations can be combined (i.e., token approval and token spend) in an atomic transaction. Transaction sponsorship or paymaster support is extendable to EOAs. Discrete permissioning is configurable using sub-keys. ### [EIP-2537: Precompile for BLS12-381 curve operations](https://eips.ethereum.org/EIPS/eip-2537) > **CAUTION** > > This EIP and its specified precompiles are part of ArbOS 40 Callisto but are *not* enabled. However, these precompiles will be proposed for inclusion in the next ArbOS release. This EIP introduces precompiles for performing cryptographic operations on the BLS12-381 curve, focusing on enhancing the efficiency and security of these operations. This cryptographic primitive provides 120+ bits of security for operations over pairing-friendly curves, compared to the existing BN254 precompile, which offers only 80 bits of security. BLS signature verification is the primary use case for this EIP, although many other applications that rely on point additions, multiplications, and pairing operations stand to benefit from this proposal; examples include zkSNARKS, cross-chain interactions, randomness beacons, and vector commitments. ### [EIP-2935: Serve historical block hashes from state](https://eips.ethereum.org/EIPS/eip-2935) This EIP proposes storing a wider window of block hashes in the storage of a dedicated system contract. Bundling historical block hashes within the state enables efficient data retrieval for applications that require extended access to historical block hashes, like stateless clients. If approved, ArbOS 40 will adapt this EIP to the L2 and store the same number of L2 block hashes that are generated in the time it takes for 8192 L1 blocks to build—this is approximately 27 hours' worth of L2 block hashes. ### Minor Stylus fix to correct caching behavior for contracts that do not exist ([#2998](https://github.com/OffchainLabs/nitro/pull/2998)) Currently, Stylus will cache results from calling account\_code and account\_code\_size for a contract that does not exist. We would like to propose a fix to address this so that the call returns the correct information that properly reflects the latest state of the contract’s code or code size. This change will not increment the Stylus version, so re-activation of already deployed Stylus contracts is not required. ## Pectra changes that are not included in the proposed ArbOS 40 Callisto Upgrade Support and implementation for the following EIPs are not planned to be part of ArbOS 40 Callisto: * All Ethereum Consensus Layer (CL) Pectra changes (EIP-6610, EIP-7002, EIP-7251, EIP-7549, EIP-7691) because Arbitrum chains do not have a beacon chain and therefore do not have a peer-to-peer layer like Ethereum does. * [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623): Increase calldata cost: because block size variance is less of a concern on Arbitrum chains. This lack of support is due to two reasons: first, Arbitrum chains do not require nodes to send blocks over the network through their peer-to-peer layer; instead, they rely on the parent chain’s RPC to retrieve block data. Secondly, because Arbitrum block sizes are already limited to \~100KB, so increasing calldata cost is not expected to reduce Arbitrum block sizes. * [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685): General purpose execution layer requests: because Arbitrum chains do not have a beacon chain and, therefore, there is nothing to request from the EL on Arbitrum chains. * [EIP-7840](https://eips.ethereum.org/EIPS/eip-7840): Add blob schedule to EL configuration files: because Arbitrum chains do not support posting blobs on the rollup (but otherwise still does support posting blobs to Ethereum L1). ## Special note about ArbOS 40 Callisto for chains who have not yet upgraded to use Arbitrum BoLD While ArbOS 40 Callisto will be compatible with both `nitro-contracts 3.1.0` and `nitro-contracts 2.1.3`, only chains that have Arbitrum BoLD enabled can use `nitro-contracts 3.x`. This requirement means that if your chain has not yet upgraded to use BoLD, please only use `nitro-contracts 2.1.3` for your ArbOS 40 Callisto upgrade. ## Reference links for ArbOS 40 Callisto * [Nitro v3.6.5](https://github.com/OffchainLabs/nitro/releases/tag/v3.6.5) * [nitro-contracts v3.1.0](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v3.1.0) * [nitro-contracts v2.1.3](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v2.1.3) (only relevant for Arbitrum chains that do not have BoLD enabled yet) * [README for how to upgrade your rollup contracts to support ArbOS 40 on your chain](https://github.com/OffchainLabs/chain-actions?tab=readme-ov-file#nitro-contracts-upgrades) * [AIP: ArbOS Version 40 Callisto Forum Post](https://forum.arbitrum.foundation/t/constitutional-aip-arbos-version-40-callisto/28436) * [Temperature check vote on Snapshot for ArbOS 40 Callisto](https://snapshot.box/#/s:arbitrumfoundation.eth/proposal/0x7cc26491a070c74c1a4ec5a9892571d31eb690015936a35b52c0d3a97bd5497f) * [ArbOS 40 Audit Report, from Trail of Bits](https://github.com/trailofbits/publications/blob/master/reviews/2025-05-offchainlabs-arbos40nitro-securityreview.pdf) --- > For a complete page index, fetch # ArbOS 51 Dia This page is intended for all Arbitrum node operators and Arbitrum chain owners, it summarizes the changes brought by ArbOS 51 "Dia", and what you should do to ensure a seamless upgrade. The minimum Nitro version that supports ArbOS 51 "Dia" is [Nitro v3.9.6](https://github.com/OffchainLabs/nitro/releases/tag/v3.9.6), which is available on Docker Hub with the image tag `offchainlabs/nitro-node:v3.9.6-91bf578`. This release of Nitro is a mandatory upgrade for Arbitrum One and Nova node operators. For Arbitrum One and Nova, an ArbOS upgrade requires a governance vote to activate; the vote for ArbOS 51 was [passed on December 18, 2025](https://www.tally.xyz/gov/arbitrum/proposal/53154361738756237993090798888616593723057470462495169047773178676976253908001?govId=eip155:42161:0xf07DeD9dC292157749B6Fd268E37DF6EA38395B9) and was [activated on January 8, 2026](/notices/arbos51-upgrade-notice.md). As a refresher, ArbOS upgrades get treated as Arbitrum's equivalent of a hard fork. To learn more, refer to the [Arbitrum ArbOS upgrades forum post](https://forum.arbitrum.foundation/t/arbitrum-arbos-upgrades/19695). Note that ArbOS 51 Dia is an upgrade that builds upon [ArbOS 40 Callisto](/run-arbitrum-node/arbos-releases/arbos40.md). ## Requirements * Having read and understood the [ArbOS Software Releases Overview page](/run-arbitrum-node/arbos-releases/overview.md). * Following the [Guide for how to upgrade ArbOS on your Arbitrum chain](/launch-arbitrum-chain/operate/arbos-upgrade.md). * Running [Nitro v3.9.6](https://github.com/OffchainLabs/nitro/releases/tag/v3.9.6) or higher, which is available on Docker Hub with the image tag `offchainlabs/nitro-node:v3.9.6-91bf578`. * Note that it's important to run Nitro v3.9.6 only against trusted databases. If you want to use an untrusted database, you can first remove the `wasm` directory if it exists (potentially inside the `nitro` folder). Otherwise, the database may have malicious, unvalidated code that can result in remote code execution. Avoiding unvalidated code is also mitigated by ensuring you run the Arbitrum Nitro node inside Docker. * Running [nitro-contracts v3.1.0](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v3.1.0) or higher * If your chain isn't ready to activate BoLD, use [nitro-contracts v2.1.3](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v2.1.3) instead; v3.0.0 or higher can't be used without activating BoLD. * If you want to enable [Native Token Mint/Burn capabilities](#native-token-mint-and-burn) for your chain, you must use [nitro-contracts v3.1.1](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v3.1.1) or higher. * WASM module root (consensus-v51.1): `0xc2c02df561d4afaf9a1d6785f70098ec3874765c638e3cb6dbe8d3c83333e14c` * If the Arbitrum chain posts blobs to a Fusaka-enabled Parent chain, ensure that the consensus layer client is configured correctly as per the [Fusaka upgrade notice](/notices/fusaka-upgrade-notice.md). ## High-level description of ArbOS 51 changes ArbOS 51 Dia is an upgrade for Arbitrum Chains to support the relevant EVM changes that are a part of Ethereum's [Fusaka upgrade](https://ethereum.org/en/roadmap/fusaka/), as well as additional improvements to the gas pricing algorithm, `MaxTxGasLimit` allowing full block utilization, changes to instrument gas based on specific resource usage, native token mint/burn capabilities, and a few bug fixes. Ethereum's mainnet upgrade to Fusaka was completed on [December 3, 2025 at epoch `411392`](https://notes.ethereum.org/@bbusa/fusaka-bpo-timeline). Here's the list of all changes included in ArbOS 51 Dia: ### [EIP-7951: Precompile for secp256r1 curve support](https://eips.ethereum.org/EIPS/eip-7951) This EIP implements the same functionality and interface as [RIP-7212](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md), which was activated as part of [ArbOS 31 Bianca](https://forum.arbitrum.foundation/t/aip-arbos-31-bianca-activation-of-arbitrum-stylus-rip-7212-support-nova-fee-router-proposal/25904). The main difference here is to add a point-at-infinity check and to update the comparison step in the signature verification algorithm. Developers should expect the same behavior as the EIP being proposed on Ethereum after Fusaka is activated. ### [EIP-7825: Transaction gas limit cap](https://eips.ethereum.org/EIPS/eip-7825) This EIP introduces a gas cap for individual transactions. The goal is to ensure fairer access to block space and improve network stability. For Arbitrum One and Arbitrum Nova we're proposing a 32 million gas limit (Child chain execution gas, not including parent chain gas) per transaction, which is the same as the current block gas limit. This 32 million gas limit diverges from the EIP's proposed limit of 16 million gas per transaction for Ethereum parent chain. Arbitrum chains can customize this value according to their chains' needs. ### [EIP-7642: eth/69 - history expiry and simpler receipts](https://eips.ethereum.org/EIPS/eip-7642) This networking upgrade removes deprecated fields used prior to Ethereum's Proof of Stake (PoS) transition. We're including this EIP as part of Geth upstream. This is a networking change that impacts mainly parent chain nodes. As Arbitrum nodes don't have a P2P layer, we don't expect this to have any impact on Arbitrum node operators. ### [EIP-7939: Count leading zeros (CLZ) opcode](https://eips.ethereum.org/EIPS/eip-7939) This EIP adds a new CLZ (Count Leading Zeros) opcode to efficiently count the number of zero bits at the start of a 256-bit number. This is a fundamental mathematical operation used in many algorithms, especially for mathematical computations, data compression, and cryptographic operations. Currently, implementing this operation in Solidity requires complex and expensive code—this opcode makes it much cheaper and faster. ### [EIP-7823: Set upper bounds for ModExp](https://eips.ethereum.org/EIPS/eip-7823) This EIP introduces an 8192-bit (1024 byte) limit on each input to the ModExp cryptographic precompile. ModExp has been a source of consensus bugs due to unbounded inputs. By setting practical limits that cover real-world use cases (like RSA verification), this reduces the testing surface area and paves the way for future replacement with more efficient EVM code. ### [EIP-7883: ModExp gas cost increase](https://eips.ethereum.org/EIPS/eip-7883) This EIP increases the gas cost of the ModExp cryptographic precompile to address underpriced operations. It raises the minimum cost from 200 to 500 gas and doubles the costs for large inputs over 32 bytes. ### [EIP-7910: eth\_config JSON-RPC method](https://eips.ethereum.org/EIPS/eip-7910) This EIP provides a new RPC method that allows the Arbitrum Nitro node to respond with key configuration variables, offering node operators the ability to gain greater confidence that their Nitro nodes are correctly configured and prepared for upcoming forks. In future Nitro releases, we expect to include additional fields specific to Arbitrum chains. This update is at the RPC level and may be enabled later than the ArbOS 51 Dia upgrade. ### [EIP-2537: Precompile for BLS12-381 curve operations](https://eips.ethereum.org/EIPS/eip-2537) As [disclosed previously](https://forum.arbitrum.foundation/t/disclosure-of-support-for-eip-2537-on-arbitrum-one-and-nova/29720), the precompiled contracts for performing various operations over the BLS12-381 elliptic curve, including BLS Signature verification, were added but not properly enabled in [ArbOS 40 Callisto](/run-arbitrum-node/arbos-releases/arbos40.md) as originally expected. ArbOS 51 Dia will now enable `EIP-2537`. ### ArbOS block limit change: Effective block gas limit Since ArbOS 51 introduces a `MaxTxGasLimit`, the State Transition Function (STF) will be relaxed in ArbOS 51 to allow the final transaction in a block to use up to the `MaxTxGasLimit` even if it would cause the block to exceed `MaxBlockGasLimit`. This means that the "Effective Block Gas Limit" is really `MaxBlockGasLimit + MaxTxGasLimit`. In previous versions of ArbOS, the Sequencer would skip transactions if the transaction request's `GasLimit` minus the parent chain data posting gas exceeded the gas remaining in the block. The new algorithm is more efficient because the sequencer doesn't need to keep searching through the queue of transactions to find one that fits in the remaining block gas, and can continue to add transactions until the unused block gas is 0. This change doesn't affect the `GasTarget`, and therefore doesn't affect how much overall gas per second the chain will use—only how transactions using that gas could be divided between different blocks. ### Raising the gas target, increasing the min L2 base fee, & improving the pricing algorithm As part of our strategy for scaling Arbitrum technology, the ArbOS 51 release includes a slight change to improve the pricing algorithm for Arbitrum One and Nova. This improvement is aimed at reducing the severity, frequency, and duration of high L2 gas prices during periods of elevated demand on the network. Concretely, the changes are to: * Replace the current single gas target and single adjustment window, with a new model that employs multiple (higher) gas targets, measured over multiple adjustment windows. * Increase the default minimum L2 base fee from 0.01 gwei per gas to 0.02 gwei per gas. To read more about this change, see the [AIP for raising the gas target & improvements to the pricing algorithm](https://forum.arbitrum.foundation/t/aip-raise-the-gas-target-implement-improvements-to-the-pricing-algorithm/30182). ### A constraint-based pricing change: STF instrumentation to track multi-gas We've instrumented Arbitrum's State Transition Function (STF) to track gas usage across multiple resource types including computation, storage access, storage growth, and history growth, rather than only a single total based on opcodes. This work lays the foundation for dynamic, constraint-based pricing where gas fees can adjust based on the most constrained resource at the network level. The goal is to create more stable prices, improve responsiveness to spikes, and allow the network to safely increase throughput without overloading node hardware. In this release, none of the constraints are enabled, so there won't be any impact on current gas prices. This update simply adds the ability to measure and record per-resource usage, with actual pricing changes coming in a later version once constraints are configured, benchmarked and tested. To read more about this feature, see the [dynamic pricing explainer](https://blog.arbitrum.io/dynamic-pricing-explainer/). ### Native token mint and burn Native token mint and burn is a feature that allows Arbitrum chains to use interoperability-enabled token standards (e.g., LayerZero OFTs, xERC20s, native USDC) as native gas tokens on their chains. Currently, Arbitrum chains are designed to "lock and mint" native gas tokens on the chain's canonical Bridge. However, doing so means that these "locked and minted" native gas tokens can't interact with third-party cross-chain adapter contracts. This new feature lets an Arbitrum chain delegate minting and burning of its native gas token to a trusted bridge provider (e.g., LayerZero OFT). Native token mint and burn is included in ArbOS 51 Dia for the benefit of Arbitrum chains (reducing the need for forks) and to streamline development and testing into a single codebase. There are no plans to enable this feature on Arbitrum One or Arbitrum Nova, consequently this feature will be explicitly left disabled for Arbitrum One and Arbitrum Nova. To read more about this feature, see the [Native Token Mint/Burn enablement guide](/launch-arbitrum-chain/chain-config/costs/configure-native-mint-burn.md). Warning If you want to enable Native Token Mint/Burn capabilities for your chain, you must use [nitro-contracts v3.1.0](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v3.1.0) or higher. ### A few bug fixes * ArbOS didn't get updated for parent chain calldata price increase * This change standardizes the calculation of gas units for compressed Batch calldata across the codebase by replacing hard-coded values with a method call (tokenGasUnits). * `EIP-7702` precompile delegation behavior divergence * Previously, calls to precompiles could execute an INVALID opcode instead of succeeding with no execution. ArbOS 51 Dia will update code to align with `EIP-7702` spec to treat precompile code as empty during delegation. * ARM and x86 divergence * This change adds a map to store transaction hash along with its gas used to bypass transaction execution for a problematic transaction execution which diverged between ARM and x86 architectures. This was added in to hardcode one transaction that caused the divergence on Arbitrum Sepolia, as disclosed in the [security council emergency Action report](https://forum.arbitrum.foundation/t/security-council-emergency-action-10-13-2025/30093). * The default WASM Stack Depth value in ArbOS is now set to 22,000, preventing new chains from encountering the same divergence issue. ## Fusaka EIPs that aren't proposed to be in ArbOS 51 Dia Support and implementation for the following EIPs aren't planned to be part of ArbOS 51 Dia: * [EIP-7594](https://eips.ethereum.org/EIPS/eip-7594), [EIP-7918](https://eips.ethereum.org/EIPS/eip-7918), and [EIP-7892](https://eips.ethereum.org/EIPS/eip-7892), since Arbitrum chains don't have blob data markets (though they do support posting blob data to a non-Arbitrum parent chain) * [EIP-7917](https://eips.ethereum.org/EIPS/eip-7917), since Arbitrum chains don't have a beacon chain and therefore don't have a peer-to-peer layer like Ethereum does * [EIP-7934](https://eips.ethereum.org/EIPS/eip-7934), this EIP is to help propagating blocks between nodes. Arbitrum doesn't do that—it sends messages (which are limited) and each node builds every block by itself * [EIP-7907](https://eips.ethereum.org/EIPS/eip-7907), this EIP is no longer Scheduled For Inclusion (SFI) for Fusaka, as agreed upon by Client teams during [ACDE 216](https://ethereum-magicians.org/t/allcoredevs-execution-acde-216-july-17-2025/24770/2) on July 17, 2025. We're currently exploring alternative ways to increase the Smart Contract size limit that don't interfere with the ability for Arbitrum chains to support `EIP-7907` in the future. See this [forum post reply](https://forum.arbitrum.foundation/t/non-constitutional-proposal-to-direct-the-arbitrum-foundation-to-implement-an-extended-version-of-eip-7907-and-for-the-dao-to-ratify-its-deployment-on-arbitrum-one/29375/3) for more details about this. * [EIP-7935](https://eips.ethereum.org/EIPS/eip-7935), since Arbitrum chains already have a default gas target of 28Mgas/s and we have separate, alternative plans for increasing the gas limit through other means, as mentioned in [Scaling Arbitrum everywhere](https://blog.arbitrum.io/scaling-arbitrum-everywhere/). ## Special note about ArbOS 51 Dia for chains that haven't yet upgraded to use Arbitrum BoLD While ArbOS 51 Dia will be compatible with both `nitro-contracts 3.1.X` and `nitro-contracts 2.1.3`, only chains that have Arbitrum BoLD enabled can use `nitro-contracts 3.x`. This requirement means that if your chain hasn't yet upgraded to use BoLD, only use `nitro-contracts 2.1.3` for your ArbOS 51 Dia upgrade. ### Special note for chains posting data to [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623) enabled parent chains ArbOS 51 introduces the `gasFloorPerToken` parameter for chains that post to [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623)-enabled parent chains. This setting enforces a minimum price for data-heavy transactions to ensure that the cost of posting calldata to the parent chain is fully recovered, even when execution gas is low. **Background on EIP-7623 support** EIP-7623 was part of the Ethereum Pectra upgrade and it was intentionally not enabled for Arbitrum chains in [ArbOS 40 Callisto](https://docs.arbitrum.io/run-arbitrum-node/arbos-releases/arbos40#pectra-changes-that-are-not-included-in-the-proposed-arbos-40-callisto-upgrade). This is because block size variance is less of a concern on Arbitrum chains.
Hence, this setting does not apply to chains settling to Arbitrum One or Nova, but is required for chains settling to parent chains that have EIP-7623 enabled using calldata. Refer to [gas floor per token guide](/launch-arbitrum-chain/chain-config/costs/gas-optimization.md#gas-floor-per-token) for detailed instructions on how to configure this value via the `ArbOwner` precompile. ### Reference links for ArbOS 51 Dia * [Guide for how to upgrade ArbOS on your Arbitrum chain](/launch-arbitrum-chain/operate/arbos-upgrade.md) * [README for how to upgrade your rollup contracts to support ArbOS 51 on your chain](https://github.com/OffchainLabs/chain-actions?tab=readme-ov-file#nitro-contracts-upgrades) * [Nitro v3.9.6](https://github.com/OffchainLabs/nitro/releases/tag/v3.9.6) * [nitro-contracts v3.1.1](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v3.1.1) * [nitro-contracts v2.1.3](https://github.com/OffchainLabs/nitro-contracts/releases/tag/v2.1.3) (only relevant for Arbitrum chains that don't have BoLD enabled yet) * [Onchain vote to Activate ArbOS 51 (Dia) and Gas Pricing Updates](https://www.tally.xyz/gov/arbitrum/proposal/53154361738756237993090798888616593723057470462495169047773178676976253908001?govId=eip155:42161:0xf07DeD9dC292157749B6Fd268E37DF6EA38395B9) * [Temperature check vote on Snapshot for "AIP: ArbOS Version 50 Dia"](https://snapshot.box/#/s:arbitrumfoundation.eth/proposal/0x33754da4006d0ef38666ec5d5e85fd0966a891a594ab9dc21f23beedea2d330b) * [Temperature check vote on Snapshot for "AIP: Raising the gas target & improvements to pricing algorithm"](https://snapshot.box/#/s:arbitrumfoundation.eth/proposal/0x4a96a91d162975de0d402b83ca8b8a24e808ca357150120fc0d44ae0bf1cc4a5) * [Forum post for "AIP: ArbOS Version 50 Dia"](https://forum.arbitrum.foundation/t/constitutional-aip-arbos-version-50-dia/) * [Forum post for "AIP: Raising the gas target & improvements to pricing algorithm"](https://forum.arbitrum.foundation/t/aip-raise-the-gas-target-implement-improvements-to-the-pricing-algorithm/30182) * [ArbOS 50 & ArbOS 51 Audit Report, from Trail of Bits](https://github.com/trailofbits/publications/blob/master/reviews/2025-12-offchain-arbos50-and-51-securityreview.pdf) --- > For a complete page index, fetch # ArbOS software releases: Overview > **INFO** > > This document provides an overview of Nitro node software releases that upgrade ArbOS. Visit the [Nitro Github repository](https://github.com/OffchainLabs/nitro/releases) for a detailed index of Nitro releases. Arbitrum chains are powered by Arbitrum nodes running the Nitro software stack. The Nitro software stack includes [ArbOS](https://forum.arbitrum.foundation/t/arbitrum-arbos-upgrades/19695), the child chain EVM hypervisor that facilitates the execution environment of an Arbitrum chain. Although new Nitro releases are shipped regularly, only a subset of Nitro releases carry ArbOS upgrades. These special Nitro releases are significant because ArbOS upgrades are Arbitrum's equivalent to a ["hard fork"](https://ethereum.org/en/history/)—an upgrade that alters a node's ability to produce valid Arbitrum blocks. This is why validator nodes supporting a public Arbitrum chain (One, Nova) **must update Nitro** whenever a new ArbOS version is released and voted for adoption by the ArbitrumDAO. > **NOTE** > > Every Nitro release is backwards compatible. In other words, the latest version of Nitro will support all previous ArbOS releases. This means that your validator's Nitro version must be greater than or equal to the version that includes the latest ArbOS upgrade. > **INFO** — How often should I be upgrading my ArbOS version? > > It is strongly recommended to keep your Nitro's node software up-to-date as best you can to ensure you are benefting from the latest improvements to the Arbitrum technology stack. ArbOS version bumps are especially important because these upgrades change how Arbitrum nodes produce and validate assertions on a rollup's state. ArbOS upgrades are carried out by the chain's owner; in the case of Arbitrum One and Nova, the owner is the Arbitrum DAO and so an upgrade will require a governance proposal and vote to pass to complete the upgrade. [This is an example of a Nitro release that contains an ArbOS version bump, specifically to ArbOS 11](https://github.com/OffchainLabs/nitro/releases/tag/v2.2.0). Visit [How Arbitrum works](/how-arbitrum-works/inside-arbitrum-nitro.md) to learn more about Nitro's architecture; more information about ArbOS software releases is available on [the Arbitrum DAO forum](https://forum.arbitrum.foundation/t/arbitrum-arbos-upgrades/19695). ## List of available ArbOS releases * [Dia (ArbOS 51)](/run-arbitrum-node/arbos-releases/arbos51.md) * [Callisto (ArbOS 40)](/run-arbitrum-node/arbos-releases/arbos40.md) * [Bianca (ArbOS 32)](/run-arbitrum-node/arbos-releases/arbos32.md) * [Atlas (ArbOS 20)](/run-arbitrum-node/arbos-releases/arbos20.md) * [ArbOS 11](/run-arbitrum-node/arbos-releases/arbos11.md) ## Naming and numbering scheme Beginning with ArbOS 20, ArbOS releases use the name of planetary moons in our solar system, ascending in alphabetical order (i.e., the next ArbOS upgrade after ArbOS 20 "Atlas" will be a planetary moon that begins with the letter "B"). The number used to denote each upgrade will increment by 10, starting from ArbOS 20 (i.e., the next ArbOS upgrade after ArbOS 20 will be ArbOS 31). This was done because there are teams who have customized their Arbitrum chain's [behavior](/launch-arbitrum-chain/extend-the-protocol/stf.md) or [precompiles](/launch-arbitrum-chain/extend-the-protocol/precompiles.md) and who may wish to use ArbOS's naming schema between official ArbOS version bumps (e.g., ArbOS 12 could be the name of a customized version of ArbOS for a project's L3 Arbitrum chain). Note that there may be cases where special optimizations or critical fixes are needed for a specific family of ArbOS releases that will diverge from the standard numbering scheme described above. For example, ArbOS 32 will be the canonical ArbOS version for the “Bianca” family of releases. Node operators and chain owners are expected to upgrade from ArbOS 20 directly to ArbOS 32 (instead of ArbOS 30 or ArbOS 31). ## Network status To view the status and timeline of network upgrades on Arbitrum One and Nova, [please visit this page](https://docs.arbitrum.foundation/network-upgrades). ## Expectations for Arbitrum chain owners For Arbitrum chain owners or maintainers: it is important to note that *before* upgrading your Arbitrum chain(s) to the newest ArbOS release, we strongly encourage waiting at least four weeks after the new ArbOS release becomes active on Arbitrum One and Nova before attempting the upgrade yourself. The rationale behind this short time buffer is to allow the Offchain Labs team to address any upgrade issues or stability concerns that may arise with the initial rollout so that we can minimize the chances of your chain(s) hitting the same or similar issues and to maximize the likelihood of a smooth upgrade. Arbitrum chains, as always, can pick up new features and enable new customizations as they see fit. However, this delay ensures a consistent user experience (UX) across all Arbitrum chain owners and managers for these critical upgrades. Enabling an ArbOS upgrade isn't as simple as bumping your chain's Nitro node version. Instead, there are other steps required that are outlined in our docs on [How to upgrade ArbOS on your Arbitrum chain](/launch-arbitrum-chain/operate/arbos-upgrade.md). Be sure to follow them and let us know if you encounter any issues. ## Stay up to date To stay up to date with proposals, timelines, and statuses of network upgrades to Arbitrum One and Nova: * Subscribe to the [Arbitrum Node Upgrade Announcement channel on Telegram](https://t.me/arbitrumnodeupgrade) * Join both the `#dev-announcements` and `#node-runners` Discord channels in the [Arbitrum Discord server](https://discord.gg/arbitrum) * Follow the official Arbitrum ([`@Arbitrum`](https://twitter.com/arbitrum)) and Arbitrum Developers ([`@ArbitrumDevs`](https://twitter.com/ArbitrumDevs)) X accounts, formerly Twitter. --- > For a complete page index, fetch # How to assign roles to a Nitro node A Nitro node's *role* is not a separate piece of software. Every role in this article runs the same `nitro` binary; what makes a node a sequencer, a batch poster, a validator, or a plain RPC node is the set of configuration flags you pass at startup. The exceptions are the feed relay — a small standalone `relay` binary shipped inside the same Docker image — and the optional split-validation setup, which moves block validation into a separate `nitro-val` binary; both are built from the same repository. Because roles are just configuration, you assign or change a role by editing flags and restarting. This page explains what each role's defining flags are, what else each role needs to operate (a funded parent-chain wallet, a bond, feed connectivity, a parent-chain connection), and how to convert a node from one role to another safely. If you already know which role you want, go straight to its guide — each one is linked from the table below. Read this page when you're deciding which role to run, want to see how the roles relate to each other, or need to convert an existing node from one role to another. Flags can be passed on the command line or collected in a JSON configuration file loaded with `--conf.file`. For how flag names, defaults, and the config file relate to each other, see [Nitro configuration system](/run-arbitrum-node/nitro/configuration-system.md). This article covers only the flags that define and support each role; for the complete flag list, see the [CLI flags reference](/run-arbitrum-node/nitro/cli-flags-reference.md). Roles can combine on a single node (a sequencer can also post batches, for example), but this article describes them separately so that each role's requirements are clear. ## Roles at a glance | Role | Defining flags | Wallet and bond | Runs how many? | Full guide | | ------------------------------------ | ------------------------------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------- | | RPC node (full node) | none (the default) | No wallet, no bond | Many (horizontal scaling) | [Run a full node](/run-arbitrum-node/run-full-node.md) | | Archive node | `--execution.caching.archive` | No wallet, no bond | Many (same as RPC nodes) | [Run an archive node](/run-arbitrum-node/more-types/run-archive-node.md) | | Sequencer | `--node.sequencer` + `--execution.sequencer.enable` | No wallet, no bond | One per chain (or one coordinator set) | [Run a sequencer node](/run-arbitrum-node/sequencer/run-sequencer-node.md) | | Batch poster | `--node.batch-poster.enable` | Funded parent-chain wallet; no bond | One active (Redis lock coordinates a set) | [Run a batch poster](/launch-arbitrum-chain/run-a-node/batch-poster.md) | | Validator (staker) | `--node.staker.strategy` set to an active value | Funded wallet and bond for active strategies only | One active per wallet | [Run a validator](/run-arbitrum-node/more-types/run-validator-node.md) | | Validation server (split validation) | `nitro-val` binary + `--node.block-validator.validation-server.url` on the node | No wallet, no bond (all onchain action stays on the node) | Many (one node can use several) | [Run a split validator node](/launch-arbitrum-chain/run-a-node/split-validator-node.md) | | Feed relay | `relay` binary + `--node.feed.input.url` + `--chain.id` | No wallet, no bond, no parent-chain connection | Many (stateless fan-out) | [Run a feed relay](/run-arbitrum-node/run-feed-relay.md) | Every role except the feed relay and the split-validation server needs a parent-chain connection through `--parent-chain.connection.url`. Nodes whose parent chain is Ethereum L1 also need `--parent-chain.blob-client.beacon-url` to read batches posted as EIP-4844 blobs. For well-known chains, setting `--chain.id` or `--chain.name` auto-fills defaults such as `--execution.forwarding-target` and `--node.feed.input.url` from the chain info embedded in the binary (the forwarding target is only auto-filled when the sequencer is disabled). ## RPC node (full node) An RPC node — often just called a full node — is the default role: it follows the chain and serves JSON-RPC, but it does not sequence, post batches, or actively validate. None of the role-defining flags are enabled by default. | Flag | Default | Description | | ------------------------------- | ------- | ---------------------------------------------------------------------------------------------- | | `--node.sequencer` | false | Consensus-side sequencer switch; off, so the node does not order transactions | | `--execution.sequencer.enable` | false | Execution-side sequencer switch; off, so the node does not build blocks itself | | `--node.batch-poster.enable` | false | Off, so the node never posts batches to the parent chain | | `--node.staker.enable` | true | Staker module on, but with the default watchtower strategy it only observes (see admonition) | | `--execution.forwarding-target` | `""` | Where the node forwards `eth_sendRawTransaction`; auto-filled from chain info for known chains | Beyond those defaults, a full node needs only the shared basics — `--parent-chain.connection.url` and `--chain.id` (or `--chain.name`) — plus `--http.addr` or `--ws.addr` to expose JSON-RPC; the servers stay off until an address is set. Operationally, a full node needs a parent-chain connection but no funded wallet and no bond. Feed connectivity is strongly recommended for low latency but is not required: a node with no feed input still syncs by reading batches from the parent chain, just with higher latency. You can run as many RPC nodes as you like; they share no state and need no coordination. Info `--node.staker.enable` defaults to `true`, so a default full node is technically a watchtower validator: it watches onchain assertions and logs when one disagrees with its locally computed state. With the default watchtower strategy it loads no wallet and posts no bond, so it takes no onchain action. This is why "converting to a validator" is mostly a matter of changing the strategy and providing a wallet and bond, rather than enabling a module. To silence the watchtower entirely, set `--node.staker.enable=false`. For the full setup, including snapshots, pruning, and Docker details, see [How to run a full node](/run-arbitrum-node/run-full-node.md). ## Archive node An archive node is an RPC node that retains every historical state instead of garbage-collecting old ones, so it can serve `eth_call`, balance, and trace queries at arbitrary past blocks. It is a storage variant of the full node, not a different protocol role: same binary, one extra caching flag. | Flag | Default | Description | | ---------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `--execution.caching.archive` | false | Retains past block state on disk instead of pruning it | | `--execution.caching.state-scheme` | hash | State storage scheme (`hash` or `path`); `path` produces a much smaller archive but cannot be used on a node that must validate | | `--execution.rpc.classic-redirect` | `""` | Arbitrum One only: URL of an Arbitrum Classic node that serves queries for pre-Nitro blocks | | `--init.latest` | `""` | Set to `archive` to bootstrap from the latest archive snapshot (`hash` scheme only; see the archive guide for `path`-scheme snapshots) | An archive node needs no wallet and no bond, and you can run as many as you like. The cost is disk: an Arbitrum One archive database is measured in terabytes (see the archive guide for current figures). Enabling archive also makes the node keep everything else — Nitro automatically disables the consensus message pruner and retains the full transaction-hash lookup index. Arbitrum One is the only chain with pre-Nitro (Classic) history, so only Arbitrum One archive nodes need `--execution.rpc.classic-redirect` pointed at an [Arbitrum Classic node](/run-arbitrum-node/more-types/run-classic-node.md); every other chain launched directly on Nitro. For snapshots, hardware sizing, and the full setup, see [How to run an archive node](/run-arbitrum-node/more-types/run-archive-node.md). ## Sequencer The sequencer orders incoming transactions, produces blocks, and publishes the sequencer feed. Enabling it requires two flags that must agree with each other. | Flag | Default | Description | | ------------------------------ | ------- | -------------------------------------------------------------------------------- | | `--execution.sequencer.enable` | false | Execution-layer sequencer: the node orders queued transactions and builds blocks | | `--node.sequencer` | false | Consensus-layer counterpart; must agree with `--execution.sequencer.enable` | Nitro does not stop you if the two flags disagree — it logs an error and keeps running in a broken half-configuration — so always change them together. A sequencer must also declare how it coordinates with other sequencer instances, or it refuses to start. Enable the Redis-backed coordinator, or explicitly opt out with the dangerous single-sequencer flag. | Flag | Default | Description | | ------------------------------------------- | ------- | ------------------------------------------------------------------------------------------- | | `--node.seq-coordinator.enable` | false | Enables the Redis-based coordinator for a redundant sequencer set | | `--node.dangerous.no-sequencer-coordinator` | false | DANGEROUS: allows sequencing without a coordinator (single-sequencer or development setups) | | `--execution.forwarding-target` | `""` | Must stay empty on a sequencer; setting it with the sequencer enabled is a hard error | | `--node.feed.output.enable` | false | Starts the broadcaster that publishes the sequencer feed | | `--node.delayed-sequencer.enable` | false | Includes parent-chain (delayed inbox) messages once their block is safe (default) or final | The coordinator's Redis URLs, the feed server's address and port, and feed signing are setup details covered in the sequencer guide. A sequencer needs a parent-chain connection (`--parent-chain.connection.url`), which the coordinator hard-requires. It needs no funded wallet and no bond; sequencing posts nothing onchain. The only case where a pure sequencer loads a key is `--node.feed.output.signed=true`, and even then the key is used to sign feed messages, not to fund transactions. Batch posting and bonding are separate roles. For the complete sequencer setup and the coordinator design, see [How to run a sequencer node](/run-arbitrum-node/sequencer/run-sequencer-node.md) and [How to run a Sequencer Coordination Manager (SQM)](/run-arbitrum-node/sequencer/run-sequencer-coordination-manager.md). ## Batch poster The batch poster compresses queued sequencer messages into batches and posts them to the parent chain's Sequencer Inbox. It does not need to be the sequencer; a separate node can post batches from the messages it receives over the feed. | Flag | Default | Description | | ---------------------------- | ------- | --------------------------------------------------- | | `--node.batch-poster.enable` | false | Master switch: enables building and posting batches | The batch poster needs a signer for its parent-chain transactions (a local wallet or an external signer) and, for a redundant set, a Redis lock. | Flag | Default | Description | | ----------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------- | | `--node.batch-poster.parent-chain-wallet.*` | — | The funded parent-chain account (keystore or private key) that signs and pays for batches | | `--node.batch-poster.data-poster.external-signer.url` | `""` | External signer RPC; replaces the local wallet for signing batch transactions | | `--node.batch-poster.redis-url` | `""` | Redis URL for the leader lock that keeps only one poster active in a redundant set | A batch poster requires a **funded parent-chain wallet**: it pays parent-chain gas for every batch it posts. Fund the account with enough native currency of the parent chain, and make sure the poster's address is allowlisted as a batch poster on the chain's Sequencer Inbox contract, or its transactions revert. It posts no bond. On AnyTrust chains, the node still requires the local batch-poster key even when an external signer submits the batch transactions; the key signs the data availability store requests. The address the batch poster uses must not be the same address as the staker when the staker is active (a non-watchtower validator); the node rejects a shared address in that case. For chain-owner configuration, blob posting, and troubleshooting, see [Run a batch poster](/launch-arbitrum-chain/run-a-node/batch-poster.md). ## Validator (staker) A validator watches the chain's onchain assertions and, depending on its strategy, participates in BoLD and legacy disputes. The behavior is set by the strategy, not by a separate enable switch. | Flag | Default | Description | | ------------------------------- | ---------- | ------------------------------------------------------------------------------------------ | | `--node.staker.enable` | true | Enables the staker module (passive by default because of the watchtower strategy) | | `--node.staker.strategy` | Watchtower | Selects behavior: `watchtower`, `defensive`, `stakeLatest`, `resolveNodes`, or `makeNodes` | | `--node.block-validator.enable` | false | Block-by-block validation; force-enabled for any non-watchtower (active) strategy | With the default `watchtower` strategy the validator is observe-only: it loads no wallet and posts no bond, and it merely logs if an assertion disagrees with its local state. Any other strategy is *active* and needs a funded wallet and a bond. | Flag | Default | Description | | ----------------------------------------------- | ------- | --------------------------------------------------------------------------------------- | | `--node.staker.parent-chain-wallet.*` | — | The funded parent-chain wallet (keystore or private key) an active validator acts from | | `--node.staker.data-poster.external-signer.url` | `""` | External signer RPC; an alternative to a local key | | `--node.staker.redis-url` | `""` | Redis URL backing the staker's transaction queue (queue persistence, not a leader lock) | An active validator needs a funded parent-chain wallet for gas and posts a **bond** when it acts. Under BoLD, the node reads the required bond size from the chain's contracts (it is an onchain chain parameter, not a node flag), and with `--node.bold.auto-deposit` and `--node.bold.auto-increase-allowance` enabled by default, it deposits and approves the stake token automatically when entering a bond. Active strategies also require block validation, which the node force-enables. On chains where the validator allowlist is active, the wallet address must also be allowlisted; the allowlist is enforced by the chain's contracts, so transactions from a non-allowlisted validator revert. There is no leader-election or Redis-lock mechanism for the validator, unlike the batch poster or the sequencer coordinator. `--node.staker.redis-url` only moves the pending-transaction queue into Redis for failover of a single logical staker; it is not a lock. You must ensure only one active staker runs per wallet. For the strategy comparison table, wallet setup, and BoLD details, see [How to run a validator](/run-arbitrum-node/more-types/run-validator-node.md). ### Split validation Block validation — re-executing blocks against the chain's WASM machines — is the CPU-heavy part of validating. By default this work stays inside the node: with block validation on, the default `--node.block-validator.validation-server.url` value of `self-auth` starts an internal validation server reached over the node's own authenticated websocket loopback. Split validation moves that work to a separate machine running the `nitro-val` binary, while the wallet, the bond, and the record of validation progress all stay on the main node. On the validation server (`nitro-val` binary): | Flag | Default | Description | | ----------------------------- | ----------- | -------------------------------------------------------------------------------------------------- | | `--auth.addr` | `127.0.0.1` | Interface the JWT-authenticated validation API listens on; set it to an address the node can reach | | `--auth.port` | `8549` | Port for the validation API | | `--auth.jwtsecret` | `""` | Path to the shared 32-byte hex JWT secret file; auto-generated if unset | | `--validation.wasm.root-path` | `""` | Path to the folders holding the validation machines (one per WASM module root) | On the main node (`nitro` binary): | Flag | Default | Description | | ------------------------------------------------------- | --------- | --------------------------------------------------------------------------------- | | `--node.block-validator.validation-server.url` | self-auth | Where validation work goes; set to the `ws://host:port` of the `nitro-val` server | | `--node.block-validator.validation-server.jwtsecret` | `""` | Path to the same JWT secret file the server uses | | `--node.block-validator.validation-server-configs-list` | default | JSON array of server configs; lets one node fan validation out to several servers | The validation server holds no wallet and posts no bond, and it is stateless — it runs with an ephemeral data directory, so you can replace a server in place, or add servers with a node restart, without losing any validation progress. The `--auth.*` flags exist on both binaries with the same defaults; on the main node they back the default `self-auth` loopback. Switching between in-process and split validation is a configuration change plus a restart; no database migration is involved. For Docker images, Helm charts, and the full setup, see [Run a split validator node](/launch-arbitrum-chain/run-a-node/split-validator-node.md) and [Docker images and CLI binaries](/run-arbitrum-node/nitro/docker-and-cli-binaries.md). ## Feed relay The feed relay is not a mode of the `nitro` binary. It is a separate `relay` binary built from the same repository and shipped in the same Docker image; you run it by overriding the container entrypoint to `relay`. The relay subscribes to a sequencer feed and re-broadcasts it to downstream nodes. It is stateless fan-out: no parent-chain connection, no wallet, no bond, and no database. | Flag | Default | Description | | --------------------------------- | ------- | -------------------------------------------------------------------------------- | | `--node.feed.input.url` | `[]` | Sequencer feed source(s) the relay subscribes to; the relay will not start empty | | `--chain.id` | 0 | Chain ID; required, and embedded in the feed handshake for downstream clients | | `--node.feed.output.addr` | `""` | Address to bind the relay's feed output to (empty binds all interfaces) | | `--node.feed.output.port` | `9642` | Port the relay's feed output listens on | | `--node.feed.input.secondary-url` | `[]` | Failover feed source(s), started in order when the primary feeds fail | Downstream nodes then point their own `--node.feed.input.url` at the relay. You can run as many relays as you like: each one independently subscribes upstream and serves its downstream clients, and relays can even be chained. A node connected to multiple feeds or relays deduplicates messages by sequence number, so redundant feed sources are safe. For setup, Docker commands, and Kubernetes charts, see [How to run a feed relay](/run-arbitrum-node/run-feed-relay.md). ## Converting between roles Converting a node from one role to another means changing its flags and restarting, plus arranging whatever the target role needs operationally (funding a wallet, arranging a bond and allowlisting, provisioning Redis, or opening a feed port). Before changing any role, read the safety rules below. Warning Some roles must not be duplicated for the same chain: * **Never run two sequencers for the same chain** unless they are in one coordinator (SQM) set. Two uncoordinated sequencers fork the feed and double-order transactions; only the ordering that reaches the parent-chain inbox survives, and nodes that followed the other ordering must reorg or halt. * **Never run two batch posters for the same chain outside the Redis lock.** They race on nonces and batch positions, and the loser's parent-chain transaction reverts, burning gas. The node-side protections are the Redis lock and revert polling. * **Never run two active stakers with the same wallet.** There is no built-in staker leader election, so they conflict on nonces and stall each other. * **Safe to run many:** RPC nodes, archive nodes, feed relays, and validation servers. They share no protocol state and need no coordination. Role-specific conversion notes: * **RPC node to sequencer:** set both `--node.sequencer=true` and `--execution.sequencer.enable=true` (they must agree), clear `--execution.forwarding-target` (setting it with the sequencer enabled is a hard error), publish the feed with `--node.feed.output.enable=true`, enable `--node.delayed-sequencer.enable=true`, and decide on coordination — either the Redis coordinator or `--node.dangerous.no-sequencer-coordinator=true`. * **Sequencer to RPC node:** set both sequencer flags to false, re-set `--execution.forwarding-target` to the chain's sequencer endpoint (or `null`, or rely on the chain-info default), disable `--node.delayed-sequencer.enable` and `--node.seq-coordinator.enable`, and re-add `--node.feed.input.url`. * **Enabling the batch poster:** fund the parent-chain wallet and get its address allowlisted on the Sequencer Inbox before you start posting; provide a signer and, for a redundant set, a shared Redis URL. * **Watchtower to active validator:** set `--node.staker.strategy` to an active value, provide and fund a wallet, arrange the bond, and let block validation come on automatically. This adds substantial CPU, memory, and disk needs. * **Full node to archive node:** flipping `--execution.caching.archive=true` on an existing database only archives state from that point forward; history the node already pruned is not backfilled. For full history, re-initialize from an archive snapshot (`--init.latest archive`) or — on a `hash`-scheme database that still holds an early state — rebuild missing states by re-execution with `--init.recreate-missing-state-from` (very slow, and not supported with the `path` state scheme). No wallet or funding is involved. * **Moving validation to a separate machine (split validation):** start `nitro-val` on the new machine with the validation machine folders and a reachable `--auth.addr`, share the JWT secret file between the two processes, and point the node's `--node.block-validator.validation-server.url` and `.jwtsecret` at the server (the full guide uses the equivalent `--node.block-validator.validation-server-configs-list` JSON form, which is also how you configure multiple servers). Validation progress stays in the node's database, so this is a flags-and-restart change in both directions. * **Decommissioning an active validator:** do not simply kill the node. On pre-BoLD chains, a staker that keeps running with its wallet and an active strategy (`defensive` keeps the wallet loaded without seeking new bonds) automatically returns its old deposit and withdraws the funds once the assertion it bonded on is confirmed; only then switch to `watchtower` or disable the staker and shut down. Switching to `watchtower` too early doesn't work: it loads no wallet, so nothing can withdraw. On BoLD chains, withdrawing the bond is a manual onchain operation. Either way, stopping the node early leaves the bond locked onchain until you withdraw it. For background on how nodes fit together, see the [Nodes overview](/run-arbitrum-node/overview.md) and, for the data flow between the sequencer, feed, and full nodes, [Data availability](/run-arbitrum-node/data-availability.md#how-full-nodes-sync-the-data-from-the-sequencer-feed). --- > For a complete page index, fetch # Beacon Nodes: Historical Blobs Layer 2 network operators must connect to an Ethereum beacon chain node with historical blob data to ensure proper functioning of the Nitro node software, or risk failure in fetching blob data. If you don't run your own beacon node, see [Ethereum beacon chain RPC providers](/run-arbitrum-node/l1-ethereum-beacon-chain-rpc-providers.md) for a curated list of third-party providers and their historical-blob support status. ## Impacted audiences Required action will be required from: RPC nodes, Arbitrum One / Nova node operators, Arbitrum chain node operators #### If you run a Nitro node and use an external L1 Ethereum beacon chain RPC URL * Confirm that your external L1 beacon chain RPC provider has configured their L1 beacon chain node to subscribe to all subnets. #### If your external L1 beacon chain RPC doesn't subscribe to all subnets: * Switch to a provider that does. #### If you run a Nitro node and operate your own L1 Ethereum beacon chain node: * Add the new flag (refer to [specific client flags](#specific-client-flags)) to your beacon node's configuration. > **INFO** — Note > > Ensure that the external L1 beacon chain RPC provider you're using subscribes to all subnets. ## L1 beacon chain node flags ### Prysm Consensus Layer clients Prysm nodes have a new beacon node flag `--subscribe-all-data-subnets` that needs to be added to P2P options. Refer to the [Prysm command-line options documentation](https://prysm.offchainlabs.com/docs/configure-prysm/parameters/) for configuration details. This flag is available as of Prysm v6.1.0. We recommend upgrading to the [latest stable Prysm releases](https://github.com/OffchainLabs/prysm/releases) to ensure you have the most recent features and security updates. ### Other Consensus Layer clients Other Consensus Layer nodes also have flags to ensure they sync data from across all subnets. > **WARNING** — Verification > > The Offchain Labs team hasn't verified the accuracy of the flags below, including the corresponding versions that support these flags. Consult the respective release notes and documentation for non-Prysm consensus layer clients to ensure you're adding the correct flags. ### Specific client flags > **NOTE** — Sepolia only > > Currently, the following clients are supported for Sepolia only. | Client | Compatible with Nitro | Required Nitro Flag | Required flag for subscribing to all subnets | Required flag to serve historical blobs | | -------------------- | --------------------- | ------------------- | --------------------------------------------- | ---------------------------------------------------------------- | | Prysm 7.1.0 or newer | ✅ | None | | `--blob-retention-epochs` `--semi-supernode` `--enable-backfill` | | Lighthouse | ✅ | None | `--supernode` | `--prune-blobs false` or `--blob-prune-margin-epochs` | | Teku | ✅ | None | `--p2p-subscribe-all-custody-subnets-enabled` | None exists | | Lodestar | ✅ | None | `--supernode` | `--chain.archiveDataEpochs` | For additional information regarding specific client flags visit their docs: [Prysm](https://prysm.offchainlabs.com/docs/learn/concepts/blobs), [Lighthouse](https://lighthouse-book.sigmaprime.io/advanced_blobs.html), [Teku](https://docs.teku.consensys.io/concepts/proto-danksharding#what-are-blobs), and [Lodestar](https://chainsafe.github.io/lodestar/run/beacon-management/beacon-cli/). We recommend using Prysm 7.1.0 or newer with the flags `--semi-supernode`, `--enable-backfill`, and removing `--subscribe-all-data-subnets`. ## Checklist To maintain uninterrupted node operation and blob availability: * Add the appropriate flag for your consensus layer client. * Verify your Sepolia beacon endpoint’s configuration. * Verify your Mainnet beacon endpoint’s configuration. --- > For a complete page index, fetch # Data Availability ## How Arbitrum data availability works ## What is the general view of Arbitrum data flow? Arbitrum currently supports two primary data availability mechanisms: ### Rollup Mode In this mode, all transaction data is included in either the calldata of transactions submitted to the parent chain (e.g., Ethereum mainnet for Arbitrum One) or the blobs submitted by the transaction. This inclusion ensures that all data is readily available onchain for anyone to download and verify. ### AnyTrust Mode In AnyTrust mode, transaction data initially gets submitted to a group of nodes known as the Data Availability Committee (DAC). The DAC stores and distributes the data. Instead of including the entire dataset onchain, only a cryptographic proof that the data has been stored by the DAC (Data Availability Certificate, or DACert) is submitted to the parent chain. This proof significantly reduces the amount of data stored onchain, reducing costs. Because of those data availability mechanisms, Arbitrum Nitro nodes synchronize their data differently than Ethereum nodes or other layer-one network nodes. While Go-Ethereum nodes utilize a sophisticated P2P network to synchronize with the Ethereum blockchain by discovering other nodes, exchanging data, and participating in the consensus mechanism, Arbitrum nodes diverge from this traditional approach and use a trustless process. Here's how Arbitrum data flow works: 1. Batching and submission: 1. The sequencer queues transactions and batches them together. 2. These batches get submitted to the parent chain: 1. In Rollup mode, the sequencer submits the batch of transactions directly to the sequencer inbox contract on the parent chain. (Blob or calldata directly) 2. In AnyTrust mode, the sequencer sends the batch to the Data Availability Committee (DAC) and then submits the Data Availability Certificate (DACert) which is returned and generated by the DAS to the parent chain. 2. Node synchronization: 1. Upon joining the network, a full node: 1. In Rollup mode, data is read directly from the parent chain calldata or blobs (depending on how the sequencer posts the data). 2. In AnyTrust mode, it checks the DACert to verify data availability and queries the data from the DAC. 2. The node continues to follow this process to catch up with the latest chain height. 3. Once caught up, the node receives updates on new sequencer-queued messages directly from the sequencer feed (we will provide details of this process in the last section). 3. Catching up: 1. If a node falls behind the chain, it reverts to the process described in Step 2 to resynchronize with the latest state. In essence, Arbitrum nodes prioritize data retrieval from the parent chain and rely on the sequencer for real-time updates, deviating from the traditional P2P synchronization approach used by Ethereum nodes. For operational guides that build on these concepts, see [How to run a sequencer node](/run-arbitrum-node/sequencer/run-sequencer-node.md) and [How to run a validator](/run-arbitrum-node/more-types/run-validator-node.md). ## How full nodes decode the data from the parent chain Arbitrum full nodes decode data received from the parent chain (and, in the case of AnyTrust chains, the DAC) to update their local state. This process involves monitoring events, parsing data, and processing messages. 1. Event querying: 1. Full nodes subscribe to the `SequencerBatchDelivered` event emitted by the inbox contract on the parent chain. This event signifies the arrival of a new batch of transactions. 2. Event parsing: 1. Upon receiving the `SequencerBatchDelivered` event, the node parses the event data into a `SequencerInboxBatch` struct. This struct typically includes: 1. `BlockHash`: The hash of the parent chain block containing the batch. 2. `ParentChainBlockNumber`: The block number of the parent chain block. 3. `SequenceNumber`: The sequence number of the batch. 4. `TimeBounds`: Time constraints for the batch. 5. `AfterDelayedAcc`: Accumulator hash after processing delayed messages. 6. `AfterDelayedCount`: Count of delayed messages. 7. `rawLog`: The raw event log data. 3. Data serialization: 1. The `SequencerInboxBatch` struct serializes into a byte array. 2. The serialized data adheres to a specific format: 1. `TimeBounds.MinTimestamp` (8 bytes) 2. `TimeBounds.MaxTimestamp` (8 bytes) 3. `TimeBounds.MinBlockNumber` (8 bytes) 4. `TimeBounds.MaxBlockNumber` (8 bytes) 5. `AfterDelayedCount` (8 bytes) 6. `payload` (variable length) 1. The `payload` field further contains the following: 1. **Type:** Indicates the header of payload (e.g., DACert, blob message). 2. **Content:** The actual data associated with the payload header (e.g., DACert, BlobHashes, brotli compressed data). 4. Data decoding and retrieval: 1. Based on the `payload` header: 1. **DAS Message header:** The node queries the Data Availability Servers (DAS) to retrieve the raw data. 2. **Blob message header:** The node decodes the blob message to obtain the raw data. 3. **Brotli Message header:** No extra steps are needed here; continue to the next step. 2. Data decompression: If the raw data is Brotli-compressed, the node decompresses it. It's worth noting that the raw data we get from above i and ii might also be Brotli-compressed data. 5. Message processing: 1. After decoding and decompressing the data, the node obtains a series of batch segment messages. 1. Message Types: | Batch Segment Message type | What is the usage of this message | | -------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `BatchSegmentKindL2Message` | This message will contain raw data on a series of transactions. Usually, this is a single block. | | `BatchSegmentKindL2MessageBrotli` | The message is the same as the above one, but this is brotli compressed data. | | `BatchSegmentKindDelayedMessages` | This message contains a new delayed message read from the parent chain Delayed Inbox. | | `BatchSegmentKindAdvanceTimestamp` | This message will notify the State Transition Function (STF) to advance a second of the timestamp state. | | `BatchSegmentKindAdvanceL1BlockNumber` | This message will notify STF to advance a new parent chain block number. | 2. State transition: finally, the State Transition Function (STF) processes these messages, and the STF will follow the rules to execute and update the Arbitrum node's local state. ## How full nodes sync the data from the sequencer feed Once Arbitrum full nodes have caught up with the chain, they switch from initial synchronization to a real-time update mode. This switch involves receiving data from the sequencer feed, which continuously broadcasts updates about newly queued transactions. 1. Data acquisition: 1. Full nodes maintain a connection to the sequencer feed or your private feed. For how to run a private feed, please refer to [**How to run a feed relay**](/run-arbitrum-node/run-feed-relay.md) 2. The sequencer feed transmits data packets containing information about the latest queued transactions. 2. Data decoding: 1. Full nodes decode the received data packets using the methods described in [How to read the sequencer feed](/run-arbitrum-node/sequencer/read-sequencer-feed.md). 3. Message processing: 1. After successful decoding, the full nodes obtain the same type of data as outlined in the previous section's Step 5. 2. Send the message to the State Transition Function (STF) and execute. (This step is the same as the previous section's Step 5) --- > For a complete page index, fetch # Ethereum beacon chain RPC providers > **INFO** — Note > > This reference document provides an overview of Ethereum beacon chain RPC providers for Arbitrum validators to use for accessing blob data following Ethereum's Dencun upgrade in March 2024. The list curated here is **not comprehensive and in no way does Offchain Labs endorse or benefit from your use of any of these providers.** Following [Ethereum's Dencun upgrade in March 2024](https://eips.ethereum.org/EIPS/eip-7569), child blockchains like Arbitrum will be able to roll up and post batches of transaction data on Ethereum in the form of a new transaction format called a Blob. This Blob data will be part of the beacon chain and is fully downloadable by all consensus nodes. This means that data stored in blobs are inaccessible by the EVM, unlike calldata. ## What does this mean for node operators? To run a node for a child Arbitrum chain (i.e., Arbitrum One, Arbitrum Nova, and L3 Arbitrum chains), your node will need access to blob data to sync up to the latest state of your Arbitrum child chain. Blob data on Ethereum is stored on the beacon chain and is inaccessible to the EVM, hence why dedicated RPC endpoints for the beacon chain will be required after the Dencun upgrade. You can find more details on node requirements in the [Run a full node guide](/run-arbitrum-node/run-full-node.md). Furthermore, new node operators joining a network or node operators who come online following an extended period of offline time will require access to *historical* blob data to sync up to the latest state of their Arbitrum chain. If you run your own beacon node, see [Beacon nodes: historical blobs](/run-arbitrum-node/beacon-nodes-historical-blobs.md) for the client flags needed to retain and serve historical blob data through the Fusaka upgrade. ## List of Ethereum beacon chain RPC providers | Provider | Mainnet Beacon chain APIs? | Mainnet Historical blob data? | Sepolia Beacon chain APIs? | | --------------------------------------------------------------------------- | -------------------------- | ----------------------------- | -------------------------- | | [Ankr](https://www.ankr.com/docs/rpc-service/chains/chains-api/eth-beacon/) | ✅ | ✅ | ✅ | | [Chainbase](https://chainbase.com/) | ✅ | | | | [Chainstack](https://docs.chainstack.com/reference/beacon-chain) | ✅ | ✅ | ✅ | | [Conduit](https://conduit.xyz/)\* | ✅ | ✅ | | | [BlastAPI](https://blastapi.io/public-api/ethereum) | | | | | [Nirvana Labs](https://nirvanalabs.io) | ✅ | ✅ | | | [NodeReal](https://nodereal.io/) | ✅ | | | | [QuickNode](https://www.quicknode.com/docs/ethereum) | ✅ | ✅ | ✅ | | [dRPC](https://drpc.org/chainlist/eth-beacon-chain) | ✅ | ✅ | ✅ | Please reach out to these teams individually if you need assistance with setting up your validator with any of the above providers. **Case-by-case basis, please contact them directly for help** --- > For a complete page index, fetch # How to run an archive node An Arbitrum **archive node** is a full node that maintains an archive of historical chain states. This how-to walks you through the process of configuring an archive node on your local machine so that you can query both pre-Nitro and post-Nitro state data. For how archive mode relates to the other Nitro node roles, see [How to assign roles to a Nitro node](/run-arbitrum-node/assign-node-roles.md). > **CAUTION** > > **Most users won't need to configure an archive node**. This node type is great for a small number of use cases––for example if you need to process historical data. ## Before we begin Before the Nitro upgrade, Arbitrum One ran on the Classic stack for about one year (before block height 22207817). Although the Nitro chain uses the latest snapshot of the Classic chain's state as its genesis state, **the Nitro stack can serve all but six RPC requests for pre-Nitro blocks. Full details are outlined in [Do you need to run a Classic node?](/run-arbitrum-node/more-types/run-classic-node.md#do-you-need-to-run-a-classic-node).** Running an Arbitrum One **full node** in **archive mode** lets you access both pre-Nitro and post-Nitro blocks, but it requires you to run **both Classic and Nitro nodes** together. You may not need to do this, depending on your use case: | Use case | Required node type(s) | Docs | | ------------------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Access the **Arbitrum network** without running your own node | Fully managed by third-parties, exposed via RPC endpoints | [RPC endpoints and providers](/arbitrum-essentials/reference/node-providers.md) | | Run an **archive node** for **Arbitrum Sepolia (testnet)** or **Arbitrum Nova** | Full node (Nitro) | [How to run a full node (Nitro)](/run-arbitrum-node/run-full-node.md) | | Send **post-Nitro** archive requests | Full node (Nitro) | [How to run a full node (Nitro)](/run-arbitrum-node/run-full-node.md) | | Send **pre-Nitro** archive requests | Full node (Classic) | [How to run a full node (Classic, pre-Nitro)](/run-arbitrum-node/more-types/run-classic-node.md) | | Send **post-Nitro** *and* **pre-Nitro** archive requests | Full node (Nitro) *and* full node (Classic) | That's what this how-to is for; you're in the right place. | ## Use PathDB for archive nodes **We recommend PathDB for archive nodes on Nitro v3.9.x or later.** PathDB is a path-based state scheme. It cuts archive disk use substantially, and while syncing and serving RPC calls a PathDB archive node performs about the same as, or slightly better than, a HashDB one. Benefits: * **Lower disk usage.** The Arbitrum One `archive-path` snapshot is about 3.7 TB. The equivalent HashDB `archive` snapshot is considerably larger — Nova's is about 8 TB, for a chain whose full node database is a quarter the size of Arbitrum One's. * **Automatic, online pruning.** PathDB removes old state as the node runs, without your intervention. You never schedule a prune, and the node never goes offline to run one. On HashDB you prune manually, and the node stops serving RPC requests until it finishes — days, on a chain the size of Arbitrum One. This benefit applies to full nodes on PathDB too. * **Configurable retention.** You choose how much state history to keep with `--execution.caching.state-history`. Limitations: * **PathDB cannot validate blocks.** If a node requires the block validator, Nitro exits at startup with `path cannot be used as execution.caching.state-scheme when validator is required`. * **Fast, local NVMe SSD storage is required** for reasonable sync speed. * **`--init.latest` cannot download a PathDB snapshot.** Use `--init.url`, as described in [Initialize a PathDB archive node](#initialize-a-pathdb-archive-node). To enable PathDB on your archive node, start it with these flags: | Flag | Purpose | | --------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `--execution.caching.archive` | Enables archive mode; under PathDB, builds the state-history index so RPC can serve historical state | | `--execution.caching.state-scheme=path` | Selects PathDB instead of the default HashDB | On Nitro before v3.10.0, also set \`--execution.caching.state-history=0\` `--execution.caching.state-history` controls how many blocks of state history PathDB retains. From v3.10.0 onward, setting `--execution.caching.archive` makes it default to `0`, which keeps the entire chain, so you can leave it unset. Before v3.10.0 it defaulted to 24 hours' worth of blocks even in archive mode. Nitro logs a warning — `Path scheme archive mode enabled, but state-history is not zero` — and then silently retains only recent history, leaving you with an archive node that cannot serve older state. Pass `--execution.caching.state-history=0` explicitly on those versions. For cache sizing and memory tuning that applies to archive nodes generally, see [node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md). ### Initialize a PathDB archive node A snapshot only works with a node whose state scheme matches the snapshot's. `--init.latest` accepts only `archive`, `pruned`, and `genesis`, and all three resolve to HashDB snapshots, so it cannot bootstrap a PathDB node. Instead, pass the `archive-path` snapshot URL to `--init.url`. These snapshots are published for Arbitrum One and Arbitrum Sepolia only; Arbitrum Nova has no `archive-path` snapshot. Each chain publishes a pointer file naming its current `archive-path` snapshot directory. Read that file to find the URL: ```shell curl https://snapshot.arbitrum.foundation/arb1/latest-archive-path.txt ``` The file contains a directory path, such as `arb1/2026-08-02-291ce8d7/`. Pass the full URL to that directory, including the trailing slash: ```shell --init.url https://snapshot.arbitrum.foundation/arb1/2026-08-02-291ce8d7/ ``` Nitro reads the `.manifest.txt` file in that directory to enumerate the snapshot parts, downloads each one, and verifies its checksum. The directory also holds a `metadata.json` naming the snapshot's `state_scheme`, `snapshot_kind`, and total size, so you can confirm you have the right snapshot before downloading terabytes. For Arbitrum Sepolia, substitute `sepolia-rollup` for `arb1` in both URLs. ## System requirements The minimum storage requirements will change as the Nitro chains grow (see the growth rates below). We recommend exceeding the minimum requirements as much as you can to minimize risk and maintenance overhead. The disk size for archive nodes is expected to grow over time. Carefully monitor your node's disk requirements and disk growth rates to ensure your node has adequate resources. | Resource | Minimum requirements | Recommended | | ------------ | ---------------------------------------------------------------------------------- | -------------------------------------------------------------- | | RAM (DDR5) | 64 GB | 128 GB or more | | CPU | 8 core 3rd generation CPUs (for AWS, a `i4i.2xlarge` instance) | 16 core CPU or higher and more recent/newer generation of CPUs | | Storage type | NVMe SSD drives with locally attached drives strongly recommended | Same | | Storage size | Depends on the chain and its traffic over time, but ideally several terabytes (TB) | Same, but higher if possible | 1. **Docker images:** We'll specify these in the below commands; you don't need to download them manually. * Latest Docker image for **Arbitrum One Nitro**: `offchainlabs/nitro-node:v3.11.3-beb2108` * Latest Docker image for **Arbitrum One Classic**: `offchainlabs/arb-node:v1.4.6-551a39b3` 2. **Database snapshots:** * Nitro database snapshot * Use the parameter `--init.url=` on the first startup to initialize the Nitro database (you can find a list of snapshots [here](https://snapshot-explorer.arbitrum.io/)). Example: `--init.url="https://snapshot.arbitrum.foundation/arb1/nitro-archive.tar"` * Arbitrum One Classic database snapshot * Download the latest Arbitrum One Classic database snapshot at and place it in the mounted point directory * Note that other chains don't have Classic blocks and thus don't require an initial genesis database. * Snapshot Explorer * You can find more snapshots on our [snapshot explorer](https://snapshot-explorer.arbitrum.io/) * [Archive-Path section of the snapshot explorer](https://snapshot-explorer.arbitrum.io/?chain=arb1) ## Review and configure ports * RPC: `8547` * Sequencer Feed: `9642` * WebSocket: `8548` ## Review and configure parameters | Arbitrum Nitro | Arbitrum Classic | Description | | ---------------------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--parent-chain.connection.url=` | `--l1.url=` | Provide a standard L1 node RPC endpoint that you run yourself or from a third-party node provider (see [RPC endpoints and providers](/arbitrum-essentials/reference/node-providers.md)) | | `--chain.id=` | `--l2.chain-id=` | See [RPC endpoints and providers](/arbitrum-essentials/reference/node-providers.md) for a list of Arbitrum chains and the respective child chain IDs | | `--execution.caching.archive` | `--node.caching.archive` | Required for running an **Arbitrum One Nitro** archival node and retains past block state | | `--execution.caching.state-scheme` | - | Default: `hash`. Sets the scheme Nitro uses to store its state trie, inherited from Geth. Set it to `path` to enable PathDB. PathDB cannot validate blocks. | | `--execution.caching.state-history` | - | PathDB only. Number of recent blocks of state history to retain on disk. On an archive node, leave it unset: from Nitro v3.10.0 onward, `--execution.caching.archive` makes it default to `0`, which retains the entire chain. Before v3.10.0, the default was 345,600 blocks (24 hours), so set `--execution.caching.state-history=0` explicitly. | | `--execution.caching.pathdb-max-diff-layers` | - | Default: 128 layers. Maximum number of diff layers kept in the node's memory before flushing to disk. Increasing the number of diff layers may cause the node to fall behind the chain head during busy periods since doing so slows down block processing speed and reduces sync speed. This configuration is primarily used to improve performance of shallow re-orgs (which are a concern on Ethereum but not on Arbitrum chains) and for efficient access to recent state. | | - | `--node.cache.allow-slow-lookup` | Required for running an **Arbitrum One Classic** archival node. When this option is present, it will load old blocks from disk if not in memory cache. | | - | `--core.checkpoint-gas-frequency=156250000` | Required for running an **Arbitrum One Classic** archival node. | ## Run the Docker image(s) When running a Docker image, an external volume should be mounted to persist the database across restarts. The mount point should be `/home/user/.arbitrum/mainnet`. To run both Arbitrum Nitro and/or Arbitrum Classic in archive mode, follow one or more of the below examples: * **Arbitrum One Nitro archive node**: ```shell docker run --rm -it -v /some/local/dir/arbitrum:/home/user/.arbitrum -p 0.0.0.0:8547:8547 -p 0.0.0.0:8548:8548 offchainlabs/nitro-node:v3.11.3-beb2108 --parent-chain.connection.url https://l1-node:8545 --chain.id=42161 --http.api=net,web3,eth --http.corsdomain=* --http.addr=0.0.0.0 --http.vhosts=* --execution.caching.archive ``` * **Arbitrum One Classic archive node**: ```shell docker run --rm -it -v /some/local/dir/arbitrum-mainnet/:/home/user/.arbitrum/mainnet -p 0.0.0.0:8547:8547 -p 0.0.0.0:8548:8548 offchainlabs/arb-node:v1.4.6-551a39b3 --l1.url=https://l1-node:8545/ --node.chain-id=42161 --l2.disable-upstream --node.cache.allow-slow-lookup --core.checkpoint-gas-frequency=156250000 --core.lazy-load-core-machine ``` * **Arbitrum One Nitro archive node with forwarding classic execution support**: ```shell docker run --rm -it -v /some/local/dir/arbitrum:/home/user/.arbitrum -p 0.0.0.0:8547:8547 -p 0.0.0.0:8548:8548 offchainlabs/nitro-node:v3.11.3-beb2108 --parent-chain.connection.url https://l1-node:8545 --chain.id=42161 --execution.rpc.classic-redirect= --http.api=net,web3,eth --http.corsdomain=* --http.addr=0.0.0.0 --http.vhosts=* --execution.caching.archive ``` Note that the above commands both map to port `8547` on their hosts. To run both on the same host, you should edit those mapping to different ports and specify your Classic node RPC URL as `` in your Nitro start command. To verify the connection health of your node(s), see [Docker network between containers - Docker Networking Example](https://www.middlewareinventory.com/blog/docker-network-example/). ### A note on permissions The Docker image is configured to run as non-root `UID 1000`. If you're running in Linux and you're getting permission errors when trying to run the Docker image, run this command to allow all users to update the persistent folders, replacing `arbitrum-mainnet` as needed: ```shell mkdir /some/local/dir/arbitrum-mainnet chmod -fR 777 /some/local/dir/arbitrum-mainnet ``` ## Optional parameters Both Nitro and Classic have multiple other parameters that can be used to configure your node. For a full comprehensive list of the available parameters, use the flag `--help`. ## PathDB configuration reference These flags apply only when you set `--execution.caching.state-scheme=path`. ### `--execution.caching.state-scheme` Selects the TrieDB implementation Nitro uses to store its state trie. Set it to `path` to enable PathDB; the default is `hash`. The two schemes are not interchangeable. A node can only start from a snapshot created with the same state scheme, so you cannot convert an existing database. To move to PathDB, either start from a `path`-scheme snapshot or sync from genesis. On Arbitrum One, syncing from genesis also requires a [Classic state import](/run-arbitrum-node/nitro/migrate-state-and-history-from-classic.md#step-3-initialize-your-nitro-node-importing-the-exported-data). ### `--execution.caching.state-history` Sets how many blocks of state history Nitro retains, counting back from the chain head. `0` means the entire chain, and is therefore required — but not enough on its own — to run an archive node on PathDB. You also need `--execution.caching.archive`, described below. When you leave `--execution.caching.state-history` unset, Nitro chooses the default at startup: | Node type | Default `state-history` | | ------------------------------------------------- | ----------------------------------------------------------- | | Full node (`--execution.caching.archive=false`) | 345,600 blocks — 24 hours at the default 250 ms block speed | | Archive node (`--execution.caching.archive=true`) | `0`, meaning the entire chain | From v3.10.0, Nitro derives this default from the archive flag. On earlier versions the default was always 24 hours' worth of blocks, so an archive node needs an explicit `--execution.caching.state-history=0`. Nitro stores state history as reverse diffs in the cold database, also called the state freezer or ancients. State history lets the node recover historical state, which should allow a reorg to a historical block. Offchain Labs has not tested that reorg path. **State history alone does not let RPC calls read historical state.** For that, see `--execution.caching.archive` below. Here, historical state means state older than `pathdb-max-diff-layers + 1` blocks — 129 by default — from the node's current head. ### `--execution.caching.archive` Enables archive mode. Under PathDB, archive mode also builds the state history index, an extra dataset Nitro persists in the hot database (Pebble). That index is what allows RPC calls to read state older than `pathdb-max-diff-layers + 1` blocks from the head. ### `--execution.caching.pathdb-max-diff-layers` Sets the maximum number of diff layers Nitro keeps in memory before flushing to disk. Default: 128. Raising it can make the node fall behind the chain head during busy periods, because it slows block processing and reduces sync speed. It mainly improves performance for shallow reorgs, which matter on Ethereum but not on Arbitrum chains, and for efficient access to recent state. ## Troubleshooting If you run into any issues, visit the [node-running troubleshooting guide](/run-arbitrum-node/troubleshooting.md). --- > For a complete page index, fetch # How to run a Classic node ## Do you need to run a Classic node? Arbitrum One has been upgraded to Nitro, the latest Arbitrum tech stack. "Arbitrum Classic" is our term for the old, pre-Nitro tech stack. The Nitro node databases have the raw data of all blocks, including pre-Nitro blocks. However, Nitro nodes cannot execute anything on pre-Nitro blocks. You need an Arbitrum Classic archive node to execute data on pre-Nitro blocks. When querying archive blocks, the following commands can only be handled by Arbitrum Classic nodes: * `eth_call` * `eth_estimateGas` * `eth_getBalance` * `eth_getCode` * `eth_getTransactionCount` * `eth_getStorageAt` 🔉 Note that Arbitrum Nova and Arbitrum Sepolia started as a Nitro chain, so they don't have classic blocks. ## Required artifacts * Latest Docker Image: `offchainlabs/arb-node:v1.4.6-551a39b3` * Latest classic snapshot for Arbitrum One: ## Required parameters * `--l1.url=` * Must provide standard Ethereum node RPC endpoint. * `--node.chain-id=` * Must use `42161` for Arbitrum One ## Important ports * RPC: `8547` * WebSocket: `8548` ## Putting it all together * When running docker image, an external volume should be mounted to persist the database across restarts. The mount point should be `/home/user/.arbitrum/mainnet`. * Here is an example of how to run a classic archive node for Arbitrum One (only needed for archive requests on pre-Nitro blocks, so you'll probably want to enable the archive mode in your nitro node as well): ```shell docker run --rm -it -v /some/local/dir/arbitrum-mainnet/:/home/user/.arbitrum/mainnet -p 0.0.0.0:8547:8547 -p 0.0.0.0:8548:8548 offchainlabs/arb-node:v1.4.6-551a39b3 --l1.url=https://l1-node:8545 --node.chain-id=42161 --l2.disable-upstream ``` ## Note on permissions * The Docker image is configured to run as non-root UID 1000. This means if you are running in Linux and you are getting permission errors when trying to run the docker image, run this command to allow all users to update the persistent folders. ```shell mkdir /some/local/dir/arbitrum-mainnet chmod -fR 777 /some/local/dir/arbitrum-mainnet ``` ## Optional parameters We show here a list of the parameters that are most commonly used when running a Classic node. You can also use the flag `--help` for a full comprehensive list of the available parameters. * `--core.cache.timed-expire` * Defaults to `20m`, or 20 minutes. Age of oldest blocks to hold in cache so that disk lookups are not required * `--node.rpc.max-call-gas` * Maximum amount of gas that a node will use in call, default is `5000000` * `--core.checkpoint-gas-frequency` * Defaults to `1000000000`. Amount of gas between saving checkpoints to disk. When making archive queries node has to load closest previous checkpoint and then execute up to the requested block. The farther apart the checkpoints, the longer potential execution required. However, saving checkpoints more often slows down the node in general. * `--node.cache.allow-slow-lookup` * When this option is present, will load old blocks from disk if not in memory cache * If archive support is desired, recommend using `--node.cache.allow-slow-lookup --core.checkpoint-gas-frequency=156250000` * `--node.rpc.tracing.enable` * Note that you also need to have a database populated with an archive node if you want to trace previous transactions * This option enables the ability to call a tracing API which is inspired by the parity tracing API with some differences * Example: `curl http://arbnode -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"arbtrace_call","params":[{"to": "0x6b175474e89094c44da98b954eedeac495271d0f","data": "0x70a082310000000000000000000000006E0d01A76C3Cf4288372a29124A26D4353EE51BE"},["trace"], "latest"],"id":67}'` * The `trace_*` methods are renamed to `arbtrace_*`, except `trace_rawTransaction` is not supported * Only `trace` type is supported. `vmTrace` and `stateDiff` types are not supported * The self-destruct opcode is not included in the trace. To get the list of self-destructed contracts, you can provide the `deletedContracts` parameter to the method ## Feed relay * Arbitrum classic does not communicate with Nitro sequencer, so the classic relay is no longer used. ## Why classic nodes serve RPC methods so slowly? When you call RPC methods on Arbitrum Classic nodes, the request time may take a long time, because classic nodes must perform multiple sequential reads from the database to reconstruct blockchain state for state-accessing operations. Nodes need to load execution cursors from disk, rebuild the machine state from checkpoints, and execute transactions to reach the target state—all of which involve expensive disk I/O operations. **Solutions:** * Use sequential block access: Query consecutive blocks (`N`, `N+1`, `N+2`) instead of random blocks to benefit from caching * Hardware optimization: Use NVMe SSDs with low latency (NVMe PCIe 4.0 or higher) --- > For a complete page index, fetch # How to run a validator Validators are nodes that choose to participate in the rollup protocol to advance the state of the chain securely. Since the activation of BoLD, chains can now choose to make validation permissionless. You can learn more in the [BoLD introduction](/how-arbitrum-works/bold/gentle-introduction.md). This page describes the different strategies a validator may follow and provides instructions on how to run a validator for an Arbitrum chain. This how-to assumes that you're familiar with the following: * How to run a full node (see instructions [here](/run-arbitrum-node/run-full-node.md)) * [How the Rollup protocol works](/how-arbitrum-works/inside-arbitrum-nitro.md) * [How BoLD works](/how-arbitrum-works/bold/bold-technical-deep-dive.md#how-bold-uses-ethereum), if you're running a validator for a chain that has BoLD activated * [Data availability](/run-arbitrum-node/data-availability.md), for how Rollup-mode and AnyTrust-mode chains differ in the data your validator must reconstruct * [How to assign roles to a Nitro node](/run-arbitrum-node/assign-node-roles.md), for how the validator role relates to the other node roles and what converting a node to a validator involves ## Validation strategies Validators can be configured to follow a specific validation strategy. Here we describe what strategies are available in Nitro: | Strategy | Description | Gas usage | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | **`Defensive`** | This validator will follow the chain and if it's local state disagrees with an onchain assertion, this validator will post a bond and create a challenge to defend the chain | Only acts if a bad assertion is found | | **`StakeLatest`** | This validator will initially bond on the latest correct assertion found, and then move the bond whenever new correct assertions are created. It will also challenge any bad assertions that it finds (this strategy is only available in pre-BoLD chains) | Gas used every time a new assertion is created | | **`ResolveNodes`** | This validator will stay bonded on the latest assertion found, resolve any unconfirmed assertions, and it will challenge any bad assertions that it finds | Gas used every time a new assertion is created and to resolve unconfirmed assertions | | **`MakeNodes`** | This validator continuously creates new assertions, resolves any unconfirmed assertions, and challenges bad assertions found. Note that if there is more than one `MakeNodes` validator running, they might all try to create a new assertion simultaneously. In that case, only one will be successful, while the others will have their transactions reverted | Gas used to create new assertions, move the bond to the latest one, and resolve unconfirmed assertions | ### The watchtower strategy One more validation strategy is available for all types of nodes: `watchtower`. This strategy is enabled by default in all nodes (full and archive)—see [Watchtower mode in the full node guide](/run-arbitrum-node/run-full-node.md#watchtower-mode) for the default-behavior context—and it doesn't require a wallet, as it never takes any action onchain. A node in `watchtower` mode will immediately log an error if an onchain assertion deviates from the locally computed chain state. ```shell found incorrect assertion in watchtower mode ``` To verify that the watchtower mode is enabled, this line should appear in the logs: ```shell INFO [09-28|18:43:49.367] running as validator txSender=nil actingAsWallet=nil whitelisted=false strategy=Watchtower ``` Additionally, the following logs indicate whether all components are working correctly: * The log line `validation succeeded` shows that the node is validating chain blocks successfully * The log line `found correct assertion` shows that the node is finding assertions on the parent chain successfully Watchtower mode adds a small amount of execution and memory overhead to your node. You can deactivate this mode using the parameter `--node.staker.enable=false`. ## How to run a validator node This section explains how to configure your node to act as a validator. ### Step 0: prerequisites A validator node is a regular full node with validation enabled, so you'll have to know how to configure a full node. You can find instructions in the [full node setup guide](/run-arbitrum-node/start-here.md). Additionally, you'll need a wallet with enough funds to perform actions onchain and enough tokens to bond. Keep in mind that: * The token used to perform actions onchain is the native token of the parent chain (usually **ETH**) * For chains with BoLD activated, the token used to bond depends on the chain configuration. For Arbitrum One and Arbitrum Nova, the staking token is **WETH** * For chains that don't have BoLD activated, the token used to bond is the native token of the parent chain (usually **ETH**) ### Step 1: configure and run your validator On top of the configuration of a regular full node, you'll need to configure the following parameters for it to act as a validator: | Parameter | Value | Description | | ----------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--node.staker.enable` | `true` | Enables validation | | `--node.staker.strategy` | `Watchtower`, `Defensive`, `StakeLatest`, `ResolveNodes`, `MakeNodes` | Strategy that your node will use | | `--node.staker.parent-chain-wallet.private-key` | 0xPrivateKey | Private key of the wallet used to perform the operations onchain. Use either `private-key` or `password` (below) | | `--node.staker.parent-chain-wallet.password` | Password | Password of a wallet generated with nitro (see instructions [here](#use-nitro-to-create-a-wallet-for-your-validator)). Use either `private-key` (above) or `password` | | `--node.bold.enable` | true | Enables validation with BoLD (not needed if BoLD is not activated, only needed before nitro v3.6.0) | Here's an example of how to run a defensive validator for Arbitrum One: ```shell docker run --rm -it -v /some/local/dir/arbitrum:/home/user/.arbitrum offchainlabs/nitro-node:v3.11.3-beb2108 --parent-chain.connection.url=https://l1-mainnet-node:8545 --chain.id=42161 --node.staker.enable --node.staker.strategy=Defensive --node.staker.parent-chain-wallet.password="SOME SECURE PASSWORD" --node.staker.strategy=Defensive ``` ### Step 2: verify that your node is running as a validator To verify that your node is acting as a validator, you can look for the following log line: ```shell INFO [09-28|18:43:49.367] running as validator txSender=0x... actingAsWallet=0x... whitelisted=true strategy=Defensive ``` Note that `strategy` should be the configured strategy. `txSender` and `actingAsWallet` should both be present and not `nil`. Furthermore, the following logs will indicate that all components are working as intended: * The log line `validation succeeded` shows that the node is validating chain blocks successfully * The log line `found correct assertion` shows that the node is finding assertions on the parent chain successfully ## Run a validator for an Arbitrum chain Validation for Arbitrum chains works the same way as for DAO-governed Arbitrum chains. However, as specified in [How to run a node](/run-arbitrum-node/start-here.md#required-parameters), you need to include the information of the chain when configuring your node by using `--chain.info-json`. ```shell --chain.info-json= ``` Additionally, keep in mind that some chains might not have BoLD activated yet, so BoLD-specific parameters will not be needed. ## Advanced features ### Use Nitro to create a wallet for your validator > **WARNING** — Clear passwords in the command line > > This section shows how to manage a validator wallet using a password. Like any command that requires passing a password or private key, you should take extra precautions to secure your credentials. Failure to protect your password may compromise your validator wallet. Nitro includes a tool to create a validator wallet for a specific chain automatically. You can access it by using the option `--node.staker.parent-chain-wallet.only-create-key` and setting a password for the wallet with `--node.staker.parent-chain-wallet.password`. Here is an example of how to create a validator wallet for Arbitrum One and exit: ```shell docker run --rm -it -v /some/local/dir/arbitrum:/home/user/.arbitrum offchainlabs/nitro-node:v3.11.3-beb2108 --parent-chain.connection.url=https://l1-mainnet-node:8545 --chain.id=42161 --node.staker.enable --node.staker.parent-chain-wallet.only-create-key --node.staker.parent-chain-wallet.password="SOME SECURE PASSWORD" ``` The wallet file will be created under the mounted directory inside the `/wallet/` directory (for example, `arb1/wallet/` for Arbitrum One, or `nova/wallet/` for Arbitrum Nova). Be sure to backup the wallet file, as it will be the only way to withdraw the bond when desired. Once the wallet is created, you can instruct your validator to use it by adding the option `--node.staker.parent-chain-wallet.password="SOME SECURE PASSWORD"` when running your node. > **TIP** — Use environment variables > > If you prefer not to include your password in the command line, you can use environment variables or a secure secrets management solution to supply the password at runtime. ### How to add new validators to the allowlist (Arbitrum chains) On permissioned validation setups, the set of validators that can act on a given chain is limited to the ones added to the allowlist of validators in the Rollup contract. Follow these instructions to add a new validator address to the allowlist. Remember that you need to be able to perform admin actions to the chain to complete this operation. 1. Find your `upgradeExecutor` contract address 2. Call the `executeCall` method of the `upgradeExecutor` contract: * set the `target` address to your Rollup contract's address * set the `targetCalldata` to `0xa3ffb772{Your new allowlist validator address}` (`0xa3ffb772` is the signature of `setValidator(address[],bool[])`). For example, if you want to add the address `0x1234567890123456789012345678901234567890`, your `targetCalldata` should be `0xa3ffb7721234567890123456789012345678901234567890`. 3. Call your Rollup contract's `isValidator(address)` and check the result After performing this operation, the new validator will be able to run a validator node to participate in the chain. --- > For a complete page index, fetch # Nitro support policy ## Nitro support This policy defines which versions of Nitro the Offchain team actively supports—and what "support" actually means. It exists so node operators, chain operators, and integrators know what to run, when to upgrade, and what kind of help to expect when they don't upgrade. ### Currently supported Nitro versions | Currently supported | Supported until | Supported deployment | | -------------------------------------------------------------------------- | ------------------------------------------ | -------------------- | | [Nitro 3.11.x](https://github.com/OffchainLabs/nitro/releases/tag/v3.11.2) | 30 calendar days after 3.12.x is released. | Docker | | [Nitro 3.10.2](https://github.com/OffchainLabs/nitro/releases/tag/v3.10.2) | July 2, 2026 | Docker | ### Special note about ArbOS and Arbitrum Classic * **Only the ArbOS release that is activated on mainnet Arbitrum One** will receive security updates and urgent vulnerability patches. We strongly recommend that you stay up to date with the latest ArbOS, as only the latest version receives security updates. The activation timestamps for ArbOS upgrades on Arbitrum One are available on the **[Arbitrum Foundation network upgrades page](https://docs.arbitrum.foundation/network-upgrades),** with the most recent entry corresponding to the ArbOS version currently in use on Arbitrum One. * Alternatively, you can verify the ArbOS version on Arbitrum One using the `arbOSVersion()` method on the `ArbSys` precompile at `0x000....64`. The result will be offset by 55 as the first version of Nitro is known as version 56 (for example, a response of 106 indicates that the ArbOS version is 51). * Arbitrum Classic will also continue to be supported. ### Our support windows, explained We will always support the current minor release of Nitro. The previous minor release will be supported for 30 calendar days after a newer minor release becomes available. In other words, support for a Nitro minor release stops once a minor release of the Nitro node software is made available for 30 calendar days. This means that we will effectively support at most two minor releases for a maximum of 30 calendar days. > **CAUTION** > > In exceptional cases, such as for stability or security fixes, we may cut a new minor Nitro release within 30 days of a previous minor release and immediately drop support for that release. When this happens, we may ask teams to upgrade sooner to the new release for security and stability reasons. Below is an illustrative example of how these windows overlap over the course of a year, along with several placeholder versions. Note that this is an example. ![Nitro support windows](/img/nitro-support-policy.png) Nitro support windows The currently supported versions can be found on the [Nitro start here](/run-arbitrum-node/start-here.md#recommended-nitro-version) page. ### What "supported" means If you are running a supported Nitro version, we will work to: * Respond to bug reports (Slack, Telegram, etc.) and ship fixes and security patches against it * Work with you to help debug operational issues you uncover * Treat the release as a valid baseline for compatibility testing If you are running an unsupported Nitro version: * We may decline to investigate issues and will recommend upgrading first before investigating * We will not back-port non-critical fixes or features * You assume responsibility for any operational, consensus, or security risk You are free to run any version of our software. This policy describes where we will spend our time, not what we will permit. ### Carve-out: ArbOS upgrades on older Nitro versions If we determine an ArbOS upgrade is required—including for security—we do **not** commit to porting those ArbOS changes back to any version. Operators on unsupported Nitro must first upgrade Nitro to receive the ArbOS change. This is intentional: backporting state-transition logic into stale node code is high-risk, and a guarantee here would dilute the upgrade pressure that keeps the network on supportable client versions. ### Security fixes Security fixes are handled inside the supported window: 1. Private disclosure to chain operators when actionable 2. Coordinated release across all in-window versions (current minor + still-in-window prior minor) 3. Public disclosure after operators have had time to upgrade A client version exception does not retroactively extend support for older versions. If you are out of support when a fix lands, the path forward is to upgrade. ## Out of scope This policy covers the upstream release of Nitro by Offchain. It does not cover: * Forks or custom builds maintained by third parties * Builds compiled from non-release commits * Configurations or patches not present in the upstream release We will help where we can, but we cannot commit to a support level on builds other than official releases. ## Staying up to date * Subscribe to the [Nitro GitHub repository](https://github.com/OffchainLabs/nitro) for GitHub notifications on new releases. * Follow our [X handle](https://x.com/ArbitrumDevs) for all things geared towards developers building with and on Arbitrum. * The [Arbitrum Discord server](https://discord.com/invite/arbitrum) for all announcements and discussions * Telegram channels: * `@arbitrumnodeupgrade` for important updates about Arbitrum node software upgrade notices and announcements * `@arbitrum` for general announcements about things happening in the Arbitrum ecosystem * `@OffchainLabsannouncements` for Offchain-specific announcements or notices! --- > For a complete page index, fetch # How to build Nitro locally (Debian, Ubuntu, macOS) Arbitrum Nitro is the software that powers all Arbitrum chains. This how-to shows how you can build a Docker image, or binaries, directly from Nitro's source code. If you want to run a node for one of the Arbitrum chains, however, it is recommended that you use the docker image available on DockerHub, as explained in [How to run a full node](/run-arbitrum-node/run-full-node.md). This how-to assumes that you're running one of the following operating systems: * [Debian 12 (bookworm)](https://www.debian.org/releases/bookworm/) * [Ubuntu 24.04 (amd64)](https://releases.ubuntu.com/noble/) * [MacOS Sequoia 15](https://developer.apple.com/documentation/macos-release-notes/macos-15-release-notes). ## Build a Docker image ### Step 1. Configure [Docker](https://docs.docker.com/engine/install) #### For [Debian](https://docs.docker.com/engine/install/debian)/[Ubuntu](https://docs.docker.com/engine/install/ubuntu) ```shell for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done # Add Docker's official GPG key: sudo apt-get update sudo apt-get install ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg # Add the repository to Apt sources: echo \ "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \ "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo service docker start ``` > **NOTE** > > If you are running Ubuntu 22.04, you might get an `Unable to locate package docker-buildx-plugin` error. Try `sudo apt install docker-buildx` instead. #### For [MacOS](https://docs.docker.com/desktop/install/mac-install/) Depending on whether your Mac has an Intel processor or Apple silicon, download the corresponding disk image from [Docker](https://docs.docker.com/desktop/install/mac-install/), and move it into your Applications folder. #### \[Optional] Run Docker from a different user After installing Docker, you might want to be able to run it with your current user instead of root. You can run the following commands to do so. ```shell sudo groupadd docker sudo usermod -aG docker $USER newgrp docker ``` For troubleshooting, check Docker's section in [their documentation](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user) ### Step 2. Download the Nitro source code ```shell git clone --branch v3.11.3 https://github.com/OffchainLabs/nitro.git cd nitro git submodule update --init --recursive --force ``` ### Step 3. Build the Nitro node Docker image ```shell docker build . --tag nitro-node ``` That command will build a Docker image called `nitro-node` from the local source. ## Build Nitro's binaries natively If you want to build the node binaries natively, execute Steps 1-3 of the [Build a Docker image](#build-a-docker-image) section and continue with the steps described here. Notice that even though we are building the binaries outside of Docker, it is still used to help build some WebAssembly components. ### Step 4. Configure prerequisites #### For Debian/Ubuntu ```shell apt install git curl build-essential cmake npm golang clang make gotestsum wabt lld-13 python3 npm install --global yarn ln -s /usr/bin/wasm-ld-13 /usr/local/bin/wasm-ld ``` #### For MacOS Install [Homebrew](https://brew.sh/) package manager and add it to your `PATH` environment variable: ```shell /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" echo "export PATH=/opt/homebrew/bin:$PATH" >> ~/.zprofile && source ~/.zprofile ``` > **NOTE** > > Replace `~/.zprofile` with `~/.bash_profile` if you use bash instead of zsh). Install essentials: ```shell brew install git curl make cmake npm wabt llvm lld libusb gotestsum npm install --global yarn sudo mkdir -p /usr/local/bin echo "export PATH=/opt/homebrew/opt/llvm/bin:$PATH" >> ~/.zprofile && source ~/.zprofile ``` ### Step 5. Configure node [24](https://github.com/nvm-sh/nvm) #### For Debian/Ubuntu ```shell curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash source "$HOME/.bashrc" nvm install 24 nvm use 24 ``` #### For MacOS ```shell curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" nvm install 24 nvm use 24 ``` ### Step 6. Configure [Rust](https://www.rust-lang.org/tools/install) #### Note that you may also need to use `rustup toolchain remove nightly...` to remove other Rust nightly toolchains that are installed ```shell curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" rustup install 1.93.0 rustup default 1.93.0 rustup target add wasm32-unknown-unknown --toolchain 1.93.0 rustup target add wasm32-wasip1 --toolchain 1.93.0 cargo install cbindgen ``` ### Step 7. Install Bison [3.8.2](https://savannah.gnu.org/projects/bison/) #### For Debian/Ubuntu ```shell sudo apt-get install bison ``` #### For MacOS ```shell brew install bison ``` ### Step 8. Configure Go [1.25](https://github.com/moovweb/gvm) #### Install and configure Go ```shell bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer) source "$HOME/.gvm/scripts/gvm" gvm install go1.25 gvm use go1.25 --default curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.2 ``` > **NOTE** > > If you use zsh, replace `bash` with `zsh`. #### Install foundry version 1.2.3 ```shell curl -L https://foundry.paradigm.xyz | bash foundryup -i 1.2.3 ``` ### Step 9. Check dependencies ```shell ./scripts/check-build.sh ``` If this script shows any errors, fix them before proceeding to the next step. ### Step 10. Start build ```shell make ``` ### Step 11. Produce binaries ```shell make build ``` #### Warnings on MacOS In MacOS with Apple Silicon, warnings like the following might appear but they will not hinder the compilation process. ```shell ld: warning: object file was built for newer 'macOS' version (14.4) than being linked (14.0) ``` To silence these warnings, export the following environment variables before building Nitro. ```shell export MACOSX_DEPLOYMENT_TARGET=$(sw_vers -productVersion) export CGO_LDFLAGS=-Wl,-no_warn_duplicate_libraries ``` ### Step 12. Run your node To run your node using the generated binaries, use the following command from the `nitro` folder, with your desired parameters ```shell ./target/bin/nitro ``` #### WASM module root error (v2.3.4 or later) Since v2.3.4, the State Transition Function (STF) contains code that is not yet activated on the current mainnet and testnet chains. Because of that, you might receive the following error when connecting your built node to those chains: ```shell ERROR[05-21|21:59:17.415] unable to find validator machine directory for the on-chain WASM module root err="stat {WASM_MODULE_ROOT}: no such file or directory" ``` Try add flag: ```shell --validation.wasm.allowed-wasm-module-roots={WASM_MODULE_ROOT} ``` --- > For a complete page index, fetch # CLI flags reference Auto-generated reference This page lists every CLI flag accepted by the Nitro node binary. For explanations, examples, and recommended configurations, see the curated guides: * [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) * [Docker and CLI binaries](/run-arbitrum-node/nitro/docker-and-cli-binaries.md) * [Node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md) * [DA tools reference](/run-arbitrum-node/nitro/da-tools-reference.md) **Total flags:** 724 across 23 namespaces. Pass flags on the command line with `--` prefix: ```shell nitro --http.addr=0.0.0.0 --http.port=8547 --node.feed.input.url=wss://arb1.arbitrum.io/feed ``` Or set them in a JSON configuration file: ```shell nitro --conf.file=/path/to/config.json ``` ## auth Related guide: [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) auth flags (5) | Flag | Type | Default | Description | | ---------------- | ------- | -------------- | ------------------------------------------ | | `auth.addr` | string | `127.0.0.1` | AUTH-RPC server listening interface | | `auth.api` | strings | `[validation]` | APIs offered over the AUTH-RPC interface | | `auth.jwtsecret` | string | - | Path to file holding JWT secret (32B hex) | | `auth.origins` | strings | `[localhost]` | Origins from which to accept AUTH requests | | `auth.port` | int | `8549` | AUTH-RPC server listening port | ## chain Related guide: [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) chain flags (9) | Flag | Type | Default | Description | | ---------------------------------- | ------- | ------------------------------ | ------------------------------------------- | | `chain.dev-wallet.account` | string | `is first account in keystore` | account to use | | `chain.dev-wallet.only-create-key` | bool | - | if true, creates new key then exits | | `chain.dev-wallet.password` | string | `PASSWORD_NOT_SET` | wallet passphrase | | `chain.dev-wallet.pathname` | string | - | pathname for wallet | | `chain.dev-wallet.private-key` | string | - | private key for wallet | | `chain.id` | uint | - | L2 chain ID (determines Arbitrum network) | | `chain.info-files` | strings | - | L2 chain info json files | | `chain.info-json` | string | - | L2 chain info in json string format | | `chain.name` | string | - | L2 chain name (determines Arbitrum network) | ## conf Related guide: [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) conf flags (9) | Flag | Type | Default | Description | | -------------------- | ------- | ------- | ------------------------------------------------------------------------------------------- | | `conf.dump` | bool | - | print out currently active configuration file | | `conf.env-prefix` | string | - | environment variables with given prefix will be loaded as configuration values | | `conf.file` | strings | - | name of configuration file | | `conf.s3.access-key` | string | - | S3 access key for fetching the node configuration file from S3 | | `conf.s3.bucket` | string | - | S3 bucket containing the node configuration file | | `conf.s3.object-key` | string | - | S3 object key of the node configuration file (JSON format) | | `conf.s3.region` | string | - | S3 region of the bucket containing the node configuration file | | `conf.s3.secret-key` | string | - | S3 secret key for fetching the node configuration file from S3 (triggers S3 config loading) | | `conf.string` | string | - | configuration as JSON string | ## ensure-rollup-deployment Related guide: [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) ensure-rollup-deployment flags (1) | Flag | Type | Default | Description | | -------------------------- | ---- | ------- | -------------------------------------------------------------------------------------- | | `ensure-rollup-deployment` | bool | `true` | before starting the node, wait until the transaction that deployed rollup is finalized | ## execution Related guide: [Node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md) execution flags (164) | Flag | Type | Default | Description | | ---------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `execution.block-metadata-api-blocks-limit` | uint | `100` | maximum number of blocks allowed to be queried for blockMetadata per arb\_getRawBlockMetadata query. Enabled by default, set 0 to disable the limit | | `execution.block-metadata-api-cache-size` | uint | `104857600` | size (in bytes) of lru cache storing the blockMetadata to service arb\_getRawBlockMetadata | | `execution.caching.archive` | bool | - | retain past block state | | `execution.caching.block-age` | duration | `30m0s` | minimum age of recent blocks to keep in memory | | `execution.caching.block-count` | uint | `128` | minimum number of recent blocks to keep in memory | | `execution.caching.database-cache` | int | `2048` | amount of memory in megabytes to cache database contents with | | `execution.caching.disable-stylus-cache-metrics-collection` | bool | - | disable metrics collection for the stylus cache | | `execution.caching.enable-preimages` | bool | - | enable recording of preimages | | `execution.caching.head-rewind-blocks-limit` | uint | `2419200` | maximum number of blocks rolled back to recover chain head (0 = use geth default limit) | | `execution.caching.max-amount-of-gas-to-skip-state-saving` | uint | - | maximum amount of gas in blocks to skip saving state to Persistent storage (archive node only) -- warning: this option seems to cause issues | | `execution.caching.max-number-of-blocks-to-skip-state-saving` | uint32 | - | maximum number of blocks to skip state saving to persistent storage (archive node only) -- warning: this option seems to cause issues | | `execution.caching.pathdb-max-diff-layers` | int | `128` | maximum number of diff layers to keep in pathdb (path state-scheme only) | | `execution.caching.snapshot-cache` | int | `400` | amount of memory in megabytes to cache state snapshots with | | `execution.caching.snapshot-restore-gas-limit` | uint | `300000000000` | maximum gas rolled back to recover snapshot | | `execution.caching.state-history` | uint | `18446744073709551615` | number of recent blocks to retain state history for (path state-scheme only) | | `execution.caching.state-scheme` | string | `hash` | scheme to use for state trie storage (hash, path) | | `execution.caching.state-size-tracking` | bool | - | enable tracking of state size over time | | `execution.caching.stylus-lru-cache-capacity` | uint32 | `256` | capacity, in megabytes, of the LRU cache that keeps initialized stylus programs | | `execution.caching.trie-cap-batch-size` | uint32 | - | batch size in bytes used in the TrieDB Cap operation (0 = use geth default) | | `execution.caching.trie-cap-limit` | uint32 | `100` | amount of memory in megabytes to be used in the TrieDB Cap operation during maintenance | | `execution.caching.trie-clean-cache` | int | `600` | amount of memory in megabytes to cache unchanged state trie nodes with | | `execution.caching.trie-commit-batch-size` | uint32 | - | batch size in bytes used in the TrieDB Commit operation (0 = use geth default) | | `execution.caching.trie-dirty-cache` | int | `1024` | amount of memory in megabytes to cache state diffs against disk with (larger cache lowers database growth) | | `execution.caching.trie-time-limit` | duration | `1h0m0s` | maximum block processing time before trie is written to hard-disk | | `execution.caching.trie-time-limit-before-flush-maintenance` | duration | - | Execution will suggest that maintenance is run if the block processing time required to reach trie-time-limit is smaller or equal than trie-time-limit-before-flush-maintenance | | `execution.caching.trie-time-limit-random-offset` | duration | - | if greater then 0, the block processing time period of each trie write to hard-disk is shortened by a random value from range \[0, trie-time-limit-random-offset) | | `execution.consensus-rpc-client.arg-log-limit` | uint | `2048` | limit size of arguments in log entries | | `execution.consensus-rpc-client.connection-wait` | duration | - | how long to wait for initial connection | | `execution.consensus-rpc-client.jwtsecret` | string | - | path to file with jwtsecret for validation - ignored if url is self or self-auth | | `execution.consensus-rpc-client.retries` | uint | `3` | number of retries in case of failure(0 mean one attempt) | | `execution.consensus-rpc-client.retry-delay` | duration | - | delay between retries | | `execution.consensus-rpc-client.retry-errors` | string | `websocket: close.*\|dial tcp .*\|.*i/o timeout\|.*connection reset by peer\|.*connection refused` | Errors matching this regular expression are automatically retried | | `execution.consensus-rpc-client.timeout` | duration | - | per-response timeout (0-disabled) | | `execution.consensus-rpc-client.url` | string | - | url of server, use self for loopback websocket, self-auth for loopback with authentication | | `execution.consensus-rpc-client.websocket-message-size-limit` | int | `268435456` | websocket message size limit used by the RPC client. 0 means no limit | | `execution.disable-arbowner-ethcall` | bool | - | disable ArbOwner precompile calls outside on-chain execution (ethcall, gas estimation) | | `execution.enable-prefetch-block` | bool | `true` | enable prefetching of blocks | | `execution.forwarder.connection-timeout` | duration | `30s` | total time to wait before cancelling connection | | `execution.forwarder.idle-connection-timeout` | duration | `15s` | time until idle connections are closed | | `execution.forwarder.max-idle-connections` | int | `1` | maximum number of idle connections to keep open | | `execution.forwarder.redis-url` | string | - | the Redis URL to recommend target via | | `execution.forwarder.retry-interval` | duration | `100ms` | minimal time between update retries | | `execution.forwarder.update-interval` | duration | `1s` | forwarding target update interval | | `execution.forwarding-target` | string | - | transaction forwarding target URL, or "null" to disable forwarding (iff not sequencer) | | `execution.legacy-zero-base-fee-until` | uint | - | orbit-chain compat: re-enables the pre-v3.7 behavior of treating ArbOS<=40 blocks with zero base fee as non-arbitrum, for blocks with unix timestamp strictly less than this value (0 disables; set to a timestamp past the last zero-basefee block on the chain) | | `execution.parent-chain-reader.enable` | bool | `true` | enable reader connection | | `execution.parent-chain-reader.old-header-timeout` | duration | `5m0s` | warns if the latest l1 block is at least this old | | `execution.parent-chain-reader.poll-interval` | duration | `15s` | interval when polling endpoint | | `execution.parent-chain-reader.poll-only` | bool | - | do not attempt to subscribe to header events | | `execution.parent-chain-reader.poll-timeout` | duration | `5s` | timeout when polling endpoint | | `execution.parent-chain-reader.subscribe-err-interval` | duration | `5m0s` | interval for subscribe error | | `execution.parent-chain-reader.tx-timeout` | duration | `5m0s` | timeout when waiting for a transaction | | `execution.parent-chain-reader.use-finality-data` | bool | `true` | use l1 data about finalized/safe blocks | | `execution.recording-database.max-prepared` | int | `1000` | max references to store in the recording database | | `execution.recording-database.trie-clean-cache` | int | `16` | like trie-clean-cache for the separate, recording database (used for validation) | | `execution.recording-database.trie-dirty-cache` | int | `1024` | like trie-dirty-cache for the separate, recording database (used for validation) | | `execution.rpc-server.authenticated` | bool | `true` | rpc is authenticated | | `execution.rpc-server.enable` | bool | - | enable execution node to serve over rpc | | `execution.rpc-server.public` | bool | - | rpc is public | | `execution.rpc.allow-method` | strings | - | list of whitelisted rpc methods | | `execution.rpc.arbdebug.block-range-bound` | uint | `256` | bounds the number of blocks arbdebug calls may return | | `execution.rpc.arbdebug.timeout-queue-bound` | uint | `512` | bounds the length of timeout queues arbdebug calls may return | | `execution.rpc.block-redirects-list` | string | `default` | array of node configs to redirect block requests given as a json string. time duration should be supplied in number indicating nanoseconds | | `execution.rpc.classic-redirect` | string | - | url to redirect classic requests, use "error:\[CODE:]MESSAGE" to return specified error instead of redirecting | | `execution.rpc.classic-redirect-timeout` | duration | - | timeout for forwarded classic requests, where 0 = no timeout | | `execution.rpc.evm-timeout` | duration | `5s` | timeout used for eth\_call (0=infinite) | | `execution.rpc.feehistory-max-block-count` | uint | `1024` | max number of blocks a fee history request may cover | | `execution.rpc.filter-log-cache-size` | int | `32` | log filter system maximum number of cached blocks | | `execution.rpc.filter-timeout` | duration | `5m0s` | log filter system maximum time filters stay active | | `execution.rpc.gas-cap` | uint | `50000000` | cap on computation gas that can be used in eth\_call/estimateGas (0=infinite) | | `execution.rpc.log-export-checkpoints` | string | - | export log index checkpoints to file | | `execution.rpc.log-history` | uint | `9400000` | maximum number of blocks from head where a log search index is maintained | | `execution.rpc.log-no-history` | bool | - | no log search index is maintained | | `execution.rpc.max-recreate-state-depth` | int | `-2` | maximum depth for recreating state, measured in l2 gas (0=don't recreate state, -1=infinite, -2=use default value for archive or non-archive node (whichever is configured)) | | `execution.rpc.tx-allow-unprotected` | bool | `true` | allow transactions that aren't EIP-155 replay protected to be submitted over the RPC | | `execution.rpc.tx-fee-cap` | float | `1` | cap on transaction fee (in ether) that can be sent via the RPC APIs (0 = no cap) | | `execution.rpc.tx-sync-default-timeout` | duration | `20s` | default timeout for eth\_sendRawTransactionSync | | `execution.rpc.tx-sync-max-timeout` | duration | `1m0s` | maximum allowed timeout for eth\_sendRawTransactionSync | | `execution.secondary-forwarding-target` | strings | - | secondary transaction forwarding target URL | | `execution.sequencer.enable` | bool | - | act and post to l1 as sequencer | | `execution.sequencer.enable-profiling` | bool | - | enable CPU profiling and tracing | | `execution.sequencer.expected-surplus-gas-price-mode` | string | `BlobPrice` | gas price setting to be used in calculating estimated surplus. Allowed values- CalldataPrice, BlobPrice and CalldataPrice7523 | | `execution.sequencer.expected-surplus-hard-threshold` | string | `default` | if expected surplus is lower than this value, new incoming transactions will be denied | | `execution.sequencer.expected-surplus-soft-threshold` | string | `default` | if expected surplus is lower than this value, warnings are posted | | `execution.sequencer.forwarder.connection-timeout` | duration | `30s` | total time to wait before cancelling connection | | `execution.sequencer.forwarder.idle-connection-timeout` | duration | `1m0s` | time until idle connections are closed | | `execution.sequencer.forwarder.max-idle-connections` | int | `100` | maximum number of idle connections to keep open | | `execution.sequencer.forwarder.redis-url` | string | - | the Redis URL to recommend target via | | `execution.sequencer.forwarder.retry-interval` | duration | `100ms` | minimal time between update retries | | `execution.sequencer.forwarder.update-interval` | duration | `1s` | forwarding target update interval | | `execution.sequencer.max-acceptable-timestamp-delta` | duration | `1h0m0s` | maximum acceptable time difference between the local time and the latest L1 block's timestamp | | `execution.sequencer.max-block-speed` | duration | `250ms` | minimum delay between blocks (sets a maximum speed of block production) | | `execution.sequencer.max-revert-gas-reject` | uint | - | maximum gas executed in a revert for the sequencer to reject the transaction instead of posting it (anti-DOS) | | `execution.sequencer.max-tx-data-size` | int | `95000` | maximum transaction size the sequencer will accept | | `execution.sequencer.nonce-cache-size` | int | `1024` | size of the tx sender nonce cache | | `execution.sequencer.nonce-failure-cache-expiry` | duration | `1s` | maximum amount of time to wait for a predecessor before rejecting a tx with nonce too high | | `execution.sequencer.nonce-failure-cache-size` | int | `1024` | number of transactions with too high of a nonce to keep in memory while waiting for their predecessor | | `execution.sequencer.queue-size` | int | `1024` | size of the pending tx queue | | `execution.sequencer.queue-timeout` | duration | `12s` | maximum amount of time transaction can wait in queue | | `execution.sequencer.read-from-tx-queue-timeout` | duration | `10ms` | timeout for reading new messages | | `execution.sequencer.sender-whitelist` | strings | - | comma separated whitelist of authorized senders (if empty, everyone is allowed) | | `execution.sequencer.timeboost.auction-contract-address` | string | - | Address of the proxy pointing to the ExpressLaneAuction contract | | `execution.sequencer.timeboost.auctioneer-address` | string | - | Address of the Timeboost Autonomous Auctioneer | | `execution.sequencer.timeboost.early-submission-grace` | duration | `2s` | period of time before the next round where submissions for the next round will be queued | | `execution.sequencer.timeboost.enable` | bool | - | enable timeboost based on express lane auctions | | `execution.sequencer.timeboost.express-lane-advantage` | duration | `200ms` | specify the express lane advantage | | `execution.sequencer.timeboost.max-future-sequence-distance` | uint | `1000` | maximum allowed difference (in terms of sequence numbers) between a future express lane tx and the current sequence count of a round | | `execution.sequencer.timeboost.queue-timeout-in-blocks` | uint | `5` | maximum amount of time (measured in blocks) that Express Lane transactions can wait in the sequencer's queue | | `execution.sequencer.timeboost.redis-update-events-channel-size` | uint | `500` | size of update events' buffered channels in timeboost redis coordinator | | `execution.sequencer.timeboost.redis-url` | string | `unset` | the Redis URL for ExpressLaneService to coordinate via | | `execution.sequencer.timeboost.sequencer-http-endpoint` | string | `http://localhost:8547` | this sequencer's http endpoint | | `execution.stylus-target.allow-fallback` | bool | `true` | if true, fall back to an alternative compiler when compilation of a Stylus program fails | | `execution.stylus-target.amd64` | string | `x86_64-linux-unknown+sse4.2+lzcnt+bmi` | stylus programs compilation target for amd64 linux | | `execution.stylus-target.arm64` | string | `arm64-linux-unknown+neon` | stylus programs compilation target for arm64 linux | | `execution.stylus-target.extra-archs` | strings | `[wavm]` | Comma separated list of extra architectures to cross-compile stylus program to and cache in wasm store (additionally to local target). Currently must include at least wavm. (supported targets: wavm, arm64, amd64, host) | | `execution.stylus-target.host` | string | - | stylus programs compilation target for system other than 64-bit ARM or 64-bit x86 | | `execution.stylus-target.max-stylus-call-depth` | uint16 | - | max number of Stylus frames simultaneously on the call stack (counts only Stylus frames; EVM frames between two Stylus frames do not decrement it); exceeding the limit rejects non-on-chain calls; 0 disables the limit | | `execution.stylus-target.max-stylus-open-pages` | uint16 | `128` | max open WASM pages per tx; exceeding the limit rejects non-on-chain calls and filters sequencer-committed txs (delayed inbox is exempt); 0 disables the limit | | `execution.stylus-target.native-stack-size` | uint | - | initial native stack size in bytes for Wasmer coroutines used by Stylus execution (0 = default 1MB) | | `execution.sync-monitor.finalized-block-wait-for-block-validator` | bool | - | wait for block validator to complete before returning finalized block number | | `execution.sync-monitor.msg-lag` | duration | `1s` | allowed message lag while still considered in sync | | `execution.sync-monitor.safe-block-wait-for-block-validator` | bool | - | wait for block validator to complete before returning safe block number | | `execution.transaction-filtering.address-filter.address-checker-queue-size` | int | `8192` | work queue size for address checker | | `execution.transaction-filtering.address-filter.address-checker-worker-count` | int | `4` | number of workers for address checker | | `execution.transaction-filtering.address-filter.cache-size` | int | `10000` | LRU cache size for address lookup results | | `execution.transaction-filtering.address-filter.enable` | bool | - | enable restricted address synchronization service | | `execution.transaction-filtering.address-filter.poll-interval` | duration | `5m0s` | interval between polling S3 for hash list updates | | `execution.transaction-filtering.address-filter.s3.access-key` | string | - | S3 access key | | `execution.transaction-filtering.address-filter.s3.bucket` | string | - | S3 bucket name | | `execution.transaction-filtering.address-filter.s3.chunk-size-mb` | int | `32` | S3 multipart download part size in MB | | `execution.transaction-filtering.address-filter.s3.concurrency` | int | `10` | S3 multipart download concurrency | | `execution.transaction-filtering.address-filter.s3.endpoint` | string | - | custom S3 endpoint URL (for MinIO, localstack, or other S3-compatible services) | | `execution.transaction-filtering.address-filter.s3.max-retries` | int | `3` | maximum retries for S3 part body download | | `execution.transaction-filtering.address-filter.s3.object-key` | string | - | S3 object key (path) to the file | | `execution.transaction-filtering.address-filter.s3.region` | string | - | S3 region | | `execution.transaction-filtering.address-filter.s3.secret-key` | string | - | S3 secret key | | `execution.transaction-filtering.disable-delayed-sequencing-filter` | bool | - | disable delayed sequencing filter | | `execution.transaction-filtering.enable-ethcall-filter` | bool | - | enable address filtering for eth\_estimateGas and eth\_call | | `execution.transaction-filtering.event-filter.path` | string | - | path to JSON file containing event filter rules | | `execution.transaction-filtering.filtering-report-rpc-client.arg-log-limit` | uint | `2048` | limit size of arguments in log entries | | `execution.transaction-filtering.filtering-report-rpc-client.connection-wait` | duration | - | how long to wait for initial connection | | `execution.transaction-filtering.filtering-report-rpc-client.jwtsecret` | string | - | path to file with jwtsecret for validation - ignored if url is self or self-auth | | `execution.transaction-filtering.filtering-report-rpc-client.retries` | uint | `3` | number of retries in case of failure(0 mean one attempt) | | `execution.transaction-filtering.filtering-report-rpc-client.retry-delay` | duration | - | delay between retries | | `execution.transaction-filtering.filtering-report-rpc-client.retry-errors` | string | `websocket: close.*\|dial tcp .*\|.*i/o timeout\|.*connection reset by peer\|.*connection refused` | Errors matching this regular expression are automatically retried | | `execution.transaction-filtering.filtering-report-rpc-client.timeout` | duration | - | per-response timeout (0-disabled) | | `execution.transaction-filtering.filtering-report-rpc-client.url` | string | - | url of server, use self for loopback websocket, self-auth for loopback with authentication | | `execution.transaction-filtering.filtering-report-rpc-client.websocket-message-size-limit` | int | `268435456` | websocket message size limit used by the RPC client. 0 means no limit | | `execution.transaction-filtering.transaction-filterer-rpc-client.arg-log-limit` | uint | `2048` | limit size of arguments in log entries | | `execution.transaction-filtering.transaction-filterer-rpc-client.connection-wait` | duration | - | how long to wait for initial connection | | `execution.transaction-filtering.transaction-filterer-rpc-client.jwtsecret` | string | - | path to file with jwtsecret for validation - ignored if url is self or self-auth | | `execution.transaction-filtering.transaction-filterer-rpc-client.retries` | uint | `3` | number of retries in case of failure(0 mean one attempt) | | `execution.transaction-filtering.transaction-filterer-rpc-client.retry-delay` | duration | - | delay between retries | | `execution.transaction-filtering.transaction-filterer-rpc-client.retry-errors` | string | `websocket: close.*\|dial tcp .*\|.*i/o timeout\|.*connection reset by peer\|.*connection refused` | Errors matching this regular expression are automatically retried | | `execution.transaction-filtering.transaction-filterer-rpc-client.timeout` | duration | - | per-response timeout (0-disabled) | | `execution.transaction-filtering.transaction-filterer-rpc-client.url` | string | - | url of server, use self for loopback websocket, self-auth for loopback with authentication | | `execution.transaction-filtering.transaction-filterer-rpc-client.websocket-message-size-limit` | int | `268435456` | websocket message size limit used by the RPC client. 0 means no limit | | `execution.tx-indexer.enable` | bool | `true` | enables transaction indexer | | `execution.tx-indexer.min-batch-delay` | duration | `1s` | minimum delay between transaction indexing/unindexing batches; the bigger the delay, the more blocks can be included in each batch | | `execution.tx-indexer.threads` | int | `2` | number of threads used to RLP decode blocks during indexing/unindexing of historical transactions | | `execution.tx-indexer.tx-lookup-limit` | uint | `126230400` | retain the ability to lookup transactions by hash for the past N blocks (0 = all blocks) | | `execution.tx-pre-checker.required-state-age` | int | `2` | how long ago should the storage conditions from eth\_SendRawTransactionConditional be true, 0 = don't check old state | | `execution.tx-pre-checker.required-state-max-blocks` | uint | `4` | maximum number of blocks to look back while looking for the \ seconds old state, 0 = don't limit the search | | `execution.tx-pre-checker.strictness` | uint | `20` | how strict to be when checking txs before forwarding them. 0 = accept anything, 10 = should never reject anything that'd succeed, 20 = likely won't reject anything that'd succeed, 30 = full validation which may reject txs that would succeed | ## file-logging Related guide: [Node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md) file-logging flags (8) | Flag | Type | Default | Description | | -------------------------- | ------ | ----------- | -------------------------------------------------------------------------------------------------------------- | | `file-logging.buf-size` | int | `512` | size of intermediate log records buffer | | `file-logging.compress` | bool | `true` | enable compression of old log files | | `file-logging.enable` | bool | `true` | enable logging to file | | `file-logging.file` | string | `nitro.log` | path to log file | | `file-logging.local-time` | bool | - | if true: local time will be used in old log filename timestamps | | `file-logging.max-age` | int | - | maximum number of days to retain old log files based on the timestamp encoded in their filename (0 = no limit) | | `file-logging.max-backups` | int | `20` | maximum number of old log files to retain (0 = no limit) | | `file-logging.max-size` | int | `5` | log file size in Mb that will trigger log file rotation (0 = trigger disabled) | ## graphql Related guide: [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) graphql flags (3) | Flag | Type | Default | Description | | -------------------- | ------- | ------------- | ---------------------------------------------------------------------------------------------------------------- | | `graphql.corsdomain` | strings | - | Comma separated list of domains from which to accept cross origin requests (browser enforced) | | `graphql.enable` | bool | - | Enable graphql endpoint on the rpc endpoint | | `graphql.vhosts` | strings | `[localhost]` | Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '\*' wildcard | ## http Related guide: [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) http flags (10) | Flag | Type | Default | Description | | ------------------------------------------ | -------- | -------------------- | ---------------------------------------------------------------------------------------------------------------- | | `http.addr` | string | - | HTTP-RPC server listening interface | | `http.api` | strings | `[net,web3,eth,arb]` | APIs offered over the HTTP-RPC interface | | `http.corsdomain` | strings | - | Comma separated list of domains from which to accept cross origin requests (browser enforced) | | `http.port` | int | `8547` | HTTP-RPC server listening port | | `http.rpcprefix` | string | - | HTTP path path prefix on which JSON-RPC is served. Use '/' to serve on all paths | | `http.server-timeouts.idle-timeout` | duration | `2m0s` | the maximum amount of time to wait for the next request when keep-alives are enabled (http.Server.IdleTimeout) | | `http.server-timeouts.read-header-timeout` | duration | `30s` | the amount of time allowed to read the request headers (http.Server.ReadHeaderTimeout) | | `http.server-timeouts.read-timeout` | duration | `30s` | the maximum duration for reading the entire request (http.Server.ReadTimeout) | | `http.server-timeouts.write-timeout` | duration | `30s` | the maximum duration before timing out writes of the response (http.Server.WriteTimeout) | | `http.vhosts` | strings | `[localhost]` | Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '\*' wildcard | ## init Related guide: [Docker and CLI binaries](/run-arbitrum-node/nitro/docker-and-cli-binaries.md) init flags (29) | Flag | Type | Default | Description | | --------------------------------------- | -------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `init.accounts-per-sync` | uint | `100000` | during init - sync database every X accounts. Lower value for low-memory systems. 0 disables. | | `init.dev-init` | bool | - | init with dev data (1 account with balance) instead of file import | | `init.dev-init-address` | string | - | Address of dev-account. Leave empty to use the dev-wallet. | | `init.dev-init-blocknum` | uint | - | Number of preinit blocks. Must exist in ancient database. | | `init.dev-max-code-size` | uint | - | Max code size for dev accounts | | `init.download-path` | string | - | path to save temp downloaded file | | `init.download-poll` | duration | `1m0s` | how long to wait between polling attempts | | `init.empty` | bool | - | init with empty state | | `init.force` | bool | - | if true: in case database exists init code will be reexecuted and genesis block compared to database | | `init.genesis-json-file` | string | - | path for genesis json file | | `init.genesis-json-file-directory` | string | - | directory path for genesis json files - will search for a file named by the chain ID | | `init.import-file` | string | - | path for json data to import | | `init.import-wasm` | bool | - | if set, import the wasm directory when downloading a database (contains executable code - only use with highly trusted source) | | `init.latest` | string | - | if set, searches for the latest snapshot of the given kind (accepted values: "archive" \| "pruned" \| "genesis") | | `init.latest-base` | string | `https://snapshot.arbitrum.foundation/` | base url used when searching for the latest | | `init.prune` | string | - | pruning for a given use: "full" for full nodes serving RPC requests, or "validator" for validators | | `init.prune-bloom-size` | uint | `2048` | the amount of memory in megabytes to use for the pruning bloom filter (higher values prune better) | | `init.prune-parallel-storage-traversal` | bool | - | if true: use parallel pruning per account | | `init.prune-threads` | int | `2` | the number of threads to use when pruning | | `init.prune-trie-clean-cache` | int | `600` | amount of memory in megabytes to cache unchanged state trie nodes with when traversing state database during pruning | | `init.rebuild-local-wasm` | string | `auto` | rebuild local wasm database on boot if needed (otherwise-will be done lazily). Three modes are supported "auto"- (enabled by default) if any previous rebuilding attempt was successful then rebuilding is disabled else continues to rebuild, "force"- force rebuilding which would commence rebuilding despite the status of previous attempts, "false"- do not rebuild on startup | | `init.recreate-missing-state-from` | uint | - | block number to start recreating missing states from (0 = disabled) | | `init.reorg-to-batch` | int | `-1` | rolls back the blockchain to a specified batch number | | `init.reorg-to-block-batch` | int | `-1` | rolls back the blockchain to the first batch at or before a given block number | | `init.reorg-to-message-batch` | int | `-1` | rolls back the blockchain to the first batch at or before a given message index | | `init.then-quit` | bool | - | quit after init is done | | `init.url` | string | - | url to download initialization data - will poll if download fails | | `init.validate-checksum` | bool | `true` | if true: validate the checksum after downloading the snapshot | | `init.validate-genesis-assertion` | bool | `true` | tests genesis assertion posted on parent chain against the genesis block created on init | ## ipc Related guide: [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) ipc flags (1) | Flag | Type | Default | Description | | ---------- | ------ | ------- | ------------------------------------------------------------------------- | | `ipc.path` | string | - | Requested location to place the IPC endpoint. An empty path disables IPC. | ## log-level Related guide: [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) log-level flags (1) | Flag | Type | Default | Description | | ----------- | ------ | ------- | ----------------------------------------------------------------- | | `log-level` | string | `INFO` | log level, valid values are CRIT, ERROR, WARN, INFO, DEBUG, TRACE | ## log-type Related guide: [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) log-type flags (1) | Flag | Type | Default | Description | | ---------- | ------ | ----------- | ---------------------------- | | `log-type` | string | `plaintext` | log type (plaintext or json) | ## metrics Related guide: [Node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md) metrics flags (1) | Flag | Type | Default | Description | | --------- | ---- | ------- | -------------- | | `metrics` | bool | - | enable metrics | ## metrics-server Related guide: [Node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md) metrics-server flags (3) | Flag | Type | Default | Description | | -------------------------------- | -------- | ----------- | ------------------------------ | | `metrics-server.addr` | string | `127.0.0.1` | metrics server address | | `metrics-server.port` | int | `6070` | metrics server port | | `metrics-server.update-interval` | duration | `3s` | metrics server update interval | ## node Related guide: [Node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md) node flags (408) | Flag | Type | Default | Description | | ------------------------------------------------------------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `node.batch-poster.anytrust-retention-period` | duration | `360h0m0s` | In AnyTrust mode, the period which AnyTrust nodes are requested to retain the stored batches. | | `node.batch-poster.check-batch-correctness` | bool | `true` | setting this to true will run the batch against an inbox multiplexer and verifies that it produces the correct set of messages | | `node.batch-poster.compression-level` | int | - | DEPRECATED: use compression-levels instead. batch compression level | | `node.batch-poster.compression-levels` | CompressionLevelStepList | `null` | JSON array of compression level steps. Format: \[{"backlog":\,"level":\,"recompression-level":\},...]. First entry must have backlog:0. Both Level and recomp-level must be 0-11, weakly descending. Example: \[{"backlog":0,"level":11,"recompression-level":11},{"backlog":21,"level":6,"recompression-level":11}] | | `node.batch-poster.data-poster.allocate-mempool-balance` | bool | `true` | if true, don't put transactions in the mempool that spend a total greater than the batch poster's balance | | `node.batch-poster.data-poster.blob-tx-replacement-times` | durationSlice | `[5m0s,10m0s,30m0s,1h0m0s,4h0m0s,8h0m0s,16h0m0s,22h0m0s]` | comma-separated list of durations since first posting a blob transaction to attempt a replace-by-fee | | `node.batch-poster.data-poster.disable-new-tx` | bool | - | disable posting new transactions, data poster will still keep confirming existing batches | | `node.batch-poster.data-poster.elapsed-time-base` | duration | `10m0s` | unit to measure the time elapsed since creation of transaction used for maximum fee cap calculation | | `node.batch-poster.data-poster.elapsed-time-importance` | float | `10` | weight given to the units of time elapsed used for maximum fee cap calculation | | `node.batch-poster.data-poster.external-signer.address` | string | - | external signer address | | `node.batch-poster.data-poster.external-signer.client-cert` | string | - | rpc client cert | | `node.batch-poster.data-poster.external-signer.client-private-key` | string | - | rpc client private key | | `node.batch-poster.data-poster.external-signer.insecure-skip-verify` | bool | - | skip TLS certificate verification | | `node.batch-poster.data-poster.external-signer.method` | string | `eth_signTransaction` | external signer method | | `node.batch-poster.data-poster.external-signer.root-ca` | string | - | external signer root CA | | `node.batch-poster.data-poster.external-signer.url` | string | - | external signer url | | `node.batch-poster.data-poster.legacy-storage-encoding` | bool | - | encodes items in a legacy way (as it was before dropping generics) | | `node.batch-poster.data-poster.max-blob-tx-tip-cap-gwei` | float | `1` | the maximum tip cap to post EIP-4844 blob carrying transactions at | | `node.batch-poster.data-poster.max-fee-bid-multiple-bips` | uint | `100000` | the maximum multiple of the current price to bid for a transaction's fees (may be exceeded due to min rbf increase, 0 = unlimited) | | `node.batch-poster.data-poster.max-fee-cap-formula` | string | - | mathematical formula to calculate maximum fee cap gwei the result of which would be float64. This expression is expected to be evaluated please refer to find all available mathematical operators. Currently available variables to construct the formula are BacklogOfBatches, UrgencyGWei, ElapsedTime, ElapsedTimeBase, ElapsedTimeImportance, and TargetPriceGWei (default "((BacklogOfBatches \_ UrgencyGWei) \*\* 2) + ((ElapsedTime/ElapsedTimeBase) \*\* 2) \_ ElapsedTimeImportance + TargetPriceGWei") | | `node.batch-poster.data-poster.max-mempool-transactions` | uint | `18` | the maximum number of transactions to have queued in the mempool at once (0 = unlimited) | | `node.batch-poster.data-poster.max-mempool-weight` | uint | `18` | the maximum number of weight (weight = min(1, tx.blobs)) to have queued in the mempool at once (0 = unlimited) | | `node.batch-poster.data-poster.max-queued-transactions` | int | - | the maximum number of unconfirmed transactions to track at once (0 = unlimited) | | `node.batch-poster.data-poster.max-tip-cap-gwei` | float | `1.2` | the maximum tip cap to post transactions at | | `node.batch-poster.data-poster.min-blob-tx-tip-cap-gwei` | float | `1` | the minimum tip cap to post EIP-4844 blob carrying transactions at | | `node.batch-poster.data-poster.min-tip-cap-gwei` | float | `0.05` | the minimum tip cap to post transactions at | | `node.batch-poster.data-poster.nonce-rbf-soft-confs` | uint | `1` | the maximum probable reorg depth, used to determine when a transaction will no longer likely need replaced-by-fee | | `node.batch-poster.data-poster.redis-signer.fallback-verification-key` | string | - | a fallback key used for message verification | | `node.batch-poster.data-poster.redis-signer.signing-key` | string | - | a 32-byte (64-character) hex string used to sign messages, or a path to a file containing it | | `node.batch-poster.data-poster.replacement-times` | durationSlice | `[5m0s,10m0s,20m0s,30m0s,1h0m0s,2h0m0s,4h0m0s,6h0m0s,8h0m0s,12h0m0s,16h0m0s,18h0m0s,20h0m0s,22h0m0s]` | comma-separated list of durations since first posting to attempt a replace-by-fee | | `node.batch-poster.data-poster.target-price-gwei` | float | `60` | the target price to use for maximum fee cap calculation | | `node.batch-poster.data-poster.urgency-gwei` | float | `2` | the urgency to use for maximum fee cap calculation | | `node.batch-poster.data-poster.use-db-storage` | bool | `true` | uses database storage when enabled | | `node.batch-poster.data-poster.use-noop-storage` | bool | - | uses noop storage, it doesn't store anything | | `node.batch-poster.data-poster.wait-for-l1-finality` | bool | `true` | only treat a transaction as confirmed after L1 finality has been achieved (recommended) | | `node.batch-poster.delay-buffer-always-updatable` | bool | `true` | always treat delay buffer as updatable | | `node.batch-poster.delay-buffer-threshold-margin` | uint | `25` | the number of blocks to post the batch before reaching the delay buffer threshold | | `node.batch-poster.disable-dap-fallback-store-data-on-chain` | bool | - | If unable to batch to DA provider, disable fallback storing data on chain | | `node.batch-poster.enable` | bool | - | enable posting batches to l1 | | `node.batch-poster.error-delay` | duration | `10s` | how long to delay after error posting batch | | `node.batch-poster.ethda-fallback-batch-count` | int | `10` | number of batches to post to EthDA before retrying AltDA after a fallback | | `node.batch-poster.extra-batch-gas` | uint | `50000` | use this much more gas than estimation says is necessary to post batches | | `node.batch-poster.gas-estimate-base-fee-multiple-bips` | uint | `15000` | for gas estimation, use this multiple of the basefee (measured in basis points) as the max fee per gas | | `node.batch-poster.gas-refunder-address` | string | - | The gas refunder contract address (optional) | | `node.batch-poster.ignore-blob-price` | bool | - | if the parent chain supports 4844 blobs and ignore-blob-price is true, post 4844 blobs even if it's not price efficient | | `node.batch-poster.l1-block-bound` | string | - | only post messages to batches when they're within the max future block/timestamp as of this L1 block tag ("safe", "finalized", "latest", or "ignore" to ignore this check) | | `node.batch-poster.l1-block-bound-bypass` | duration | `1h0m0s` | post batches even if not within the layer 1 future bounds if we're within this margin of the max delay | | `node.batch-poster.max-4844-batch-size` | int | - | maximum estimated compressed 4844 blob enabled batch size | | `node.batch-poster.max-calldata-batch-size` | int | `100000` | maximum estimated compressed calldata batch size | | `node.batch-poster.max-delay` | duration | `1h0m0s` | maximum batch posting delay | | `node.batch-poster.max-empty-batch-delay` | duration | `72h0m0s` | maximum empty batch posting delay, batch poster will only be able to post an empty batch if this time period building a batch has passed; if 0, disable automatic empty batch posting | | `node.batch-poster.max-size` | int | - | DEPRECATED: use node.batch-poster.max-calldata-batch-size instead | | `node.batch-poster.parent-chain-eip7623` | string | `auto` | if parent chain uses EIP7623 ("yes", "no", "auto") | | `node.batch-poster.parent-chain-wallet.account` | string | `is first account in keystore` | account to use | | `node.batch-poster.parent-chain-wallet.only-create-key` | bool | - | if true, creates new key then exits | | `node.batch-poster.parent-chain-wallet.password` | string | `PASSWORD_NOT_SET` | wallet passphrase | | `node.batch-poster.parent-chain-wallet.pathname` | string | `batch-poster-wallet` | pathname for wallet | | `node.batch-poster.parent-chain-wallet.private-key` | string | - | private key for wallet | | `node.batch-poster.poll-interval` | duration | `10s` | how long to wait after no batches are ready to be posted before checking again | | `node.batch-poster.post-4844-blobs` | bool | - | if the parent chain supports 4844 blobs and they're well priced, post EIP-4844 blobs | | `node.batch-poster.redis-lock.background-lock` | bool | - | should node always try grabbing lock in background | | `node.batch-poster.redis-lock.enable` | bool | `true` | if false, always treat this as locked and don't write the lock to redis | | `node.batch-poster.redis-lock.key` | string | - | key for lock | | `node.batch-poster.redis-lock.lockout-duration` | duration | `1m0s` | how long lock is held | | `node.batch-poster.redis-lock.my-id` | string | - | this node's id prefix when acquiring the lock (optional) | | `node.batch-poster.redis-lock.refresh-duration` | duration | `10s` | how long between consecutive calls to redis | | `node.batch-poster.redis-url` | string | - | if non-empty, the Redis URL to store queued transactions in | | `node.batch-poster.reorg-resistance-margin` | duration | `10m0s` | do not post batch if its within this duration from layer 1 minimum bounds. Requires l1-block-bound option not be set to "ignore" | | `node.batch-poster.use-access-lists` | bool | `true` | post batches with access lists to reduce gas usage (disabled for L3s) | | `node.batch-poster.wait-for-max-delay` | bool | - | wait for the max batch delay, even if the batch is full | | `node.block-metadata-fetcher.api-blocks-limit` | uint | `100` | maximum number of blocks per arb\_getRawBlockMetadata query | | `node.block-metadata-fetcher.enable` | bool | - | enable syncing blockMetadata using a bulk blockMetadata api | | `node.block-metadata-fetcher.max-sync-interval` | duration | `32m0s` | maximum time between blockMetadata requests | | `node.block-metadata-fetcher.source.arg-log-limit` | uint | `2048` | limit size of arguments in log entries | | `node.block-metadata-fetcher.source.connection-wait` | duration | - | how long to wait for initial connection | | `node.block-metadata-fetcher.source.jwtsecret` | string | - | path to file with jwtsecret for validation - ignored if url is self or self-auth | | `node.block-metadata-fetcher.source.retries` | uint | `3` | number of retries in case of failure(0 mean one attempt) | | `node.block-metadata-fetcher.source.retry-delay` | duration | - | delay between retries | | `node.block-metadata-fetcher.source.retry-errors` | string | `websocket: close.*\|dial tcp .*\|.*i/o timeout\|.*connection reset by peer\|.*connection refused` | Errors matching this regular expression are automatically retried | | `node.block-metadata-fetcher.source.timeout` | duration | `10s` | per-response timeout (0-disabled) | | `node.block-metadata-fetcher.source.url` | string | `self-auth` | url of server, use self for loopback websocket, self-auth for loopback with authentication | | `node.block-metadata-fetcher.source.websocket-message-size-limit` | int | `268435456` | websocket message size limit used by the RPC client. 0 means no limit | | `node.block-metadata-fetcher.sync-interval` | duration | `1m0s` | minimum time between blockMetadata requests | | `node.block-validator.batch-cache-limit` | uint32 | `20` | limit number of old batches to keep in block-validator | | `node.block-validator.block-inputs-file-path` | string | `./target/validation_inputs` | directory to write block validation inputs files | | `node.block-validator.current-module-root` | string | `current` | current wasm module root ('current' read from chain, 'latest' from machines/latest dir, or provide hash) | | `node.block-validator.enable` | bool | - | enable block-by-block validation | | `node.block-validator.failure-is-fatal` | bool | `true` | failing a validation is treated as a fatal error | | `node.block-validator.forward-blocks` | uint | `128` | prepare entries for up to that many blocks ahead of validation (stores batch-copy per block) | | `node.block-validator.memory-free-limit` | string | `default` | minimum free-memory limit after reaching which the blockvalidator pauses validation. Enabled by default as 1GB, to disable provide empty string | | `node.block-validator.pending-upgrade-module-root` | string | `latest` | pending upgrade wasm module root to additionally validate (hash, 'latest' or empty) | | `node.block-validator.prerecorded-blocks` | uint | `4` | record that many blocks ahead of validation (larger footprint) | | `node.block-validator.recording-iter-limit` | uint | `20` | limit on block recordings sent per iteration | | `node.block-validator.redis-validation-client-config.create-streams` | bool | `true` | create redis streams if it does not exist | | `node.block-validator.redis-validation-client-config.name` | string | `redis validation client` | validation client name | | `node.block-validator.redis-validation-client-config.producer-config.check-result-interval` | duration | `5s` | interval in which producer checks pending messages whether consumer processing them is inactive | | `node.block-validator.redis-validation-client-config.producer-config.request-timeout` | duration | `3h0m0s` | timeout after which the message in redis stream is considered as errored, this prevents workers from working on wrong requests indefinitely | | `node.block-validator.redis-validation-client-config.redis-url` | string | - | redis url | | `node.block-validator.redis-validation-client-config.room` | int32 | `2` | validation client room | | `node.block-validator.redis-validation-client-config.stream-prefix` | string | - | prefix for stream name | | `node.block-validator.redis-validation-client-config.stylus-archs` | strings | `[wavm]` | archs required for stylus workers | | `node.block-validator.validation-poll` | duration | `1s` | poll time to check validations | | `node.block-validator.validation-sent-limit` | uint | `1024` | limit on block validations to keep in validation sent state | | `node.block-validator.validation-server-configs-list` | string | `default` | array of execution rpc configs given as a json string. time duration should be supplied in number indicating nanoseconds | | `node.block-validator.validation-server.arg-log-limit` | uint | `2048` | limit size of arguments in log entries | | `node.block-validator.validation-server.connection-wait` | duration | - | how long to wait for initial connection | | `node.block-validator.validation-server.jwtsecret` | string | - | path to file with jwtsecret for validation - ignored if url is self or self-auth | | `node.block-validator.validation-server.retries` | uint | `3` | number of retries in case of failure(0 mean one attempt) | | `node.block-validator.validation-server.retry-delay` | duration | - | delay between retries | | `node.block-validator.validation-server.retry-errors` | string | `websocket: close.*\|dial tcp .*\|.*i/o timeout\|.*connection reset by peer\|.*connection refused` | Errors matching this regular expression are automatically retried | | `node.block-validator.validation-server.timeout` | duration | `10s` | per-response timeout (0-disabled) | | `node.block-validator.validation-server.url` | string | `self-auth` | url of server, use self for loopback websocket, self-auth for loopback with authentication | | `node.block-validator.validation-server.websocket-message-size-limit` | int | `268435456` | websocket message size limit used by the RPC client. 0 means no limit | | `node.block-validator.validation-spawning-allowed-attempts` | uint | `1` | number of attempts allowed when trying to spawn a validation before erroring out | | `node.block-validator.validation-spawning-allowed-timeouts` | uint | `3` | number of timeout errors allowed per validation attempt before treating it as a fatal error (separate from allowed-attempts) | | `node.bold.api` | bool | - | enable api | | `node.bold.api-db-path` | string | `bold-api-db` | bold api db path | | `node.bold.api-host` | string | `127.0.0.1` | bold api host | | `node.bold.api-port` | uint16 | `9393` | bold api port | | `node.bold.assertion-confirming-interval` | duration | `1m0s` | confirm assertion interval | | `node.bold.assertion-posting-interval` | duration | `15m0s` | assertion posting interval | | `node.bold.assertion-scanning-interval` | duration | `1m0s` | scan assertion interval | | `node.bold.auto-deposit` | bool | `true` | auto-deposit stake token whenever making a move in BoLD that does not have enough stake token balance | | `node.bold.auto-increase-allowance` | bool | `true` | auto-increase spending allowance of the stake token by the rollup and challenge manager contracts | | `node.bold.check-staker-switch-interval` | duration | `1m0s` | how often to check if staker can switch to bold | | `node.bold.delegated-staking.custom-withdrawal-address` | string | - | enable a custom withdrawal address for staking on the rollup contract, useful for delegated stakers | | `node.bold.delegated-staking.enable` | bool | - | enable delegated staking by having the validator call newStake on startup | | `node.bold.enable-fast-confirmation` | bool | - | enable fast confirmation | | `node.bold.max-get-log-blocks` | int | `5000` | maximum size for chunk of blocks when using get logs rpc | | `node.bold.minimum-gap-to-parent-assertion` | duration | `1m0s` | minimum duration to wait since the parent assertion was created to post a new assertion | | `node.bold.parent-chain-block-time` | duration | `12s` | the average block time of the parent chain where assertions are posted | | `node.bold.rpc-block-number` | string | `finalized` | define the block number to use for reading data onchain, either latest, safe, or finalized | | `node.bold.start-validation-from-staked` | bool | `true` | assume staked nodes are valid | | `node.bold.state-provider-config.check-batch-finality` | bool | `true` | check batch finality | | `node.bold.state-provider-config.machine-leaves-cache-path` | string | `machine-hashes-cache` | path to machine cache | | `node.bold.state-provider-config.validator-name` | string | `default-validator` | name identifier for cosmetic purposes | | `node.bold.track-challenge-parent-assertion-hashes` | strings | - | only track challenges/edges with these parent assertion hashes | | `node.consensus-execution-syncer.sync-interval` | duration | `300ms` | Interval in which finality and sync data is pushed from consensus to execution | | `node.da.anytrust.enable` | bool | - | enable Anytrust Data Availability mode | | `node.da.anytrust.max-batch-size` | int | `1000000` | maximum batch size for AnyTrust DA (compressed) | | `node.da.anytrust.panic-on-error` | bool | - | whether the Data Availability Service should fail immediately on errors (not recommended) | | `node.da.anytrust.request-timeout` | duration | `5s` | Data Availability Service timeout duration for Store requests | | `node.da.anytrust.rest-aggregator.connection-wait` | duration | `1s` | how long to wait for initial connection | | `node.da.anytrust.rest-aggregator.enable` | bool | - | enable retrieval of sequencer batch data from a list of remote REST endpoints; if other AnyTrust storage types are enabled, this mode is used as a fallback | | `node.da.anytrust.rest-aggregator.max-per-endpoint-stats` | int | `20` | number of stats entries (latency and success rate) to keep for each REST endpoint; controls whether strategy is faster or slower to respond to changing conditions | | `node.da.anytrust.rest-aggregator.online-url-list` | string | - | a URL to a list of URLs of REST AnyTrust endpoints that is checked at startup; additive with the url option | | `node.da.anytrust.rest-aggregator.online-url-list-fetch-interval` | duration | `1h0m0s` | time interval to periodically fetch url list from online-url-list | | `node.da.anytrust.rest-aggregator.simple-explore-exploit-strategy.exploit-iterations` | uint32 | `1000` | number of consecutive GetByHash calls to the aggregator where each call will cause it to select from REST endpoints in order of best latency and success rate, before switching to explore mode | | `node.da.anytrust.rest-aggregator.simple-explore-exploit-strategy.explore-iterations` | uint32 | `20` | number of consecutive GetByHash calls to the aggregator where each call will cause it to randomly select from REST endpoints until one returns successfully, before switching to exploit mode | | `node.da.anytrust.rest-aggregator.strategy` | string | `simple-explore-exploit` | strategy to use to determine order and parallelism of calling REST endpoint URLs; valid options are 'simple-explore-exploit' | | `node.da.anytrust.rest-aggregator.strategy-update-interval` | duration | `10s` | how frequently to update the strategy with endpoint latency and error rate data | | `node.da.anytrust.rest-aggregator.sync-to-storage.delay-on-error` | duration | `1s` | time to wait if encountered an error before retrying | | `node.da.anytrust.rest-aggregator.sync-to-storage.eager` | bool | - | eagerly sync batch data to this AnyTrust server's storage from the rest endpoints, using L1 as the index of batch data hashes; otherwise only sync lazily | | `node.da.anytrust.rest-aggregator.sync-to-storage.eager-lower-bound-block` | uint | - | when eagerly syncing, start indexing forward from this L1 block. Only used if there is no sync state | | `node.da.anytrust.rest-aggregator.sync-to-storage.ignore-write-errors` | bool | `true` | log only on failures to write when syncing; otherwise treat it as an error | | `node.da.anytrust.rest-aggregator.sync-to-storage.parent-chain-blocks-per-read` | uint | `100` | when eagerly syncing, max l1 blocks to read per poll | | `node.da.anytrust.rest-aggregator.sync-to-storage.retention-period` | duration | `360h0m0s` | period to request storage to retain synced data | | `node.da.anytrust.rest-aggregator.sync-to-storage.state-dir` | string | - | directory to store the sync state in, ie the block number currently synced up to, so that we don't sync from scratch each time | | `node.da.anytrust.rest-aggregator.sync-to-storage.sync-expired-data` | bool | `true` | sync even data that is expired; needed for mirror configuration | | `node.da.anytrust.rest-aggregator.urls` | strings | - | list of URLs including 'http\://' or 'https\://' prefixes and port numbers to REST AnyTrust endpoints; additive with the online-url-list option | | `node.da.anytrust.rest-aggregator.wait-before-try-next` | duration | `2s` | time to wait until trying the next set of REST endpoints while waiting for a response; the next set of REST endpoints is determined by the strategy selected | | `node.da.anytrust.rpc-aggregator.assumed-honest` | int | - | Number of assumed honest backends (H). If there are N backends, K=N+1-H valid responses are required to consider an Store request to be successful. | | `node.da.anytrust.rpc-aggregator.backends` | backendConfigList | `null` | JSON RPC backend configuration. This can be specified on the command line as a JSON array, eg: \[{"url": "...", "pubkey": "..."},...], or as a JSON array in the config file. | | `node.da.anytrust.rpc-aggregator.enable` | bool | - | enable storage of sequencer batch data from a list of RPC endpoints; this should only be used by the batch poster and not in combination with other AnyTrust storage types | | `node.da.anytrust.rpc-aggregator.rpc-client.data-stream.max-store-chunk-body-size` | int | `5242880` | maximum HTTP body size for chunked store requests | | `node.da.anytrust.rpc-aggregator.rpc-client.data-stream.rpc-methods.finalize-stream` | string | `das_commitChunkedStore` | name of the RPC method to finalize a chunked data stream | | `node.da.anytrust.rpc-aggregator.rpc-client.data-stream.rpc-methods.start-stream` | string | `das_startChunkedStore` | name of the RPC method to start a chunked data stream | | `node.da.anytrust.rpc-aggregator.rpc-client.data-stream.rpc-methods.stream-chunk` | string | `das_sendChunk` | name of the RPC method to send a chunk of data | | `node.da.anytrust.rpc-aggregator.rpc-client.enable-chunked-store` | bool | `true` | enable data to be sent to AnyTrust in chunks instead of all at once | | `node.da.anytrust.rpc-aggregator.rpc-client.rpc.arg-log-limit` | uint | `2048` | limit size of arguments in log entries | | `node.da.anytrust.rpc-aggregator.rpc-client.rpc.connection-wait` | duration | - | how long to wait for initial connection | | `node.da.anytrust.rpc-aggregator.rpc-client.rpc.jwtsecret` | string | - | path to file with jwtsecret for validation - ignored if url is self or self-auth | | `node.da.anytrust.rpc-aggregator.rpc-client.rpc.retries` | uint | `3` | number of retries in case of failure(0 mean one attempt) | | `node.da.anytrust.rpc-aggregator.rpc-client.rpc.retry-delay` | duration | - | delay between retries | | `node.da.anytrust.rpc-aggregator.rpc-client.rpc.retry-errors` | string | `websocket: close.*\|dial tcp .*\|.*i/o timeout\|.*connection reset by peer\|.*connection refused` | Errors matching this regular expression are automatically retried | | `node.da.anytrust.rpc-aggregator.rpc-client.rpc.timeout` | duration | `10s` | per-response timeout (0-disabled) | | `node.da.anytrust.rpc-aggregator.rpc-client.rpc.url` | string | `self-auth` | url of server, use self for loopback websocket, self-auth for loopback with authentication | | `node.da.anytrust.rpc-aggregator.rpc-client.rpc.websocket-message-size-limit` | int | `268435456` | websocket message size limit used by the RPC client. 0 means no limit | | `node.da.external-provider.data-stream.max-store-chunk-body-size` | int | `5242880` | maximum HTTP body size for chunked store requests | | `node.da.external-provider.data-stream.rpc-methods.finalize-stream` | string | `daprovider_commitChunkedStore` | name of the RPC method to finalize a chunked data stream | | `node.da.external-provider.data-stream.rpc-methods.start-stream` | string | `daprovider_startChunkedStore` | name of the RPC method to start a chunked data stream | | `node.da.external-provider.data-stream.rpc-methods.stream-chunk` | string | `daprovider_sendChunk` | name of the RPC method to send a chunk of data | | `node.da.external-provider.enable` | bool | - | enable daprovider client | | `node.da.external-provider.rpc.arg-log-limit` | uint | `2048` | limit size of arguments in log entries | | `node.da.external-provider.rpc.connection-wait` | duration | - | how long to wait for initial connection | | `node.da.external-provider.rpc.jwtsecret` | string | - | path to file with jwtsecret for validation - ignored if url is self or self-auth | | `node.da.external-provider.rpc.retries` | uint | `3` | number of retries in case of failure(0 mean one attempt) | | `node.da.external-provider.rpc.retry-delay` | duration | - | delay between retries | | `node.da.external-provider.rpc.retry-errors` | string | `websocket: close.*\|dial tcp .*\|.*i/o timeout\|.*connection reset by peer\|.*connection refused` | Errors matching this regular expression are automatically retried | | `node.da.external-provider.rpc.timeout` | duration | - | per-response timeout (0-disabled) | | `node.da.external-provider.rpc.url` | string | - | url of server, use self for loopback websocket, self-auth for loopback with authentication | | `node.da.external-provider.rpc.websocket-message-size-limit` | int | `268435456` | websocket message size limit used by the RPC client. 0 means no limit | | `node.da.external-provider.store-rpc-method` | string | `daprovider_store` | name of the store rpc method on the daprovider server (used when data streaming is disabled) | | `node.da.external-provider.use-data-streaming` | bool | - | use data streaming protocol for storing large payloads | | `node.da.external-provider.with-writer` | bool | - | implies if the daprovider rpc server supports writer interface | | `node.data-availability.enable` | bool | - | enable Anytrust Data Availability mode | | `node.data-availability.max-batch-size` | int | `1000000` | maximum batch size for AnyTrust DA (compressed) | | `node.data-availability.panic-on-error` | bool | - | whether the Data Availability Service should fail immediately on errors (not recommended) | | `node.data-availability.request-timeout` | duration | `5s` | Data Availability Service timeout duration for Store requests | | `node.data-availability.rest-aggregator.connection-wait` | duration | `1s` | how long to wait for initial connection | | `node.data-availability.rest-aggregator.enable` | bool | - | enable retrieval of sequencer batch data from a list of remote REST endpoints; if other AnyTrust storage types are enabled, this mode is used as a fallback | | `node.data-availability.rest-aggregator.max-per-endpoint-stats` | int | `20` | number of stats entries (latency and success rate) to keep for each REST endpoint; controls whether strategy is faster or slower to respond to changing conditions | | `node.data-availability.rest-aggregator.online-url-list` | string | - | a URL to a list of URLs of REST AnyTrust endpoints that is checked at startup; additive with the url option | | `node.data-availability.rest-aggregator.online-url-list-fetch-interval` | duration | `1h0m0s` | time interval to periodically fetch url list from online-url-list | | `node.data-availability.rest-aggregator.simple-explore-exploit-strategy.exploit-iterations` | uint32 | `1000` | number of consecutive GetByHash calls to the aggregator where each call will cause it to select from REST endpoints in order of best latency and success rate, before switching to explore mode | | `node.data-availability.rest-aggregator.simple-explore-exploit-strategy.explore-iterations` | uint32 | `20` | number of consecutive GetByHash calls to the aggregator where each call will cause it to randomly select from REST endpoints until one returns successfully, before switching to exploit mode | | `node.data-availability.rest-aggregator.strategy` | string | `simple-explore-exploit` | strategy to use to determine order and parallelism of calling REST endpoint URLs; valid options are 'simple-explore-exploit' | | `node.data-availability.rest-aggregator.strategy-update-interval` | duration | `10s` | how frequently to update the strategy with endpoint latency and error rate data | | `node.data-availability.rest-aggregator.sync-to-storage.delay-on-error` | duration | `1s` | time to wait if encountered an error before retrying | | `node.data-availability.rest-aggregator.sync-to-storage.eager` | bool | - | eagerly sync batch data to this AnyTrust server's storage from the rest endpoints, using L1 as the index of batch data hashes; otherwise only sync lazily | | `node.data-availability.rest-aggregator.sync-to-storage.eager-lower-bound-block` | uint | - | when eagerly syncing, start indexing forward from this L1 block. Only used if there is no sync state | | `node.data-availability.rest-aggregator.sync-to-storage.ignore-write-errors` | bool | `true` | log only on failures to write when syncing; otherwise treat it as an error | | `node.data-availability.rest-aggregator.sync-to-storage.parent-chain-blocks-per-read` | uint | `100` | when eagerly syncing, max l1 blocks to read per poll | | `node.data-availability.rest-aggregator.sync-to-storage.retention-period` | duration | `360h0m0s` | period to request storage to retain synced data | | `node.data-availability.rest-aggregator.sync-to-storage.state-dir` | string | - | directory to store the sync state in, ie the block number currently synced up to, so that we don't sync from scratch each time | | `node.data-availability.rest-aggregator.sync-to-storage.sync-expired-data` | bool | `true` | sync even data that is expired; needed for mirror configuration | | `node.data-availability.rest-aggregator.urls` | strings | - | list of URLs including 'http\://' or 'https\://' prefixes and port numbers to REST AnyTrust endpoints; additive with the online-url-list option | | `node.data-availability.rest-aggregator.wait-before-try-next` | duration | `2s` | time to wait until trying the next set of REST endpoints while waiting for a response; the next set of REST endpoints is determined by the strategy selected | | `node.data-availability.rpc-aggregator.assumed-honest` | int | - | Number of assumed honest backends (H). If there are N backends, K=N+1-H valid responses are required to consider an Store request to be successful. | | `node.data-availability.rpc-aggregator.backends` | backendConfigList | `null` | JSON RPC backend configuration. This can be specified on the command line as a JSON array, eg: \[{"url": "...", "pubkey": "..."},...], or as a JSON array in the config file. | | `node.data-availability.rpc-aggregator.enable` | bool | - | enable storage of sequencer batch data from a list of RPC endpoints; this should only be used by the batch poster and not in combination with other AnyTrust storage types | | `node.data-availability.rpc-aggregator.rpc-client.data-stream.max-store-chunk-body-size` | int | `5242880` | maximum HTTP body size for chunked store requests | | `node.data-availability.rpc-aggregator.rpc-client.data-stream.rpc-methods.finalize-stream` | string | `das_commitChunkedStore` | name of the RPC method to finalize a chunked data stream | | `node.data-availability.rpc-aggregator.rpc-client.data-stream.rpc-methods.start-stream` | string | `das_startChunkedStore` | name of the RPC method to start a chunked data stream | | `node.data-availability.rpc-aggregator.rpc-client.data-stream.rpc-methods.stream-chunk` | string | `das_sendChunk` | name of the RPC method to send a chunk of data | | `node.data-availability.rpc-aggregator.rpc-client.enable-chunked-store` | bool | `true` | enable data to be sent to AnyTrust in chunks instead of all at once | | `node.data-availability.rpc-aggregator.rpc-client.rpc.arg-log-limit` | uint | `2048` | limit size of arguments in log entries | | `node.data-availability.rpc-aggregator.rpc-client.rpc.connection-wait` | duration | - | how long to wait for initial connection | | `node.data-availability.rpc-aggregator.rpc-client.rpc.jwtsecret` | string | - | path to file with jwtsecret for validation - ignored if url is self or self-auth | | `node.data-availability.rpc-aggregator.rpc-client.rpc.retries` | uint | `3` | number of retries in case of failure(0 mean one attempt) | | `node.data-availability.rpc-aggregator.rpc-client.rpc.retry-delay` | duration | - | delay between retries | | `node.data-availability.rpc-aggregator.rpc-client.rpc.retry-errors` | string | `websocket: close.*\|dial tcp .*\|.*i/o timeout\|.*connection reset by peer\|.*connection refused` | Errors matching this regular expression are automatically retried | | `node.data-availability.rpc-aggregator.rpc-client.rpc.timeout` | duration | `10s` | per-response timeout (0-disabled) | | `node.data-availability.rpc-aggregator.rpc-client.rpc.url` | string | `self-auth` | url of server, use self for loopback websocket, self-auth for loopback with authentication | | `node.data-availability.rpc-aggregator.rpc-client.rpc.websocket-message-size-limit` | int | `268435456` | websocket message size limit used by the RPC client. 0 means no limit | | `node.delayed-sequencer.enable` | bool | - | enable delayed sequencer | | `node.delayed-sequencer.filtered-tx-full-retry-interval` | duration | `30s` | how often to do a full re-execution when halted on a filtered delayed message | | `node.delayed-sequencer.finalize-distance` | int | `20` | how many blocks in the past L1 block is considered final (ignored when using Merge finality) | | `node.delayed-sequencer.require-full-finality` | bool | - | whether to wait for full finality before sequencing delayed messages | | `node.delayed-sequencer.rescan-interval` | duration | `1s` | frequency to rescan for new delayed messages (the parent chain reader's poll-interval config is more important than this) | | `node.delayed-sequencer.use-merge-finality` | bool | `true` | whether to use The Merge's notion of finality before sequencing delayed messages | | `node.execution-rpc-client.arg-log-limit` | uint | `2048` | limit size of arguments in log entries | | `node.execution-rpc-client.connection-wait` | duration | - | how long to wait for initial connection | | `node.execution-rpc-client.jwtsecret` | string | - | path to file with jwtsecret for validation - ignored if url is self or self-auth | | `node.execution-rpc-client.retries` | uint | `3` | number of retries in case of failure(0 mean one attempt) | | `node.execution-rpc-client.retry-delay` | duration | - | delay between retries | | `node.execution-rpc-client.retry-errors` | string | `websocket: close.*\|dial tcp .*\|.*i/o timeout\|.*connection reset by peer\|.*connection refused` | Errors matching this regular expression are automatically retried | | `node.execution-rpc-client.timeout` | duration | - | per-response timeout (0-disabled) | | `node.execution-rpc-client.url` | string | - | url of server, use self for loopback websocket, self-auth for loopback with authentication | | `node.execution-rpc-client.websocket-message-size-limit` | int | `268435456` | websocket message size limit used by the RPC client. 0 means no limit | | `node.feed.input.enable-compression` | bool | `true` | enable per message deflate compression support | | `node.feed.input.reconnect-initial-backoff` | duration | `1s` | initial duration to wait before reconnect | | `node.feed.input.reconnect-maximum-backoff` | duration | `1m4s` | maximum duration to wait before reconnect | | `node.feed.input.require-chain-id` | bool | - | require chain id to be present on connect | | `node.feed.input.require-feed-version` | bool | - | require feed version to be present on connect | | `node.feed.input.secondary-url` | strings | - | list of secondary URLs of sequencer feed source. Would be started in the order they appear in the list when primary feeds fails | | `node.feed.input.timeout` | duration | `20s` | duration to wait before timing out connection to sequencer feed | | `node.feed.input.url` | strings | - | list of primary URLs of sequencer feed source | | `node.feed.input.verify.accept-sequencer` | bool | `true` | accept verified message from sequencer | | `node.feed.input.verify.allowed-addresses` | strings | - | a list of allowed addresses | | `node.feed.output.addr` | string | - | address to bind the relay feed output to | | `node.feed.output.backlog.enable-backlog-deep-copy` | bool | - | enable deep copying of L2 messages for memory profiling (debug only) | | `node.feed.output.backlog.segment-limit` | int | `240` | the maximum number of messages each segment within the backlog can contain | | `node.feed.output.client-delay` | duration | - | delay the first messages sent to each client by this amount | | `node.feed.output.client-timeout` | duration | `15s` | duration to wait before timing out connections to client | | `node.feed.output.connection-limits.enable` | bool | - | enable broadcaster per-client connection limiting | | `node.feed.output.connection-limits.per-ip-limit` | int | `5` | limit clients, as identified by IPv4/v6 address, to this many connections to this relay | | `node.feed.output.connection-limits.per-ipv6-cidr-48-limit` | int | `20` | limit ipv6 clients, as identified by IPv6 address masked with /48, to this many connections to this relay | | `node.feed.output.connection-limits.per-ipv6-cidr-64-limit` | int | `10` | limit ipv6 clients, as identified by IPv6 address masked with /64, to this many connections to this relay | | `node.feed.output.connection-limits.reconnect-cooldown-period` | duration | - | time to wait after a relay client disconnects before the disconnect is registered with respect to the limit for this client | | `node.feed.output.enable` | bool | - | enable broadcaster | | `node.feed.output.enable-compression` | bool | - | enable per message deflate compression support | | `node.feed.output.handshake-timeout` | duration | `1s` | duration to wait before timing out HTTP to WS upgrade | | `node.feed.output.limit-catchup` | bool | - | only supply catchup buffer if requested sequence number is reasonable | | `node.feed.output.log-connect` | bool | - | log every client connect | | `node.feed.output.log-disconnect` | bool | - | log every client disconnect | | `node.feed.output.max-catchup` | int | `-1` | the maximum size of the catchup buffer (-1 means unlimited) | | `node.feed.output.max-send-queue` | int | `4096` | maximum number of messages allowed to accumulate before client is disconnected | | `node.feed.output.ping` | duration | `5s` | duration for ping interval | | `node.feed.output.port` | string | `9642` | port to bind the relay feed output to | | `node.feed.output.queue` | int | `100` | queue size for HTTP to WS upgrade | | `node.feed.output.read-timeout` | duration | `1s` | duration to wait before timing out reading data (i.e. pings) from clients | | `node.feed.output.require-compression` | bool | - | require clients to use compression | | `node.feed.output.require-version` | bool | - | don't connect if client version not present | | `node.feed.output.signed` | bool | - | sign broadcast messages with the batch poster wallet | | `node.feed.output.workers` | int | `100` | number of threads to reserve for HTTP to WS upgrade | | `node.feed.output.write-timeout` | duration | `2s` | duration to wait before timing out writing data to clients | | `node.inbox-reader.check-delay` | duration | `1m0s` | the maximum time to wait between inbox checks (if not enough new blocks are found) | | `node.inbox-reader.default-blocks-to-read` | uint | `100` | the default number of blocks to read at once (will vary based on traffic by default) | | `node.inbox-reader.delay-blocks` | uint | - | number of latest blocks to ignore to reduce reorgs | | `node.inbox-reader.max-blocks-to-read` | uint | `2000` | if adjust-blocks-to-read is enabled, the maximum number of blocks to read at once | | `node.inbox-reader.min-blocks-to-read` | uint | `1` | the minimum number of blocks to read at once (when caught up lowers load on L1) | | `node.inbox-reader.read-mode` | string | `latest` | mode to only read latest or safe or finalized L1 blocks. Enabling safe or finalized disables feed input and output. Defaults to latest. Takes string input, valid strings- latest, safe, finalized | | `node.inbox-reader.target-messages-read` | uint | `500` | if adjust-blocks-to-read is enabled, the target number of messages to read at once | | `node.maintenance.check-interval` | duration | `1m0s` | how often to check if maintenance should be run | | `node.maintenance.enable` | bool | - | enable maintenance runner | | `node.maintenance.lock.background-lock` | bool | - | should node always try grabbing lock in background | | `node.maintenance.lock.enable` | bool | `true` | if false, always treat this as locked and don't write the lock to redis | | `node.maintenance.lock.key` | string | - | key for lock | | `node.maintenance.lock.lockout-duration` | duration | `1m0s` | how long lock is held | | `node.maintenance.lock.my-id` | string | - | this node's id prefix when acquiring the lock (optional) | | `node.maintenance.lock.refresh-duration` | duration | `10s` | how long between consecutive calls to redis | | `node.message-extraction.blocks-to-prefetch` | uint | `499` | the number of blocks to prefetch relevant logs from. Recommend using max allowed range for eth\_getLogs rpc query | | `node.message-extraction.enable` | bool | - | enable message extraction service | | `node.message-extraction.log-extraction-status-frequency-blocks` | uint | `100` | frequency of logging message extraction status in terms of number of blocks processed | | `node.message-extraction.read-mode` | string | `latest` | mode to only read latest or safe or finalized L1 blocks. Enabling safe or finalized disables feed input and output. Defaults to latest. Takes string input, valid strings- latest, safe, finalized | | `node.message-extraction.retry-interval` | duration | `500ms` | wait time before retring upon a failure | | `node.message-extraction.stall-tolerance` | uint | `10` | max times the MEL fsm is allowed to be stuck without logging error | | `node.message-pruner.enable` | bool | `true` | enable message pruning | | `node.message-pruner.min-batches-left` | uint | `1000` | min number of batches not pruned | | `node.message-pruner.prune-interval` | duration | `1m0s` | interval for running message pruner | | `node.parent-chain-reader.enable` | bool | `true` | enable reader connection | | `node.parent-chain-reader.old-header-timeout` | duration | `5m0s` | warns if the latest l1 block is at least this old | | `node.parent-chain-reader.poll-interval` | duration | `15s` | interval when polling endpoint | | `node.parent-chain-reader.poll-only` | bool | - | do not attempt to subscribe to header events | | `node.parent-chain-reader.poll-timeout` | duration | `5s` | timeout when polling endpoint | | `node.parent-chain-reader.subscribe-err-interval` | duration | `5m0s` | interval for subscribe error | | `node.parent-chain-reader.tx-timeout` | duration | `5m0s` | timeout when waiting for a transaction | | `node.parent-chain-reader.use-finality-data` | bool | `true` | use l1 data about finalized/safe blocks | | `node.resource-mgmt.mem-free-limit` | string | - | Decline RPC calls if free memory excluding the page cache is below this amount | | `node.rpc-server.authenticated` | bool | `true` | rpc is authenticated | | `node.rpc-server.enable` | bool | - | enable consensus node to serve over rpc | | `node.rpc-server.public` | bool | - | rpc is public | | `node.seq-coordinator.block-metadata-duration` | duration | `240h0m0s` | expiration duration for block metadata keys in Redis | | `node.seq-coordinator.chosen-healthcheck-addr` | string | - | if non-empty, launch an HTTP service binding to this address that returns status code 200 when chosen and 503 otherwise | | `node.seq-coordinator.delete-finalized-msgs` | bool | `true` | enable deleting of finalized messages from redis | | `node.seq-coordinator.enable` | bool | - | enable sequence coordinator | | `node.seq-coordinator.handoff-timeout` | duration | `30s` | the maximum amount of time to spend waiting for another sequencer to accept the lockout when handing it off on shutdown or db compaction | | `node.seq-coordinator.lockout-duration` | duration | `1m0s` | duration to hold the sequencer lockout after acquiring it | | `node.seq-coordinator.lockout-spare` | duration | `30s` | time to subtract from lockout duration to ensure timely renewal | | `node.seq-coordinator.msg-per-poll` | uint | `2000` | will only be marked as wanting the lockout if not too far behind | | `node.seq-coordinator.my-url` | string | `<?INVALID-URL?>` | url for this sequencer if it is the chosen | | `node.seq-coordinator.new-redis-url` | string | - | switch to the new Redis URL to coordinate via | | `node.seq-coordinator.redis-quorum-size` | uint | `1` | the quorum size needed to qualify a redis GET as valid | | `node.seq-coordinator.redis-url` | string | - | the Redis URL to coordinate via | | `node.seq-coordinator.release-retries` | int | `4` | the number of times to retry releasing the wants lockout and chosen one status on shutdown | | `node.seq-coordinator.retry-interval` | duration | `50ms` | interval to wait before retrying after an error | | `node.seq-coordinator.safe-shutdown-delay` | duration | `5s` | if non-zero will add delay after transferring control | | `node.seq-coordinator.seq-num-duration` | duration | `240h0m0s` | expiration duration for message count keys in Redis | | `node.seq-coordinator.signer.ecdsa.accept-sequencer` | bool | `true` | accept verified message from sequencer | | `node.seq-coordinator.signer.ecdsa.allowed-addresses` | strings | - | a list of allowed addresses | | `node.seq-coordinator.signer.symmetric-fallback` | bool | - | if to fall back to symmetric hmac | | `node.seq-coordinator.signer.symmetric-sign` | bool | - | if to sign with symmetric hmac | | `node.seq-coordinator.signer.symmetric.fallback-verification-key` | string | - | a fallback key used for message verification | | `node.seq-coordinator.signer.symmetric.signing-key` | string | - | a 32-byte (64-character) hex string used to sign messages, or a path to a file containing it | | `node.seq-coordinator.update-interval` | duration | `250ms` | interval between sequencer coordinator update attempts | | `node.sequencer` | bool | - | enable sequencer | | `node.staker.confirmation-blocks` | int | `12` | confirmation blocks | | `node.staker.contract-wallet-address` | string | - | validator smart contract wallet public address | | `node.staker.data-poster.allocate-mempool-balance` | bool | `true` | if true, don't put transactions in the mempool that spend a total greater than the batch poster's balance | | `node.staker.data-poster.disable-new-tx` | bool | - | disable posting new transactions, data poster will still keep confirming existing batches | | `node.staker.data-poster.elapsed-time-base` | duration | `10m0s` | unit to measure the time elapsed since creation of transaction used for maximum fee cap calculation | | `node.staker.data-poster.elapsed-time-importance` | float | `10` | weight given to the units of time elapsed used for maximum fee cap calculation | | `node.staker.data-poster.external-signer.address` | string | - | external signer address | | `node.staker.data-poster.external-signer.client-cert` | string | - | rpc client cert | | `node.staker.data-poster.external-signer.client-private-key` | string | - | rpc client private key | | `node.staker.data-poster.external-signer.insecure-skip-verify` | bool | - | skip TLS certificate verification | | `node.staker.data-poster.external-signer.method` | string | `eth_signTransaction` | external signer method | | `node.staker.data-poster.external-signer.root-ca` | string | - | external signer root CA | | `node.staker.data-poster.external-signer.url` | string | - | external signer url | | `node.staker.data-poster.legacy-storage-encoding` | bool | - | encodes items in a legacy way (as it was before dropping generics) | | `node.staker.data-poster.max-fee-bid-multiple-bips` | uint | `100000` | the maximum multiple of the current price to bid for a transaction's fees (may be exceeded due to min rbf increase, 0 = unlimited) | | `node.staker.data-poster.max-fee-cap-formula` | string | - | mathematical formula to calculate maximum fee cap gwei the result of which would be float64. This expression is expected to be evaluated please refer to find all available mathematical operators. Currently available variables to construct the formula are BacklogOfBatches, UrgencyGWei, ElapsedTime, ElapsedTimeBase, ElapsedTimeImportance, and TargetPriceGWei (default "((BacklogOfBatches \_ UrgencyGWei) \*\* 2) + ((ElapsedTime/ElapsedTimeBase) \*\* 2) \_ ElapsedTimeImportance + TargetPriceGWei") | | `node.staker.data-poster.max-mempool-transactions` | uint | `1` | the maximum number of transactions to have queued in the mempool at once (0 = unlimited) | | `node.staker.data-poster.max-mempool-weight` | uint | `1` | the maximum number of weight (weight = min(1, tx.blobs)) to have queued in the mempool at once (0 = unlimited) | | `node.staker.data-poster.max-queued-transactions` | int | - | the maximum number of unconfirmed transactions to track at once (0 = unlimited) | | `node.staker.data-poster.max-tip-cap-gwei` | float | `1.2` | the maximum tip cap to post transactions at | | `node.staker.data-poster.min-tip-cap-gwei` | float | `0.05` | the minimum tip cap to post transactions at | | `node.staker.data-poster.nonce-rbf-soft-confs` | uint | `1` | the maximum probable reorg depth, used to determine when a transaction will no longer likely need replaced-by-fee | | `node.staker.data-poster.redis-signer.fallback-verification-key` | string | - | a fallback key used for message verification | | `node.staker.data-poster.redis-signer.signing-key` | string | - | a 32-byte (64-character) hex string used to sign messages, or a path to a file containing it | | `node.staker.data-poster.replacement-times` | durationSlice | `[5m0s,10m0s,20m0s,30m0s,1h0m0s,2h0m0s,4h0m0s,6h0m0s,8h0m0s,12h0m0s,16h0m0s,18h0m0s,20h0m0s,22h0m0s]` | comma-separated list of durations since first posting to attempt a replace-by-fee | | `node.staker.data-poster.target-price-gwei` | float | `60` | the target price to use for maximum fee cap calculation | | `node.staker.data-poster.urgency-gwei` | float | `2` | the urgency to use for maximum fee cap calculation | | `node.staker.data-poster.use-db-storage` | bool | `true` | uses database storage when enabled | | `node.staker.data-poster.use-noop-storage` | bool | - | uses noop storage, it doesn't store anything | | `node.staker.data-poster.wait-for-l1-finality` | bool | `true` | only treat a transaction as confirmed after L1 finality has been achieved (recommended) | | `node.staker.disable-challenge` | bool | - | disable validator challenge | | `node.staker.enable` | bool | `true` | enable validator | | `node.staker.enable-fast-confirmation` | bool | - | enable fast confirmation | | `node.staker.extra-gas` | uint | `50000` | use this much more gas than estimation says is necessary to post transactions | | `node.staker.gas-refunder-address` | string | - | The gas refunder contract address (optional) | | `node.staker.log-query-batch-size` | uint | - | range ro query from eth\_getLogs | | `node.staker.make-assertion-interval` | duration | `1h0m0s` | if configured with the makeNodes strategy, how often to create new assertions (bypassed in case of a dispute) | | `node.staker.only-create-wallet-contract` | bool | - | only create smart wallet contract and exit | | `node.staker.parent-chain-wallet.account` | string | `is first account in keystore` | account to use | | `node.staker.parent-chain-wallet.only-create-key` | bool | - | if true, creates new key then exits | | `node.staker.parent-chain-wallet.password` | string | `PASSWORD_NOT_SET` | wallet passphrase | | `node.staker.parent-chain-wallet.pathname` | string | `validator-wallet` | pathname for wallet | | `node.staker.parent-chain-wallet.private-key` | string | - | private key for wallet | | `node.staker.posting-strategy.high-gas-delay-blocks` | int | - | high gas delay blocks | | `node.staker.posting-strategy.high-gas-threshold` | float | - | high gas threshold | | `node.staker.redis-url` | string | - | redis url for L1 validator | | `node.staker.staker-interval` | duration | `1m0s` | how often the L1 validator should check the status of the L1 rollup and maybe take action with its stake | | `node.staker.start-validation-from-staked` | bool | `true` | assume staked nodes are valid | | `node.staker.strategy` | string | `Watchtower` | L1 validator strategy, either watchtower, defensive, stakeLatest, or makeNodes | | `node.staker.use-smart-contract-wallet` | bool | - | use a smart contract wallet instead of an EOA address | | `node.sync-monitor.msg-lag` | duration | `1s` | allowed msg lag while still considered in sync | | `node.transaction-streamer.execute-message-loop-delay` | duration | `100ms` | delay when polling calls to execute messages | | `node.transaction-streamer.max-broadcaster-queue-size` | int | `50000` | maximum cache of pending broadcaster messages | | `node.transaction-streamer.max-reorg-resequence-depth` | int | `1024` | maximum number of messages to attempt to resequence on reorg (0 = never resequence, -1 = always resequence) | | `node.transaction-streamer.shutdown-on-blockhash-mismatch` | bool | - | if set the node gracefully shuts down upon detecting mismatch in feed and locally computed blockhash. This is turned off by default | | `node.transaction-streamer.sync-till-block` | uint | - | node will not sync past this block | | `node.transaction-streamer.track-block-metadata-from` | uint | - | block number to start saving blockmetadata, 0 to disable | | `node.version-alerter-server.enable` | bool | - | enable arb\_getMinRequiredNitroVersion endpoint that returns minimum required version of the nitro node software | | `node.version-alerter-server.min-required-nitro-by-date` | string | - | minimum required version of the nitro node software by date. Second string in the result of querying arb\_getMinRequiredNitroVersion endpoint | | `node.version-alerter-server.min-required-nitro-by-version` | string | - | minimum required version of the nitro node software. First string in the result of querying arb\_getMinRequiredNitroVersion endpoint | | `node.version-alerter-server.upgrade-deadline` | string | - | deadline to upgrade the nitro node software. Third string in the result of querying arb\_getMinRequiredNitroVersion endpoint | ## parent-chain Related guide: [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) parent-chain flags (14) | Flag | Type | Default | Description | | ------------------------------------------------------ | -------- | ----------- | -------------------------------------------------------------------------------------------------------------------------- | | `parent-chain.blob-client.authorization` | string | - | Value to send with the HTTP Authorization: header for Beacon REST requests, must include both scheme and scheme parameters | | `parent-chain.blob-client.beacon-url` | string | - | Beacon Chain RPC URL to use for fetching blobs (normally on port 3500) | | `parent-chain.blob-client.blob-directory` | string | - | Full path of the directory to save fetched blobs | | `parent-chain.blob-client.secondary-beacon-url` | string | - | Backup beacon Chain RPC URL to use for fetching blobs (normally on port 3500) when unable to fetch from primary | | `parent-chain.connection.arg-log-limit` | uint | `2048` | limit size of arguments in log entries | | `parent-chain.connection.connection-wait` | duration | `1m0s` | how long to wait for initial connection | | `parent-chain.connection.jwtsecret` | string | - | path to file with jwtsecret for validation - ignored if url is self or self-auth | | `parent-chain.connection.retries` | uint | `2` | number of retries in case of failure(0 mean one attempt) | | `parent-chain.connection.retry-delay` | duration | - | delay between retries | | `parent-chain.connection.retry-errors` | string | - | Errors matching this regular expression are automatically retried | | `parent-chain.connection.timeout` | duration | `1m0s` | per-response timeout (0-disabled) | | `parent-chain.connection.url` | string | - | url of server, use self for loopback websocket, self-auth for loopback with authentication | | `parent-chain.connection.websocket-message-size-limit` | int | `268435456` | websocket message size limit used by the RPC client. 0 means no limit | | `parent-chain.id` | uint | - | if set other than 0, will be used to validate database and L1 connection | ## persistent Related guide: [Docker and CLI binaries](/run-arbitrum-node/nitro/docker-and-cli-binaries.md) persistent flags (8) | Flag | Type | Default | Description | | ---------------------------------------------- | ------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `persistent.ancient` | string | - | directory of ancient where the chain freezer can be opened | | `persistent.chain` | string | - | directory to store chain state | | `persistent.db-engine` | string | - | backing database implementation to use. If set to empty string the database type will be autodetected and if no pre-existing database is found it will default to creating new pebble database ('leveldb', 'pebble' or '' = auto-detect) | | `persistent.global-config` | string | `.arbitrum` | directory to store global config | | `persistent.handles` | int | `512` | number of file descriptor handles to use for the database | | `persistent.log-dir` | string | - | directory to store log file | | `persistent.pebble.max-concurrent-compactions` | int | `2` | maximum number of concurrent compactions | | `persistent.pebble.sync-mode` | bool | - | if true sync mode is used (data needs to be written to WAL before the write is marked as completed) | ## pprof Related guide: [Node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md) pprof flags (1) | Flag | Type | Default | Description | | ------- | ---- | ------- | ------------ | | `pprof` | bool | - | enable pprof | ## pprof-cfg Related guide: [Node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md) pprof-cfg flags (2) | Flag | Type | Default | Description | | ---------------- | ------ | ----------- | -------------------- | | `pprof-cfg.addr` | string | `127.0.0.1` | pprof server address | | `pprof-cfg.port` | int | `6071` | pprof server port | ## rpc Related guide: [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) rpc flags (2) | Flag | Type | Default | Description | | ----------------------------- | ---- | ---------- | ------------------------------------------------------------------------------------- | | `rpc.batch-request-limit` | int | `1000` | the maximum number of requests in a batch (0 means no limit) | | `rpc.max-batch-response-size` | int | `10000000` | the maximum response size for a JSON-RPC request measured in bytes (0 means no limit) | ## validation Related guide: [Node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md) validation flags (26) | Flag | Type | Default | Description | | --------------------------------------------------------------------------------------------- | -------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `validation.api-auth` | bool | `true` | validate is an authenticated API | | `validation.api-public` | bool | - | validate is a public API | | `validation.arbitrator.execution-run-timeout` | duration | `15m0s` | timeout before discarding execution run | | `validation.arbitrator.execution.cached-challenge-machines` | uint | `4` | how many machines to store in cache while working on a challenge (should be even) | | `validation.arbitrator.execution.initial-steps` | uint | `100000` | initial steps between machines | | `validation.arbitrator.output-path` | string | `./target/output` | path to write machines to | | `validation.arbitrator.redis-validation-server-config.buffer-reads` | bool | `true` | buffer reads (read next while working) | | `validation.arbitrator.redis-validation-server-config.consumer-config.idletime-to-autoclaim` | duration | `5m0s` | After a message spends this amount of time in PEL (Pending Entries List i.e claimed by another consumer but not Acknowledged) it will be allowed to be autoclaimed by other consumers. This option should be set to the same value for all consumers and producers. | | `validation.arbitrator.redis-validation-server-config.consumer-config.max-retry-count` | int | `-1` | number of message retries after which this consumer will set an error response and Acknowledge the message (-1 = no limit) | | `validation.arbitrator.redis-validation-server-config.consumer-config.response-entry-timeout` | duration | `1h0m0s` | timeout for response entry | | `validation.arbitrator.redis-validation-server-config.consumer-config.retry` | bool | `true` | enables autoclaim for this consumer, if set to false this consumer will not check messages from PEL (Pending Entries List) | | `validation.arbitrator.redis-validation-server-config.module-roots` | strings | - | Supported module root hashes | | `validation.arbitrator.redis-validation-server-config.redis-url` | string | - | url of redis server | | `validation.arbitrator.redis-validation-server-config.stream-prefix` | string | - | prefix for stream name | | `validation.arbitrator.redis-validation-server-config.stream-timeout` | duration | `10m0s` | Timeout on polling for existence of redis streams | | `validation.arbitrator.redis-validation-server-config.workers` | int | - | number of validation threads (0 to use number of CPUs) | | `validation.arbitrator.workers` | int | - | number of concurrent validation threads | | `validation.jit.cranelift` | bool | `true` | use Cranelift instead of LLVM when validating blocks using the jit-accelerated block validator | | `validation.jit.jit-path` | string | - | path to jit executable, if empty, attempts to find jit executable relative to nitro binary or in PATH | | `validation.jit.max-execution-time` | duration | `10m0s` | if execution time used by a jit wasm exceeds this limit, a rpc error is returned | | `validation.jit.wasm-memory-usage-limit` | int | `4294967296` | if memory used by a jit wasm exceeds this limit, a warning is logged | | `validation.jit.workers` | int | - | number of concurrent validation threads | | `validation.use-jit` | bool | `true` | use jit for validation | | `validation.wasm.allowed-wasm-module-roots` | strings | - | list of WASM module roots or machine base paths to match against on-chain WasmModuleRoot | | `validation.wasm.enable-wasmroots-check` | bool | `true` | enable check for compatibility of on-chain WASM module root with node | | `validation.wasm.root-path` | string | - | path to machine folders, each containing wasm files (machine.v2.wavm.br, replay.wasm) | ## version-alerter Related guide: [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) version-alerter flags (12) | Flag | Type | Default | Description | | --------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `version-alerter.connection.arg-log-limit` | uint | `2048` | limit size of arguments in log entries | | `version-alerter.connection.connection-wait` | duration | - | how long to wait for initial connection | | `version-alerter.connection.jwtsecret` | string | - | path to file with jwtsecret for validation - ignored if url is self or self-auth | | `version-alerter.connection.retries` | uint | `3` | number of retries in case of failure(0 mean one attempt) | | `version-alerter.connection.retry-delay` | duration | - | delay between retries | | `version-alerter.connection.retry-errors` | string | `websocket: close.*\|dial tcp .*\|.*i/o timeout\|.*connection reset by peer\|.*connection refused` | Errors matching this regular expression are automatically retried | | `version-alerter.connection.timeout` | duration | `10s` | per-response timeout (0-disabled) | | `version-alerter.connection.url` | string | `self-auth` | url of server, use self for loopback websocket, self-auth for loopback with authentication | | `version-alerter.connection.websocket-message-size-limit` | int | `268435456` | websocket message size limit used by the RPC client. 0 means no limit | | `version-alerter.enable` | bool | - | enable querying arb\_getMinRequiredNitroVersion endpoint in regular intervals and firing alerts if the node software is below the required version | | `version-alerter.ping-interval` | duration | `5m0s` | how often the nitro version alerter should ping arb\_getMinRequiredNitroVersion for data | | `version-alerter.upgrade-grace-period` | duration | - | represents grace period up until the upgrade deadline received from arb\_getMinRequiredNitroVersion, determines escalation of messages regarding node software upgrade | ## ws Related guide: [Configuration system](/run-arbitrum-node/nitro/configuration-system.md) ws flags (6) | Flag | Type | Default | Description | | --------------- | ------- | -------------------- | ------------------------------------------------------------------------------ | | `ws.addr` | string | - | WS-RPC server listening interface | | `ws.api` | strings | `[net,web3,eth,arb]` | APIs offered over the WS-RPC interface | | `ws.expose-all` | bool | - | expose private api via websocket | | `ws.origins` | strings | - | Origins from which to accept websockets requests | | `ws.port` | int | `8548` | WS-RPC server listening port | | `ws.rpcprefix` | string | - | WS path path prefix on which JSON-RPC is served. Use '/' to serve on all paths | --- > For a complete page index, fetch # Nitro configuration system Nitro nodes accept configuration from multiple sources. When the same setting appears in more than one source, Nitro applies a deterministic precedence order so you always know which value wins. This guide explains each configuration source, how they interact, and how to inspect the merged result. ## Config sources and precedence Nitro loads configuration in the following order. Each source overrides values set by sources listed before it: 1. **Hardcoded defaults** — built into the Nitro binary. Every flag has a default value defined in the Go source code. 2. **S3 config** (`--conf.s3.*`) — a JSON config file fetched from an Amazon S3 bucket at startup. 3. **Config files** (`--conf.file`) — one or more local JSON files. If you pass multiple `--conf.file` flags, later files override earlier ones. 4. **Config string** (`--conf.string`) — an inline JSON string passed directly on the command line. 5. **CLI flags** — individual flags like `--http.port 8547`. CLI flags are re-applied after each config source loads, so they override config files and config strings. 6. **Environment variables** — variables matching the configured prefix (see [Environment variables](#environment-variables)). These are applied last and override everything else, including CLI flags. > **TIP** > > Because environment variables have the highest precedence, they are the safest way to inject secrets or per-environment overrides without modifying config files. ## Config file format Nitro config files use **JSON format only** (not YAML or TOML). Keys use flat dot-separated names that match the corresponding CLI flag names. Nitro uses the [koanf](https://github.com/knadh/koanf) library with its JSON parser to load config files. Here is an example `node-config.json` for a full node on Arbitrum One: ```json { "parent-chain": { "connection": { "url": "https://your-parent-chain-rpc.example.com" } }, "chain": { "id": 42161 }, "http": { "addr": "0.0.0.0", "port": 8547, "vhosts": ["*"], "corsdomain": ["*"], "api": ["net", "web3", "eth", "arb"] }, "node": { "feed": { "input": { "url": ["wss://arb1.arbitrum.io/feed"] } } }, "persistent": { "chain": "/home/user/data/" }, "init": { "url": "https://snapshot.arbitrum.foundation/arb1/nitro-pruned.tar" }, "log-level": "INFO", "metrics": false } ``` Pass the file to Nitro with the `--conf.file` flag: ```shell nitro --conf.file /path/to/node-config.json ``` You can pass multiple config files. Later files override values from earlier ones: ```shell nitro --conf.file /path/to/base.json --conf.file /path/to/overrides.json ``` ## Environment variables Set the `--conf.env-prefix` flag to tell Nitro which environment variables to read. For example, `--conf.env-prefix NITRO` causes Nitro to load any environment variable starting with `NITRO_`. Nitro transforms environment variable names into config keys using these rules (applied in order): 1. Strip the prefix and its trailing underscore (e.g., `NITRO_`). 2. Convert all characters to **lowercase**. 3. Replace every `__` (double underscore) with `-` (dash). 4. Replace every `_` (single underscore) with `.` (dot). The following table shows how environment variable names map to CLI flags: | Environment variable | Equivalent CLI flag | | ------------------------------------------------ | ------------------------------------------- | | `NITRO_PARENT__CHAIN_CONNECTION_URL=http://...` | `--parent-chain.connection.url=http://...` | | `NITRO_HTTP_PORT=8547` | `--http.port=8547` | | `NITRO_NODE_FEED_INPUT_URL=wss://feed.example` | `--node.feed.input.url=wss://feed.example` | | `NITRO_NODE_BATCH__POSTER_ENABLE=true` | `--node.batch-poster.enable=true` | | `NITRO_LOG__LEVEL=DEBUG` | `--log-level=DEBUG` | | `NITRO_EXECUTION_FORWARDING__TARGET=https://...` | `--execution.forwarding-target=https://...` | | `NITRO_PERSISTENT_CHAIN=/data` | `--persistent.chain=/data` | ### Comma-separated list values Certain config keys accept lists. When you set these through environment variables, Nitro automatically splits the value on commas. The full list of comma-split fields: | Config key | Description | | ------------------------------------------------------------------- | ----------------------------- | | `auth.api` | Auth RPC API namespaces | | `auth.origins` | Auth allowed origins | | `chain.info-files` | Chain info file paths | | `conf.file` | Config file paths | | `execution.secondary-forwarding-target` | Secondary forwarding targets | | `execution.sequencer.sender-whitelist` | Allowed sender addresses | | `graphql.corsdomain` | GraphQL CORS domains | | `graphql.vhosts` | GraphQL virtual hosts | | `http.api` | HTTP RPC API namespaces | | `http.corsdomain` | HTTP CORS domains | | `http.vhosts` | HTTP virtual hosts | | `node.da.anytrust.rest-aggregator.urls` | DA REST aggregator URLs | | `node.feed.input.secondary-url` | Secondary feed URLs | | `node.feed.input.url` | Feed input URLs | | `node.feed.input.verify.allowed-addresses` | Feed signature addresses | | `node.seq-coordinator.signer.ecdsa.allowed-addresses` | Sequencer coordinator signers | | `p2p.bootnodes` | P2P bootstrap nodes | | `p2p.bootnodes-v5` | P2P v5 bootstrap nodes | | `validation.api-auth` | Validation API auth | | `validation.arbitrator.redis-validation-server-config.module-roots` | Arbitrator WASM module roots | | `validation.wasm.allowed-wasm-module-roots` | Allowed WASM module roots | | `ws.api` | WebSocket API namespaces | | `ws.origins` | WebSocket allowed origins | For example, to enable multiple HTTP API namespaces: ```shell export NITRO_HTTP_API=net,web3,eth,arb ``` ## Mounting config in Docker When running Nitro in Docker, mount your config file into the container and reference it with `--conf.file`: ```shell docker run --rm -it -v /path/to/local/node-config.json:/home/user/node-config.json \ -p 8547:8547 -p 8548:8548 \ offchainlabs/nitro-node:v3.5.3-3b3e2fa \ --conf.file /home/user/node-config.json ``` Mount to `/home/user/` because Nitro runs as a non-root user inside the container with that home directory. The `-v` flag maps your local file path to the container path. ## Dumping running config Use the `--conf.dump` flag to inspect the fully merged configuration. Nitro loads all config sources (defaults, S3, files, config string, CLI flags, environment variables), scrubs sensitive fields, prints the merged result as JSON to stdout, and then exits immediately: ```shell docker run --rm offchainlabs/nitro-node:v3.5.3-3b3e2fa \ --conf.dump \ --conf.file /path/to/node-config.json ``` To save the output to a file: ```shell docker run --rm offchainlabs/nitro-node:v3.5.3-3b3e2fa \ --conf.dump \ --conf.file /path/to/node-config.json > merged-config.json ``` ### Scrubbed fields For security, `--conf.dump` replaces the following fields with empty strings in the output: | Scrubbed field | | --------------------------------------------------- | | `node.batch-poster.parent-chain-wallet.password` | | `node.batch-poster.parent-chain-wallet.private-key` | | `node.staker.parent-chain-wallet.password` | | `node.staker.parent-chain-wallet.private-key` | | `chain.dev-wallet.password` | | `chain.dev-wallet.private-key` | If you need to verify that secrets are being picked up correctly, check that the node starts successfully rather than relying on `--conf.dump` output. ## S3 config loading Nitro can fetch a base configuration file from Amazon S3 at startup. This is useful for managing fleet-wide configuration where many nodes share the same base settings stored in a central S3 bucket. Configure S3 loading with the following flags: | Flag | Description | | ---------------------- | ---------------------------------------- | | `--conf.s3.access-key` | AWS access key ID | | `--conf.s3.secret-key` | AWS secret access key | | `--conf.s3.region` | S3 bucket region | | `--conf.s3.bucket` | S3 bucket name | | `--conf.s3.object-key` | Object key (file path within the bucket) | The S3 config file must be in JSON format. It is loaded early in the precedence chain, so local config files, CLI flags, and environment variables all override S3 values: ```shell nitro --conf.s3.access-key AKIAIOSFODNN7EXAMPLE \ --conf.s3.secret-key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \ --conf.s3.region us-east-1 \ --conf.s3.bucket my-nitro-configs \ --conf.s3.object-key production/node-config.json \ --conf.file /path/to/local-overrides.json ``` In this example, the S3 config provides the base settings and the local file provides environment-specific overrides. --- > For a complete page index, fetch # Data availability tools reference Nitro ships three binaries for operating [AnyTrust](/how-arbitrum-works/deep-dives/anytrust-protocol.md) data availability (DA) infrastructure: `anytrusttool` for key management and testing, `anytrustserver` for running a DA committee member, and `daprovider` for running a DA provider server that exposes a unified JSON-RPC interface over different DA backends. ## anytrusttool `anytrusttool` is a command-line utility for generating cryptographic keys, computing keyset hashes, and interacting with DA committee members through RPC and REST endpoints. It has four subcommands: ```text anytrusttool [keygen | dumpkeyset | client | generatehash] ... ``` Deprecation notice The `datool` binary name is deprecated, and current Docker images (v3.11.2 and later) no longer include it. Update your scripts to use `anytrusttool`. ### keygen Generate BLS or ECDSA keys for a Data Availability Server (DAS). #### Flags | Flag | Type | Default | Description | | ---------- | -------- | ---------- | ---------------------------------------------------------------- | | `--dir` | `string` | (required) | Directory to write generated key files into | | `--ecdsa` | `bool` | `false` | Generate an ECDSA keypair instead of BLS | | `--wallet` | `bool` | `false` | Generate the ECDSA keypair in a wallet file (requires `--ecdsa`) | #### Generate BLS keys BLS keys are the default. The DAS uses BLS keys to sign data availability certificates. ```shell anytrusttool keygen --dir /path/to/keys ``` This creates two files in the target directory: * `das_bls.pub` — base64-encoded BLS public key * `das_bls` — base64-encoded BLS private key #### Generate ECDSA keys ECDSA keys are used for signing store requests when interacting with a DA committee. ```shell anytrusttool keygen --dir /path/to/keys --ecdsa ``` This creates two files: * `ecdsa.pub` — hex-encoded ECDSA public key * `ecdsa` — hex-encoded ECDSA private key #### Generate ECDSA wallet To store the ECDSA key in an encrypted wallet file instead of a plaintext file: ```shell anytrusttool keygen --dir /path/to/keystore --ecdsa --wallet ``` You are prompted for a password to encrypt the wallet. #### Docker example ```shell docker run --rm \ --entrypoint anytrusttool \ -v "$(pwd)/keys:/data/keys" \ offchainlabs/nitro-node:latest \ keygen --dir /data/keys ``` ### dumpkeyset Compute the keyset bytes and keyset hash from a DA committee backend configuration. You need the keyset hash when configuring the `SequencerInbox` contract on the parent chain. #### Usage `dumpkeyset` reads its configuration from a JSON file passed with `--conf.file`. The JSON file specifies the committee members and the assumed-honest count: ```json { "keyset": { "assumed-honest": 1, "backends": [ { "url": "https://das-member-1.example.com:9876", "pubkey": "BASE64_BLS_PUBLIC_KEY_1" }, { "url": "https://das-member-2.example.com:9876", "pubkey": "BASE64_BLS_PUBLIC_KEY_2" }, { "url": "https://das-member-3.example.com:9876", "pubkey": "BASE64_BLS_PUBLIC_KEY_3" } ] } } ``` The `assumed-honest` field (H) defines the trust assumption for the committee. With N backends, K = N + 1 - H valid responses are required for a store request to succeed. In the example above, H = 1 and N = 3, so K = 3 valid signatures are required. #### Running dumpkeyset ```shell anytrusttool dumpkeyset --conf.file /path/to/keyset-config.json ``` Expected output: ```text Keyset: 0x00000003... KeysetHash: 0xabcdef12... ``` The `Keyset` value is the hex-encoded keyset bytes. The `KeysetHash` value is the hash you set in the `SequencerInbox` contract. #### Docker example ```shell docker run --rm \ --entrypoint anytrusttool \ -v "$(pwd)/config:/config" \ offchainlabs/nitro-node:latest \ dumpkeyset --conf.file /config/keyset-config.json ``` ### client Interact with DA committee members through RPC (store data) or REST (retrieve data). #### client rpc store Store a message on the DA committee through an RPC endpoint. ##### Flags | Flag | Type | Default | Description | | ----------------------------- | ---------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--message` | `string` | `""` | Message to store. Either `--message` or `--random-message-size` is required. | | `--random-message-size` | `int` | `0` | Store a message of the specified number of random bytes | | `--signing-key` | `string` | `""` | ECDSA private key to sign the message with. Treated as hex if prefixed with `0x`, otherwise treated as a file path. If not specified, the message is not signed. | | `--signing-wallet` | `string` | `""` | Path to a wallet file containing the ECDSA signing key | | `--signing-wallet-password` | `string` | (prompt) | Password to unlock the wallet. If not specified, you are prompted interactively. | | `--anytrust-retention-period` | `duration` | `24h` | Period that AnyTrust nodes are requested to retain the stored batch | | `--rpc-client.rpc.url` | `string` | `""` | URL of the AnyTrust RPC endpoint | ##### Example ```shell anytrusttool client rpc store \ --message "Hello, AnyTrust" \ --rpc-client.rpc.url http://localhost:9876 \ --signing-key 0xYOUR_PRIVATE_KEY_HEX ``` Expected output: ```text Hex Encoded Cert: 0x... Hex Encoded Data Hash: 0x... ``` #### client rest getbyhash Retrieve a message from a DA server by its data hash through a REST endpoint. ##### Flags | Flag | Type | Default | Description | | ------------- | -------- | ----------------------- | ------------------------------------------------------------------------------------------------ | | `--url` | `string` | `http://localhost:9877` | URL of the AnyTrust REST server | | `--data-hash` | `string` | `""` | Hash of the message to retrieve. Treated as hex if prefixed with `0x`, otherwise base64-encoded. | ##### Example ```shell anytrusttool client rest getbyhash \ --url http://localhost:9877 \ --data-hash 0xYOUR_DATA_HASH ``` Expected output: ```text Message: ``` ### generatehash Compute the data hash of a message string. This is the same hash used by the DA committee to identify stored data. ```shell anytrusttool generatehash "Hello, AnyTrust" ``` Expected output: ```text Hex Encoded Data Hash: 0x... ``` The argument is a positional parameter (not a flag). ## anytrustserver `anytrustserver` runs a DA committee member server that stores and serves batch data. It exposes RPC and REST interfaces for clients to store and retrieve data. Deprecation notice The `daserver` binary name is deprecated, and current Docker images (v3.11.2 and later) no longer include it. Update your scripts to use `anytrustserver`. ### Key flags | Flag | Type | Default | Description | | ---------------------------------------- | -------- | ----------- | --------------------------------------------------------------------------------- | | `--enable-rpc` | `bool` | `false` | Enable the HTTP-RPC server | | `--rpc-addr` | `string` | `localhost` | HTTP-RPC server listening interface | | `--rpc-port` | `uint` | `9876` | HTTP-RPC server listening port | | `--rpc-server-body-limit` | `int` | `0` | Maximum request body size in bytes (0 uses geth's 5 MB limit) | | `--enable-rest` | `bool` | `false` | Enable the REST server | | `--rest-addr` | `string` | `localhost` | REST server listening interface | | `--rest-port` | `uint` | `9877` | REST server listening port | | `--parent-chain.node-url` | `string` | `""` | URL of the parent chain node | | `--parent-chain.connection-attempts` | `int` | `15` | Parent chain RPC connection attempts (0 retries infinitely) | | `--parent-chain.sequencer-inbox-address` | `string` | `""` | Parent chain address of the `SequencerInbox` contract. Set to `none` for testing. | | `--log-level` | `string` | `INFO` | Log level: `CRIT`, `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE` | | `--log-type` | `string` | `plaintext` | Log format: `plaintext` or `json` | | `--metrics` | `bool` | `false` | Enable Prometheus metrics | | `--pprof` | `bool` | `false` | Enable pprof profiling endpoint | At least one of `--enable-rpc` or `--enable-rest` is required. The `--data-availability.*` flags configure the server's storage backend, BLS key, and caching. Run `anytrustserver --help` for the full list of data availability options. ### Example ```shell anytrustserver \ --enable-rpc \ --rpc-addr 0.0.0.0 \ --rpc-port 9876 \ --enable-rest \ --rest-addr 0.0.0.0 \ --rest-port 9877 \ --parent-chain.node-url https://your-parent-chain-rpc.example.com \ --parent-chain.sequencer-inbox-address 0xYOUR_SEQUENCER_INBOX_ADDRESS \ --data-availability.key.key-dir /path/to/bls-keys \ --data-availability.local-file-storage.enable \ --data-availability.local-file-storage.data-dir /path/to/das-data \ --conf.file /path/to/das-config.json ``` ### Docker example ```shell docker run --rm \ -v "$(pwd)/das-data:/data" \ -v "$(pwd)/keys:/keys" \ -p 9876:9876 \ -p 9877:9877 \ --entrypoint anytrustserver \ offchainlabs/nitro-node:latest \ --enable-rpc \ --rpc-addr 0.0.0.0 \ --enable-rest \ --rest-addr 0.0.0.0 \ --parent-chain.node-url https://your-parent-chain-rpc.example.com \ --parent-chain.sequencer-inbox-address 0xYOUR_SEQUENCER_INBOX_ADDRESS \ --data-availability.key.key-dir /keys \ --data-availability.local-file-storage.enable \ --data-availability.local-file-storage.data-dir /data ``` ## daprovider `daprovider` runs a DA provider server that implements a unified JSON-RPC interface on top of a specific DA backend. It supports two modes: `anytrust` (for AnyTrust DA committees) and `referenceda` (for a reference DA implementation). ### Key flags | Flag | Type | Default | Description | | ----------------------------------------- | -------- | ----------- | -------------------------------------------------------------------------------------- | | `--mode` | `string` | (required) | DA provider mode: `anytrust` or `referenceda` | | `--provider-server.addr` | `string` | `localhost` | JSON-RPC server listening interface | | `--provider-server.port` | `uint` | `9880` | JSON-RPC server listening port | | `--provider-server.jwtsecret` | `string` | `""` | Path to file containing a JWT secret for request validation | | `--provider-server.enable-da-writer` | `bool` | `false` | Enable the DA writer interface for store requests | | `--provider-server.rpc-server-body-limit` | `int` | `0` | Maximum request body size in bytes (0 uses geth's 5 MB limit) | | `--with-data-signer` | `bool` | `false` | Enable data signing for store requests. Requires `--data-signer-wallet` configuration. | | `--parent-chain.node-url` | `string` | `""` | URL of the parent chain node | | `--parent-chain.connection-attempts` | `int` | `15` | Parent chain RPC connection attempts (0 retries infinitely) | | `--parent-chain.sequencer-inbox-address` | `string` | `""` | Parent chain address of the `SequencerInbox` contract | | `--log-level` | `string` | `INFO` | Log level: `CRIT`, `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE` | | `--log-type` | `string` | `plaintext` | Log format: `plaintext` or `json` | | `--metrics` | `bool` | `false` | Enable Prometheus metrics | | `--pprof` | `bool` | `false` | Enable pprof profiling endpoint | Mode-specific flags are prefixed with `--anytrust.*` or `--referenceda.*`. Run `daprovider --help` for the full list. ### AnyTrust mode example ```shell daprovider \ --mode anytrust \ --provider-server.addr 0.0.0.0 \ --provider-server.port 9880 \ --parent-chain.node-url https://your-parent-chain-rpc.example.com \ --parent-chain.sequencer-inbox-address 0xYOUR_SEQUENCER_INBOX_ADDRESS \ --anytrust.enable \ --anytrust.key.key-dir /path/to/bls-keys \ --anytrust.local-file-storage.enable \ --anytrust.local-file-storage.data-dir /path/to/das-data ``` ### ReferenceDA mode example ```shell daprovider \ --mode referenceda \ --provider-server.addr 0.0.0.0 \ --provider-server.port 9880 \ --referenceda.enable \ --parent-chain.node-url https://your-parent-chain-rpc.example.com ``` ### When to use daprovider versus anytrustserver `anytrustserver` is the standalone DA server binary that exposes native RPC and REST interfaces for a single AnyTrust committee member. `daprovider` is a newer binary that wraps DA backends behind a unified JSON-RPC interface, supports multiple DA modes (including `referenceda`), and is designed for integration with external systems that consume the provider API. Use `anytrustserver` when running a traditional AnyTrust DA committee member. Use `daprovider` when you need a standardized provider interface or are using a non-AnyTrust DA backend. --- > For a complete page index, fetch # Docker images and CLI binaries The Nitro Docker images bundle multiple CLI binaries that serve different roles in the Arbitrum ecosystem. This page documents the image variants, the binaries they contain, and how to override entrypoints to run specific tools. ## Docker image variants Nitro is published as several Docker image targets, each building on the previous one and adding more functionality: | Image target | Repository tag | Entrypoint | Description | | ---------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `nitro-node-slim` | `offchainlabs/nitro-node:*-slim` | `/usr/local/bin/nitro` | Minimal node image with `nitro`, `relay`, `nitro-val`, `seq-coordinator-manager`, `prover`, and `dbconv`. No legacy WASM module roots for validation. | | `nitro-node` | `offchainlabs/nitro-node:*` | `/usr/local/bin/nitro --validation.wasm.allowed-wasm-module-roots /home/user/nitro-legacy/machines,/home/user/target/machines` | Full node image with DA tools, Timeboost binaries, and both legacy and current WASM module roots for validation. | | `nitro-node-validator` | `offchainlabs/nitro-node:*-validator` | `/usr/local/bin/split-val-entry.sh` | Runs a single split validation server (`nitro-val`) alongside the main `nitro` process via `split-val-entry.sh`. | | `nitro-node-dev` | `offchainlabs/nitro-node:*-dev` | `/usr/local/bin/split-val-entry.sh` | Extends `nitro-node-validator` with `deploy`, `seq-coordinator-invalidate`, `mockexternalsigner`, and the latest locally-built WASM module root. Intended for development and testing only. | All images run as a non-root `user` account by default. The working directory is `/home/user/`. ## Default entrypoint and flags The `nitro-node` image (the standard production image) uses this entrypoint: ```shell /usr/local/bin/nitro --validation.wasm.allowed-wasm-module-roots /home/user/nitro-legacy/machines,/home/user/target/machines ``` The `--validation.wasm.allowed-wasm-module-roots` flag tells the node which WASM module roots are valid for block validation. The comma-separated value includes: * `/home/user/nitro-legacy/machines` — WASM roots from earlier Nitro versions, needed to validate historical blocks. * `/home/user/target/machines` — Current WASM roots shipped with this image version. Warning If you override the entrypoint (for example, `--entrypoint relay`), the default flags are **not** applied. You must pass `--validation.wasm.allowed-wasm-module-roots` manually if the binary you are running requires validation support. ### The split validator entrypoint The `nitro-node-validator` image uses `split-val-entry.sh` as its entrypoint. This script: 1. Generates a shared JWT secret for authentication between the node and its validation servers. 2. Launches a `nitro-val` instance on loopback addresses: * Port `52000` — current validator using `/home/user/target/machines`. 3. Waits for the validation server to become available. 4. Launches the main `nitro` process with `--node.block-validator.validation-server-configs-list` pointing to that validator. You can pass validator-specific flags using the `--val-options` and `--val-options-latest` prefixes, separated by `--`. The legacy validator was removed upstream, so passing `--val-options-legacy` now causes the container to exit immediately. ```shell docker run offchainlabs/nitro-node:v3.11.3-beb2108-validator \ --val-options-latest --validation.api-auth=false -- \ --node.block-validator.enable ``` ## Binary catalog ### Core node | Binary | Description | | ----------- | ----------------------------------------------------------------------------------------------------------------- | | `nitro` | Main Arbitrum Nitro node. Runs as sequencer, full node, archive node, or batch poster depending on configuration. | | `nitro-val` | Standalone validation server. Validates state transitions using WASM fraud proofs. | | `relay` | Sequencer feed relay. Receives the transaction feed from a sequencer and redistributes it to downstream nodes. | | `deploy` | Deploys Arbitrum Rollup contracts to the parent chain. Used when launching new Arbitrum chains. Dev image only. | | `prover` | WASM prover binary for generating and verifying fraud proofs. | | `jit` | Just-in-time (JIT) WASM execution machine. Faster alternative to the interpreted prover, used during validation. | ### Data availability | Binary | Description | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `anytrustserver` | AnyTrust Data Availability Committee (DAC) server. Stores and serves batch data for AnyTrust chains. Formerly named `daserver`; renamed in [nitro#4142](https://github.com/OffchainLabs/nitro/pull/4142). The old name is no longer present in the image. | | `anytrusttool` | CLI tool for AnyTrust key generation, keyset management, and certificate inspection. Formerly named `datool`; renamed in [nitro#4142](https://github.com/OffchainLabs/nitro/pull/4142). The old name is no longer present in the image. | | `daprovider` | Unified data availability provider. Serves data from multiple backends (AnyTrust, blobs, or other DA layers). | ### Timeboost | Binary | Description | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `autonomous-auctioneer` | Runs the Timeboost express lane auction. Accepts bids and determines express lane controllers. | | `bidder-client` | Submits bids to the Timeboost express lane auction on behalf of a searcher or application. | | `el-proxy` | Express lane proxy for testing. Wraps `eth_sendRawTransaction` calls and forwards them as express lane transactions. Testing only. | ### Sequencer coordination | Binary | Description | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `seq-coordinator-manager` | Interactive terminal interface (TUI) for managing sequencer coordination via Redis. Displays and controls active sequencer selection. | | `seq-coordinator-invalidate` | Invalidates a sequencer coordination message in Redis. Usage: `seq-coordinator-invalidate [redis url] [signing key] [msg index]`. Dev image only. | ### Utilities | Binary | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `dbconv` | Converts node databases between storage backends (for example, LevelDB to Pebble). | | `genesis-generator` | Generates genesis state and initialization data for new Arbitrum chains. | | `transaction-filterer` | Filters transactions based on configurable rules before they reach the sequencer. Exposes a `/liveness` health endpoint. | | `mockexternalsigner` | Mock external transaction signer for testing. Accepts a private key and exposes an RPC signing interface. Dev image only. | ### Image availability Not all binaries are present in every image. The following table shows which image targets include each binary. The images also bundle some additional internal utility binaries not listed in this reference (for example, `blobtool`, `filtering-report`, and `validator` in `nitro-node`, and `stylus-raw-deploycode` in `nitro-node-dev`). | Binary | `nitro-node:*-slim` | `nitro-node` | `nitro-node-validator` | `nitro-node-dev` | | ---------------------------- | ------------------- | ------------ | ---------------------- | ---------------- | | `nitro` | Yes | Yes | Yes | Yes | | `relay` | Yes | Yes | Yes | Yes | | `nitro-val` | Yes | Yes | Yes | Yes | | `prover` | Yes | Yes | Yes | Yes | | `dbconv` | Yes | Yes | Yes | Yes | | `seq-coordinator-manager` | Yes | Yes | Yes | Yes | | `jit` | No | Yes | Yes | Yes | | `anytrustserver` | No | Yes | Yes | Yes | | `anytrusttool` | No | Yes | Yes | Yes | | `daprovider` | No | Yes | Yes | Yes | | `autonomous-auctioneer` | No | Yes | Yes | Yes | | `bidder-client` | No | Yes | Yes | Yes | | `el-proxy` | No | Yes | Yes | Yes | | `genesis-generator` | No | Yes | Yes | Yes | | `transaction-filterer` | No | Yes | Yes | Yes | | `deploy` | No | No | No | Yes | | `seq-coordinator-invalidate` | No | No | No | Yes | | `mockexternalsigner` | No | No | No | Yes | ## Overriding entrypoints Use `--entrypoint` to run a different binary from the same Docker image. The examples below use the `nitro-node` image. ### Feed relay ```shell docker run --rm \ --entrypoint relay \ offchainlabs/nitro-node:v3.11.3-beb2108 \ --node.feed.input.url wss://arb1.arbitrum.io/feed \ --chain.id 42161 ``` ### Validation node ```shell docker run --rm \ --entrypoint nitro-val \ offchainlabs/nitro-node:v3.11.3-beb2108 \ --help ``` ### DA keyset dump ```shell docker run --rm \ --entrypoint anytrusttool \ offchainlabs/nitro-node:v3.11.3-beb2108 \ dumpkeyset --url https://your-dac-url ``` ### DA key generation ```shell docker run --rm \ -v /path/to/keys:/data/keys \ --entrypoint anytrusttool \ offchainlabs/nitro-node:v3.11.3-beb2108 \ keygen --dir /data/keys ``` ## Docker Compose example The following `docker-compose.yml` runs an Arbitrum One full node. Replace the placeholder URLs with your actual parent chain and beacon endpoints. ```yaml # Arbitrum One full node — Docker Compose # Requires: parent chain RPC endpoint and beacon chain endpoint version: '3.8' services: nitro: image: offchainlabs/nitro-node:v3.11.3-beb2108 restart: unless-stopped ports: # HTTP JSON-RPC endpoint - '8547:8547' # WebSocket JSON-RPC endpoint - '8548:8548' volumes: # Persistent data directory for the node database - nitro-data:/home/user/.arbitrum command: # Structured JSON logging. Nitro reads this from the --log-type flag, # not an environment variable (NITRO_LOG_TYPE has no effect). - --log-type=json # Parent chain connection (Ethereum mainnet RPC) - --parent-chain.connection.url=https://your-parent-chain-rpc-url # Beacon chain endpoint for EIP-4844 blob retrieval - --parent-chain.blob-client.beacon-url=https://your-beacon-chain-url # Chain ID for Arbitrum One - --chain.id=42161 # Initialize from the latest pruned snapshot to avoid syncing from genesis. # --init.latest accepts: archive | pruned | genesis. A full node uses the pruned snapshot. # To pin a specific snapshot instead, use --init.url=. - --init.latest=pruned # Enable HTTP RPC on all interfaces so the port mapping works - --http.addr=0.0.0.0 - --http.port=8547 - --http.vhosts=* - --http.corsdomain=* - --http.api=net,web3,eth,arb # Enable WebSocket RPC - --ws.addr=0.0.0.0 - --ws.port=8548 - --ws.origins=* - --ws.api=net,web3,eth,arb volumes: nitro-data: driver: local ``` To start the node: ```shell docker compose up -d ``` To follow the logs: ```shell docker compose logs -f nitro ``` ## Ports and endpoints | Port | Protocol | Service | Notes | | ---- | --------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 8547 | HTTP | JSON-RPC | Main RPC endpoint for `eth_`, `net_`, `web3_`, and `arb_` namespaces. | | 8548 | WebSocket | JSON-RPC | WebSocket RPC endpoint. Supports subscriptions (`eth_subscribe`). | | 8549 | HTTP | Auth RPC | Default JWT-authenticated RPC (the `--auth.*` engine API), bound to loopback (`127.0.0.1`) by default. The split validator reaches its `nitro-val` over `ws://127.0.0.10:52000`, not this port. | | 9642 | WebSocket | Sequencer feed | Outbound transaction feed. Downstream nodes and relays connect here to receive new transactions. | | 6070 | HTTP | Metrics server | Prometheus metrics endpoint at `/debug/metrics/prometheus`. | ### Health check endpoints The main `nitro` binary does not expose a dedicated health check endpoint. Health is typically inferred by checking RPC availability (for example, calling `eth_chainId` on port 8547). Other binaries provide explicit health endpoints: * `transaction-filterer` exposes `/liveness` for health monitoring. * `anytrustserver` (DA server) exposes `/health` for readiness checks. --- > For a complete page index, fetch # How to convert databases from leveldb to pebble Switching from LevelDB to Pebble in Ethereum's Geth client provides several advantages, particularly in terms of resilience to data corruption during unexpected shutdowns, which helps maintain the integrity of your Ethereum node's database. While Pebble provides better resilience and potentially improved performance, deciding to switch should consider your specific needs and the possible effects of a full resync on your operations. For background on how the database backend interacts with Nitro's caches and memory usage, see [node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md). Note It's important to understand that transitioning to Pebble may necessitate a complete resynchronization of your Ethereum node. This process can take many days/weeks and could lead to downtime. If you are not facing significant issues with LevelDB and do not need Pebble's specific advantages, there may not be a strong reason to make the switch at this time. ## convert-databases.bash script The script can be found in the [Nitro Docker image](https://github.com/OffchainLabs/nitro/blob/7fdc5681c410bf283029e62bde4a1367b32f647d/Dockerfile#L283). ```shell Usage: convert-databases.bash [OPTIONS..] OPTIONS: --dbconv dbconv binary path (default: "/usr/local/bin/dbconv") --src directory containing source databases (default: "/home/user/.arbitrum/arb1/nitro") --dst destination directory --force remove destination directory if it exists --skip-existing skip convertion of databases which directories already exist in the destination directory --clean sets what should be removed in case of error, possible values: "failed" - remove database which conversion failed (default) "none" - remove nothing, leave unfinished and potentially corrupted databases "all" - remove whole destination directory ``` Upon successful completion, the script prints out: ```shell == Conversion status: l2chaindata database: converted l2chaindata database freezer (ancient): copied arbitrumdata database: converted wasm database: converted classic-msg database: converted ``` ### Running the conversion script in Docker ```shell docker run \ --detach \ --rm \ --name convert_db \ -v /path/to/src/data/arbitrum:/home/user/.arbitrum \ -v /path/to/dst/data/arbitrum:/home/user/dst \ -it \ --entrypoint /bin/bash \ nitro-node \ -c "convert-databases.bash --dst /home/user/dst/arb1/nitro" ``` ## dbconv tool `dbconv` tool can be used to: * Convert single database: copy all entries from a single source database to the destination database that may use a different database engine (`leveldb` or `pebble`) * Compact single database: run compaction on the destination database * Verify database contents: check if all keys (and optionally values) from the source database are present in the destination database Selected command arguments: ```shell --dst.data string destination directory of stored chain state --dst.db-engine string backing database implementation to use ('leveldb' or 'pebble') (default "pebble") --src.data string source directory of stored chain state --src.db-engine string backing database implementation to use ('leveldb' or 'pebble') (default "leveldb") --convert enables conversion step --compact enables compaction step --verify string enables verification step ("" = disabled, "keys" = only keys, "full" = keys and values) ``` To see all possible configuration options, run: `dbconv --help` ### example usage * Converting the `leveldb` database to `pebble` and compacting the resulting database. ```shell ./target/bin/dbconv --src.data /path/to/source/database/ --src.db-engine leveldb --dst.data /path/to/destination/database/ --dst.db-engine "pebble" --convert --compact ``` * Verifying that all source DB entries are in the destination DB (checking only if keys exist) ```shell ./target/bin/dbconv --src.data /path/to/source/database/ --src.db-engine leveldb --dst.data /path/to/destination/database/ --dst.db-engine "pebble" --verify "keys" ``` * Converting the `leveldb` database to `pebble`, compacting the resulting database, and then verifying that all keys from the source database exist in the destination database ```shell ./target/bin/dbconv --src.data /path/to/source/database/ --src.db-engine leveldb --dst.data /path/to/destination/database/ --dst.db-engine "pebble" --convert --compact --verify "keys" ``` --- > For a complete page index, fetch # How to migrate state and history from a classic (pre-Nitro) node to a Nitro node When running a Nitro node for the first time on a chain that produced [classic blocks](/arbitrum-essentials/public-chains.md#classic-deprecated) in the past (like Arbitrum One), you need to initialize its database to, at least, the state of the chain after executing the last classic block. The common, and recommended, way of doing that is to provide a database snapshot using the `--init.url` option (as mentioned in [How to run a full node (Nitro)](/run-arbitrum-node/run-full-node.md)). In this how-to we show you an alternative way for doing that, migrating the state and history of the chain from a fully synced classic node. > **INFO** — Is this How-to for you? > > As mentioned, the recommended way of initializing a Nitro node is by using a pre-initialized database snapshot with the `--init.url` option. This guide is for those that are interested in re-creating the full state of the chain from the genesis block using their own classic node. > > Keep in mind that this process only applies to Arbitrum One. Other Arbitrum chains didn't produce classic blocks in the past, they started as Nitro chains. ## Prerequisites To successfully migrate the state and history of the chain from a classic (pre-Nitro) node to a Nitro node, you'll need: * A fully synced classic node: you can find instructions on how to run a classic node in [this page](/run-arbitrum-node/more-types/run-classic-node.md). * A clean, uninitialized Nitro node: you can find instructions on how to set up a Nitro node in [this page](/run-arbitrum-node/run-full-node.md). ## Step 1: Enable export options in your classic node Launch your classic node with the option `--node.rpc.nitroexport.enable=true`. All exported data will be written to directory "nitroexport" under the classic instance directory (e.g., `${HOME}/.arbitrum/mainnet/nitroexport`). Make sure the classic node has read the entire rollup state. > **CAUTION** > > Enabling the export options is only recommended for nodes with no public/external interfaces. > **INFO** — Exported file contents are not deterministic > > Exporting the state of your own classic node should produce the same state as using files supplied by the Arbitrum Foundation (i.e., the same genesis blockhash). However, multiple exports of the same state will not necessarily create identical intermediate files. For example, state export is done in parallel, so the order of entries in the file is not deterministic. ## Step 2: Export information from your classic node ### Block & transaction history These are block headers, transactions and receipts executed in the classic node. Nitro node uses the history to be able to answer simple requests, like `eth_getTransactionReceipt`, from the classic history. The last block in the chain is the only one that affects the genesis block: timestamp is copied from the last block, and `parentHash` is taken from the last block's `blockHash`. * Call the RPC method `arb_exportHistory` with parameter `"latest"` to initiate history export. It will return immediately. * Calling `arb_exportHistoryStatus` will return the latest block exported, or an error if the export failed. * Data will be stored in the directory `nitroexport/nitro/l2chaindata/ancient`. ### Rollup state The rollup state is exported as a series of JSON files. State read from these JSON files will be added to Nitro's genesis block. * Call the RPC method `arb_exportState` with parameter `latest` to initiate state export. Unless disconnected, this will only return after the state export is done. * Data will be stored in the directory `nitroexport/state//`. ### Outbox messages (optional) This data does not impact consensus and is optional. It allows a Nitro node to provide the information required when executing a withdrawal made on the classic rollup. * Call the RPC method `arb_exportOutbox` with parameter `"0xffffffffffffffff"` to initiate outbox export. It will return immediately. * Calling `arb_exportOutboxStatus` will return the latest outbox batch exported, or an error if the export failed. * Data will be stored in the directory `nitroexport/nitro/classic-msg`. ## Step 3: Initialize your Nitro node importing the exported data * Place the `l2chaindata` and `classic-msg` (if exported) directories in Nitro's instance directory (e.g., `${HOME}/.arbitrum/arb1-nitro/`). * Launch the Nitro node with the argument `--init.import-file=/path/to/state/index.json` > **CAUTION** > > This state import operation requires more resources than a regular run of a Nitro node. ### Other useful Nitro options | Flag | Description | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--init.accounts-per-sync` | Allows the node to make partial database writes to hard-disk during initialization, allowing memory to be freed. This should be used if memory load is very high. A reasonable initial value to try would be 100000. Systems with constrained memory might require a lower value. | | `--init.then-quit` | Causes the node to quit after initialization is done. | | `--init.force` | For an already-initialized node, forces the node to recalculate Nitro's genesis block. If the genesis blockhash doesn't match what's in the database, the node will panic. | ## See also * [How to run a full node (Nitro)](/run-arbitrum-node/run-full-node.md) * [How to run a full node (Classic, pre-Nitro)](/run-arbitrum-node/more-types/run-classic-node.md) --- > For a complete page index, fetch # Nitro database snapshots Nitro stores the chain state and data in a database in the local filesystem. When starting Nitro for the first time, it will initialize an empty database by default and start processing transactions from Genesis. It takes a long time for the node to sync from Genesis, so starting from a database snapshot is advisable instead. Moreover, for the Arbitrum One chain, you must start from a snapshot because Nitro cannot process transactions from the Classic Arbitrum node. For an overview of snapshots in the context of initial node setup, see the [Database snapshots section of Prepare to run a node](/run-arbitrum-node/start-here.md#database-snapshots). ## Supply the snapshot URL to Nitro There are multiple ways to supply Nitro with the database snapshot. The most straightforward way is to provide the configuration, so Nitro downloads the snapshot by itself. It is also possible to download the database manually and supply it to Nitro. ## Downloading the latest snapshot Nitro has a CLI configuration for downloading the latest snapshot from a remote server. Set the flag `--init.latest` to either `archive`, `pruned`, or `genesis`, and Nitro will download the preferred snapshot. Nitro rejects any other value. You may also change the `--init.latest-base` flag to set the base URL when searching for the latest snapshot. > **INFO** > > All three kinds that `--init.latest` accepts resolve to HashDB snapshots, so `--init.latest` cannot bootstrap a PathDB node. See [PathDB snapshots](#pathdb-snapshots). ### How it works When searching for the latest snapshot, Nitro uses the chain name provided in `--chain.name`. Make sure to set it correctly; otherwise, Nitro might be unable to find the snapshot. Nitro will look for a remote file in `//latest-.txt`, where `` is the option supplied to `--init.latest`. This file should contain either the path or the full URL to the snapshot; if it only contains the path, Nitro will use the `` as the base URL. After finding the latest snapshot URL, Nitro will download the archive and temporarily store it in the directory specified in `--init.download-path`. Nitro looks for a SHA256 checksum on the remote server and verifies the checksum of the snapshot after finishing the download. (It is possible to disable this feature by setting `--init.validate-checksum` to false.) The snapshot can be a single archive file or a series of parts. Nitro first tries to download the snapshot as a single archive. In this case, Nitro will look for a checksum file in `.sha256`. If the remote server returns not found (404 status code), Nitro will proceed to download the snapshot in parts. When downloading in parts, Nitro will look for a manifest file in `.manifest.txt` containing each part's name and checksum. In this case, Nitro will download each part in the manifest file and concatenate them into a single archive. Finally, Nitro decompresses and extracts the snapshot archive, placing it in the database directory. Nitro will delete the archive after extracting it, so if you need to set up multiple nodes with the same snapshot, consider downloading it manually, as explained below. *** ## Downloading the snapshot from a URL Instead of letting Nitro search for the latest snapshot, you can provide a specific URL to download by setting the flag `--init.url` with the snapshot URL. If the URL points to a remote server, it should start with the `https://` protocol definition. Given the URL, Nitro will download the snapshot as described in the "Downloading the Latest Snapshot" section. Nitro also supports importing files from the local file system. In this case, you should provide the file path to `--init.url` starting with the prefix `file://` followed by the file path. Beware that when running Nitro inside a Docker container, you must mount a volume containing the provided snapshot using the docker flag `-v` (see [Docker documentation](https://docs.docker.com/reference/cli/docker/container/run/#volume)). Otherwise, the Nitro container running inside Docker won’t be able to find the snapshot in your local filesystem. *** ## PathDB snapshots A snapshot only works with a node whose state scheme matches the snapshot's. A PathDB snapshot won't initialize a HashDB node, and the reverse also fails. `--init.latest` can't help you here: it accepts only `archive`, `pruned`, and `genesis`, and all three resolve to HashDB snapshots. Snapshots built with the `path` scheme are published under two separate kinds that `--init.latest` doesn't accept: | Kind | Node type | Available for | | -------------- | --------- | --------------------------- | | `full-path` | Full node | Arbitrum One, Nova, Sepolia | | `archive-path` | Archive | Arbitrum One, Sepolia | Every PathDB snapshot is pruned to its state-history window, because PathDB prunes as the node runs. There is no unpruned `path` variant to publish. ### Initialize a PathDB node Each chain publishes a pointer file naming the current snapshot directory for each kind. Read it to find the URL: ```shell curl https://snapshot.arbitrum.foundation/arb1/latest-full-path.txt ``` Use `latest-archive-path.txt` for an archive node. For Arbitrum Sepolia, substitute `sepolia-rollup` for `arb1`. The file contains a directory path, such as `arb1/2026-08-01-a78755ba/`. Pass the full URL to that directory, including the trailing slash: ```shell --init.url https://snapshot.arbitrum.foundation/arb1/2026-08-01-a78755ba/ ``` Nitro reads the `.manifest.txt` file in that directory to enumerate the snapshot parts, downloads each part, and verifies its checksum. Before downloading terabytes, confirm you have the right snapshot. Each directory publishes a `metadata.json` naming its `state_scheme`, `snapshot_kind`, and total size: ```shell curl https://snapshot.arbitrum.foundation/arb1/2026-08-01-a78755ba/metadata.json ``` Your node's `--execution.caching.state-scheme` must match the snapshot's `state_scheme`. See [Choose a state scheme](/run-arbitrum-node/run-full-node.md#choose-a-state-scheme). *** ## Downloading the snapshot manually > **TIP** — For most users, automatic download is recommended > > Use `--init.latest pruned` (or `archive`) to enable Nitro's automatic handling of multi-part downloads, checksum verification, and extraction. In contrast, manual downloading is preferable only when you need to host a single snapshot locally for multiple nodes, offering direct control but requiring additional effort. It is possible to download the snapshot manually and supply the archive instead of having Nitro download it. The first step is downloading the snapshot. The command below illustrates how to do that on the command line using wget. The `-c` flag tells the wget to continue the download from where it left off, which is helpful because snapshots can be huge files, and the download can fail mid-way. The `-P` flag tells wget to place the snapshot on the temporary dir. ```shell wget -c -P /tmp "$SNAPSHOT_URL" ``` After downloading the snapshot, make sure to verify whether the checksum matches the one provided by the remote server. To fetch the checksum, you may run the command below. ```shell wget -q -O - "$SNAPSHOT_URL".sha256 ``` Once you know the expected snapshot checksum, run the command below to compute the checksum of the downloaded snapshot. Then, compare both and see if they are the same. If they are not the same, consider redownloading the snapshot. You must provide a valid snapshot to Nitro; otherwise, it won’t work properly. ```shell sha256sum $PATH_TO_SNAPSHOT ``` Finally, you can provide a path to the downloaded snapshot archive to Nitro using the `--init.url` flag, as described in the "Download the Snapshot from a URL" section. ### Downloading snapshot parts If the snapshot is divided into parts, you should first download the manifest file in `.manifest.txt`. This manifest contains the names and checksums of each part. For instance, the snippet below shows how the manifest file should look. You may use the commands described previously to download each part of the snapshot and verify their checksums. > **NOTE** > > For directory-style snapshot URLs, the manifest is the `.manifest.txt` file inside the directory. For example, `https://snapshot.arbitrum.foundation/arb1/2026-08-01-a78755ba/.manifest.txt`. Its part names include the directory prefix, so resolve them against the parent of the snapshot URL. > > The same directory also holds a `metadata.json` giving each part's filename, size, and both SHA-256 and xxHash checksums. Either file works for a manual download; Nitro itself reads `.manifest.txt`. ```shell a938e029605b81e03cd4b9a916c52d96d74c985ac264e2f298b90495c619af74 archive.tar.part0 9e095ce82e70fa62bb6e7b4421e7f2c04b2cd9e21d2bc62cbbaaeb877408357b archive.tar.part1 e92172d6eaf770a76c7477e6768f742fc51555a5050de606bd0f837e59c7a61d archive.tar.part2 d1b6fb9aeeb23903cdbb2a7cca8e6909bff4ee8e51c8a5acac2a142b3e3a5437 archive.tar.part3 f37e4552453202f2044e58b307bab7e466205bd280426abbc84f8646c6430cfa archive.tar.part4 972c5f513faca6ac4fadd22c70bea97707c6d38e9a646432bc311f0ca10497ed archive.tar.part5 ``` After downloading all the parts and verifying their checksums, you may use the command below to join them into a single archive. ```shell cat archive.tar.part* > archive.tar ``` *** ## Extracting the snapshot manually It is also possible to extract the snapshot archive and place the files manually. First, you need to download the snapshot archive as described in "Manually Downloading the Snapshot". Then, create the directory where Nitro will look for its database. By default, Nitro stores the database on `$HOME/.arbitrum/$CHAIN/nitro`. Move the archive to this directory and extract it. The commands below exemplify this process for the Arbitrum Sepolia chain. ```shell export CHAIN=sepolia-rollup export ARCHIVE_PATH=/tmp/archive.tar.gz mkdir -p $HOME/.arbitrum/$CHAIN/nitro cd $HOME/.arbitrum/$CHAIN/nitro tar zxfv $ARCHIVE_PATH ``` You should see the following subdirectories in this directory after extracting the archive. ```shell arbitrumdata l2chaindata nodes ``` *** ## Creating a snapshot To generate a snapshot for the Nitro database, you first need to stop the process gracefully. You must not generate the snapshot while Nitro runs because the database might be in an intermediary state. Nitro should print logs like the ones described below when stopping. ```shell ^CINFO [08-22|18:10:55.015] shutting down because of sigint INFO [08-22|18:10:55.016] delayed sequencer: context done err="context canceled" INFO [08-22|18:10:55.016] rpc response method=eth_getBlockByNumber logId=123 err="context canceled" result=null attempt=0 args="[\"0x405661\", false]" INFO [08-22|18:10:55.293] Writing cached state to disk block=39988 hash=8bebf3..939ab2 root=4f7a22..00c334 INFO [08-22|18:10:55.297] Persisted trie from memory database nodes=643 size=156.31KiB time=3.673459ms gcnodes=329 gcsize=102.61KiB gctime="248.708µs" livenodes=2448 livesize=806.00KiB INFO [08-22|18:10:55.297] Writing cached state to disk block=39987 hash=ddcd60..fe0fc3 root=d6973e..7b9265 INFO [08-22|18:10:55.298] Persisted trie from memory database nodes=34 size=11.19KiB time="283.875µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=2414 livesize=794.81KiB INFO [08-22|18:10:55.298] Writing cached state to disk block=39861 hash=2a9dd3..f00ff0 root=139d5a..d6bf21 INFO [08-22|18:10:55.298] Persisted trie from memory database nodes=73 size=24.88KiB time="502.916µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=2341 livesize=769.93KiB INFO [08-22|18:10:55.299] Writing cached state to disk block=39861 hash=2a9dd3..f00ff0 root=139d5a..d6bf21 INFO [08-22|18:10:55.299] Persisted trie from memory database nodes=0 size=0.00B time="1.417µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=2341 livesize=769.93KiB INFO [08-22|18:10:55.299] Writing snapshot state to disk root=bd18ce..3b0763 INFO [08-22|18:10:55.299] Persisted trie from memory database nodes=0 size=0.00B time="1.125µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=2341 livesize=769.93KiB INFO [08-22|18:10:55.304] Blockchain stopped ``` After Nitro stops, go to the database directory and generate an archive file for the directories `arbitrumdata`, `l2chaindata`, and `nodes`. By default, the database directory for Nitro is `$HOME/.arbitrum/$CHAIN/nitro`. The commands below exemplify how to generate the snapshot for Nitro. ```shell export CHAIN=sepolia-rollup export ARCHIVE_PATH=/tmp/archive.tar.gz cd $HOME/.arbitrum/$CHAIN/nitro tar zcfv $ARCHIVE_PATH arbitrumdata l2chaindata nodes ``` This command purposely omits the `wasm` directory from the snapshot archive. The `wasm` contains native-code executables, so it might be a security concern for users downloading the snapshot. If the user downloading the snapshot trusts you, or if you are storing it for your own use, you may include the `wasm` directory in it. ### Optional: divide it into parts It is possible to divide the snapshot into smaller parts to facilitate its download. This is particularly useful for archive snapshots of heavily used chains, such as Arbitrum One. These kinds of snapshots can reach terabytes, so dividing them into smaller parts is helpful. The snippet below illustrates how to divide the snapshot into parts using the split command. The `-b` argument tells the split to divide the snapshot into 100 GB parts. The `-d` argument tells split to enumerate the parts using a numeric suffix instead of an alphabetic one. ```shell split -b 100g -d archive.tar.gz archive.tar.gz.part ``` After dividing it into parts, you should generate the manifest file containing the parts' names and checksums. Nitro will use these files to know how many parts there are and to validate their checksum. The command below exemplifies how to do that. --- > For a complete page index, fetch # Node tuning and monitoring Although Nitro is a Go application, it can use significantly more memory than Go's runtime reports. Nitro relies on multiple allocators: the Go garbage-collected heap, CGO (Go's mechanism for calling C code) allocations via `calloc`, and direct `mmap` system calls, each with its own accounting. Understanding where memory lives, which configuration knobs control it, how to tune validators, and how to monitor node health is essential for stable operation. ## Memory allocators in Nitro Nitro's total resident memory (RSS) is the sum of four distinct categories: | Allocator | What uses it | Visible in Go `memstats`? | Controlled by | | ----------------------- | -------------------------------------------------------------------------------------- | ------------------------- | --------------------------------------------- | | **Go heap** | State trie (dirty), transaction processing, goroutine stacks, general application data | Yes | `GOMEMLIMIT`, `trie-dirty-cache` | | **calloc** | Pebble block cache, Pebble memtables, Stylus WASM cache | No | `database-cache`, `stylus-lru-cache-capacity` | | **mmap** | fastcache (trie-clean and snapshot caches) | No | `trie-clean-cache`, `snapshot-cache` | | **glibc malloc arenas** | Per-thread arena overhead for CGO allocations | No | `MALLOC_ARENA_MAX` | Only the Go heap is subject to Go's garbage collector and `GOMEMLIMIT`. The CGO and `mmap` allocations are invisible to Go's runtime. They don't appear in `runtime.MemStats` or standard Go memory profiles, but they still consume container memory and count toward your memory limit. ### Go heap The Go runtime manages its own heap for all pure-Go allocations. Key consumers include: * **Dirty trie cache** (`trie-dirty-cache`): Modified state trie nodes held in memory before being flushed to disk. Defaults to 1024 MB and is one of the largest bounded caches on the Go heap. * **Contract code cache**: An LRU cache of contract bytecode, hardcoded at 256 MB. Isn't configurable. * **Activated WASM cache**: Compiled Stylus WASM modules cached on the Go heap, hardcoded at 64 MB. * **fastcache index maps**: Although fastcache stores its data via `mmap`, each instance maintains a Go-side index (bucket maps of `uint64` to `uint64`). With two large fastcache instances (trie-clean and snapshot), this index metadata can consume hundreds of MB on the Go heap. * **Snapshot diff layers**: Up to 128 diff layers can accumulate, each holding Go maps of modified accounts and storage slots. * **Goroutine stacks, block/receipt caches, and GC overhead**: Goroutine stacks, recently accessed blocks/receipts, and Go's own GC metadata collectively add further pressure. Go reports its total memory usage via `runtime.MemStats.Sys`, which includes the heap, stack space, and GC metadata. This is the portion of memory that `GOMEMLIMIT` governs. ### CGO allocations (Pebble and Stylus) Nitro's on-disk database, Pebble, allocates its block cache and memtables through CGO `calloc()` calls (see `pebble/internal/manual/manual.go` in the source). These allocations go through the C memory allocator and are out of scope for Go's memory tracking. **Pebble block cache** is the largest CGO consumer. It caches frequently read database blocks in memory to avoid disk I/O. Its size is set directly by the `database-cache` configuration parameter. **Pebble memtables** buffer recent writes before they are flushed to disk. Nitro configures four memtables, each sized at `database-cache / 8`, for a combined maximum of `database-cache / 2`. For the default `database-cache` of 2048 MB, this means up to 1024 MB of memtable space (four memtables of 256 MB each). **Stylus WASM cache** stores compiled WebAssembly modules for Stylus smart contracts. Rust allocates this cache (invoked through CGO), and `stylus-lru-cache-capacity` bounds its size. ### Raw `mmap` allocations (fastcache) Two caches use [fastcache](https://github.com/VictoriaMetrics/fastcache), a library that allocates memory via direct `mmap` system calls, bypassing both Go's allocator and CGO: * **Trie-clean cache** (`trie-clean-cache`): Caches unchanged state trie nodes. Default: 600 MB. * **Snapshot cache** (`snapshot-cache`): Caches state snapshot data for fast reads. Default: 400 MB. Because fastcache uses raw `mmap`, this memory doesn't appear in Go's `memstats` or standard profiling tools. You can only see it by inspecting `/proc//smaps` at the OS level. Each fastcache instance allocates memory in 64 MB chunks, making these regions identifiable when analyzing process memory maps. ### glibc malloc arenas When Nitro makes CGO calls (for Pebble, Stylus, etc.), the resulting C-side allocations go through the system's default C memory allocator: glibc `malloc`. Unlike Go's garbage-collected heap, `malloc` manages memory by requesting large regions from the OS and subdividing them to satisfy individual allocation requests. Freed memory is returned to the allocator's internal free lists rather than immediately back to the OS, so the process's RSS can remain elevated even after allocations are freed. To handle concurrent allocations efficiently, glibc `malloc` uses arenas, which are independent memory pools, each with its own lock. When a thread allocates memory, it picks an arena, reducing contention compared to a single global lock. By default, glibc creates up to `8 x CPU_count` arenas, each reserving a 64 MB region. The worst-case overhead for arenas is: ```shell Arena overhead = 8 x CPU_count x 64 MB ``` In containerized environments, glibc detects the underlying host CPU count (not the container's CPU requests), which often results in far more arenas than needed. As the process runs and more threads make CGO calls, glibc creates and retains new arenas, causing RSS to drift upward over days or weeks even though no individual allocation is leaking. This can be controlled with the `MALLOC_ARENA_MAX` environment variable: ```shell MALLOC_ARENA_MAX=2 ``` Setting `MALLOC_ARENA_MAX=2` caps glibc to two arenas, reducing worst-case arena overhead from gigabytes to \~128 MB. In testing, this eliminated the slow memory growth with no measurable performance impact on RPC throughput. Without `MALLOC_ARENA_MAX`, a Nitro node on a large host can accumulate gigabytes of arena overhead that appears as a "memory leak" because RSS grows steadily while Go reports stable usage. This is the most common cause of unexplained memory growth in long-running Nitro nodes. ### Thread stacks Nitro spawns native threads for CGO operations (Pebble, compression libraries) and Stylus execution. ## Cache configuration reference All cache sizes are configured under `execution.caching`: | Parameter | Default | Allocator | Description | | --------------------------- | ------- | ------------------ | -------------------------------------------------------- | | `database-cache` | 2048 MB | CGO (`calloc`) | Pebble block cache size. Also determines memtable sizes. | | `trie-dirty-cache` | 1024 MB | Go heap | Modified trie nodes awaiting flush to disk. | | `trie-clean-cache` | 600 MB | `mmap` (fastcache) | Unchanged trie nodes cached for read performance. | | `snapshot-cache` | 400 MB | `mmap` (fastcache) | State snapshot data for fast lookups. | | `stylus-lru-cache-capacity` | 256 MB | Rust (via CGO) | Compiled Stylus WASM modules. | > **TIP** > > All of these caches are bounded by configuration and won't grow beyond their configured limits. This means total non-Go memory is predictable and can be calculated from your configuration. ## Calculating `GOMEMLIMIT` `GOMEMLIMIT` is an environment variable that sets a soft memory limit for the Go runtime. When set, Go's garbage collector (GC) runs more aggressively as heap usage approaches the limit, helping to keep total Go memory usage below the target. Without it, the GC relies solely on the `GOGC` environment variable (which defaults to 100, meaning the GC triggers when the heap doubles in size since the last collection) and has no awareness of an absolute memory ceiling. For `GOMEMLIMIT` to work correctly in a containerized environment, you must reserve enough headroom for all the non-Go memory that competes for the container's memory limit. ### Non-Go memory budget Sum all memory that lives outside the Go heap: ```shell Non-Go Memory = database-cache # Pebble block cache (CGO) + (database-cache / 2) # Pebble memtables, max (CGO) + trie-clean-cache # fastcache (mmap) + snapshot-cache # fastcache (mmap) + stylus-lru-cache-capacity # Stylus WASM (Rust) + malloc arena overhead # glibc arenas + ~300 MB # Thread stacks (varies by workload) ``` With `MALLOC_ARENA_MAX=2`, arena overhead is \~128 MB. Without it, arena overhead can grow to several gigabytes depending on host CPU count. See [glibc malloc arenas](#glibc-malloc-arenas) above. ### Formula ```shell GOMEMLIMIT = Container_Memory_Limit - Non_Go_Memory - Safety_Margin ``` You should use a safety margin of 300-500 MB to account for allocator overhead, transient allocations, and kernel page cache. ### Example: 16 GB container with defaults | Component | Size | Source | | ---------------------- | ------------ | ---------------------------------- | | Pebble block cache | 2,048 MB | `database-cache` (CGO) | | Pebble memtables (max) | 1,024 MB | `database-cache / 2` (CGO) | | Trie-clean cache | 600 MB | `trie-clean-cache` (fastcache) | | Snapshot cache | 400 MB | `snapshot-cache` (fastcache) | | Stylus WASM cache | 256 MB | `stylus-lru-cache-capacity` (Rust) | | Malloc arenas | 128 MB | `MALLOC_ARENA_MAX=2` | | Thread stacks | 300 MB\* | \~2 MB per thread | | **Total non-Go** | **4,756 MB** | | \*Thread stack usage depends on the number of active threads, which varies by workload. ```shell GOMEMLIMIT = 16,384 MB - 4,756 MB - 400 MB safety = ~11,228 MB ≈ 11 GB ``` > **CAUTION** > > If `GOMEMLIMIT` is set too high (not accounting for non-Go memory), the Go garbage collector defers collection, expecting more room than actually exists. The OS then OOM-kills the process when total RSS (Go heap plus all non-Go allocations) exceeds the container limit. ### Helm chart shortcut The community Helm chart calculates `GOMEMLIMIT` automatically using a multiplier of `0.9` against the container memory limit. It subtracts non-Go memory defaults (database-cache 2048, trie-clean-cache 600, snapshot-cache 400, stylus-lru-cache 256, malloc arena size 64) before applying the multiplier. If you use the Helm chart with default values, `GOMEMLIMIT` is set for you. ## Validator tuning Validators perform CPU-intensive WASM execution to verify blocks. Their memory and CPU profiles differ from the main Nitro node. ### GOMEMLIMIT for validators Validators use a lower `GOMEMLIMIT` multiplier of `0.75` (compared to `0.9` for the main node) because WASM execution creates more transient Go heap allocations that need GC headroom. For a 16 GB validator container: ```shell GOMEMLIMIT ≈ 16,384 MB × 0.75 = ~12,288 MB ≈ 12 GB ``` ### Memory free limits Nitro can throttle RPC and pause block validation when system free memory drops below a configurable threshold. This protection is **disabled by default in Nitro** — it only kicks in once you set the corresponding flags. The [community Helm chart](https://github.com/OffchainLabs/community-helm-charts/tree/main/charts/nitro) enables these limits with the following multipliers against the container memory limit: * **`resourceMgmtMemFreeLimit`**: Multiplier `0.05` (5% of container memory). When free memory drops below this threshold, the node throttles incoming RPC requests to prevent OOM conditions. * **`blockValidatorMemFreeLimit`**: Multiplier `0.05` (5% of container memory). When free memory drops below this threshold, the validator pauses block validation until memory is reclaimed. Note The `0.05` multipliers above come from the community Helm chart's defaults, not from Nitro itself. If you're not running the Helm chart, set the equivalent Nitro flags explicitly to enable this protection. ### GOMAXPROCS `GOMAXPROCS` controls how many OS threads Go uses for goroutine scheduling. For validators, the Helm chart defaults to a multiplier of `2` against the container CPU limit (for example, 4 CPUs yields `GOMAXPROCS=8`). This higher-than-usual setting helps because validators frequently block on CGO calls during WASM execution, and additional Go threads keep non-CGO goroutines progressing. For the main Nitro node, the Helm chart does not override `GOMAXPROCS`, letting Go's runtime auto-detect from the container CPU limit. ## Metrics and monitoring Nitro exposes Prometheus-compatible metrics through a dedicated metrics server. ### Enable the metrics server Pass the `--metrics` flag when starting the node. Configure the server with these flags: | Flag | Default | Description | | ---------------------------------- | ----------- | ---------------------------------------- | | `--metrics-server.addr` | `127.0.0.1` | Listen address for the metrics server | | `--metrics-server.port` | `6070` | Listen port for the metrics server | | `--metrics-server.update-interval` | `3s` | How often internal metrics are refreshed | Example: ```shell nitro --metrics \ --metrics-server.addr 0.0.0.0 \ --metrics-server.port 6070 ``` ### Prometheus scrape endpoint Once metrics are enabled, Nitro exposes a Prometheus-compatible endpoint at: ```shell http://:/debug/metrics/prometheus ``` `pprof` runs as a separate server at `/debug/pprof/` for CPU and memory profiling, enabled with --pprof and configured via --pprof-cfg.addr / --pprof-cfg.port (default 127.0.0.1:6071). It is not served on the metrics port. ### Key metrics to monitor | Metric | What it tells you | | --------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `container_memory_rss` | Actual RSS of the container. Compare against Go-reported heap to understand non-Go memory usage. | | `arb_feed_backlog_messages` | Number of messages in the feed backlog. A growing backlog indicates the node is falling behind the sequencer feed. | | Batch poster backlog | For batch poster nodes, tracks how many batches are pending submission to the parent chain. | Tip Always alert on `container_memory_rss`, not Go heap metrics. As described in [Memory allocators in Nitro](#memory-allocators-in-nitro), most of Nitro's memory is invisible to Go's runtime. ### Kubernetes ServiceMonitor If you run Nitro on Kubernetes with the Prometheus Operator, configure a `ServiceMonitor` to scrape the metrics endpoint: ```yaml apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: nitro spec: selector: matchLabels: app: nitro endpoints: - port: metrics path: /debug/metrics/prometheus interval: 5s ``` ## Health check patterns Nitro components expose health information through different mechanisms depending on the binary. ### Main Nitro node The main `nitro` binary does not expose a dedicated `/health` endpoint. Node health is inferred from RPC availability: if the HTTP RPC port (default `8547`) responds with HTTP 200 to a valid JSON-RPC request, the node is healthy. Example liveness check: ```shell curl -sf -X POST http://localhost:8547 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' ``` ### Auxiliary components * **Transaction filterer**: Exposes a `/liveness` endpoint for health checks. * **DA server**: Exposes a `/health` endpoint. ### Kubernetes probe strategy Initial sync can take days or weeks depending on chain history. The community Helm chart uses an aggressive startup probe to allow time for initial sync without marking the pod as failed: ```yaml startupProbe: httpGet: path: / port: 8547 failureThreshold: 2419200 periodSeconds: 1 ``` This configuration allows up to 28 days (2,419,200 seconds) for the node to become responsive. Once the startup probe succeeds, standard liveness and readiness probes take over. ## Tuning recommendations ### General rules 1. **Set `MALLOC_ARENA_MAX=2`**: This is the single most impactful change for containerized nodes. Without it, glibc can waste gigabytes on arena overhead, causing RSS to drift upward over days. Set this environment variable on every Nitro container. 2. **Start from the formula**: Calculate `GOMEMLIMIT` using the [formula above](#formula) with your actual cache configuration values. Do not set it to the container memory limit. 3. **Monitor RSS, not just Go heap**: Set container memory alerts based on actual RSS (`container_memory_rss` in Prometheus / cAdvisor), not Go-reported memory. 4. **All caches are bounded**: Unlike memory leaks, all non-Go memory in Nitro is bounded by configuration. With `MALLOC_ARENA_MAX` set, if RSS is stable and predictable, the node is behaving correctly. The memory is simply allocated outside Go's visibility. ### Example configurations #### Full node with 32 GB RAM Use default cache sizes. Calculate `GOMEMLIMIT` from the non-Go budget: ```shell # Non-Go: 2048 + 1024 + 600 + 400 + 256 + 128 + 300 = 4,756 MB # GOMEMLIMIT = 32,768 - 4,756 - 500 safety ≈ 27,512 MB ≈ 27 GB MALLOC_ARENA_MAX=2 \ GOMEMLIMIT=27512MiB \ nitro \ --metrics \ --metrics-server.addr 0.0.0.0 ``` #### Archive node with 128 GB RAM Archive nodes benefit from larger caches to reduce disk I/O for historical queries: ```shell # Non-Go: 4096 + 2048 + 1200 + 800 + 256 + 128 + 300 = 8,828 MB # GOMEMLIMIT = 131,072 - 8,828 - 500 safety ≈ 121,744 MB ≈ 119 GB MALLOC_ARENA_MAX=2 GOMEMLIMIT=121744MiB nitro \ --execution.caching.database-cache 4096 \ --execution.caching.trie-dirty-cache 2048 \ --execution.caching.trie-clean-cache 1200 \ --execution.caching.snapshot-cache 800 \ --metrics \ --metrics-server.addr 0.0.0.0 ``` #### Validator with 16 GB RAM Validators need less cache but more CPU headroom. Use reduced cache sizes and the lower `GOMEMLIMIT` multiplier: ```shell # Non-Go: 1024 + 512 + 300 + 200 + 256 + 128 + 300 = 2,720 MB # GOMEMLIMIT = 16,384 × 0.75 ≈ 12,288 MB # Alternatively: 16,384 - 2,720 - 500 = 13,164 MB (use the lower value) MALLOC_ARENA_MAX=2 GOMEMLIMIT=12288MiB GOMAXPROCS=4 nitro \ --metrics \ --metrics-server.addr 0.0.0.0 ``` --- > For a complete page index, fetch # Arbitrum nodes: an overview Note There is no protocol-level incentive to run an Arbitum full node. If you’re interested in accessing an Arbitrum chain but don’t want to set up a node locally, see our [RPC endpoints and providers](/arbitrum-essentials/reference/node-providers.md) to get RPC access to fully managed nodes hosted by a third-party provider. > **CAUTION** — API security disclaimer > > When exposing API endpoints to the Internet or any untrusted/hostile network, the following risks may arise: > > * **Increased risk of crashes due to Out-of-Memory (OOM)**: Exposing endpoints increases the risk of OOM crashes. > * **Increased risk of not keeping up with chain progression**: Resource starvation (IO or CPU) may occur, leading to an inability to keep up with chain progression. > > We strongly advise against exposing API endpoints publicly. Users considering such exposure should exercise caution and implement the right measures to enhance resilience. To be able to *interact with* or *build applications on* any of the Arbitrum chains, you need access to the corresponding Arbitrum node. Options are: * You can use [third party node providers ](/arbitrum-essentials/reference/node-providers.md)to get RPC access to fully-managed nodes * You can run your own Arbitrum node, especially if you want always to know the state of the Arbitrum chain The rest of this series focuses on the second approach: running your own Arbitrum node. ::: To be able to *interact with* or *build applications on* any of the Arbitrum chains, you need access to the corresponding Arbitrum node. Options are: When interacting with the Arbitrum network, users have the option to run either a full node or an archive node. There are distinct advantages to running an Arbitrum full node. In this quick start, we will explore the reasons why a user may prefer to run a full node instead of an archive node. By understanding the benefits and trade-offs of each node type, users can make an informed decision based on their specific requirements and objectives. Beyond full and archive nodes, a Nitro node can take on other roles — sequencer, batch poster, validator, or feed relay — purely through configuration. For the flags that define each role and how to convert a node between roles, see [How to assign roles to a Nitro node](/run-arbitrum-node/assign-node-roles.md). ## Considerations for running an Arbitrum full node * **Transaction validation and security**: Running a full node allows users to independently validate transactions and verify the state of the Arbitrum blockchain. Users can have complete confidence in the authenticity and integrity of the transactions they interact with. * **Reduced trust requirements**: By running a full node, users can interact with the Arbitrum network without relying on third-party services or infrastructure. This independence reduces the need to trust external entities and mitigates the risk of potential centralized failures or vulnerabilities. * **Lower resource requirements**: Compared to archive nodes, full nodes generally require fewer resources such as storage and computational power. These requirements make it more accessible to users with limited hardware capabilities or those operating in resource-constrained environments. For detailed instructions, read [how to run an Arbitrum full node](/run-arbitrum-node/run-full-node.md). ## Considerations for running an Arbitrum archive node While full nodes offer numerous advantages, there are situations where running an archive node may be more appropriate. Archive nodes store the complete history of the Arbitrum network, making them suitable for users who require access to extensive historical data or advanced analytical purposes. However, it's important to note that archive nodes are more resource-intensive, requiring significant storage capacity and computational power. For detailed instructions, read [how to run an Arbitrum archive node](/run-arbitrum-node/more-types/run-archive-node.md). ## Considerations for running an Arbitrum classic node The significance of running an Arbitrum classic node is mainly applicable to individuals with specific needs for an archive node and access to classic-related commands. For detailed instructions, read [how to run an Arbitrum classic node](/run-arbitrum-node/more-types/run-classic-node.md). ## Considerations for running a feed relay If you are running a single node, there is no requirement to set up a feed relay. However, if you have multiple nodes, it is highly recommended to have a single feed relay per data center. This setup offers several advantages, including reducing ingress fees and enhancing network stability. Soon, feed endpoints will mandate compression using a custom dictionary. Therefore, if you plan to connect to a feed using anything other than a standard node, it is strongly advised to run a local feed relay. This local feed relay will ensure that you have access to an uncompressed feed by default, maintaining optimal performance and compatibility. For detailed instructions, read [how to run an Arbitrum feed relay](/run-arbitrum-node/run-feed-relay.md). ## Support policy To view the short and long term support policy, visit the [Nitro support policy](/run-arbitrum-node/nitro-support-policy.md) page. --- > For a complete page index, fetch # How to run a feed relay > **CAUTION** > > If running a single node, there is no need to run a feed relay. When running more than one node, it is strongly recommended to run a single feed relay per data center, which will reduce ingress fees and improve stability. > > Feed endpoints will soon require compression with a custom dictionary, so if connecting to a feed with anything other than a standard node, it is strongly suggested to run a local feed relay, which will provide an uncompressed feed by default. For context on where the feed relay fits into Arbitrum's overall data flow, see [Data availability](/run-arbitrum-node/data-availability.md#how-full-nodes-sync-the-data-from-the-sequencer-feed). For the wire format the feed delivers, see [How to read the sequencer feed](/run-arbitrum-node/sequencer/read-sequencer-feed.md). For how the feed relay compares to the other Nitro node roles, see [How to assign roles to a Nitro node](/run-arbitrum-node/assign-node-roles.md). > **INFO** — Websocket connection stability > > Websocket connections to feed endpoints may occasionally be reset by infrastructure providers. The feed client in the relay automatically restarts immediately on disconnect. For most users, a delay of several blocks is not noticeable. > > For latency-sensitive applications, you can configure a redundant feed connection at the relay level by providing multiple comma-separated URLs to the same endpoint: > > ```shell > --node.feed.input.url=wss://arb1.arbitrum.io/feed,wss://arb1.arbitrum.io/feed > ``` > > **Important considerations:** > > * This doubles the bandwidth usage at the relay > * Excessive connections to feed endpoints may be rate-limited > * This redundancy should be implemented at the relay level, not at individual nodes > * Generally not recommended unless your application specifically requires minimal latency > * Do not use more than one redundant feed connection The feed relay is in the same Docker image as the Nitro node. * Here is an example of how to run the feed relay for Arbitrum One: ```shell docker run --rm -it -p 0.0.0.0:9642:9642 --entrypoint relay offchainlabs/nitro-node:v3.11.3-beb2108 --node.feed.output.addr=0.0.0.0 --node.feed.input.url=wss://arb1-feed.arbitrum.io/feed --chain.id=42161 ``` * Here is an example of how to run nitro-node for Arbitrum One with a custom relay: ```shell docker run --rm -it -v /some/local/dir/arbitrum:/home/user/.arbitrum -p 0.0.0.0:8547:8547 -p 0.0.0.0:8548:8548 offchainlabs/nitro-node:v3.11.3-beb2108 --parent-chain.connection.url=https://l1-mainnet-node:8545 --chain.id=42161 --http.api=net,web3,eth --http.corsdomain=* --http.addr=0.0.0.0 --http.vhosts=* --node.feed.input.url=ws://local-relay-address:9642 ``` Note that Arbitrum Classic does not communicate with Nitro sequencer, so classic relay is no longer used. ## Helm charts (Kubernetes) If you are using [Kubernetes](https://kubernetes.io/) to run your feed relay, a helm chart is available at [ArtifactHUB](https://artifacthub.io/packages/helm/offchainlabshelm/relay). It supports running a Nitro relay by providing the feed input URL. Find more information in the [OCL community Helm charts repository](https://github.com/OffchainLabs/community-helm-charts/tree/main/charts/relay). --- > For a complete page index, fetch # How to run a full node for an Arbitrum chain > **INFO** — Prerequisites > > This page assumes you've completed the steps from the [Start here](/run-arbitrum-node/start-here.md) page. If you haven't, you'll need to do so, as it gathers RPC endpoints, Nitro version, database snapshots, and other information required to run the node. > > To view the short and long term support policy, visit the [Nitro support policy](/run-arbitrum-node/nitro-support-policy.md) page. > > If you're looking to run a node with a different role — sequencer, batch poster, validator, archive node, or feed relay — see [How to assign roles to a Nitro node](/run-arbitrum-node/assign-node-roles.md) for the flags that define each role. Running on Kubernetes or want a more reliable setup? This page covers running a node with Docker. If you want to deploy on Kubernetes, or you want a more production-ready setup with monitoring, log signals, and network egress guidance, follow [How to run a full node with Helm on Kubernetes](/launch-arbitrum-chain/run-a-node/run-full-node-with-helm.md) instead. ## Choose a state scheme Nitro stores its state trie using one of two schemes: **HashDB** (the default) or **PathDB**. Choose before you initialize the database. You cannot switch an existing database — moving between schemes means re-initializing from a snapshot built with the scheme you want. | Property | HashDB (default) | PathDB | | ----------------------------- | ---------------------------------------- | ------------------------------------------ | | Flag | None; Nitro uses HashDB by default | `--execution.caching.state-scheme=path` | | State trie pruning | Manual and offline, via `--init.prune` | Automatic and online | | Block validation | Supported | Not supported | | Full node snapshot | `pruned`, all three DAO-governed chains | `full-path`, all three DAO-governed chains | | Archive snapshot | `archive`, all three DAO-governed chains | `archive-path`, Arbitrum One and Sepolia | | Download with `--init.latest` | Supported | Not supported; use `--init.url` | | Minimum Nitro version | Any | v3.9.x | Both schemes have a published full node snapshot, so either one can initialize without syncing from genesis. At comparable dates the two are close in size: on Arbitrum One, the `pruned` HashDB snapshot is about 2.3 TB and the `full-path` PathDB snapshot is about 2.4 TB. Pick based on how you want to handle pruning and whether you need block validation: * **Choose HashDB** if your node runs the block validator, or if you want the simplest initialization. `--init.latest pruned` downloads and verifies the snapshot for you. You then prune manually, and your node is offline while it does. * **Choose PathDB** if you want to avoid manual prune cycles. Nitro prunes online, so the node keeps serving RPC and disk use stays within the window you set with `--execution.caching.state-history`. Initialization takes more work: you pass the snapshot URL to `--init.url` yourself. On **archive** nodes the case for PathDB is stronger, because it cuts disk use substantially. See [How to run an archive node](/run-arbitrum-node/more-types/run-archive-node.md). PathDB and block validation PathDB cannot validate blocks. If a node requires the block validator, Nitro exits at startup with `path cannot be used as execution.caching.state-scheme when validator is required`. A default full node is unaffected: it runs the watchtower strategy, which does not require the block validator. Nitro rejects `path` when you set `--node.block-validator.enable=true`, choose a staker strategy other than `Watchtower`, or enable fast confirmation. ## Putting it into practice: run a node > **WARNING** — Caution > > If you are running more than one node, you should [run a feed relay](/run-arbitrum-node/run-feed-relay.md). > **WARNING** — PathDB and `--init.latest` > > `--init.latest` accepts only `archive`, `pruned`, and `genesis`, and every snapshot it resolves for those kinds uses HashDB. Adding `--execution.caching.state-scheme=path` to the examples below fails, because the downloaded snapshot's scheme won't match. > > PathDB snapshots are published under the separate `full-path` and `archive-path` kinds, which `--init.latest` does not accept. To initialize a PathDB database, pass the snapshot URL to `--init.url` instead. See [PathDB snapshots](/run-arbitrum-node/nitro/nitro-database-snapshots.md#pathdb-snapshots). > **WARNING** — Docker volume mount > > To ensure the database persists across restarts, mount an external volume to **`/home/user/.arbitrum`** inside the Docker container. Make sure to: > > * Create the host directory **before** running Docker (e.g., `mkdir -p /some/local/dir/arbitrum`), otherwise Docker may create it as `root`, and the container (which runs as UID 1000) won't be able to write to it. > * If you encounter permission errors on Linux or macOS, run `chmod -fR 777 /some/local/dir/arbitrum` on the host directory. > **NOTE** — Node config file > > If using a `node-config.json` file with Docker to mount, use the following command: > > ```text > docker run --rm -it -v /Path/to/mount/arbitrum:/home/user/.arbitrum -v /Path/to/node-config.json:/home/user/.arbitrum/node-config.json -p 0.0.0.0:8450:8450 offchainlabs/nitro-node:v3.11.3-beb2108 --conf.file /home/user/.arbitrum/node-config.json > ``` * Here is an example of how to run `nitro-node`:
Arbitrum One, Nova, Sepolia ```shell docker run --rm -it -v /some/local/dir/arbitrum:/home/user/.arbitrum -p 0.0.0.0:8547:8547 -p 0.0.0.0:8548:8548 offchainlabs/nitro-node:v3.11.3-beb2108 --parent-chain.connection.url= --parent-chain.blob-client.beacon-url= --chain.id= --init.latest=pruned --http.api=net,web3,eth --http.corsdomain=* --http.addr=0.0.0.0 --http.vhosts=* ```
Arbitrum chains ```shell docker run --rm -it -v /some/local/dir/arbitrum:/home/user/.arbitrum -p 0.0.0.0:8547:8547 -p 0.0.0.0:8548:8548 offchainlabs/nitro-node:v3.11.3-beb2108 --parent-chain.connection.url= --chain.info-json= --chain.name= --node.feed.input.url= --execution.forwarding-target= --http.api=net,web3,eth --http.corsdomain=* --http.addr=0.0.0.0 --http.vhosts=* ``` * You can see an example of `--chain.info-json` in the section above.
* Note that it is important that `/some/local/dir/arbitrum` already exists; otherwise, the directory might be created with `root` as owner, and the Docker container won't be able to write to it. * Note that if you are running a node for the parent chain (e.g., Ethereum for Arbitrum One or Nova) on localhost, you may need to add `--network host` right after `docker run` to use Docker host-based networking * When shutting down the Docker image, it is important to allow a graceful shutdown to save the current state to disk. Here is an example of how to do a graceful shutdown of all Docker images currently running ```shell docker stop --time=1800 $(docker ps -aq) ``` ### Important ports | Protocol | Default port | | ----------------- | ------------ | | `RPC`/`http` | `8547` | | `RPC`/`websocket` | `8548` | | `Sequencer Feed` | `9642` | * Please note: the `RPC`/`websocket` protocol requires some ports to be enabled, you can use the following flags: * `--ws.port=8548` * `--ws.addr=0.0.0.0` * `--ws.origins=\*` ### Note on permissions * The Docker image is configured to run as non-root UID 1000. This configuration means if you are running in Linux or OSX and you are getting permission errors when trying to run the Docker image, run this command to allow all users to update the persistent folders: ```shell mkdir /data/arbitrum chmod -fR 777 /data/arbitrum ``` ### Watchtower mode * By default, the full node runs in Watchtower mode, meaning that it watches the onchain assertions and, if it disagrees with them, logs an error containing the string `found incorrect assertion in watchtower mode`. For a BoLD-enabled chain like Arbitrum One or Arbitrum Nova if you are running Nitro before v3.6.0, the `--node.bold.enable=true` flag should be set to ensure your node can monitor for onchain assertions properly. * Setting this flag is not required as your node will continue to operate correctly, validate the Arbitrum One/Nova chain, and serve RPC requests as usual, regardless of this flag. * Note that watchtower mode adds a small amount of execution and memory overhead. You can deactivate this mode using the parameter `--node.staker.enable=false`. * For details on watchtower mode alongside the other validator strategies (`defensive`, `stakeLatest`, `resolveNodes`, `makeNodes`), see [How to run a validator](/run-arbitrum-node/more-types/run-validator-node.md). ### Pruning Pruning removes older, unnecessary state from the local copy of the chain your node maintains. It saves disk space and slightly improves the node's efficiency. How you prune depends on the [state scheme](#choose-a-state-scheme) you chose. On **HashDB**, the default, pruning is a *manual, opt-in* operation. It removes all states from blocks older than the latest 128. You decide when to run it, and the node stops serving RPC requests until it finishes. On a chain the size of Arbitrum One, that can take days. On **PathDB**, pruning is automatic and runs online. Nitro discards state older than the `--execution.caching.state-history` window as it goes, so you never schedule a prune and the node never goes offline to run one. Disk use stays within that window instead of growing between manual prunes. When you leave `--execution.caching.state-history` unset, Nitro chooses the default at startup: | Node type | Default `state-history` | | ------------------------------------------------- | ----------------------------------------------------------- | | Full node (`--execution.caching.archive=false`) | 345,600 blocks — 24 hours at the default 250 ms block speed | | Archive node (`--execution.caching.archive=true`) | `0`, meaning the entire chain | From v3.10.0, Nitro derives this default from the archive flag. On earlier versions the default was always 24 hours' worth of blocks, so an archive node needs an explicit `--execution.caching.state-history=0`. > **NOTE** > > The pruning process occurs when the node starts (upon initialization) and will not serve RPC requests during pruning. If you are using the default storage scheme (HashDB), then you can activate pruning by using the parameter: * `--init.prune `, where `` can be one of: * `minimal`: The most aggressive prune and retains only the genesis state and the head state (at the latest snapshot). Takes the least amount of time to complete. Duration depends on the chain and database size (several hours for smaller chains; potentially days for large chains like Arbitrum One). * `full` : Mostly intended for full nodes serving RPC requests, this mode retains the genesis state, the state of the latest confirmed block, and the head state (at the latest snapshot). Will not work if the node is in `validator` mode. Duration varies significantly by chain and database size—for Arbitrum One, this may take multiple days on NVMe SSDs. For smaller chains, it will be much faster. If pruning takes too long, consider downloading a fresh pruned snapshot with `--init.latest pruned` instead. * `validator`: Meant to be used by validator nodes and requires an RPC URL for L1 Ethereum. This mode retains the genesis state, the state of the latest confirmed block, the latest confirmed assertion root (obtained from L1), the last locally validated block root, and the head state (at the latest snapshot). This mode is expected to take longer than `full` pruning mode. For validator-specific setup details, see [How to run a validator](/run-arbitrum-node/more-types/run-validator-node.md). ### Memory management Under heavy RPC load or during operations such as large `debug_traceBlockByNumber` calls, Nitro nodes can consume significant memory. To prevent out-of-memory (OOM) crashes, consider configuring the following: * `--node.resource-mgmt.mem-free-limit`: Declines incoming RPC requests when free system memory drops below the specified threshold (e.g., `--node.resource-mgmt.mem-free-limit=4GiB`). This helps protect the node from OOM under high request load. * `GOMEMLIMIT` environment variable: Sets a soft memory limit for the Go runtime garbage collector (e.g., `GOMEMLIMIT=48GiB` for a 64 GB machine), helping reduce memory spikes. For an in-depth breakdown of Nitro's memory allocators, cache tuning, and OOM mitigations, see [node tuning and monitoring](/run-arbitrum-node/nitro/node-tuning-and-monitoring.md). > **TIP** > > For Docker deployments, you can set these in your `docker run` command: > > ```shell > docker run ... -e GOMEMLIMIT=48GiB ... offchainlabs/nitro-node:... --node.resource-mgmt.mem-free-limit=4GiB ... > ``` ### Transaction prechecker * Enabling the transaction prechecker will add extra checks before your node forwards `eth_sendRawTransaction` to the Sequencer endpoint. * Below, we list the flags to set up the prechecker: | Flag | Description | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--execution.tx-pre-checker.strictness` | How strict to be when checking transactions before forwarding them. 0 = accept anything, 10 = should never reject anything that'd succeed, 20 = likely won't reject anything that'd succeed, 30 = full validation which may reject transactions that would succeed (default 20) | | `--execution.tx-pre-checker.required-state-age` | How long ago should the storage conditions from `eth_SendRawTransactionConditional` be true, 0 = don't check old state (default 2) | | `--execution.tx-pre-checker.required-state-max-blocks` | Maximum number of blocks to look back while looking for the `` seconds old state, 0 = don't limit the search (default 4) | ### Optional parameters Below, we listed the most commonly used parameters when running a node. You can also use the flag `--help` for a comprehensive list of the available parameters. | Flag | Description | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--http.api` | Offers APIs over the HTTP-RPC interface. Default: `net,web3,eth,arb`. Add `debug` for tracing. | | `--http.corsdomain` | Accepts cross-origin requests from these comma-separated domains (browser enforced). | | `--http.vhosts` | Accepts requests from these comma-separated virtual hostnames (server enforced). Default: `localhost`. Accepts `*`. | | `--http.addr` | Sets the address to bind RPC to. May require `0.0.0.0` for Docker networking. | | `--execution.caching.archive` | Retains past block state. For archive nodes. | | `--execution.caching.state-scheme` | Default: `hash`. Sets the scheme Nitro uses to store its state trie, inherited from Geth. Set it to `path` to enable PathDB, which prunes automatically while the node keeps running. PathDB cannot validate blocks, and `--init.latest` cannot download its snapshots — use `--init.url`. See [Choose a state scheme](/run-arbitrum-node/run-full-node.md#choose-a-state-scheme). | | `--execution.caching.state-history` | PathDB only. Number of recent blocks of state history to retain on disk. Set to `0` to retain the entire chain. When unset, Nitro defaults to 345,600 blocks (24 hours at the default 250 ms block speed) on a full node, or `0` on an archive node. | | `--execution.caching.pathdb-max-diff-layers` | Default: 128 layers. Maximum number of diff layers kept in the node's memory before flushing to disk. Increasing the number of diff layers may cause the node to fall behind the chain head during busy periods since doing so slows down block processing speed and reduces sync speed. This configuration is primarily used to improve performance of shallow re-orgs (which are a concern on Ethereum but not on Arbitrum chains) and for efficient access to recent state. | | `--node.feed.input.url=` | Sets the sequencer feed address to this URL. Default: `wss://.arbitrum.io/feed`. ⚠️ One feed relay per datacenter is advised. See [feed relay guide](/run-arbitrum-node/run-feed-relay.md). | | `--execution.forwarding-target=` | Sets the sequencer endpoint to forward requests to. | | `--execution.rpc.evm-timeout` | Default: `5s`. Timeout for `eth_call`. (0 == no timeout). | | `--execution.rpc.gas-cap` | Default: `50000000`. Gas cap for `eth_call`/`estimateGas`. (0 = no cap). | | `--execution.rpc.tx-fee-cap` | Default: `1`. Transaction fee cap (in ether) for RPC APIs. (0 = no cap). | | `--execution.tx-lookup-limit` | Default: `126230400`, \~1 year worth of blocks at 250ms/block. Maximum number of blocks from head whose transaction indices are reserved (for example, `eth_getTransactionReceipt` and `eth_getTransactionByHash` only return results for indexed transactions). Set to 0 to index transactions for all blocks. Changing this parameter reindexes all missing transactions without the need to resync the chain. | | `--execution.rpc.classic-redirect=` | (Arbitrum One only) Redirects archive requests for pre-nitro blocks to this RPC of an Arbitrum Classic node with archive database. | | `--node.resource-mgmt.mem-free-limit` | Declines incoming RPC requests when free system memory (excluding page cache) drops below this threshold. Accepts values with suffixes like `4GiB`, `512MiB`. Helps prevent OOM crashes under heavy load. | | `--ipc.path` | Filename for IPC socket/pipe within datadir. 🔉 Not supported on macOS. The path is within the Docker container. | | `--init.prune` | Prunes the database before starting the node. Can be "full" or "validator". | | `--init.url=""` | (Required for Arbitrum One) URL to download the genesis database from. Only required for Arbitrum One nodes, when running them for the first time. See the [Nitro database snapshots guide](/run-arbitrum-node/nitro/nitro-database-snapshots.md) for more information. | | `--init.download-path="/path/to/dir"` | Temporarily saves the downloaded database snapshot. Defaults to `/tmp/`. Used with `--init.url`. | | `--init.latest` | Searches for the latest snapshot of the given kind (accepted values: `archive`, `pruned`, `genesis`) | | `--init.latest-base` | Base URL used when searching for the latest snapshot. Example value used for Arb1: `https://snapshot.arbitrum.foundation/`. Different chains will have different Base URLs, but only if they provide snapshots. Talk to chain owner for value to use. | | `--init.then-quit` | Allows any `--init.*` parameters to complete, and then the node automatically quits. It doesn't initiate pruning by itself but works in conjunction with other `--init.*` parameters, making it easier to script tasks like database backups after initialization processes finish. | --- > For a complete page index, fetch # How to run a local full chain simulation ## Overview A local full-chain simulation allows you to deploy and test smart contracts in a fully controlled environment. This how-to walks you through the process of setting up and running a complete development environment on your local machine, including a Nitro node, a dev-mode Geth parent chain, and multiple instances with different roles. Note that the node is now Stylus-enabled by default, and the setup instructions remain the same as for running a Stylus dev node. ## Step 1. Install prerequisites You'll need [Docker](https://docs.docker.com/get-docker/) and [docker compose](https://docs.docker.com/compose/) to run your node. Follow the instructions on their site to install them. ## Step 2. Clone the [nitro-testnode](https://github.com/OffchainLabs/nitro-testnode) repo You'll need the `release` branch. ```bash `git clone -b release --recurse-submodules https://github.com/OffchainLabs/nitro-testnode.git && cd nitro-testnode` ``` ## Step 3. Run your node ```bash ./test-node.bash --init ``` ## Step 4. Successive runs To relaunch the node after the first installation, run the following command. ```bash ./test-node.bash ``` > **INFO** — Clear local data > > Running the --init flag will clear all chain data and redeploy! ## Rollup contract addresses and chain configuration You can obtain the rollup chain configuration by running the following command. The chain configuration also includes the addresses of the core contracts. ```bash docker exec nitro-testnode-sequencer-1 cat /config/l2_chain_info.json ``` You can find other available configuration files by running: ```bash docker exec nitro-testnode-sequencer-1 ls /config ``` ## Token bridge An parent-child chain token bridge can be deployed by using the parameter `--tokenbridge`. The list of contracts can be found by running: ```bash docker compose run --entrypoint sh tokenbridge -c "cat l1l2_network.json" ``` ## Running an L3 chain An L3 chain can be deployed on top of the child chain (L2), by using the parameter `--l3node`. Its chain configuration can be found by running: ```bash docker exec nitro-testnode-sequencer-1 cat /config/l3_chain_info.json ``` When deploying an L3 chain, the following parameters are also available: `--l3-fee-token`: Uses a custom gas token for the L3 (symbol $APP), deployed on L2 at address `0x9b7c0fcc305ca36412f87fd6bd08c194909a7d4e` `--l3-token-bridge`: Deploys an L2-L3 token bridge. The list of contracts can be found by running `docker compose run --entrypoint sh tokenbridge -c "cat l2l3_network.json"`. ## Additional arguments You can find a list of additional arguments to use with `test-node.bash` by using `--help`. ```bash ./test-node.bash --help ``` ## Helper scripts The repository includes a set of helper scripts for basic actions like funding accounts or bridging funds. You can see a list of the available scripts by running: ```bash ./test-node.bash script --help ``` If you want to see information of a particular script, you can add the name of the script to the help command. ```bash ./test-node.bash script send-l1 --help ``` Here's an example of how to run the script that funds an address on L2. Replace `0x11223344556677889900` with the address you want to fund. ```bash ./test-node.bash script send-l2 --to address_0x11223344556677889900 --ethamount 5 ``` ## Blockscout Nitro comes with a local [Blockscout](https://www.blockscout.com/) block explorer. To access it, add the param `--blockscout` when running your node. ```bash ./test-node.bash --blockscout ``` The block explorer will be available at `http://localhost:4000` ## Default endpoints and addresses Node RPC endpoints are available at: | Node | Chain id | RPC endpoint | | --------------------- | -------- | ------------------------------------------------- | | L1 geth devnet | 1337 | `http://localhost:8545` | | L2 nitro devnet | 412346 | `http://localhost:8547` and `ws://localhost:8548` | | L3 nitro (if enabled) | 333333 | `http://localhost:3347` | Some important addresses: | Role | Public address | Private key | | ---------------------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------- | | Sequencer | `0xe2148eE53c0755215Df69b2616E552154EdC584f` | `0xcb5790da63720727af975f42c79f69918580209889225fa7128c92402a6d3a65` | | Validator | `0x6A568afe0f82d34759347bb36F14A6bB171d2CBe` | `0x182fecf15bdf909556a0f617a63e05ab22f1493d25a9f1e27c228266c772a890` | | L2 rollup owner | `0x5E1497dD1f08C87b2d8FE23e9AAB6c1De833D927` | `0xdc04c5399f82306ec4b4d654a342f40e2e0620fe39950d967e1e574b32d4dd36` | | L3 rollup owner (if enabled) | `0x863c904166E801527125D8672442D736194A3362` | `0xecdf21cb41c65afb51f91df408b7656e2c8739a5877f2814add0afd780cc210e` | | L3 sequencer (if enabled) | `0x3E6134aAD4C4d422FF2A4391Dc315c4DDf98D1a5` | `0x90f899754eb42949567d3576224bf533a20857bf0a60318507b75fcb3edc6f5f` | | Dev account (prefunded with **ETH** in all networks) | `0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E` | `0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659` | You can fund other addresses by using the scripts `send-l1` and `send-l2` as explained [here](#helper-scripts). > **CAUTION** — Private keys publicly known > > Do not use any of these addresses in a production environment. ## Optional parameters Here, We show a list of the parameters that might be useful when running a local devnode. You can also use the flag `./test-node.bash --help` to get them. | Flag | Description | | ------------------------- | ------------------------------------------------------------------------------------------------------- | | `--init` | Removes all the data, rebuilds, and deploys a new rollup | | `--pos` | L1 is a proof-of-stake chain (using Prysm for consensus) | | `--validate` | Validates all blocks in WASM, heavy computation | | `--l3node` | Deploys an L3 node on top of the L2 | | `--l3-fee-token` | Sets up the L3 chain to use a custom fee token. Only valid if `--l3node` flag is provided | | `--l3-fee-token-decimals` | Number of decimals to use for a custom fee token. Only valid if `--l3-fee-token` flag is provided | | `--l3-token-bridge` | Deploys an L2-L3 token bridge. Only valid if `--l3node` flag is provided | | `--batchposters` | Batch posters \[0-3] | | `--redundantsequencers` | Redundant sequencers \[0-3] | | `--detach` | Detaches from nodes after running them | | `--blockscout` | Builds or launches the Blockscout | | `--simple` | Runs a simple configuration: one node as a sequencer/batch-poster/bonder (default unless using `--dev`) | | `--tokenbridge` | Deploy an L1-L2 token bridge | | `--no-tokenbridge` | Opt out of building or launching the token bridge | | `--no-run` | Does not launch nodes (useful with build or init) | | `--no-simple` | Runs a full configuration with separate sequencer/batch-poster/validator/relayer | --- > For a complete page index, fetch # How to run a local Nitro dev node ## Overview This page provides step-by-step instructions for setting up and running a local Nitro node in `--dev` mode. This mode is ideal for developers who want to quickly test contracts using a single node, as it offers a simpler and faster setup compared to more complex environments. While some teams use `nitro-testnode` for testing cross-layer messaging, which involves launching both Geth as parent chain and Nitro as child chain, this setup can be more complex and time-consuming. If your primary goal is to test contracts on a local node without needing cross-layer interactions, Nitro's `--dev` mode offers a lightweight and efficient alternative. However, if you need more advanced functionality—such as cross-layer messaging, working with both the parent and child chains, or testing interactions between different layers—`nitro-testnode` is the preferred option. The testnode setup allows you to simulate a full parent-child chain environment, which is critical for those scenarios. See [How to run a local full chain simulation](/run-arbitrum-node/run-local-full-chain-simulation.md) for instructions. Note that Nitro `--dev` mode is ideal for Stylus contract testing, as it is much lighter and faster to set up than the full nitro-testnode environment. ## Prerequisites Before beginning, ensure the following is installed and running on your machine: * Docker: Required to run the Nitro dev node in a container. Install Docker by following [the official installation guide](https://docs.docker.com/get-started/get-docker/) for your operating system. * cast: A command-line tool from Foundry for interacting with Ethereum smart contracts. You can install it via Foundry by following [the installation instructions](https://book.getfoundry.sh/getting-started/installation). * jq: A lightweight JSON parsing tool used to extract contract addresses from the script output. Install jq by following [the official installation guide](https://jqlang.github.io/jq/download/) for your operating system. ## Clone the [nitro-devnode](https://github.com/OffchainLabs/nitro-devnode) repository Use the following command to clone the repository: ```shell git clone https://github.com/OffchainLabs/nitro-devnode.git cd nitro-devnode ``` ## Run the dev node script: Run the script to start the Nitro dev node, deploy the Stylus `Cache Manager` contract, and register it as a WASM cache manager using the default development account: ```shell ./run-dev-node.sh ``` The script will: * Start the Nitro dev node in the background using Docker. * Deploy the Stylus `Cache Manager` contract on the local Nitro network. * Register the `Cache Manager` contract as a WASM cache manager. ## Development account (used by default) In `--dev` mode, the script uses a pre-funded development account by default. This account is pre-funded with **ETH** in all networks and is used to deploy contracts, interact with the chain, and assume chain ownership. * Address: `0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E` * Private key: `0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659` You don’t need to set up a private key manually unless you prefer using your own key. ## Chain ownership in `--dev` mode In Nitro `--dev` mode, the default chain owner is set to `0x0000000000000000000000000000000000000000`. However, you can use the `ArbDebug` precompile to set the chain owner. This precompile includes the `becomeChainOwner()` function, which can be called to assume ownership of the chain. Chain ownership is important because it allows the owner to perform certain critical functions within the Arbitrum environment, such as: * Adding or removing other chain owners * Setting the parent and child chain base fees directly * Adjusting the gas pricing inertia and backlog tolerance * Modifying the computational gas target and transaction gas limits * Managing network and infrastructure fee accounts The script automatically sets the chain owner to the pre-funded dev account before registering the `Cache Manager` contract. Here’s how the `becomeChainOwner()` function is called within the script: ```shell cast send 0x00000000000000000000000000000000000000FF "becomeChainOwner()" --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 --rpc-url http://127.0.0.1:8547 ``` This step ensures that the dev account has ownership of the chain, which is necessary to register the `Cache Manager` as a WASM cache manager. At the end of the process, you'll have the Nitro `dev` mode running with the necessary components deployed. This environment is ready for testing and interacting with your contracts, including those written in Stylus, using the deployed `Cache Manager` to support enhanced functionality for Stylus-based smart contracts. --- > For a complete page index, fetch # How to read the sequencer feed [Running an Arbitrum relay locally as a feed relay](/run-arbitrum-node/run-feed-relay.md) lets you subscribe to an uncompressed sequencer feed for real-time data as the sequencer accepts and orders transactions offchain. When connected to websocket port `9642` of the local relay, you'll receive a data feed that looks something like this: ```json { "version": 1, "messages": [ { "sequenceNumber": 25757171, "message": { "message": { "header": { "kind": 3, "sender": "0xa4b000000000000000000073657175656e636572", "blockNumber": 16238523, "timestamp": 1671691403, "requestId": null, "baseFeeL1": null }, "l2Msg": "BAL40oKksUiElQL5AISg7rsAgxb6o5SZbYNoIF2DTixsqDpD2xII9GJLG4C4ZAhh6N0AAAAAAAAAAAAAAAC7EQiq1R1VYgL3/oXgvD921hYRyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArAAaAkebuEnSAUvrWVBGTxA7W+ZMNn5uyLlbOH7Nrs0bYOv6AOxQPqAo2UB0Z7vqlugjn+BUl0drDcWejBfDiPEC6jQA==" }, "delayedMessagesRead": 354560 }, "signature": null } ] } ``` Breaking this feed down a bit: the top-level data structure is defined by the [`BroadcastMessage struct`](https://github.com/OffchainLabs/nitro/blob/9b1e622102fa2bebfd7dffd327be19f8881f1467/broadcaster/broadcaster.go#L42): ```go type BroadcastMessage struct { Version int `json:"version"` // Note: the "Messages" object naming is slightly ambiguous: since there are different types of messages Messages []*BroadcastFeedMessage `json:"messages,omitempty"` ConfirmedSequenceNumberMessage *ConfirmedSequenceNumberMessage `json:"confirmedSequenceNumberMessage,omitempty"` } ``` The `messages` field is the [`BroadcastFeedMessage struct`](https://github.com/OffchainLabs/nitro/blob/9b1e622102fa2bebfd7dffd327be19f8881f1467/broadcaster/broadcaster.go#L49): ```go type BroadcastFeedMessage struct { SequenceNumber arbutil.MessageIndex `json:"sequenceNumber"` Message arbstate.MessageWithMetadata `json:"message"` Signature []byte `json:"signature"` BlockMetadata arbostypes.BlockMetadata `json:"blockMetadata"` } ``` Each `message` conforms to [`arbstate.MessageWithMetadata`](https://github.com/OffchainLabs/nitro/blob/a05f768d774f60468a58a6a94fcc1be18e4d8fae/arbstate/inbox.go#L42): ```text type MessageWithMetadata struct { Message *arbos.L1IncomingMessage `json:"message"` DelayedMessagesRead uint64 `json:"delayedMessagesRead"` } ``` Finally, we get the transaction's information in the `message` subfield as an [`L1IncomingMessage`](https://github.com/OffchainLabs/nitro/blob/9b1e622102fa2bebfd7dffd327be19f8881f1467/arbos/incomingmessage.go#L61): ```go type L1IncomingMessage struct { Header *L1IncomingMessageHeader `json:"header"` L2msg []byte `json:"l2Msg"` // Only used for `L1MessageType_BatchPostingReport` BatchGasCost *uint64 `json:"batchGasCost,omitempty" rlp:"optional"` } ``` You can use the [`ParseL2Transactions`](https://github.com/OffchainLabs/nitro/blob/9b1e622102fa2bebfd7dffd327be19f8881f1467/arbos/incomingmessage.go#L227) function to decode the message. Using the feed relay, you can also retrieve the `L2 block number` of a message: * On Arbitrum One, this can be done by adding the Arbitrum One genesis block number (22207817) to the sequence number of the feed message. * Note that in the case of Arbitrum Nova, the Nitro genesis number is `0`, so it doesn't need to be included when adding to the feed message's sequence number. > **INFO** > > Note that the `messages[0].message.message.header.blockNumber` is `L1 block number` instead of `L2 block number` --- > For a complete page index, fetch # How to run a Sequencer Coordinator Manager (SQM) The Sequencer Coordinator Manager (SQM) is a command-line tool that allows you to manage the priority list of sequencers, update their positions, add new sequencers to the list, and refresh the lists from the Redis server. The tool offers keyboard-only support. Any changes you make are stored locally until you choose to save and push them to the Redis server. * Clone and enter nitro repository: ```shell git clone --branch v3.11.3 https://github.com/OffchainLabs/nitro.git cd nitro ``` * Before starting the Sequencer Coordinator Manager, please read and follow the commands in Step 4 - Step 7 of [Build Nitro's binaries natively](/run-arbitrum-node/nitro/build-nitro-locally.md#build-nitros-binaries-natively) to install the necessary dependencies. * Here is an example of how to start the Sequencer Coordinator Manager and connect to a local Redis server: ```shell # In nitro directory make target/bin/seq-coordinator-manager ./target/bin/seq-coordinator-manager redis://127.0.0.1:6379 ``` If mouse support is enabled, you can use your mouse to explore the tool. Otherwise, use your keyboard to explore the UI. The `enter` key selects options; `c` switches focus between lists. When you bring up any form, you can navigate within the form's options using the `Tab` key and use the up/down arrow keys to select options from the dropdown menu. ![Sequencer coordinator manager](/img/run-node-seq-coordinator-manager.png) > **NOTE** > > One of the sequencers is marked with a `chosen` indicator. This visual cue helps you identify the current chosen sequencer. You can click/enter on any sequencer element in the priority list to bring up a form for updating its position. Within this form, you have the options to make changes via a dropdown menu and then either click `Update` to see the change locally, `Cancel` to cancel the operation, or `Remove` to remove the sequencer from the priority list. When a sequencer is removed, it is automatically added to the `--Not in priority list but online--` list if it is online. Please remember that all changes made using this method are local and need to be saved to the Redis server by pressing `s` from the keyboard shortcuts to make them permanent. ![Change priority](/img/run-node-change-priority.png) After selecting a sequencer from the non-priority list, you are given the option to add the selected sequencer to the priority list at any position of your choice. You can specify the position via a dropdown menu that lists all possible positions. Clicking `Update` will then display the updated priority list with the newly added sequencer in the chosen position. ![Add to priority](/img/run-node-add-to-priority.png) You can also add a new sequencer to the priority list by pressing `a` from the keyboard shortcuts. This action will bring up a form to enter the sequencer details. After adding the sequencer URL, you can click `Add` to see the changes or `Cancel` to abort the operation. There is a flag in the sequencer's config, `--node.seq-coordinator.my-url`, which needs to be set to your sequencer's endpoint URL. Ensure that the URL of your sequencer added to the sequencer coordinator manager matches this flag. ![Add new Sequencer](/img/run-node-add-new-seq.png) To exit the tool, press `q` from the keyboard shortcuts. --- > For a complete page index, fetch # How to run a normal sequencer node for an Arbitrum chain > **CAUTION** > > The following instructions are meant for Arbitrum chains only. This article only applies to test environments. If you need support spinning up a production Arbitrum chain, we recommend contacting a [provider](/launch-arbitrum-chain/integrations/infrastructure-providers.md#rollup-as-a-service-raas-providers). > > We also provide a [guide for running a high-availability sequencer node for an Arbitrum chain](/launch-arbitrum-chain/run-a-node/high-availability-sequencer.md). This how-to provides step-by-step instructions for running a sequencer node on your local machine. For background on how sequencers fit into Arbitrum's data flow (batch posting, the sequencer feed, and how full nodes consume sequencer output), see [Data availability](/run-arbitrum-node/data-availability.md). For how the sequencer role relates to the other Nitro node roles and the flags that define them, see [How to assign roles to a Nitro node](/run-arbitrum-node/assign-node-roles.md). ## Minimum hardware configuration The following are the minimum hardware configurations required to set up a Nitro full node (not archival): | Resource | Minimum requirements | Recommended | | ------------ | ---------------------------------------------------------------------------------- | -------------------------------------------------------------- | | RAM (DDR5) | 64 GB | 128 GB or more | | CPU | 8 core 3rd generation CPUs (for AWS, a `i4i.2xlarge` instance) | 16 core CPU or higher and more recent/newer generation of CPUs | | Storage type | NVMe SSD drives with locally attached drives strongly recommended | Same | | Storage size | Depends on the chain and its traffic over time, but ideally several terabytes (TB) | Same, but higher if possible | Please note that: * These minimum requirements for RAM and CPU are recommended for nodes that process a small amount of RPC requests. For nodes that require processing multiple simultaneous requests, both RAM and the number of CPU cores will need to scale with the amount of traffic served. * Single core performance is important. If the node is falling behind and a single core is 100% busy, it is recommended to update to a faster processor * The minimum storage requirements will change over time as the chain grows. Using more than the minimum requirements to run a robust full node is recommended. ## Recommended Nitro version > **CAUTION** > > Even though there are alpha and beta versions of the Arbitrum Nitro software, only use release versions when running your node. Running alpha or beta versions is unsupported and might lead to unexpected behaviors. Latest Docker image: `offchainlabs/nitro-node:v3.11.3-beb2108` ## Required parameters ### 1. Sequencer node parameters The following parameters are required to run a sequencer node: #### 1. Enable sequencer Enable the sequencer mode: ```shell --node.sequencer=true ``` #### 2. Make the node act as a sequencer and post to L1 Enable the sequencer execution: ```shell --execution.sequencer.enable=true --execution.sequencer.max-tx-data-size=85000 ``` #### 3. Enable delayed sequencer Enable your node to read and include transactions from the parent chain delayed inbox. ```shell --node.delayed-sequencer.enable=true --node.delayed-sequencer.use-merge-finality=false --node.delayed-sequencer.finalize-distance=1 ``` #### 4. Enable batch poster Enable your node to send batches to the parent chain: ```shell --node.batch-poster.enable=true --node.batch-poster.max-calldata-batch-size=90000 --node.batch-poster.parent-chain-wallet.private-key= ``` `--node.batch-poster.max-calldata-batch-size` replaces the deprecated `--node.batch-poster.max-size`, which previously also capped AnyTrust batches. On AnyTrust chains, set the AnyTrust batch limit separately with `--node.da.anytrust.max-batch-size` (see the next section). #### 4. Disable transaction forwarding Disable your sequencer's forwarding transactions, as the node will queue the transaction directly: ```shell --execution.forwarding-target="" ``` #### 5. Enable feed-out queued transactions Enable your node to feed out transactions so full node can receive queued transactions. For the wire format consumers will see on this port, see [How to read the sequencer feed](/run-arbitrum-node/sequencer/read-sequencer-feed.md). ```shell --node.feed.output.enable=true --node.feed.output.addr=0.0.0.0 --node.feed.output.port= ``` #### 5. Connect the node to data availability servers > **NOTE** > > This step is only required in Anytrust mode. For an explanation of AnyTrust mode and the role of the Data Availability Committee (DAC), see [AnyTrust Mode](/run-arbitrum-node/data-availability.md#anytrust-mode). > > As of Nitro v3.10.0, these settings live under `--node.da.anytrust.*` (the previous `--node.data-availability.*` namespace is deprecated), and the node no longer takes `sequencer-inbox-address` or `parent-chain-node-url` in this section — it uses the chain info and the `--parent-chain.connection.url` configuration instead. Enable your node to send batches to DAS and get DACerts from them. ```shell --node.da.anytrust.enable=true --node.da.anytrust.max-batch-size=90000 --node.da.anytrust.rest-aggregator.enable=true --node.da.anytrust.rest-aggregator.urls= --node.da.anytrust.rpc-aggregator.enable=true --node.da.anytrust.rpc-aggregator.assumed-honest=1 --node.da.anytrust.rpc-aggregator.backends= ``` ### 2. Putting it all together * When running a Docker image, an external volume should be mounted to persist the database across restarts. The mount point inside the Docker image should be `/home/user/.arbitrum` * Example: ```shell docker run --rm -it -v /some/local/dir/arbitrum:/home/user/.arbitrum -p 0.0.0.0:8547:8547 -p 0.0.0.0:8548:8548 offchainlabs/nitro-node:v3.11.3-beb2108 --node.sequencer=true --node.delayed-sequencer.enable=true --node.delayed-sequencer.use-merge-finality=false --node.delayed-sequencer.finalize-distance=1 --node.batch-poster.enable=true --node.batch-poster.max-calldata-batch-size=90000 --node.batch-poster.parent-chain-wallet.private-key= --node.staker.enable=true --node.staker.strategy=MakeNodes --node.staker.parent-chain-wallet.private-key= --node.da.anytrust.enable=true --node.da.anytrust.max-batch-size=90000 --node.da.anytrust.rest-aggregator.enable=true --node.da.anytrust.rest-aggregator.urls= --node.da.anytrust.rpc-aggregator.enable=true --node.da.anytrust.rpc-aggregator.assumed-honest=1 --node.da.anytrust.rpc-aggregator.backends= --execution.sequencer.enable=true --execution.sequencer.max-tx-data-size=85000 ``` * Ensure that `/some/local/dir/arbitrum` already exists; otherwise, the directory might be created with `root` as owner, and the Docker container won't be able to write to it. * Json Example: ```json { "node": { "sequencer": true, "delayed-sequencer": { "enable": true, "use-merge-finality": false, "finalize-distance": 1 }, "batch-poster": { "max-calldata-batch-size": 90000, "enable": true, "parent-chain-wallet": { "private-key": "" } }, "feed": { "output": { "enable": true, "addr": "0.0.0.0", "port": "" } }, "da": { "anytrust": { "enable": true, "max-batch-size": 90000, "rest-aggregator": { "enable": true, "urls": ["http://das-server:9877"] }, "rpc-aggregator": { "enable": true, "assumed-honest": 1, "backends": "[{\"url\":\"http://das-server:9876\",\"pubkey\":\"YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\"}]" } } } }, "execution": { "forwarding-target": "", "sequencer": { "enable": true, "max-tx-data-size": 85000 } } } ``` ### Note on permissions * The Docker image is configured to run as non-root `UID 1000`. If you are running Linux or macOS and you are getting permission errors when trying to run the Docker image, run this command to allow all users to update the persistent folders: ```shell mkdir /data/arbitrum chmod -fR 777 /data/arbitrum ``` ## Optional parameters Here's a list of the parameters that are most commonly used when running your Arbitrum chain sequencer node. You can also use the flag `--help` for a comprehensive list of available parameters. | Flag | Description | | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--execution.rpc.classic-redirect=` | Redirects archive requests for pre-nitro blocks to this RPC of an Arbitrum Classic node with an archive database, **only for Arbitrum One**. | | `--execution.rpc.classic-redirect=` | Redirects archive requests for pre-nitro blocks to this RPC from an Arbitrum Classic node with an archive database, **only for Arbitrum One**. | | `--http.api` | Which APIs need to be opened over the HTTP-RPC interface. Default: `net,web3,eth,arb`. Add `debug` for tracing. | | `--http.corsdomain` | Accepts cross origin requests from these comma-separated domains (browser enforced). | | `--http.vhosts` | Accepts requests from these comma-separated virtual hostnames (server enforced). Default: `localhost`. Accepts `*`. | | `--http.addr` | Address to bind RPC to. May require `0.0.0.0` for Docker networking. | | `--execution.caching.archive` | Will retain past block state. **For archive nodes**. | | `--node.feed.input.url=` | Default: `wss://.arbitrum.io/feed`. ⚠️ One feed relay per datacenter is advised. See [feed relay guide](/run-arbitrum-node/run-feed-relay.md). | | `--execution.rpc.evm-timeout` | Default: `5s`. Timeout for `eth_call`. (`0` == no timeout). | | `--execution.rpc.gas-cap` | Default: `50000000`. Gas cap for `eth_call`/`estimateGas`. (`0` = no cap). | | `--execution.rpc.tx-fee-cap` | Default: `1`. Transaction fee cap (in **ETH**) for RPC APIs. (`0` = no cap). | | `--ipc.path` | Filename for IPC socket/pipe within `datadir`. Not supported on macOS. **Note**: The path is within the Docker container. | | `--init.prune` | Prunes database before starting the node. It can be used for **full** or **validator** nodes. | | `--init.url=""` | **Non-Arbitrum chain Nitro nodes only**: URL from which to download the genesis database. Required only for the first startup of an Arbitrum One node. Reference to [snapshots](https://snapshot.arbitrum.foundation/index.html) and [archive node guide](/run-arbitrum-node/more-types/run-archive-node.md). | | `--init.download-path="/path/to/dir"` | **Non-Arbitrum chain Nitro nodes only**: Temporarily saves the downloaded database snapshot. Defaults to `/tmp/`. Used with `--init.url`. | | `--node.batch-poster.post-4844-blobs` | Boolean. Default: `false`. Used to enable or disable the posting of transaction data using Blobs to Ethereum mainnet. If using calldata is more expensive and the parent chain supports `EIP4844` blobs, the batch poster will use blobs when this flag is set to `true`. It can be set to `true` or `false`. | | `--node.batch-poster.ignore-blob-price` | Boolean. Default: `false`. If the parent chain supports `EIP4844` blobs and `ignore-blob-price` is set to `true`, the batch poster will use `EIP4844` blobs even if using calldata is cheaper. It can be set to `true` or `false`. | | `--execution.sequencer.enable` | Act as sequencer and post to L1. | | `--execution.sequencer.enable-profiling` | Enable CPU profiling and tracing. | | `--execution.sequencer.expected-surplus-hard-threshold` | If the expected surplus is lower than this value, new incoming transactions will be denied (default "default"). | | `--execution.sequencer.expected-surplus-soft-threshold` | Warnings are posted if the expected surplus is lower than this value (default "default"). | | `--execution.sequencer.forwarder.connection-timeout` | Total time to wait before canceling connection (default 30s). | | `--execution.sequencer.forwarder.idle-connection-timeout` | Time until idle connections are closed (default 1m0s). | | `--execution.sequencer.forwarder.max-idle-connections` | Maximum number of idle connections to keep open (default `100`). | | `--execution.sequencer.forwarder.redis-url` | The recommended Redis URL to use as target. | | `--execution.sequencer.forwarder.retry-interval` | Minimal time between update retries (default 100ms). | | `--execution.sequencer.forwarder.update-interval` | Forwarding target update interval (default 1s). | | `--execution.sequencer.max-acceptable-timestamp-delta` | Maximum acceptable time difference between the local time and the latest L1 block's timestamp (default 1h0m0s). | | `--execution.sequencer.max-block-speed` | Minimum delay between blocks (sets a maximum speed of block production) (default 250ms). | | `--execution.sequencer.max-revert-gas-reject` | Maximum gas executed in a revert for the sequencer to reject the transaction instead of posting it (anti-DOS). | | `--execution.sequencer.max-tx-data-size` | Maximum transaction size the sequencer will accept (default `95000`). | | `--execution.sequencer.nonce-cache-size` | Size of the transaction sender nonce cache (default `1024`). | | `--execution.sequencer.nonce-failure-cache-expiry` | Maximum time to wait for a predecessor before rejecting a transaction whose nonce is too high (default 1s). | | `--execution.sequencer.nonce-failure-cache-size` | Number of transactions whose nonce is too high to keep in memory while waiting for their predecessor (default `1024`). | | `--execution.sequencer.queue-size` | Size of the pending transaction queue (default `1024`). | | `--execution.sequencer.queue-timeout` | Maximum time a transaction can wait in a queue (default 12s). | | `--execution.sequencer.sender-whitelist` | Comma-separated allowlist of authorized senders (if empty, every sender is allowed). | | `--node.delayed-sequencer.finalize-distance` | Number of blocks in the past L1 block for the transaction to be considered final. This value is ignored when using merge finality. Default: `20`. | | `--node.delayed-sequencer.require-full-finality` | Whether to wait for full finality before sequencing delayed messages. | | `--node.delayed-sequencer.use-merge-finality` | Whether to use The Merge's notion of finality before sequencing delayed messages (default to `true`). | --- > For a complete page index, fetch # Prepare to run a node > **INFO** > > If you’re interested in accessing an Arbitrum chain but don’t want to set up your own node, see our [Node Providers](/arbitrum-essentials/reference/node-providers.md) to get RPC access to fully managed nodes hosted by a third-party provider. This how-to provides step-by-step instructions for preparing the information you will need to run a full node for Arbitrum on your local machine. ## Prerequisites In addition to the hardware requirements, the following prerequisites will be necessary when initially setting up your node. **It is essential not to skip over these items**. You would benefit by copying and pasting them into a notepad or text editor, as you will need to combine them with other commands and configuration/parameter options when you initially run your Arbitrum node. ### Minimum hardware configuration The following are the minimum hardware requirements to set up a Nitro full node (not archival): | Resource | Minimum requirements | Recommended | | ------------ | ---------------------------------------------------------------------------------- | -------------------------------------------------------------- | | RAM (DDR5) | 64 GB | 128 GB or more | | CPU | 8 core 3rd generation CPUs (for AWS, a `i4i.2xlarge` instance) | 16 core CPU or higher and more recent/newer generation of CPUs | | Storage type | NVMe SSD drives with locally attached drives strongly recommended | Same | | Storage size | Depends on the chain and its traffic over time, but ideally several terabytes (TB) | Same, but higher if possible | Please note that: * The minimum requirements for RAM and CPU listed here are recommended for nodes that handle a limited number of RPC requests. For nodes that need to process multiple simultaneous requests, both the RAM size and the number of CPU cores should be increased to accommodate higher levels of traffic. * Single core performance is important. If the node is falling behind and a single core is 100% busy, the recommendation is to upgrade to a faster processor. * The minimum storage requirements will change over time as the chain grows. Using more than the minimum requirements to run a robust full node is recommended. Note that snapshot extraction requires approximately 2x the snapshot size in temporary disk space, so plan accordingly during initial setup. * Nitro stores its state trie using either HashDB (the default) or PathDB. The choice affects which snapshot you can use, so make it before you initialize the database. Both schemes have published full node snapshots; PathDB prunes automatically but cannot validate blocks. See [Choose a state scheme](/run-arbitrum-node/run-full-node.md#choose-a-state-scheme). ### Parent chain (L1) client Your Arbitrum node requires a connection to a parent chain RPC endpoint (e.g., an Ethereum execution client for Arbitrum One or Nova). Keep your parent chain client up to date—incompatible L1 client versions can cause your Arbitrum node to crash or fail to sync. When upgrading your L1 client, check the [Nitro release notes](https://github.com/OffchainLabs/nitro/releases) for any noted compatibility requirements. ### Recommended Nitro version > **CAUTION** > > Although there are beta and release candidate versions of the Arbitrum Nitro software, use only the release version when running your node. Running beta or RC versions is not supported and might lead to unexpected behaviors and/or database corruption. Latest [Docker image](https://hub.docker.com/r/offchainlabs/nitro-node/tags): `offchainlabs/nitro-node:v3.11.3-beb2108` ### Database snapshots > **INFO** — Snapshots availability > > Database snapshots for Arbitrum One, Arbitrum Nova, and Arbitrum Sepolia are available in the [snapshot explorer](https://snapshot-explorer.arbitrum.io/). > > Snapshots are published per state scheme, and a snapshot only works with a node using the same scheme: > > | Snapshot kind | Node type | State scheme | Available for | > | -------------- | --------- | ------------ | --------------------------- | > | `pruned` | Full node | HashDB | Arbitrum One, Nova, Sepolia | > | `full-path` | Full node | PathDB | Arbitrum One, Nova, Sepolia | > | `archive` | Archive | HashDB | Arbitrum One, Nova, Sepolia | > | `archive-path` | Archive | PathDB | Arbitrum One, Sepolia | > > Only the HashDB kinds distinguish pruned from unpruned. PathDB prunes as it runs, so every PathDB snapshot is already pruned to its state-history window — there is no separate unpruned variant to publish. The `pruned` kind exists because a HashDB database is pruned manually, and the published HashDB full node snapshots happen to be pruned ones. > > `--init.latest` cannot download the PathDB kinds. See [PathDB snapshots](/run-arbitrum-node/nitro/nitro-database-snapshots.md#pathdb-snapshots). > > Database snapshots for other Arbitrum chains may be available at the discretion of the chain's team. Get in touch with them if you're interested in using a database snapshot for their chains. Supplying a database snapshot when starting your node for the first time is required for Arbitrum One (to provide information from the Classic era) but is optional for other chains. Supplying a database snapshot on the first run will provide the state and data for that chain up to a specific block, allowing the node to sync faster to the head of the chain. We provide a summary of the available parameters here, but we recommend reading the [complete guide](/run-arbitrum-node/nitro/nitro-database-snapshots.md) if you plan to use snapshots. * Use the parameter `--init.latest ` (accepted values: `archive`, `pruned`, `genesis`) to instruct your node to download the corresponding snapshot from the configured URL * Optionally, use the parameter `--init.latest-base` to set the base URL when searching for the latest snapshot * Note that these parameters get ignored if a database already exists * When running more than one node, it's easier to manually download the different parts of the snapshot, join them into a single archive, and host it locally for your nodes. Please see [Downloading the snapshot manually](/run-arbitrum-node/nitro/nitro-database-snapshots.md#downloading-the-snapshot-manually) for instructions on how to do that. * Only snapshots formatted with your node's specified state scheme are compatible. In other words, a PathDB snapshot won't work for a node using HashDB, and vice versa. Each snapshot directory publishes a `metadata.json` file naming its `state_scheme`; check it before downloading. > **TIP** — Which initialization mode should I use? > > | Scenario | Recommended Parameter | > | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | > | Setting up a new **full node** on **HashDB** | `--init.latest pruned` | > | Setting up a new **full node** on **PathDB** | The `full-path` snapshot with `--init.url`. `--init.latest` does not accept this kind. | > | Setting up a new **archive** node on **HashDB** | `--init.latest archive` (note: the publicly hosted `archive` snapshot is outdated on Arbitrum One and Sepolia) | > | Setting up a new **archive** node on **PathDB** | The `archive-path` snapshot with `--init.url`, on Arbitrum One and Sepolia. It is smaller and more current than the HashDB `archive` snapshot. Nova has no `archive-path` snapshot. | > | Reducing disk usage on an **existing** node | `--init.prune full` (or `minimal`) — only applicable to HashDB; PathDB handles state trie pruning automatically | > | Hosting one snapshot for **multiple nodes** | Download manually, then use `--init.url file:///path/to/archive.tar` on each node | > | Using a **custom snapshot URL** | `--init.url https://your-snapshot-url/archive.tar` | > > For more details on snapshot downloading and initialization, see the [complete snapshot guide](/run-arbitrum-node/nitro/nitro-database-snapshots.md). > **WARNING** — Fusaka upgrade: historical blobs > > If you're running a beacon node, historical data will now be in blobs. To make this transition to using historical blobs, refer to the [Historical Blobs for Beacon Nodes](/run-arbitrum-node/beacon-nodes-historical-blobs.md) guide. ### Required parameters The following list contains all the parameters needed to configure your node. Select the appropriate option depending on the chain you want to run your node for.
Arbitrum One, Nova, Sepolia #### 1. Parent chain (Ethereum) parameters The `--parent-chain.connection.url` parameter needs to provide a standard RPC endpoint for an Ethereum node, whether self-hosted or obtained from a node service provider: ```shell --parent-chain.connection.url= ``` Additionally, use the parameter `--parent-chain.blob-client.beacon-url` to provide a beacon chain RPC endpoint: ```shell --parent-chain.blob-client.beacon-url= ``` > **INFO** — Try it out > > If you choose to self-host an EVM node, the [Prysm client software](https://www.offchainlabs.com/prysm/docs) is a great choice. It's straightforward, efficient, and effective—ensuring your setup runs smoothly! You can also consult our [list of Ethereum beacon chain RPC providers](/run-arbitrum-node/l1-ethereum-beacon-chain-rpc-providers.md). Note that historical blob data is required for these chains to properly sync up if they are new or have been offline for more than 18 days. The beacon chain RPC endpoint you use may also need to provide historical blob data. Please see [Special notes on ArbOS 20: Atlas support for EIP-4844](/run-arbitrum-node/arbos-releases/arbos20.md#special-notes-on-arbos-20-atlas-support-for-eip-4844) for more details. #### 2. Arbitrum chain parameters Use the parameter `--chain.id` to specify the chain you're running this node for. See [RPC endpoints and providers](/arbitrum-essentials/reference/node-providers.md) to find the IDs of these chains. ```shell --chain.id= ``` Alternatively, you can use the parameter `--chain.name` to specify the chain you're running this node for. Use `arb1` for Arbitrum One, `nova` for Arbitrum Nova, or `sepolia-rollup` for Arbitrum Sepolia. ```shell --chain.name= ```
Arbitrum chains #### 1. Parent chain parameters The `--parent-chain.connection.url` parameter needs to provide a standard RPC endpoint for an EVM node, whether self-hosted or obtained from a node service provider: ```shell --parent-chain.connection.url= ``` > **INFO** — Try it out > > If you choose to self-host an EVM node, the [Prysm client software](https://www.offchainlabs.com/prysm/docs) is a great choice. It's straightforward, efficient, and effective—ensuring your setup runs smoothly! Additionally, if the chain is a Layer-2 (L2) chain on top of Ethereum and uses blobs to post calldata, use the parameter `--parent-chain.blob-client.beacon-url` to provide a beacon chain RPC endpoint: ```shell --parent-chain.blob-client.beacon-url= ``` > **INFO** — Public Arbitrum RPC endpoints > > [Public Arbitrum RPC endpoints](/arbitrum-essentials/reference/node-providers.md#arbitrum-public-rpc-endpoints) rate-limit connections. To avoid hitting a bottleneck, you can run a local node for the parent chain or rely on third-party RPC providers. You can find beacon providers in our [list of Ethereum beacon chain RPC providers](/run-arbitrum-node/l1-ethereum-beacon-chain-rpc-providers.md). Note that historical blob data is required for these chains to properly sync up if they are new or have been offline for more than 18 days. This means that the beacon chain RPC endpoint you use may also need to provide historical blob data. Please see [Special notes on ArbOS 20: Atlas support for EIP-4844](/run-arbitrum-node/arbos-releases/arbos20.md#special-notes-on-arbos-20-atlas-support-for-eip-4844) for more details. #### 2. Child chain parameters The parameter `--chain.info-json` specifies a JSON string that contains the information about the Arbitrum chain required by the node. ```shell --chain.info-json= ``` This information should be provided by the chain owner and will look something like the following: ```shell --chain.info-json="[{\"chain-id\":94692861356,\"parent-chain-id\":421614,\"chain-name\":\"My Arbitrum L3 Chain\",\"chain-config\":{\"chainId\":94692861356,\"homesteadBlock\":0,\"daoForkBlock\":null,\"daoForkSupport\":true,\"eip150Block\":0,\"eip150Hash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"eip155Block\":0,\"eip158Block\":0,\"byzantiumBlock\":0,\"constantinopleBlock\":0,\"petersburgBlock\":0,\"istanbulBlock\":0,\"muirGlacierBlock\":0,\"berlinBlock\":0,\"londonBlock\":0,\"clique\":{\"period\":0,\"epoch\":0},\"arbitrum\":{\"EnableArbOS\":true,\"AllowDebugPrecompiles\":false,\"DataAvailabilityCommittee\":false,\"InitialArbOSVersion\":10,\"InitialChainOwner\":\"0xAde4000C87923244f0e95b41f0e45aa3C02f1Bb2\",\"GenesisBlockNum\":0}},\"rollup\":{\"bridge\":\"0xde835286442c6446E36992c036EFe261AcD87F6d\",\"inbox\":\"0x0592d3861Ea929B5d108d915c36f64EE69418049\",\"sequencer-inbox\":\"0xf9d77199288f00440Ed0f494Adc0005f362c17b1\",\"rollup\":\"0xF5A42aDA664E7c2dFE9DDa4459B927261BF90E09\",\"validator-utils\":\"0xB11EB62DD2B352886A4530A9106fE427844D515f\",\"validator-wallet-creator\":\"0xEb9885B6c0e117D339F47585cC06a2765AaE2E0b\",\"deployed-at\":1764099}}]" ``` Use the parameter `--chain.name` to specify the chain you're running this node for. The name of the chain should match the name used in the JSON string used in `--chain.info-json`: ```shell --chain.name= ``` #### 3. Parameters to connect to the sequencer Use the parameter `--node.feed.input.url` to point at the sequencer feed endpoint, which should be provided by the chain owner. ```shell --node.feed.input.url= ``` Use the parameter `--execution.forwarding-target` to point at the sequencer node of the Arbitrum chain, which should also be provided by the chain owner. ```shell --execution.forwarding-target= ``` #### 3. Additional parameters for AnyTrust chains If you're running a node for an AnyTrust chain, you need to specify information about the Data Availability Committee (DAC) in the configuration of your node. First, enable AnyTrust data availability using the following parameters (prior to Nitro v3.10.0, these lived under the now-deprecated `--node.data-availability.*` namespace): ```shell --node.da.anytrust.enable --node.da.anytrust.rest-aggregator.enable ``` Then, choose one of these methods to specify the DAS REST endpoints that your node will read the information from. These endpoints should also be provided by the chain owner. 1. Set the DAS REST endpoints directly: ```shell --node.da.anytrust.rest-aggregator.urls= ``` 2. Set a URL that returns a list of the DAS REST endpoints: ```shell --node.da.anytrust.rest-aggregator.online-url-list= ``` > **TIP** — Setting a DAS (for chain owners) > > If you are a chain owner, please refer to the [DAC setup guide](/launch-arbitrum-chain/chain-config/data-availability/dac-get-started.md#if-you-are-a-chain-owner) to set it up. > > Additionally, for your batch poster to post data to the DAS, follow [Step 3 of How to configure a DAC](/launch-arbitrum-chain/chain-config/data-availability/configure-dac.md#step-3-craft-the-new-configuration-for-the-batch-poster) to configure your batch poster node.
## Run a full node Now that you have all the prerequisite information prepared—it's time to run your node. Follow the instructions on the [Run a full node](/run-arbitrum-node/run-full-node.md) page. --- > For a complete page index, fetch # Troubleshooting: Run a node The guidance displayed on this page will change based on your selected configuration:
Operating system:
Linux, MacOS, Arm64
Windows
Network:
Arbitrum One (Nitro)
Arbitrum One (Classic)
Arbitrum Nova
Arbitrum Sepolia
Localhost
Node type:
Full node
Archive node
Validator node
> **TIP** — Thank you! > > At the end of this troubleshooting guide, you'll find a **Generate troubleshooting report** button. Clicking this button will generate a report that includes your selected configuration. You can include this report when asking for help. > > **Using this page to generate a troubleshooting report is helpful** because it gathers the information that we need in order to resolve your issue. ## Step 1: Try the troubleshooting checklist If you're running into unexpected outputs or errors, the following checklist may help you independently resolve your issue. \[ ] 1\. Select an Operating system, Network, and Node type above The guidance displayed on this page will change based on your selected configuration. \[ ] 2\. Review the docs
Node type:
Full node
Network:
Arbitrum One (Nitro) The [How to run a full node (Nitro)](/run-arbitrum-node/run-full-node.md) may address your issue.
Arbitrum One (Classic) [How to run a full node (Classic, pre-Nitro)](/run-arbitrum-node/more-types/run-classic-node.md) may address your issue.
Arbitrum Nova The [How to run a full node (Nitro)](/run-arbitrum-node/run-full-node.md) may address your issue.
Arbitrum Sepolia The [How to run a full node (Nitro)](/run-arbitrum-node/run-full-node.md) may address your issue.
Localhost The [How to run a local dev node](/run-arbitrum-node/run-local-dev-node) may address your issue.
Archive node [How to run an archive node](/run-arbitrum-node/more-types/run-archive-node.md) may address your issue.
Validator node [How to run a validator](/run-arbitrum-node/more-types/run-validator-node.md) may address your issue.
\[ ] 3\. Review the FAQ Answers to frequently asked questions can be found in [Frequently asked questions: Run a node](/node-running/faq.md). ## Step 2: Look for your scenario Common troubleshooting scenarios and solutions are detailed below. You can check logs by different log types: info, warn, and error.
Logs type:
Info | Scenario | Solution | | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | You see `Unindex transactions`. | This is expected behavior. You'll see this when your node removes old `txlookup` indices. This is emitted from the base Geth node, so you'd see the same output from a mainnet Geth node. | | You see `Head state missing, repairing`. | This is usually because your node shuts down ungracefully. In most cases, it will recover in a few minutes, but if it not, you may have to re-sync your node. Remember to shut down your node gracefully with the following command: `docker stop —time=1800 $(docker ps -aq)`. | | Your local machine is running out of memory | Nitro (and Geth) can consume a lot of memory depending on the request load. It's possible that your machine may run out of memory when receiving tons of requests. | | Your Arbitrum node can’t connect to your parent chain node on `localhost:8545` | This is often because of a Docker port configuration issue. See . | | You specified your snapshot file path via the `--init.url` parameter, but the snapshot file isn't found. | This is usually because the snapshot file isn't mounted to your Docker container. Mount it and change the file path to your Docker container’s mount point. | | You get `403` errors from the feed URL. | This often happens when Cloudflare attempts to block botnets and other malicious actors but accidentally blocks node runners. | | You see `latest assertion not yet in our node` | This usually because your node hasn’t synced to the latest state, it’s a normal behavior. | | You see `"Post "xxx_url": context deadline exceeded"` | Please check your parent chain endpoint because there is something going wrong on that endpoint; you can check it. | | You see `Resuming state snapshot generation` | This is a normal behavior while the node is catching up to the tip of the chain; once a node has been fully synced, "Resuming state snapshot generation" shouldn't be logged unless it falls behind again. | | You see `track-block-metadata-from is set but blockMetadata fetcher is not enabled`. | You started the node with `--node.transaction-streamer.track-block-metadata-from` set to a non-zero block but did not enable the blockMetadata fetcher. The node will still run, but blockMetadata won't be backfilled. Either remove the flag, or enable the fetcher with `--node.block-metadata-fetcher.enable`—and when enabling the fetcher you'll typically also need to set `--node.block-metadata-fetcher.source.url` to point at a node that serves block metadata. |
Warn | Scenario | Solution | | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | You see `error reading inbox err="sequencer batches out of order; after batch A got batch B”` | This is because you get two discontinuous batches; this might be because of your parent chain endpoint issues. You can change to another endpoint and set nitro `--init.reorg-to-batch A` | | You see `error reading inbox err="failed to get previous message for pos x: leveldb: not found”` | This is because your node db crashed and lost some messages. You can try to set `--init.reorg-to-message-batch x-1` | | You see `Failed to load snapshot err="head doesn't match snapshot: have a, want b”` | This is usually because an ungraceful shutdown caused a corrupted database; try restarting the node without a prune flag, and after your node goes back to normal, then graceful shut it down and restart to prune it. | | You see `failed to get blobs: expected at least six blobs for slot [slot_number] but only got 0` | This often happens when you connect to a beacon chain endpoint while the blob you are querying is expired. To resolve this error, connect to a beacon endpoint that supports historical blob data (see [List of Ethereum beacon chain RPC providers](/run-arbitrum-node/l1-ethereum-beacon-chain-rpc-providers.md#list-of-ethereum-beacon-chain-rpc-providers) ). | | You see `P2P server will be useless, neither dialing nor listening` | Arbitrum Nitro doesn’t need P2P mode, so you can ignore this log. | | You see `Getting file info dir=/machines error="stat /machines: no such file or directory"`. | Nitro is checking one of the expected locations for WASM machine artifacts, but that particular `machines/` directory does not exist. This warning can be harmless if Nitro finds the required machine artifacts in another location. If it is followed by validation or WASM module-root errors, make sure you're running a supported/current Nitro image for your chain, and that the image or mounted `machines/` directory contains artifacts matching the chain's onchain WASM module root. If you're using an older Docker image, upgrade to a newer Nitro release; if you're building or running from source, mount or download the matching machine artifacts for that Nitro version/module root. | | You see `broadcaster queue jumped positions queuedMessages=N expectedNextIdx=M`. | The feed delivered messages out of the expected order; the node skipped ahead in its broadcast queue. This typically self-heals as later messages arrive. If it persists, check your feed URL connectivity or try a fallback feed via `--node.feed.input.url`. | | You see `Compression was not negotiated when connecting to feed server, non-critical: node will continue without compression`. | The feed server didn't agree to compression during the WebSocket handshake. This is informational—the node continues without compression and your sync is unaffected. You can ignore this warning. | | You see `readData returned EOF url=wss://...feed... opcode=0`. | The feed server closed the WebSocket connection. The broadcast client will automatically reconnect; no operator action is needed unless EOF repeats continuously. Persistent EOFs suggest a network/firewall issue or that all configured feeds are unreachable. | | You see `error reading inbox err="previous delayed accumulator mismatch for message N"`. | The delayed-inbox accumulator your node computed doesn't match the onchain one at message N—typically caused by a parent-chain endpoint serving inconsistent data or by a reorg. Switch to a different parent-chain RPC and restart; if it persists, re-sync from a recent snapshot. | | You see `error reading inbox err="unexpected delayed sequence number N, expected M"`. | The delayed inbox produced a sequence number that is not the next expected one—usually a parent-chain RPC inconsistency or a reorg on the parent chain. Switch to a different parent-chain RPC; if the gap persists, re-sync the node. | | You see `Trie prefetcher failed opening storage trie root=... err="missing trie node ..."`. | Your local state database is inconsistent—typically caused by an ungraceful shutdown or by enabling `--init.recreate-missing-state-from` on a non-archive node. Restart without pruning; if the node still can't recover, re-sync from a recent [official snapshot](https://snapshot-explorer.arbitrum.io/). | | You see `error reading inbox err="...pebble: not found"` (same root cause as the `leveldb: not found` case above; different database backend). | Same root cause as the `leveldb: not found` case above: your local DB is missing a message it expected to have. Use `--init.reorg-to-message-batch x-1` to roll back to before the missing index, or re-sync from a snapshot. |
Error | Scenario | Solution | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | You see `no contract code at given address` | Your parent chain node might not sync to the latest state, please wait after it finishes syncing. | | You see `staker: error checking latest staked err="latest assertion of x: globalstate not in chain: count a hash b expected c, sendroot d expected f"` | Once it catches up, the node will check the state against the latest confirmed assertion bonded onchain; if it doesn’t match, it will log this error. Usually, this is because of db corruption, so you might need to re-sync the blockchain; using a snapshot might help: | | You see `disabling L1 bound as batch posting message is close to the maximum delay blockNumber` or `batch is within reorg resistance margin from layer 1 minimum block or timestamp bounds` | It indicates that there has been an issue with batch posting on the network. This could occur if your batch poster didn't post a batch for an extended period. Common reasons include the node being shut down inadvertently or the batch poster running out of funds, leading to no new blocks being produced or posted by the batch poster.
To resolve this, If re-org doesn’t matter, you can just start the batch poster with `--node.batch-poster.reorg-resistance-margin=0` and `node.batch-poster.l1-block-bound` to ignore, If it does, you'd want to modify the time bounds on the sequencer inbox to allow the sequencer to post a batch containing the transactions with the old timestamp | | You see `on-chain WASM module root did not match with any of the allowed WASM module roots` | Usually, because you are running on an old node version, try to upgrade your node. Also, you modify your node’s code; please refer to the continue set. | | You see `error acting as staker` | In most cases, this error is caused by your parent chain endpoint's rate limit or other issues, you can check your parent chain endpoint. If the error still persists, please ask in our discord node-running [channel](https://discord.gg/arbitrum). | | You see `wrong msgIdx got X expected Y`. | The transaction streamer received a message whose index doesn't match the next expected one—your node's local DB is behind or ahead of the sequencer feed. Restart the node; if the gap persists, re-sync from a snapshot or use `--init.reorg-to-message-batch` to roll back to a known-good index. | | You see `error reading inbox err="...header not found"` or `header for hash not found`. | The parent-chain RPC returned a block header your node expected to exist—usually your parent-chain endpoint hasn't synced to the height nitro asked for, or the endpoint serves a different chain history. Wait for the parent-chain node to finish syncing, or switch to a fully-synced parent-chain RPC. | | You see `accumulator not found` or `delayed accumulator not found for index N`. | Nitro tried to read an inbox accumulator or sequencer-batch metadata entry that is not yet present in the local consensus database. This can happen transiently while the inbox reader or message extractor is still catching up, and batch-posting paths intentionally treat this as an ephemeral error for the first few minutes. If the error keeps repeating, make sure the node has caught up, your parent-chain RPC can serve the relevant SequencerInbox / delayed inbox logs, and the local database or snapshot is not missing historical inbox data. Try restarting with a healthy parent-chain RPC; if the same index remains missing, re-sync from a recent snapshot. | | You see `error initializing database err="found N unexpected files in database directory, including: ..."`. | The data directory you pointed nitro at already contains files that don't belong to a fresh DB. Either empty the directory before init, or point `--persistent.chain` at a fresh path and extract your snapshot there. | | You see `error validating feed signature error="signature not verified: signer ..." sequence number=N`. | A feed message was signed by an address that is not in your allowed signer list. Check that `--node.feed.input.verify.allowed-addresses` includes the expected sequencer feed signer for your chain; for Arbitrum One/Sepolia this should be the official feed signer published in the docs. | | You see `no connected feed` or `no connected feed on startup`. | The broadcast client failed to connect to any feed URL you configured. Check network/firewall rules for outbound WebSocket connections, verify the feed URLs in `--node.feed.input.url` are reachable, and confirm you can `curl` them from the node host. |
## Step 3: Generate a troubleshooting report 1. Complete the above troubleshooting checklist. 2. Fill in the below form. 3. Click **Generate troubleshooting report**. 4. Copy and paste the **generated report text** when asking for support on [Discord](https://discord.gg/ZpZuw7p) or any other support channel.
Node startup command (make sure to remove any sensitive information like, i.e., private keys) Unexpected output **Tip:** Paste the \~100 lines of output **before and including** the unexpected output you're asking about. You can use the following command to get the logs: `docker logs --tail 100 YOUR_CONTAINER_ID` [Generate troubleshooting report]() Complete the checklist above before generating... --- > For a complete page index, fetch ## [📄️A gentle introduction](/stylus/gentle-introduction.md) [An introduction to Stylus, which enables writing EVM-compatible smart contracts in programming languages that compile to WASM, such as Rust, C, and C++.](/stylus/gentle-introduction.md) --- > For a complete page index, fetch # ERC-20 Any contract that follows the [ERC-20 standard](https://eips.ethereum.org/EIPS/eip-20) is an ERC-20 token. ERC-20 tokens provide functionalities to * transfer tokens * allow others to transfer tokens on behalf of the token holder Here is the interface for ERC-20. ```solidity interface IERC20 { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); } ``` Example implementation of an ERC-20 token contract written in Rust. ### src/erc20.rs > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust //! Implementation of the ERC-20 standard //! //! The eponymous [`Erc20`] type provides all the standard methods, //! and is intended to be inherited by other contract types. //! //! You can configure the behavior of [`Erc20`] via the [`Erc20Params`] trait, //! which allows specifying the name, symbol, and decimals of the token. //! //! Note that this code is unaudited and not fit for production use. // Imported packages use alloc::string::String; use alloy_primitives::{Address, U256}; use alloy_sol_types::sol; use core::marker::PhantomData; use stylus_sdk::{ evm, msg, prelude::*, }; pub trait Erc20Params { /// Immutable token name const NAME: &'static str; /// Immutable token symbol const SYMBOL: &'static str; /// Immutable token decimals const DECIMALS: u8; } sol_storage! { /// Erc20 implements all ERC-20 methods. pub struct Erc20 { /// Maps users to balances mapping(address => uint256) balances; /// Maps users to a mapping of each spender's allowance mapping(address => mapping(address => uint256)) allowances; /// The total supply of the token uint256 total_supply; /// Used to allow [`Erc20Params`] PhantomData phantom; } } // Declare events and Solidity error types sol! { event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); error InsufficientBalance(address from, uint256 have, uint256 want); error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want); } /// Represents the ways methods may fail. #[derive(SolidityError)] pub enum Erc20Error { InsufficientBalance(InsufficientBalance), InsufficientAllowance(InsufficientAllowance), } // These methods aren't exposed to other contracts // Methods marked as "pub" here are usable outside of the erc20 module (i.e. they're callable from lib.rs) // Note: modifying storage will become much prettier soon impl Erc20 { /// Movement of funds between 2 accounts /// (invoked by the external transfer() and transfer_from() functions ) pub fn _transfer( &mut self, from: Address, to: Address, value: U256, ) -> Result<(), Erc20Error> { // Decreasing sender balance let mut sender_balance = self.balances.setter(from); let old_sender_balance = sender_balance.get(); if old_sender_balance < value { return Err(Erc20Error::InsufficientBalance(InsufficientBalance { from, have: old_sender_balance, want: value, })); } sender_balance.set(old_sender_balance - value); // Increasing receiver balance let mut to_balance = self.balances.setter(to); let new_to_balance = to_balance.get() + value; to_balance.set(new_to_balance); // Emitting the transfer event evm::log(Transfer { from, to, value }); Ok(()) } /// Mints `value` tokens to `address` pub fn mint(&mut self, address: Address, value: U256) -> Result<(), Erc20Error> { // Increasing balance let mut balance = self.balances.setter(address); let new_balance = balance.get() + value; balance.set(new_balance); // Increasing total supply self.total_supply.set(self.total_supply.get() + value); // Emitting the transfer event evm::log(Transfer { from: Address::ZERO, to: address, value, }); Ok(()) } /// Burns `value` tokens from `address` pub fn burn(&mut self, address: Address, value: U256) -> Result<(), Erc20Error> { // Decreasing balance let mut balance = self.balances.setter(address); let old_balance = balance.get(); if old_balance < value { return Err(Erc20Error::InsufficientBalance(InsufficientBalance { from: address, have: old_balance, want: value, })); } balance.set(old_balance - value); // Decreasing the total supply self.total_supply.set(self.total_supply.get() - value); // Emitting the transfer event evm::log(Transfer { from: address, to: Address::ZERO, value, }); Ok(()) } } // These methods are external to other contracts // Note: modifying storage will become much prettier soon #[public] impl Erc20 { /// Immutable token name pub fn name() -> String { T::NAME.into() } /// Immutable token symbol pub fn symbol() -> String { T::SYMBOL.into() } /// Immutable token decimals pub fn decimals() -> u8 { T::DECIMALS } /// Total supply of tokens pub fn total_supply(&self) -> U256 { self.total_supply.get() } /// Balance of `address` pub fn balance_of(&self, owner: Address) -> U256 { self.balances.get(owner) } /// Transfers `value` tokens from msg::sender() to `to` pub fn transfer(&mut self, to: Address, value: U256) -> Result { self._transfer(msg::sender(), to, value)?; Ok(true) } /// Transfers `value` tokens from `from` to `to` /// (msg::sender() must be able to spend at least `value` tokens from `from`) pub fn transfer_from( &mut self, from: Address, to: Address, value: U256, ) -> Result { // Check msg::sender() allowance let mut sender_allowances = self.allowances.setter(from); let mut allowance = sender_allowances.setter(msg::sender()); let old_allowance = allowance.get(); if old_allowance < value { return Err(Erc20Error::InsufficientAllowance(InsufficientAllowance { owner: from, spender: msg::sender(), have: old_allowance, want: value, })); } // Decreases allowance allowance.set(old_allowance - value); // Calls the internal transfer function self._transfer(from, to, value)?; Ok(true) } /// Approves the spenditure of `value` tokens of msg::sender() to `spender` pub fn approve(&mut self, spender: Address, value: U256) -> bool { self.allowances.setter(msg::sender()).insert(spender, value); evm::log(Approval { owner: msg::sender(), spender, value, }); true } /// Returns the allowance of `spender` on `owner`'s tokens pub fn allowance(&self, owner: Address, spender: Address) -> U256 { self.allowances.getter(owner).get(spender) } } ``` ### lib.rs ```rust // Only run this as a WASM if the export-abi feature is not set. #![cfg_attr(not(any(feature = "export-abi", test)), no_main)] extern crate alloc; // Modules and imports mod erc20; use alloy_primitives::{Address, U256}; use stylus_sdk::{ msg, prelude::* }; use crate::erc20::{Erc20, Erc20Params, Erc20Error}; /// Immutable definitions struct StylusTokenParams; impl Erc20Params for StylusTokenParams { const NAME: &'static str = "StylusToken"; const SYMBOL: &'static str = "STK"; const DECIMALS: u8 = 18; } // Define the entrypoint as a Solidity storage object. The sol_storage! macro // will generate Rust-equivalent structs with all fields mapped to Solidity-equivalent // storage slots and types. sol_storage! { #[entrypoint] struct StylusToken { // Allows erc20 to access StylusToken's storage and make calls #[borrow] Erc20 erc20; } } #[public] #[inherit(Erc20)] impl StylusToken { /// Mints tokens pub fn mint(&mut self, value: U256) -> Result<(), Erc20Error> { self.erc20.mint(msg::sender(), value)?; Ok(()) } /// Mints tokens to another address pub fn mint_to(&mut self, to: Address, value: U256) -> Result<(), Erc20Error> { self.erc20.mint(to, value)?; Ok(()) } /// Burns tokens pub fn burn(&mut self, value: U256) -> Result<(), Erc20Error> { self.erc20.burn(msg::sender(), value)?; Ok(()) } } ``` ### Cargo.toml ```toml [package] name = "stylus_erc20_example" version = "0.1.7" edition = "2021" license = "MIT OR Apache-2.0" keywords = ["arbitrum", "ethereum", "stylus", "alloy"] [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" mini-alloc = "0.4.2" stylus-sdk = "0.6.0" hex = "0.4.3" [dev-dependencies] tokio = { version = "1.12.0", features = ["full"] } ethers = "2.0" eyre = "0.6.8" [features] export-abi = ["stylus-sdk/export-abi"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # ERC-721 Any contract that follows the [ERC-721 standard](https://eips.ethereum.org/EIPS/eip-721) is an ERC-721 token. Here is the interface for ERC-721. ```solidity interface ERC721 { event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); function balanceOf(address _owner) external view returns (uint256); function ownerOf(uint256 _tokenId) external view returns (address); function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; function transferFrom(address _from, address _to, uint256 _tokenId) external payable; function approve(address _approved, uint256 _tokenId) external payable; function setApprovalForAll(address _operator, bool _approved) external; function getApproved(uint256 _tokenId) external view returns (address); function isApprovedForAll(address _owner, address _operator) external view returns (bool); } ``` Example implementation of an ERC-721 token contract written in Rust. ### src/erc721.rs > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust //! Implementation of the ERC-721 standard //! //! The eponymous [`Erc721`] type provides all the standard methods, //! and is intended to be inherited by other contract types. //! //! You can configure the behavior of [`Erc721`] via the [`Erc721Params`] trait, //! which allows specifying the name, symbol, and token uri. //! //! Note that this code is unaudited and not fit for production use. use alloc::{string::String, vec, vec::Vec}; use alloy_primitives::{Address, U256, FixedBytes}; use alloy_sol_types::sol; use core::{borrow::BorrowMut, marker::PhantomData}; use stylus_sdk::{ abi::Bytes, evm, msg, prelude::* }; pub trait Erc721Params { /// Immutable NFT name. const NAME: &'static str; /// Immutable NFT symbol. const SYMBOL: &'static str; /// The NFT's Uniform Resource Identifier. fn token_uri(token_id: U256) -> String; } sol_storage! { /// Erc721 implements all ERC-721 methods pub struct Erc721 { /// Token id to owner map mapping(uint256 => address) owners; /// User to balance map mapping(address => uint256) balances; /// Token id to approved user map mapping(uint256 => address) token_approvals; /// User to operator map (the operator can manage all NFTs of the owner) mapping(address => mapping(address => bool)) operator_approvals; /// Total supply uint256 total_supply; /// Used to allow [`Erc721Params`] PhantomData phantom; } } // Declare events and Solidity error types sol! { event Transfer(address indexed from, address indexed to, uint256 indexed token_id); event Approval(address indexed owner, address indexed approved, uint256 indexed token_id); event ApprovalForAll(address indexed owner, address indexed operator, bool approved); // Token id has not been minted, or it has been burned error InvalidTokenId(uint256 token_id); // The specified address is not the owner of the specified token id error NotOwner(address from, uint256 token_id, address real_owner); // The specified address does not have allowance to spend the specified token id error NotApproved(address owner, address spender, uint256 token_id); // Attempt to transfer token id to the Zero address error TransferToZero(uint256 token_id); // The receiver address refused to receive the specified token id error ReceiverRefused(address receiver, uint256 token_id, bytes4 returned); } /// Represents the ways methods may fail. #[derive(SolidityError)] pub enum Erc721Error { InvalidTokenId(InvalidTokenId), NotOwner(NotOwner), NotApproved(NotApproved), TransferToZero(TransferToZero), ReceiverRefused(ReceiverRefused), } // External interfaces sol_interface! { /// Allows calls to the `onERC721Received` method of other contracts implementing `IERC721TokenReceiver`. interface IERC721TokenReceiver { function onERC721Received(address operator, address from, uint256 token_id, bytes data) external returns(bytes4); } } /// Selector for `onERC721Received`, which is returned by contracts implementing `IERC721TokenReceiver`. const ERC721_TOKEN_RECEIVER_ID: u32 = 0x150b7a02; // These methods aren't external, but are helpers used by external methods. // Methods marked as "pub" here are usable outside of the erc721 module (i.e. they're callable from lib.rs). impl Erc721 { /// Requires that msg::sender() is authorized to spend a given token fn require_authorized_to_spend(&self, from: Address, token_id: U256) -> Result<(), Erc721Error> { // `from` must be the owner of the token_id let owner = self.owner_of(token_id)?; if from != owner { return Err(Erc721Error::NotOwner(NotOwner { from, token_id, real_owner: owner, })); } // caller is the owner if msg::sender() == owner { return Ok(()); } // caller is an operator for the owner (can manage their tokens) if self.operator_approvals.getter(owner).get(msg::sender()) { return Ok(()); } // caller is approved to manage this token_id if msg::sender() == self.token_approvals.get(token_id) { return Ok(()); } // otherwise, caller is not allowed to manage this token_id Err(Erc721Error::NotApproved(NotApproved { owner, spender: msg::sender(), token_id, })) } /// Transfers `token_id` from `from` to `to`. /// This function does check that `from` is the owner of the token, but it does not check /// that `to` is not the zero address, as this function is usable for burning. pub fn transfer(&mut self, token_id: U256, from: Address, to: Address) -> Result<(), Erc721Error> { let mut owner = self.owners.setter(token_id); let previous_owner = owner.get(); if previous_owner != from { return Err(Erc721Error::NotOwner(NotOwner { from, token_id, real_owner: previous_owner, })); } owner.set(to); // right now working with storage can be verbose, but this will change upcoming version of the Stylus SDK let mut from_balance = self.balances.setter(from); let balance = from_balance.get() - U256::from(1); from_balance.set(balance); let mut to_balance = self.balances.setter(to); let balance = to_balance.get() + U256::from(1); to_balance.set(balance); // cleaning app the approved mapping for this token self.token_approvals.delete(token_id); evm::log(Transfer { from, to, token_id }); Ok(()) } /// Calls `onERC721Received` on the `to` address if it is a contract. /// Otherwise it does nothing fn call_receiver( storage: &mut S, token_id: U256, from: Address, to: Address, data: Vec, ) -> Result<(), Erc721Error> { if to.has_code() { let receiver = IERC721TokenReceiver::new(to); let received = receiver .on_erc_721_received(&mut *storage, msg::sender(), from, token_id, data.into()) .map_err(|_e| Erc721Error::ReceiverRefused(ReceiverRefused { receiver: receiver.address, token_id, returned: alloy_primitives::FixedBytes(0_u32.to_be_bytes()), }))? .0; if u32::from_be_bytes(received) != ERC721_TOKEN_RECEIVER_ID { return Err(Erc721Error::ReceiverRefused(ReceiverRefused { receiver: receiver.address, token_id, returned: alloy_primitives::FixedBytes(received), })); } } Ok(()) } /// Transfers and calls `onERC721Received` pub fn safe_transfer>( storage: &mut S, token_id: U256, from: Address, to: Address, data: Vec, ) -> Result<(), Erc721Error> { storage.borrow_mut().transfer(token_id, from, to)?; Self::call_receiver(storage, token_id, from, to, data) } /// Mints a new token and transfers it to `to` pub fn mint(&mut self, to: Address) -> Result<(), Erc721Error> { let new_token_id = self.total_supply.get(); self.total_supply.set(new_token_id + U256::from(1u8)); self.transfer(new_token_id, Address::default(), to)?; Ok(()) } /// Burns the token `token_id` from `from` /// Note that total_supply is not reduced since it's used to calculate the next token_id to mint pub fn burn(&mut self, from: Address, token_id: U256) -> Result<(), Erc721Error> { self.transfer(token_id, from, Address::default())?; Ok(()) } } // these methods are external to other contracts #[public] impl Erc721 { /// Immutable NFT name. pub fn name() -> Result { Ok(T::NAME.into()) } /// Immutable NFT symbol. pub fn symbol() -> Result { Ok(T::SYMBOL.into()) } /// The NFT's Uniform Resource Identifier. #[selector(name = "tokenURI")] pub fn token_uri(&self, token_id: U256) -> Result { self.owner_of(token_id)?; // require NFT exist Ok(T::token_uri(token_id)) } /// Gets the number of NFTs owned by an account. pub fn balance_of(&self, owner: Address) -> Result { Ok(self.balances.get(owner)) } /// Gets the owner of the NFT, if it exists. pub fn owner_of(&self, token_id: U256) -> Result { let owner = self.owners.get(token_id); if owner.is_zero() { return Err(Erc721Error::InvalidTokenId(InvalidTokenId { token_id })); } Ok(owner) } /// Transfers an NFT, but only after checking the `to` address can receive the NFT. /// It includes additional data for the receiver. #[selector(name = "safeTransferFrom")] pub fn safe_transfer_from_with_data>( storage: &mut S, from: Address, to: Address, token_id: U256, data: Bytes, ) -> Result<(), Erc721Error> { if to.is_zero() { return Err(Erc721Error::TransferToZero(TransferToZero { token_id })); } storage .borrow_mut() .require_authorized_to_spend(from, token_id)?; Self::safe_transfer(storage, token_id, from, to, data.0) } /// Equivalent to [`safe_transfer_from_with_data`], but without the additional data. /// /// Note: because Rust doesn't allow multiple methods with the same name, /// we use the `#[selector]` macro attribute to simulate solidity overloading. #[selector(name = "safeTransferFrom")] pub fn safe_transfer_from>( storage: &mut S, from: Address, to: Address, token_id: U256, ) -> Result<(), Erc721Error> { Self::safe_transfer_from_with_data(storage, from, to, token_id, Bytes(vec![])) } /// Transfers the NFT. pub fn transfer_from(&mut self, from: Address, to: Address, token_id: U256) -> Result<(), Erc721Error> { if to.is_zero() { return Err(Erc721Error::TransferToZero(TransferToZero { token_id })); } self.require_authorized_to_spend(from, token_id)?; self.transfer(token_id, from, to)?; Ok(()) } /// Grants an account the ability to manage the sender's NFT. pub fn approve(&mut self, approved: Address, token_id: U256) -> Result<(), Erc721Error> { let owner = self.owner_of(token_id)?; // require authorization if msg::sender() != owner && !self.operator_approvals.getter(owner).get(msg::sender()) { return Err(Erc721Error::NotApproved(NotApproved { owner, spender: msg::sender(), token_id, })); } self.token_approvals.insert(token_id, approved); evm::log(Approval { approved, owner, token_id, }); Ok(()) } /// Grants an account the ability to manage all of the sender's NFTs. pub fn set_approval_for_all(&mut self, operator: Address, approved: bool) -> Result<(), Erc721Error> { let owner = msg::sender(); self.operator_approvals .setter(owner) .insert(operator, approved); evm::log(ApprovalForAll { owner, operator, approved, }); Ok(()) } /// Gets the account managing an NFT, or zero if unmanaged. pub fn get_approved(&mut self, token_id: U256) -> Result { Ok(self.token_approvals.get(token_id)) } /// Determines if an account has been authorized to managing all of a user's NFTs. pub fn is_approved_for_all(&mut self, owner: Address, operator: Address) -> Result { Ok(self.operator_approvals.getter(owner).get(operator)) } /// Whether the NFT supports a given standard. pub fn supports_interface(interface: FixedBytes<4>) -> Result { let interface_slice_array: [u8; 4] = interface.as_slice().try_into().unwrap(); if u32::from_be_bytes(interface_slice_array) == 0xffffffff { // special cased in the ERC165 standard return Ok(false); } const IERC165: u32 = 0x01ffc9a7; const IERC721: u32 = 0x80ac58cd; const IERC721_METADATA: u32 = 0x5b5e139f; Ok(matches!(u32::from_be_bytes(interface_slice_array), IERC165 | IERC721 | IERC721_METADATA)) } } ``` ### lib.rs ```rust // Only run this as a WASM if the export-abi feature is not set. #![cfg_attr(not(any(feature = "export-abi", test)), no_main)] extern crate alloc; // Modules and imports mod erc721; use alloy_primitives::{U256, Address}; /// Import the Stylus SDK along with alloy primitive types for use in our program. use stylus_sdk::{ msg, prelude::* }; use crate::erc721::{Erc721, Erc721Params, Erc721Error}; /// Immutable definitions struct StylusNFTParams; impl Erc721Params for StylusNFTParams { const NAME: &'static str = "StylusNFT"; const SYMBOL: &'static str = "SNFT"; fn token_uri(token_id: U256) -> String { format!("{}{}{}", "https://my-nft-metadata.com/", token_id, ".json") } } // Define the entrypoint as a Solidity storage object. The sol_storage! macro // will generate Rust-equivalent structs with all fields mapped to Solidity-equivalent // storage slots and types. sol_storage! { #[entrypoint] struct StylusNFT { #[borrow] // Allows erc721 to access StylusNFT's storage and make calls Erc721 erc721; } } #[public] #[inherit(Erc721)] impl StylusNFT { /// Mints an NFT pub fn mint(&mut self) -> Result<(), Erc721Error> { let minter = msg::sender(); self.erc721.mint(minter)?; Ok(()) } /// Mints an NFT to another address pub fn mint_to(&mut self, to: Address) -> Result<(), Erc721Error> { self.erc721.mint(to)?; Ok(()) } /// Burns an NFT pub fn burn(&mut self, token_id: U256) -> Result<(), Erc721Error> { // This function checks that msg::sender() owns the specified token_id self.erc721.burn(msg::sender(), token_id)?; Ok(()) } /// Total supply pub fn total_supply(&mut self) -> Result { Ok(self.erc721.total_supply.get()) } } ``` ### Cargo.toml ```toml [package] name = "stylus_erc721_example" version = "0.1.7" edition = "2021" license = "MIT OR Apache-2.0" keywords = ["arbitrum", "ethereum", "stylus", "alloy"] [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" mini-alloc = "0.4.2" stylus-sdk = "0.6.0" hex = "0.4.3" [dev-dependencies] tokio = { version = "1.12.0", features = ["full"] } ethers = "2.0" eyre = "0.6.8" [features] export-abi = ["stylus-sdk/export-abi"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # Multicall An Arbitrum Stylus version implementation of [Solidity Multi Call contract](https://solidity-by-example.org/app/multi-call/) that aggregates multiple queries using a for loop and RawCall. Example implementation of a Multi Call contract written in Rust: Here is the interface for TimeLock. ```solidity /** * This file was automatically generated by Stylus and represents a Rust program. * For more information, please see [The Stylus SDK](https://github.com/OffchainLabs/stylus-sdk-rs). */ // SPDX-License-Identifier: MIT-OR-APACHE-2.0 pragma solidity ^0.8.23; interface IMultiCall { function multicall(address[] memory addresses, bytes[] memory data) external view returns (bytes[] memory); error ArraySizeNotMatch(); error CallFailed(uint256); } ``` ### src/lib.rs > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust #![cfg_attr(not(feature = "export-abi"), no_main)] extern crate alloc; #[global_allocator] static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT; use alloy_primitives::U256; use alloy_sol_types::sol; use stylus_sdk::{abi::Bytes, alloy_primitives::Address, call::RawCall, prelude::*}; #[solidity_storage] #[entrypoint] pub struct MultiCall; // Declare events and Solidity error types sol! { error ArraySizeNotMatch(); error CallFailed(uint256 call_index); } #[derive(SolidityError)] pub enum MultiCallErrors { ArraySizeNotMatch(ArraySizeNotMatch), CallFailed(CallFailed), } #[external] impl MultiCall { pub fn multicall( &self, addresses: Vec
, data: Vec, ) -> Result, MultiCallErrors> { let addr_len = addresses.len(); let data_len = data.len(); let mut results: Vec = Vec::new(); if addr_len != data_len { return Err(MultiCallErrors::ArraySizeNotMatch(ArraySizeNotMatch {})); } for i in 0..addr_len { let result = RawCall::new().call(addresses[i], data[i].to_vec().as_slice()) .map_err(|_| MultiCallErrors::CallFailed(CallFailed { call_index: U256::from(i) }))?; results.push(result.into()); } Ok(results) } } ``` ### Cargo.toml ```toml [package] name = "stylus-multi-call-contract" version = "0.1.5" edition = "2021" license = "MIT OR Apache-2.0" keywords = ["arbitrum", "ethereum", "stylus", "alloy"] description = "Stylus multi call example" [dependencies] alloy-primitives = "0.3.1" alloy-sol-types = "0.3.1" mini-alloc = "0.4.2" stylus-sdk = "0.5.0" hex = "0.4.3" [dev-dependencies] tokio = { version = "1.12.0", features = ["full"] } ethers = "2.0" eyre = "0.6.8" [features] export-abi = ["stylus-sdk/export-abi"] [[bin]] name = "stylus-multi-call" path = "src/main.rs" [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # Vending Machine An example project for writing Arbitrum Stylus programs in Rust using the [stylus-sdk](https://github.com/OffchainLabs/stylus-sdk-rs). It includes a Rust implementation of a vending machine Ethereum smart contract. * distribute Cupcakes to any given address * count Cupcakes balance of any given address Here is the interface for Vending Machine. ```solidity interface IVendingMachine { // Function to distribute a cupcake to a user function giveCupcakeTo(address userAddress) external returns (bool); // Getter function for the cupcake balance of a user function getCupcakeBalanceFor(address userAddress) external view returns (uint); } ``` Example implementation of the Vending Machine contract written in Rust. ### src/lib.rs > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust //! //! Stylus Cupcake Example //! //! The program is ABI-equivalent with Solidity, which means you can call it from both Solidity and Rust. //! To do this, run `cargo stylus export-abi`. //! //! Note: this code is a template-only and has not been audited. //! // Allow `cargo stylus export-abi` to generate a main function if the "export-abi" feature is enabled. #![cfg_attr(not(feature = "export-abi"), no_main)] extern crate alloc; use alloy_primitives::{Address, Uint}; // Import items from the SDK. The prelude contains common traits and macros. use stylus_sdk::alloy_primitives::U256; use stylus_sdk::prelude::*; use stylus_sdk::{block, console}; // Define persistent storage using the Solidity ABI. // `VendingMachine` will be the entrypoint for the contract. sol_storage! { #[entrypoint] pub struct VendingMachine { // Mapping from user addresses to their cupcake balances. mapping(address => uint256) cupcake_balances; // Mapping from user addresses to the last time they received a cupcake. mapping(address => uint256) cupcake_distribution_times; } } // Declare that `VendingMachine` is a contract with the following external methods. #[public] impl VendingMachine { // Give a cupcake to the specified user if they are eligible (i.e., if at least 5 seconds have passed since their last cupcake). pub fn give_cupcake_to(&mut self, user_address: Address) -> bool { // Get the last distribution time for the user. let last_distribution = self.cupcake_distribution_times.get(user_address); // Calculate the earliest next time the user can receive a cupcake. let five_seconds_from_last_distribution = last_distribution + U256::from(5); // Get the current block timestamp. let current_time = block::timestamp(); // Check if the user can receive a cupcake. let user_can_receive_cupcake = five_seconds_from_last_distribution <= Uint::<256, 4>::from(current_time); if user_can_receive_cupcake { // Increment the user's cupcake balance. let mut balance_accessor = self.cupcake_balances.setter(user_address); let balance = balance_accessor.get() + U256::from(1); balance_accessor.set(balance); // Update the distribution time to the current time. let mut time_accessor = self.cupcake_distribution_times.setter(user_address); let new_distribution_time = block::timestamp(); time_accessor.set(Uint::<256, 4>::from(new_distribution_time)); return true; } else { // User must wait before receiving another cupcake. console!( "HTTP 429: Too Many Cupcakes (you must wait at least 5 seconds between cupcakes)" ); return false; } } // Get the cupcake balance for the specified user. pub fn get_cupcake_balance_for(&self, user_address: Address) -> Uint<256, 4> { // Return the user's cupcake balance from storage. return self.cupcake_balances.get(user_address); } } ``` ### Cargo.toml ```toml [package] name = "stylus_cupcake_example" version = "0.1.7" edition = "2021" license = "MIT OR Apache-2.0" keywords = ["arbitrum", "ethereum", "stylus", "alloy"] [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" mini-alloc = "0.4.2" stylus-sdk = "0.6.0" hex = "0.4.3" [dev-dependencies] tokio = { version = "1.12.0", features = ["full"] } ethers = "2.0" eyre = "0.6.8" [features] export-abi = ["stylus-sdk/export-abi"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # ABI Decode The `decode` can not be used for `encode_packed` data because it ignores padding when encode. (For more information you can refer to [ABI Encode](/stylus-by-example/basic_examples/abi_encode.md)) So here we show an example for using `decode` on data encoded with `abi_encode_sequence`: > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust // This should always return true pub fn encode_and_decode( &self, target: Address, value: U256, func: String, data: Bytes, timestamp: U256 ) -> Result { // define sol types tuple type TxIdHashType = (SOLAddress, Uint<256>, SOLString, SOLBytes, Uint<256>); // because the abi_encode_sequence will return alloy_primitives::Bytes rather than stylus_sdk::bytes, so we need to make sure the input and return types are the same let primative_data = alloy_primitives::Bytes::copy_from_slice(&data); // set the tuple let tx_hash_data = (target, value, func, primative_data, timestamp); // encode the tuple let tx_hash_data_encode = TxIdHashType::abi_encode_sequence(&tx_hash_data); let validate = true; // Check the result match TxIdHashType::abi_decode_sequence(&tx_hash_data_encode, validate) { Ok(res) => Ok(res == tx_hash_data), Err(_) => { return Err(HasherError::DecodedFailed(DecodedFailed{})); }, } } ``` # Full Example code: ### src/lib.rs ```rust #![cfg_attr(not(any(feature = "export-abi", test)), no_main)] extern crate alloc; /// Import items from the SDK. The prelude contains common traits and macros. use stylus_sdk::{alloy_primitives::{U256, Address}, prelude::*}; // Because the naming of `alloy_primitives` and `alloy_sol_types` is the same, we need to rename the types in `alloy_sol_types`. use alloy_sol_types::{sol_data::{Address as SOLAddress, *}, SolType, sol}; // Define error sol! { error DecodedFailed(); } // Error types for the MultiSig contract #[derive(SolidityError)] pub enum DecoderError{ DecodedFailed(DecodedFailed) } #[storage] #[entrypoint] pub struct Decoder; /// Declare that `Decoder` is a contract with the following external methods. #[public] impl Decoder { // This should always return true pub fn encode_and_decode( &self, address: Address, amount: U256 ) -> Result { // define sol types tuple type TxIdHashType = (SOLAddress, Uint<256>); // set the tuple let tx_hash_data = (address, amount); // encode the tuple let tx_hash_data_encode = TxIdHashType::abi_encode_params(&tx_hash_data); let validate = true; // Check the result match TxIdHashType::abi_decode_params(&tx_hash_data_encode, validate) { Ok(res) => Ok(res == tx_hash_data), Err(_) => { return Err(DecoderError::DecodedFailed(DecodedFailed{})); }, } } } ``` ### Cargo.toml ```rust [package] name = "stylus-decode-hashing" version = "0.1.0" edition = "2021" [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" mini-alloc = "0.4.2" stylus-sdk = "0.5.1" [features] export-abi = ["stylus-sdk/export-abi"] debug = ["stylus-sdk/debug"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # ABI Encode The `ABI Encode` has 2 types which are [`encode`](https://docs.soliditylang.org/en/latest/abi-spec.html#strict-encoding-mode) and [`encode_packed`](https://docs.soliditylang.org/en/latest/abi-spec.html#non-standard-packed-mode). * `encode` will concatenate all values and add padding to fit into 32 bytes for each values. * `encode_packed` will concatenate all values in the exact byte representations without padding. (For example, `encode_packed("a", "bc") == encode_packed("ab", "c")`) Suppose we have a tuple of values: `(target, value, func, data, timestamp)` to encode, and their `alloy primitives type` are `(Address, U256, String, Bytes, U256)`. Firstly we need to import those types we need from `alloy_primitives`, `stylus_sdk::abi` and `alloc::string`: > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust // Import items from the SDK. The prelude contains common traits and macros. use stylus_sdk::{alloy_primitives::{U256, Address, FixedBytes}, abi::Bytes, prelude::*}; // Import String from alloc use alloc::string::String; ``` Secondly because we will use the method [`abi_encode_sequence`](https://docs.rs/alloy-sol-types/latest/alloy_sol_types/trait.SolValue.html#method.abi_encode_sequence) and [`abi_encode_packed`](https://docs.rs/alloy-sol-types/latest/alloy_sol_types/trait.SolValue.html#method.abi_encode_packed) under `alloy_sol_types` to encode data, we also need to import the types from `alloy_sol_types`: ```rust // Becauce the naming of alloy_primitives and alloy_sol_types is the same, so we need to re-name the types in alloy_sol_types use alloy_sol_types::{sol_data::{Address as SOLAddress, String as SOLString, Bytes as SOLBytes, *}, SolType}; ``` ## encode Then `encode` them: ```rust // define sol types tuple type TxIdHashType = (SOLAddress, Uint<256>, SOLString, SOLBytes, Uint<256>); // set the tuple let tx_hash_data = (target, value, func, data, timestamp); // encode the tuple let tx_hash_bytes = TxIdHashType::abi_encode_sequence(&tx_hash_data); ``` ## encode\_packed There are 2 methods to `encode_packed` data: 1. `encode_packed` them: ```rust // define sol types tuple type TxIdHashType = (SOLAddress, Uint<256>, SOLString, SOLBytes, Uint<256>); // set the tuple let tx_hash_data = (target, value, func, data, timestamp); // encode the tuple let tx_hash_data_encode_packed = TxIdHashType::abi_encode_packed(&tx_hash_data); ``` 2. We can also use the following method to `encode_packed` them: ```rust let tx_hash_data_encode_packed = [&target.to_vec(), &value.to_be_bytes_vec(), func.as_bytes(), &data.to_vec(), ×tamp.to_be_bytes_vec()].concat(); ``` # Full Example code: ### src/main.rs ```rust // Allow `cargo stylus export-abi` to generate a main function. #![cfg_attr(not(feature = "export-abi"), no_main)] extern crate alloc; /// Import items from the SDK. The prelude contains common traits and macros. use stylus_sdk::{alloy_primitives::{U256, Address, FixedBytes}, abi::Bytes, prelude::*}; use alloc::string::String; // Becauce the naming of alloy_primitives and alloy_sol_types is the same, so we need to re-name the types in alloy_sol_types use alloy_sol_types::{sol_data::{Address as SOLAddress, String as SOLString, Bytes as SOLBytes, *}, SolType}; use sha3::{Digest, Keccak256}; // Define some persistent storage using the Solidity ABI. // `Encoder` will be the entrypoint. #[storage] #[entrypoint] pub struct Encoder; impl Encoder { fn keccak256(&self, data: Bytes) -> FixedBytes<32> { // prepare hasher let mut hasher = Keccak256::new(); // populate the data hasher.update(data); // hashing with keccack256 let result = hasher.finalize(); // convert the result hash to FixedBytes<32> let result_vec = result.to_vec(); FixedBytes::<32>::from_slice(&result_vec) } } /// Declare that `Encoder` is a contract with the following external methods. #[public] impl Encoder { // Encode the data and hash it pub fn encode( &self, target: Address, value: U256, func: String, data: Bytes, timestamp: U256 ) -> Vec { // define sol types tuple type TxIdHashType = (SOLAddress, Uint<256>, SOLString, SOLBytes, Uint<256>); // set the tuple let tx_hash_data = (target, value, func, data, timestamp); // encode the tuple let tx_hash_data_encode = TxIdHashType::abi_encode_params(&tx_hash_data); tx_hash_data_encode } // Packed encode the data and hash it, the same result with the following one pub fn packed_encode( &self, target: Address, value: U256, func: String, data: Bytes, timestamp: U256 )-> Vec { // define sol types tuple type TxIdHashType = (SOLAddress, Uint<256>, SOLString, SOLBytes, Uint<256>); // set the tuple let tx_hash_data = (target, value, func, data, timestamp); // encode the tuple let tx_hash_data_encode_packed = TxIdHashType::abi_encode_packed(&tx_hash_data); tx_hash_data_encode_packed } // Packed encode the data and hash it, the same result with the above one pub fn packed_encode_2( &self, target: Address, value: U256, func: String, data: Bytes, timestamp: U256 )-> Vec { // set the data to arrary and concat it directly let tx_hash_data_encode_packed = [&target.to_vec(), &value.to_be_bytes_vec(), func.as_bytes(), &data.to_vec(), ×tamp.to_be_bytes_vec()].concat(); tx_hash_data_encode_packed } // The func example: "transfer(address,uint256)" pub fn encode_with_signature( &self, func: String, address: Address, amount: U256 ) -> Vec { type TransferType = (SOLAddress, Uint<256>); let tx_data = (address, amount); let data = TransferType::abi_encode_params(&tx_data); // Get function selector let hashed_function_selector = self.keccak256(func.as_bytes().to_vec().into()); // Combine function selector and input data (use abi_packed way) let calldata = [&hashed_function_selector[..4], &data].concat(); calldata } } ``` ### Cargo.toml ```toml [package] name = "stylus-encode-hashing" version = "0.1.7" edition = "2021" license = "MIT OR Apache-2.0" keywords = ["arbitrum", "ethereum", "stylus", "alloy"] [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" mini-alloc = "0.4.2" stylus-sdk = "0.6.0" hex = "0.4.3" sha3 = "0.10" [dev-dependencies] tokio = { version = "1.12.0", features = ["full"] } ethers = "2.0" eyre = "0.6.8" [features] export-abi = ["stylus-sdk/export-abi"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # Bytes In, Bytes Out This is a simple bytes in, bytes out contract that shows a minimal `entrypoint` function (denoted by the `#[entrypoint]` proc macro). If your smart contract just has one primary function, like computing a cryptographic hash, this can be a great model because it strips out the SDK and acts like a pure function or Unix-style app. ### src/main.rs > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust #![cfg_attr(not(feature = "export-abi"), no_main)] extern crate alloc; use alloc::vec::Vec; use stylus_sdk::stylus_proc::entrypoint; #[entrypoint] fn user_main(input: Vec) -> Result, Vec> { Ok(input) } ``` ### Cargo.toml ```toml [package] name = "bytes_in_bytes_out" version = "0.1.7" edition = "2021" [dependencies] stylus-sdk = "0.6.0" [features] export-abi = ["stylus-sdk/export-abi"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" [workspace] ``` --- > For a complete page index, fetch # Constants Constants are values that are bound to a name and cannot change. They are always immutable. In Rust, constants are declared with the `const` keyword. Unlike variables declared with the `let` keyword, constants *must* be annotated with their type. Constants are valid for the entire length of the transaction. They are essentially *inlined* wherever they are used, meaning that their value is copied directly into whatever context invokes them. Since their value is hardcoded, they can save on gas cost as their value does not need to be fetched from storage. ## Learn More * [Rust docs - Constant items](https://doc.rust-lang.org/reference/items/constant-items.html) * [Solidity docs - Constant variables](https://docs.soliditylang.org/en/v0.8.19/contracts.html#constant) ### src/lib.rs > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust // Only run this as a WASM if the export-abi feature is not set. #![cfg_attr(not(any(feature = "export-abi", test)), no_main)] extern crate alloc; use alloc::vec; use alloc::vec::Vec; use stylus_sdk::alloy_primitives::Address; use stylus_sdk::prelude::*; use stylus_sdk::storage::StorageAddress; const OWNER: &str = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; #[storage] #[entrypoint] pub struct Contract { owner: StorageAddress, } #[public] impl Contract { pub fn init(&mut self) -> Result<(), Vec> { // Parse the const &str as a local Address variable let owner_address = Address::parse_checksummed(OWNER, None).expect("Invalid address"); // Save the result as the owner self.owner.set(owner_address); Ok(()) } pub fn owner(&self) -> Result> { let owner_address = self.owner.get(); Ok(owner_address) } } ``` ### Cargo.toml ```toml [package] name = "stylus_constants_example" version = "0.1.7" edition = "2021" license = "MIT OR Apache-2.0" keywords = ["arbitrum", "ethereum", "stylus", "alloy"] [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" mini-alloc = "0.4.2" stylus-sdk = "0.6.0" hex = "0.4.3" [dev-dependencies] tokio = { version = "1.12.0", features = ["full"] } ethers = "2.0" eyre = "0.6.8" [features] export-abi = ["stylus-sdk/export-abi"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # Errors In Rust Stylus contracts, error handling is a crucial aspect of writing robust and reliable smart contracts. Rust differentiates between recoverable and unrecoverable errors. Recoverable errors are represented using the `Result` type, which can either be `Ok`, indicating success, or `Err`, indicating failure. This allows developers to manage errors gracefully and maintain control over the flow of execution. Unrecoverable errors are handled with the `panic!` macro, which stops execution, unwinds the stack, and returns a dataless error. In Stylus contracts, error types are often explicitly defined, providing clear and structured ways to handle different failure scenarios. This structured approach promotes better error management, ensuring that contracts are secure, maintainable, and behave predictably under various conditions. Similar to Solidity and EVM, errors in Stylus will undo all changes made to the state during a transaction by reverting the transaction. Thus, there are two main types of errors in Rust Stylus contracts: * **Recoverable Errors**: The Stylus SDK provides features that make using recoverable errors in Rust Stylus contracts convenient. This type of error handling is strongly recommended for Stylus contracts. * **Unrecoverable Errors**: These can be defined similarly to Rust code but are not recommended for smart contracts if recoverable errors can be used instead. ## Learn More * [Solidity docs: Expressions and Control Structures](https://docs.soliditylang.org/en/latest/control-structures.html) * [`#[derive(SolidityError)]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/derive.SolidityError.html) * [`alloy_sol_types::SolError`](https://docs.rs/alloy-sol-types/latest/alloy_sol_types/trait.SolError.html) * [`Error handling: Rust book`](https://doc.rust-lang.org/book/ch09-00-error-handling.html) ## Recoverable Errors Recoverable errors are represented using the `Result` type, which can either be `Ok`, indicating success, or `Err`, indicating failure. The Stylus SDK provides tools to define custom error types and manage recoverable errors effectively. #### Example: Recoverable Errors Here's a simplified Rust Stylus contract demonstrating how to define and handle recoverable errors: > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust #![cfg_attr(not(feature = "export-abi"), no_main)] extern crate alloc; use alloy_sol_types::sol; use stylus_sdk::{abi::Bytes, alloy_primitives::{Address, U256}, call::RawCall, prelude::*}; #[storage] #[entrypoint] pub struct MultiCall; // Declare events and Solidity error types sol! { error ArraySizeNotMatch(); error CallFailed(uint256 call_index); } #[derive(SolidityError)] pub enum MultiCallErrors { ArraySizeNotMatch(ArraySizeNotMatch), CallFailed(CallFailed), } #[public] impl MultiCall { pub fn multicall( &self, addresses: Vec
, data: Vec, ) -> Result, MultiCallErrors> { let addr_len = addresses.len(); let data_len = data.len(); let mut results: Vec = Vec::new(); if addr_len != data_len { return Err(MultiCallErrors::ArraySizeNotMatch(ArraySizeNotMatch {})); } for i in 0..addr_len { let result: Result, Vec> = RawCall::new().call(addresses[i], data[i].to_vec().as_slice()); let data = match result { Ok(data) => data, Err(_data) => return Err(MultiCallErrors::CallFailed(CallFailed { call_index: U256::from(i) })), }; results.push(data.into()) } Ok(results) } } ``` * **Using `SolidityError` Derive Macro**: The `#[derive(SolidityError)]` attribute is used for the `MultiCallErrors` enum, automatically implementing the necessary traits for error handling. * **Defining Errors**: Custom errors `ArraySizeNotMatch` and `CallFailed` is declared in `MultiCallErrors` enum. `CallFailed` error includes a `call_index` parameter to indicate which call failed. * **ArraySizeNotMatch Error Handling**: The `multicall` function returns `ArraySizeNotMatch` if the size of addresses and data vectors are not equal. * **CallFailed Error Handling**: The `multicall` function returns a `CallFailed` error with the index of the failed call if any call fails. Note that we're using match to check if the result of the call is an error or a return data. We'll describe match pattern in the further sections. ## Unrecoverable Errors Here are various ways to handle such errors in the `multicall` function, which calls multiple addresses and panics in different scenarios: ### Using `panic!` Directly panics if the call fails, including the index of the failed call. ```rust for i in 0..addr_len { let result = RawCall::new().call(addresses[i], data[i].to_vec().as_slice()); let data = match result { Ok(data) => data, Err(_data) => panic!("Call to address {:?} failed at index {}", addresses[i], i), }; results.push(data.into()); } ``` Handling Call Failure with `panic!`: The function panics if any call fails and the transaction will be reverted without any data. ### Using `unwrap` Uses `unwrap` to handle the result, panicking if the call fails. ```rust for i in 0..addr_len { let result = RawCall::new().call(addresses[i], data[i].to_vec().as_slice()).unwrap(); results.push(result.into()); } ``` Handling Call Failure with `unwrap`: The function uses `unwrap` to panic if any call fails, including the index of the failed call. ### Using `match` Uses a `match` statement to handle the result of `call`, panicking if the call fails. ```rust for i in 0..addr_len { let result = RawCall::new().call(addresses[i], data[i].to_vec().as_slice()); let data = match result { Ok(data) => data, Err(_data) => return Err(MultiCallErrors::CallFailed(CallFailed { call_index: U256::from(i) })), }; results.push(data.into()); } ``` Handling Call Failure with `match`: The function uses a `match` statement to handle the result of `call`, returning error if any call fails. ### Using the `?` Operator Uses the `?` operator to propagate the error if the call fails, including the index of the failed call. ```rust for i in 0..addr_len { let result = RawCall::new().call(addresses[i], data[i].to_vec().as_slice()) .map_err(|_| MultiCallErrors::CallFailed(CallFailed { call_index: U256::from(i) }))?; results.push(result.into()); } ``` Handling Call Failure with `?` Operator: The function uses the `?` operator to propagate the error if any call fails, including the index of the failed call. Each method demonstrates a different way to handle unrecoverable errors in the `multicall` function of a Rust Stylus contract, providing a comprehensive approach to error management. **Note** that as mentioned above, it is strongly recommended to use custom error handling instead of unrecoverable error handling. ## Boilerplate ### src/lib.rs The lib.rs code can be found at the top of the page in the recoverable error example section. ### Cargo.toml ```toml [package] name = "stylus-multicall-contract" version = "0.1.7" edition = "2021" [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" stylus-sdk = "0.6.0" hex = "0.4.3" [dev-dependencies] tokio = { version = "1.12.0", features = ["full"] } ethers = "2.0" eyre = "0.6.8" [features] export-abi = ["stylus-sdk/export-abi"] [[bin]] name = "stylus-multicall-contract" path = "src/main.rs" [lib] crate-type = ["lib", "cdylib"] ``` --- > For a complete page index, fetch # Events Events allow for data to be logged publicly to the blockchain. Log entries provide the contract's address, a series of up to four topics, and some arbitrary length binary data. The Stylus Rust SDK provides a few ways to publish event logs described below. ## Learn More * [Solidity docs: Events](https://docs.soliditylang.org/en/v0.8.19/abi-spec.html#events) * [`stylus_sdk::evm::log`](https://docs.rs/stylus-sdk/latest/stylus_sdk/evm/fn.log.html) * [`alloy_sol_types::SolEvent`](https://docs.rs/alloy-sol-types/0.3.1/alloy_sol_types/trait.SolEvent.html) ## Log Using the `evm::log` function in the Stylus SDK is the preferred way to log events. It ensures that an event will be logged in a Solidity ABI-compatible format. The `log` function takes any type that implements Alloy `SolEvent` trait. It's not recommended to attempt to implement this trait on your own. Instead, make use of the provided `sol!` macro to declare your Events and their schema using Solidity-style syntax to declare the parameter types. Alloy will create ABI-compatible Rust types which you can instantiate and pass to the `evm::log` function. ### Log Usage > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust // sol! macro event declaration // Up to 3 parameters can be indexed. // Indexed parameters helps you filter the logs efficiently sol! { event Log(address indexed sender, string message); event AnotherLog(); } #[storage] #[entrypoint] pub struct Events {} #[public] impl Events { fn user_main(_input: Vec) -> ArbResult { // emits a 'Log' event, defined above in the sol! macro evm::log(Log { sender: Address::from([0x11; 20]), message: "Hello world!".to_string(), }); // no data, but 'AnotherLog' event will still emit to the chain evm::log(AnotherLog {}); Ok(vec![]) } } ``` ## Raw Log The `evm::raw_log` affordance offers the ability to send anonymous events that do not necessarily conform to the Solidity ABI. Instead, up to four raw 32-byte indexed topics are published along with any arbitrary bytes appended as data. **NOTE**: It's still possible to achieve Solidity ABI compatibility using this construct. To do so you'll have to manually compute the ABI signature for the event, [following the equation set in the Solidity docs](https://docs.soliditylang.org/en/v0.8.19/abi-spec.html#events). The result of that should be assigned to `TOPIC_0`, the first topic in the slice passed to `raw_log`. ### Raw Log Usage ```rust // set up local variables let user = Address::from([0x22; 20]); let balance = U256::from(10_000_000); // declare up to 4 topics // topics must be of type FixedBytes<32> let topics = &[user.into_word()]; // store non-indexed data in a byte Vec let mut data: Vec = vec![]; // to_be_bytes means 'to big endian bytes' data.extend_from_slice(balance.to_be_bytes::<32>().to_vec().as_slice()); // unwrap() here 'consumes' the Result evm::raw_log(topics.as_slice(), data.as_ref()).unwrap(); ``` ## Result Combining the above examples into the boiler plate provided below this section, deploying to a Stylus chain and then invoking the deployed contract will result in the following three events logged to the chain: ### logs ```json [ { "address": "0x6cf4a18ac8efd6b0b99d3200c4fb9609dd60d4b3", "topics": ["0x0738f4da267a110d810e6e89fc59e46be6de0c37b1d5cd559b267dc3688e74e0", "0x0000000000000000000000001111111111111111111111111111111111111111"], "data": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20776f726c64210000000000000000000000000000000000000000", "blockHash": "0xfef880025dc87b5ab4695a0e1a6955dd7603166ecba79ce0f503a568b2ec8940", "blockNumber": "0x94", "transactionHash": "0xc7318dae2164eb441fb80f5b869f844e3e97ae83c24a4639d46ec4d915a30818", "transactionIndex": "0x1", "logIndex": "0x0", "removed": false }, { "address": "0x6cf4a18ac8efd6b0b99d3200c4fb9609dd60d4b3", "topics": ["0xfe1a3ad11e425db4b8e6af35d11c50118826a496df73006fc724cb27f2b99946"], "data": "0x", "blockHash": "0xfef880025dc87b5ab4695a0e1a6955dd7603166ecba79ce0f503a568b2ec8940", "blockNumber": "0x94", "transactionHash": "0xc7318dae2164eb441fb80f5b869f844e3e97ae83c24a4639d46ec4d915a30818", "transactionIndex": "0x1", "logIndex": "0x1", "removed": false }, { "address": "0x6cf4a18ac8efd6b0b99d3200c4fb9609dd60d4b3", "topics": ["0x0000000000000000000000002222222222222222222222222222222222222222"], "data": "0x0000000000000000000000000000000000000000000000000000000000989680", "blockHash": "0xfef880025dc87b5ab4695a0e1a6955dd7603166ecba79ce0f503a568b2ec8940", "blockNumber": "0x94", "transactionHash": "0xc7318dae2164eb441fb80f5b869f844e3e97ae83c24a4639d46ec4d915a30818", "transactionIndex": "0x1", "logIndex": "0x2", "removed": false } ] ``` ## Boilerplate ### src/lib.rs ```rust // Only run this as a WASM if the export-abi feature is not set. #![cfg_attr(not(any(feature = "export-abi", test)), no_main)] extern crate alloc; use alloc::vec::Vec; use alloc::{string::ToString, vec}; use stylus_sdk::alloy_primitives::U256; use stylus_sdk::{alloy_primitives::Address, alloy_sol_types::sol, evm, prelude::*, ArbResult}; // sol! macro event declaration // Up to 3 parameters can be indexed. // Indexed parameters helps you filter the logs by the indexed parameter sol! { event Log(address indexed sender, string message); event AnotherLog(); } #[storage] #[entrypoint] pub struct Events {} #[public] impl Events { fn user_main(_input: Vec) -> ArbResult { // emits a 'Log' event, defined above in the sol! macro evm::log(Log { sender: Address::from([0x11; 20]), message: "Hello world!".to_string(), }); // no data, but event will still log to the chain evm::log(AnotherLog {}); // set up local variables let user = Address::from([0x22; 20]); let balance = U256::from(10_000_000); // declare up to 4 topics // topics must be of type FixedBytes<32> let topics = &[user.into_word()]; // store non-indexed data in a byte Vec let mut data: Vec = vec![]; // to_be_bytes means 'to big endian bytes' data.extend_from_slice(balance.to_be_bytes::<32>().to_vec().as_slice()); // unwrap() here 'consumes' the Result evm::raw_log(topics.as_slice(), data.as_ref()).unwrap(); Ok(Vec::new()) } } ``` ### Cargo.toml ```toml [package] name = "stylus_events_example" version = "0.1.7" edition = "2021" license = "MIT OR Apache-2.0" keywords = ["arbitrum", "ethereum", "stylus", "alloy"] [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" mini-alloc = "0.4.2" stylus-sdk = "0.6.0" hex = "0.4.3" [dev-dependencies] tokio = { version = "1.12.0", features = ["full"] } ethers = "2.0" eyre = "0.6.8" [features] export-abi = ["stylus-sdk/export-abi"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # Functions Functions are a fundamental part of any programming language, including Stylus, enabling you to encapsulate logic into reusable components. This guide covers the syntax and usage of functions, including internal and external functions, and how to return multiple values. ## Learn More * [Rust docs - Functions](https://doc.rust-lang.org/reference/items/functions.html) * [Solidity docs - Functions](https://solidity-by-example.org/function/) ## Overview A function in Stylus consists of a name, a set of parameters, an optional return type, and a body. Just as with storage, Stylus methods are Solidity ABI equivalent. This means that contracts written in different programming languages are fully interoperable. Functions are declared with the `fn` keyword. Parameters allow the function to accept inputs, and the return type specifies the output of the function. If no return type is specified, the function returns `void`. Following is an example of a function `add` that takes two `uint256` values and returns their sum. > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust fn add(a: uint256, b: uint256) -> uint256 { return a + b; } ``` ## Function Parameters Function parameters are the inputs to a function. They are specified as a list of `IDENTIFIER: Type` pairs, separated by commas. In this example, the function `add_numbers` takes two `u32` parameters, `a` and `b` and returns the sum of the two numbers. ```rust fn add_numbers(a: u32, b: u32) -> u32 { a + b } ``` ## Return Types Return types in functions are an essential part of defining the behavior and expected outcomes of your smart contract methods. Here, we explain the syntax and usage of return types in Stylus with general examples. ### Basic Syntax A function with a return type in Stylus follows this basic structure. The return type is specified after the `->` arrow. Values are returned using the `return` keyword or implicitly as the last expression of the function. In Rust and Stylus, the last expression in a function is implicitly returned, so the `return` keyword is often omitted. ```rust pub fn function_name(&self) -> ReturnType { // Function body } ``` ## Examples **Function returning a String:** This `get_greeting` function returns a `String`. The return type is specified as `String` after the `->` arrow. ```rust pub fn get_greeting() -> String { "Hello, Stylus!".into() } ``` **Function returning an Integer:** This `get_number` function returns an unsigned 32-bit integer (`u32`). ```rust pub fn get_number() -> u32 { 42 } ``` **Function returning a Result with `Ok` and `Err` variants:** The `perform_operation` function returns a `Result`. The `Result` type is used for functions that can return either a success value (`Ok`) or an error (`Err`). In this case, it returns `Ok(value)` on success and an error variant of `CustomError` on failure. ```rust pub enum CustomError { ErrorVariant, } pub fn perform_operation(value: u32) -> Result { if value > 0 { Ok(value) } else { Err(CustomError::ErrorVariant) } } ``` ## Public Functions Public functions are those that can be called by other contracts. To define a public function in a Stylus contract, you use the `#[public]` macro. This macro ensures that the function is accessible from outside the contract. Previously, all public methods were required to return a `Result` type with `Vec` as the error type. This is now optional. Specifically, if a method is "infallible" (i.e., it cannot produce an error), it does not need to return a Result type. Here's what this means: * Infallible methods: Methods that are guaranteed not to fail (no errors possible) do not need to use the `Result` type. They can return their result directly without wrapping it in `Result`. * Optional error handling: The `Result` type with `Vec` as the error type is now optional for methods that cannot produce an error. In the following example, `owner` is a public function that returns the contract owner's address. Since this function is infallible (i.e., it cannot produce an error), it does not need to return a `Result` type. ```rust #[external] impl Contract { // Define an external function to get the owner of the contract pub fn owner(&self) -> Address { self.owner.get() } } ``` ## Internal Functions Internal functions are those that can only be called within the contract itself. These functions are not exposed to external calls. To define an internal function, you simply include it within your contract's implementation without the `#[public]` macro. The choice between public and internal functions depends on the desired level of accessibility and interaction within and across contracts. In the followinge example, `set_owner` is an internal function that sets a new owner for the contract. It is only callable within the contract itself. ```rust impl Contract { // Define an internal function to set a new owner pub fn set_owner(&mut self, new_owner: Address) { self.owner.set(new_owner); } } ``` To mix public and internal functions within the same contract, you should use two separate `impl` blocks with the same contract name. Public functions are defined within an `impl` block annotated with the `#[public]` attribute, signifying that these functions are part of the contract's public interface and can be invoked from outside the contract. In contrast, internal functions are placed within a separate `impl` block that does not have the `#[public]` attribute, making them internal to the contract and inaccessible to external entities. ### src/lib.rs ```rust // Only run this as a WASM if the export-abi feature is not set. #![cfg_attr(not(any(feature = "export-abi", test)), no_main)] extern crate alloc; use alloc::vec; use stylus_sdk::alloy_primitives::Address; use stylus_sdk::prelude::*; use stylus_sdk::storage::StorageAddress; use stylus_sdk::alloy_primitives::U256; use stylus_sdk::storage::StorageU256; use stylus_sdk::console; #[storage] #[entrypoint] pub struct ExampleContract { owner: StorageAddress, data: StorageU256, } #[public] impl ExampleContract { // External function to set the data pub fn set_data(&mut self, value: U256) { self.data.set(value); } // External function to get the data pub fn get_data(&self) -> U256 { self.data.get() } // External function to get the contract owner pub fn get_owner(&self) -> Address { self.owner.get() } } impl ExampleContract { // Internal function to set a new owner pub fn set_owner(&mut self, new_owner: Address) { self.owner.set(new_owner); } // Internal function to log data pub fn log_data(&self) { let _data = self.data.get(); console!("Current data is: {:?}", _data); } } ``` ### Cargo.toml ```toml [package] name = "stylus-functions" version = "0.1.0" edition = "2021" [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" mini-alloc = "0.4.2" stylus-sdk = "0.6.0" hex = "0.4.3" sha3 = "0.10.8" [features] export-abi = ["stylus-sdk/export-abi"] debug = ["stylus-sdk/debug"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # Function selector When a smart contract is called, the first 4 bytes of the calldata sent as part of the request are called the "function selector", and identify which function of the smart contract to call. You can compute a specific function selector by using the `function_selector!` macro. Here's an example that computes the selector of a function named `foo`: > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust function_selector!("foo") // returns 0xc2985578 ``` Functions usually take a number of arguments that you need to pass in order for the call to be successful. For example, here's the signature of a function that takes 2 arguments, an address and a uint256: ```solidity function transfer(address recipient, uint256 amount) external returns (bool); ``` To compute the selector for this function, pass the types of the arguments to the `function_selector` macro: ```rust function_selector!("transfer", Address, U256) // returns 0xa9059cbb ``` `function_selector` will return a byte array containing the encoded function selector. ## Learn More * [`stylus_sdk::function_selector`](https://docs.rs/stylus-sdk/latest/stylus_sdk/macro.function_selector.html) --- > For a complete page index, fetch # Hasing with keccak256 • Stylus by Example ## Hashing with keccak256 Keccak256 is a cryptographic hash function that takes an input of an arbitrary length and produces a fixed-length output of 256 bits. Keccak256 is a member of the [SHA-3](https://en.wikipedia.org/wiki/SHA-3) family of hash functions. keccak256 computes the Keccak-256 hash of the input. Some use cases are: * Creating a deterministic unique ID from a input * Commit-Reveal scheme * Compact cryptographic signature (by signing the hash instead of a larger input) Here we will use [`stylus-sdk::crypto::keccak`](https://docs.rs/stylus-sdk/latest/stylus_sdk/crypto/fn.keccak.html) to calculate the keccak256 hash of the input data: > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust pub fn keccak>(bytes: T) -> B256 ``` # Full Example code: ### src/main.rs ```rust // Only run this as a WASM if the export-abi feature is not set. #![cfg_attr(not(any(feature = "export-abi", test)), no_main)] extern crate alloc; /// Import items from the SDK. The prelude contains common traits and macros. use stylus_sdk::{alloy_primitives::{U256, Address, FixedBytes}, abi::Bytes, prelude::*, crypto::keccak}; use alloc::string::String; use alloc::vec::Vec; // Becauce the naming of alloy_primitives and alloy_sol_types is the same, so we need to re-name the types in alloy_sol_types use alloy_sol_types::{sol_data::{Address as SOLAddress, String as SOLString, Bytes as SOLBytes, *}, SolType}; use alloy_sol_types::sol; // Define error sol! { error DecodedFailed(); } // Error types for the MultiSig contract #[derive(SolidityError)] pub enum HasherError{ DecodedFailed(DecodedFailed) } #[solidity_storage] #[entrypoint] pub struct Hasher { } /// Declare that `Hasher` is a contract with the following external methods. #[public] impl Hasher { // Encode the data and hash it pub fn encode_and_hash( &self, target: Address, value: U256, func: String, data: Bytes, timestamp: U256 ) -> FixedBytes<32> { // define sol types tuple type TxIdHashType = (SOLAddress, Uint<256>, SOLString, SOLBytes, Uint<256>); // set the tuple let tx_hash_data = (target, value, func, data, timestamp); // encode the tuple let tx_hash_data_encode = TxIdHashType::abi_encode_sequence(&tx_hash_data); // hash the encoded data keccak(tx_hash_data_encode).into() } // This should always return true pub fn encode_and_decode( &self, address: Address, amount: U256 ) -> Result { // define sol types tuple type TxIdHashType = (SOLAddress, Uint<256>); // set the tuple let tx_hash_data = (address, amount); // encode the tuple let tx_hash_data_encode = TxIdHashType::abi_encode_sequence(&tx_hash_data); let validate = true; // Check the result match TxIdHashType::abi_decode_sequence(&tx_hash_data_encode, validate) { Ok(res) => Ok(res == tx_hash_data), Err(_) => { return Err(HasherError::DecodedFailed(DecodedFailed{})); }, } } // Packed encode the data and hash it, the same result with the following one pub fn packed_encode_and_hash_1( &self, target: Address, value: U256, func: String, data: Bytes, timestamp: U256 )-> FixedBytes<32> { // define sol types tuple type TxIdHashType = (SOLAddress, Uint<256>, SOLString, SOLBytes, Uint<256>); // set the tuple let tx_hash_data = (target, value, func, data, timestamp); // encode the tuple let tx_hash_data_encode_packed = TxIdHashType::abi_encode_packed(&tx_hash_data); // hash the encoded data keccak(tx_hash_data_encode_packed).into() } // Packed encode the data and hash it, the same result with the above one pub fn packed_encode_and_hash_2( &self, target: Address, value: U256, func: String, data: Bytes, timestamp: U256 )-> FixedBytes<32> { // set the data to arrary and concat it directly let tx_hash_data_encode_packed = [&target.to_vec(), &value.to_be_bytes_vec(), func.as_bytes(), &data.to_vec(), ×tamp.to_be_bytes_vec()].concat(); // hash the encoded data keccak(tx_hash_data_encode_packed).into() } // The func example: "transfer(address,uint256)" pub fn encode_with_signature( &self, func: String, address: Address, amount: U256 ) -> Vec { type TransferType = (SOLAddress, Uint<256>); let tx_data = (address, amount); let data = TransferType::abi_encode_sequence(&tx_data); // Get function selector let hashed_function_selector: FixedBytes<32> = keccak(func.as_bytes().to_vec()).into(); // Combine function selector and input data (use abi_packed way) let calldata = [&hashed_function_selector[..4], &data].concat(); calldata } // The func example: "transfer(address,uint256)" pub fn encode_with_signature_and_hash( &self, func: String, address: Address, amount: U256 ) -> FixedBytes<32> { type TransferType = (SOLAddress, Uint<256>); let tx_data = (address, amount); let data = TransferType::abi_encode_sequence(&tx_data); // Get function selector let hashed_function_selector: FixedBytes<32> = keccak(func.as_bytes().to_vec()).into(); // Combine function selector and input data (use abi_packed way) let calldata = [&hashed_function_selector[..4], &data].concat(); keccak(calldata).into() } } ``` ### Cargo.toml ```toml [package] name = "stylus-encode-hashing" version = "0.1.0" edition = "2021" [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" mini-alloc = "0.4.2" stylus-sdk = "0.6.0" hex = "0.4.3" sha3 = "0.10.8" [features] export-abi = ["stylus-sdk/export-abi"] debug = ["stylus-sdk/debug"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # Hello World Using the `console!` macro from the `stylus_sdk` allows you to print output to the terminal for debugging purposes. To view the output, you'll need to run a local Stylus dev node as described in the [Arbitrum docs](https://docs.arbitrum.io/stylus/how-tos/local-stylus-dev-node) and ***set the debug feature flag*** as shown in line 7 of the `Cargo.toml` file below. The `console!` macro works similar to the built-in `println!` macro that comes with Rust. ### Examples > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust // Out: Stylus says: 'hello there!' console!("hello there!"); // Out: Stylus says: 'format some arguments' console!("format {} arguments", "some"); let local_variable = "Stylus"; // Out: Stylus says: 'Stylus is awesome!' console!("{local_variable} is awesome!"); // Out: Stylus says: 'When will you try out Stylus?' console!("When will you try out {}?", local_variable); ``` ### src/main.rs ```rust #![cfg_attr(not(feature = "export-abi"), no_main)] extern crate alloc; use stylus_sdk::{console, prelude::*, stylus_proc::entrypoint, ArbResult}; #[storage] #[entrypoint] pub struct Hello; #[public] impl Hello { fn user_main(_input: Vec) -> ArbResult { // Will print 'Stylus says: Hello Stylus!' on your local dev node // Be sure to add "debug" feature flag to your Cargo.toml file as // shown below. console!("Hello Stylus!"); Ok(Vec::new()) } } ``` ### Cargo.toml ```toml [package] name = "stylus_hello_world" version = "0.1.7" edition = "2021" license = "MIT OR Apache-2.0" keywords = ["arbitrum", "ethereum", "stylus", "alloy"] [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" mini-alloc = "0.4.2" stylus-sdk = { version = "0.6.0", features = ["debug"] } hex = "0.4.3" sha3 = "0.10" [dev-dependencies] tokio = { version = "1.12.0", features = ["full"] } ethers = "2.0" eyre = "0.6.8" [features] export-abi = ["stylus-sdk/export-abi"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # Inheritance The Stylus Rust SDK replicates the composition pattern of Solidity. The `#[public]` macro provides the [Router](https://docs.rs/stylus-sdk/latest/stylus_sdk/abi/trait.Router.html) trait, which can be used to connect types via inheritance, via the `#[inherit]` macro. **Please note:** Stylus doesn't support contract multi-inheritance yet. Let's see an example: > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust #[public] #[inherit(Erc20)] impl Token { pub fn mint(&mut self, amount: U256) -> Result<(), Vec> { ... } } #[public] impl Erc20 { pub fn balance_of() -> Result { ... } } ``` In the above code, we can see how `Token` inherits from `Erc20`, meaning that it will inherit the public methods available in `Erc20`. If someone called the `Token` contract on the function `balanceOf`, the function `Erc20.balance_of()` would be executed. Additionally, the inheriting type must implement the [Borrow](https://doc.rust-lang.org/core/borrow/trait.Borrow.html) trait for borrowing data from the inherited type. In the case above, `Token` should implement `Borrow`. For simplicity, `#[storage]` and `sol_storage!` provide a `#[borrow]` annotation that can be used instead of manually implementing the trait: ```rust sol_storage! { #[entrypoint] pub struct Token { #[borrow] Erc20 erc20; ... } pub struct Erc20 { ... } } ``` ## Methods search order A type can inherit multiple other types (as long as they use the `#[public]` macro). Since execution begins in the type that uses the `#[entrypoint]` macro, that type will be first checked when searching a specific method. If the method is not found in that type, the search will continue in the inherited types, in order of inheritance. If the method is not found in any of the inherited methods, the call will revert. Let's see an example: ```rust #[public] #[inherit(B, C)] impl A { pub fn foo() -> Result<(), Vec> { ... } } #[public] impl B { pub fn bar() -> Result<(), Vec> { ... } } #[public] impl C { pub fn bar() -> Result<(), Vec> { ... } pub fn baz() -> Result<(), Vec> { ... } } ``` In the code above: * calling `foo()` will search the method in `A`, find it, and execute `A.foo()` * calling `bar()` will search the method in `A` first, then in `B`, find it, and execute `B.bar()` * calling `baz()` will search the method in `A`, `B` and finally `C`, so it will execute `C.baz()` Notice that `C.bar()` won't ever be reached, since the inheritance goes through `B` first, which has a method named `bar()` too. Finally, since the inherited types can also inherit other types themselves, keep in mind that method resolution finds the first matching method by [Depth First Search](https://en.wikipedia.org/wiki/Depth-first_search). ## Overriding methods Because methods are checked in the inherited order, if two types implement the same method, the one in the higher level in the hierarchy will override the one in the lower levels, which won’t be callable. This allows for patterns where the developer imports a crate implementing a standard, like ERC-20, and then adds or overrides just the methods they want to without modifying the imported ERC-20 type. **Important warning**: The Stylus Rust SDK does not currently contain explicit `override` or `virtual` keywords for explicitly marking override functions. It is important, therefore, to carefully ensure that contracts are only overriding the functions. Let's see an example: ```rust #[public] #[inherit(B, C)] impl A { pub fn foo() -> Result<(), Vec> { ... } } #[public] impl B { pub fn foo() -> Result<(), Vec> { ... } pub fn bar() -> Result<(), Vec> { ... } } ``` In the example above, even though `B` has an implementation for `foo()`, calling `foo()` will execute `A.foo()` since the method is searched first in `A`. ## Learn more * [`Arbitrum documentation`](https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#inheritance-inherit-and-borrow) * [`inheritance, #[inherit] and #[borrow]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/attr.public.html#inheritance-inherit-and-borrow) * [`Router trait`](https://docs.rs/stylus-sdk/latest/stylus_sdk/abi/trait.Router.html) * [`Borrow trait`](https://doc.rust-lang.org/core/borrow/trait.Borrow.html) * [`BorrowMut trait`](https://doc.rust-lang.org/core/borrow/trait.BorrowMut.html) --- > For a complete page index, fetch # Primitive Data Types The **Stylus SDK** makes use of the popular **Alloy** library (from the developers of **ethers-rs** and **Foundry**) to represent various native Solidity types as Rust types and to convert between them when needed. These are needed since there are a number of custom types (like address) and large integers that are not natively supported in Rust. In this section, we'll focus on the following types: * `U256` * `I256` * `Address` * `Boolean` * `Bytes` More in-depth documentation about the available methods and types in the Alloy library can be found in their docs. It also helps to cross-reference with Solidity docs if you don't already have a solid understanding of those types. ## Learn More * [Alloy docs (v0.7.6)](https://docs.rs/alloy-primitives/0.7.6/alloy_primitives/index.html) * [`Address`](https://docs.rs/alloy-primitives/0.7.6/alloy_primitives/struct.Address.html) * [`Signed`](https://docs.rs/alloy-primitives/0.7.6/alloy_primitives/struct.Signed.html) * [`Uint`](https://docs.rs/ruint/1.10.1/ruint/struct.Uint.html) * [Stylus Rust SDK](https://docs.rs/stylus-sdk/latest/stylus_sdk/index.html) * [`Bytes`](https://docs.rs/stylus-sdk/latest/stylus_sdk/abi/struct.Bytes.html) * [Solidity docs (v0.8.19)](https://docs.soliditylang.org/en/v0.8.19/types.html) ## Integers Alloy defines a set of convenient Rust types to represent the typically sized integers used in Solidity. The type `U256` represents a 256-bit *unsigned* integer, meaning it cannot be negative. The range for a `U256` number is 0 to 2^256 - 1. Negative numbers are allowed for I types, such as `I256`. These represent signed integers. * `U256` maps to `uint256` ... `I256` maps to `int256` * `U128` maps to `uint128` ... `I128` maps to `int128` * ... * `U8` maps to `uint8` ... `I8` maps to `int8` ### Integer Usage > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust // Unsigned let eight_bit: U8 = U8::from(1); let two_fifty_six_bit: U256 = U256::from(0xff_u64); // Out: Stylus says: '8-bit: 1 | 256-bit: 255' console!("8-bit: {} | 256-bit: {}", eight_bit, two_fifty_six_bit); // Signed let eight_bit: I8 = I8::unchecked_from(-1); let two_fifty_six_bit: I256 = I256::unchecked_from(0xff_u64); // Out: Stylus says: '8-bit: -1 | 256-bit: 255' console!("8-bit: {} | 256-bit: {}", eight_bit, two_fifty_six_bit); ``` ### Expanded Integer Usage ```rust // Use `try_from` if you're not sure it'll fit let a = I256::try_from(20003000).unwrap(); // Or parse from a string let b = "100".parse::().unwrap(); // With hex characters let c = "-0x138f".parse::().unwrap(); // Underscores are ignored let d = "1_000_000".parse::().unwrap(); // Math works great let e = a * b + c - d; // Out: Stylus says: '20003000 * 100 + -5007 - 1000000 = 1999294993' console!("{} * {} + {} - {} = {}", a, b, c, d, e); // Useful constants let f = I256::MAX; let g = I256::MIN; let h = I256::ZERO; let i = I256::MINUS_ONE; // Stylus says: '5789...9967, -5789...9968, 0, -1' console!("{f}, {g}, {h}, {i}"); // As hex: Stylus says: '0x7fff...ffff, 0x8000...0000, 0x0, 0xffff...ffff' console!("{:#x}, {:#x}, {:#x}, {:#x}", f, g, h, i); ``` ## Address Ethereum addresses are 20 bytes in length, or 160 bits. Alloy provides a number of helper utilities for converting to addresses from strings, bytes, numbers, and addresses. ### Address Usage ```rust // From a 20 byte slice, all 1s let addr1 = Address::from([0x11; 20]); // Out: Stylus says: '0x1111111111111111111111111111111111111111' console!("{addr1}"); // Use the address! macro to parse a string as a checksummed address let addr2 = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); // Out: Stylus says: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' console!("{addr2}"); // Format compressed addresses for output // Out: Stylus says: '0xd8dA…6045' console!("{addr2:#}"); ``` ## Boolean Use native Rust primitives where it makes sense and where no equivalent Alloy primitive exists. ### Boolean Usage ```rust let frightened: bool = true; // Out: Stylus says: 'Boo! Did I scare you?' console!("Boo! Did I scare you?"); let response = match frightened { true => "Yes!".to_string(), false => "No!".to_string(), }; // Out: Stylus says: 'Yes!' console!("{response}"); ``` ## Bytes The Stylus SDK provides this wrapper type around `Vec` to represent a `bytes` value in Solidity. ```rust let vec = vec![108, 27, 56, 87]; let b = Bytes::from(vec); // Out: Stylus says: '0x6c1b3857' console!(String::from_utf8_lossy(b.as_slice())); let b = Bytes::from(b"Hello!".to_vec()); // Out: Stylus says: 'Hello!' console!(String::from_utf8_lossy(b.as_slice())); ``` Note: Return the `Bytes` type on your Rust function if you want to return the ABI `bytes memory` type. ## Boilerplate ### src/lib.rs ```rust #![cfg_attr(not(any(feature = "export-abi", test)), no_main)] extern crate alloc; use alloc::{string::ToString, vec::Vec}; use stylus_sdk::{ alloy_primitives::{address, Address, I256, I8, U256, U8}, console, prelude::*, ArbResult }; #[storage] #[entrypoint] pub struct Data { } #[public] impl Data { fn user_main(_input: Vec) -> ArbResult { // Use native Rust primitives where they make sense // and where no equivalent Alloy primitive exists let frightened: bool = true; // Out: Stylus says: 'Boo! Did I scare you?' console!("Boo! Did I scare you?"); let _response = match frightened { true => "Yes!".to_string(), false => "No!".to_string(), }; // Out: Stylus says: 'Yes!' console!("{_response}"); // U256 stands for a 256-bit *unsigned* integer, meaning it cannot be // negative. The range for a U256 number is 0 to 2^256 - 1. Alloy provides // a set of unsigned integer types to represent the various sizes available // in the EVM. // U256 maps to uint256 // U128 maps to uint128 // ... // U8 maps to uint8 let _eight_bit: U8 = U8::from(1); let _two_fifty_six_bit: U256 = U256::from(0xff_u64); // Out: Stylus says: '8-bit: 1 | 256-bit: 255' console!("8-bit: {} | 256-bit: {}", _eight_bit, _two_fifty_six_bit); // Negative numbers are allowed for I types. These represent signed integers. // I256 maps to int256 // I128 maps to int128 // ... // I8 maps to int8 let _eight_bit: I8 = I8::unchecked_from(-1); let _two_fifty_six_bit: I256 = I256::unchecked_from(0xff_u64); // Out: Stylus says: '8-bit: -1 | 256-bit: 255' console!("8-bit: {} | 256-bit: {}", _eight_bit, _two_fifty_six_bit); // Additional usage of integers // Use `try_from` if you're not sure it'll fit let a = I256::try_from(20003000).unwrap(); // Or parse from a string let b = "100".parse::().unwrap(); // With hex characters let c = "-0x138f".parse::().unwrap(); // Underscores are ignored let d = "1_000_000".parse::().unwrap(); // Math works great let _e = a * b + c - d; // Out: Stylus says: '20003000 * 100 + -5007 - 1000000 = 1999294993' console!("{} * {} + {} - {} = {}", a, b, c, d, _e); // Useful constants let _f = I256::MAX; let _g = I256::MIN; let _h = I256::ZERO; let _i = I256::MINUS_ONE; // Stylus says: '5789...9967, -5789...9968, 0, -1' console!("{_f}, {_g}, {_h}, {_i}"); // As hex: Stylus says: '0x7fff...ffff, 0x8000...0000, 0x0, 0xffff...ffff' console!("{:#x}, {:#x}, {:#x}, {:#x}", _f, _g, _h, _i); // Ethereum addresses are 20 bytes in length, or 160 bits. Alloy provides a number of helper utilities for converting to addresses from strings, bytes, numbers, and addresses // From a 20 byte slice, all 1s let _addr1 = Address::from([0x11; 20]); // Out: Stylus says: '0x1111111111111111111111111111111111111111' console!("{_addr1}"); // Use the address! macro to parse a string as a checksummed address let _addr2 = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); // Out: Stylus says: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' console!("{_addr2}"); // Format compressed addresses for output // Out: Stylus says: '0xd8dA…6045' console!("{_addr2:#}"); Ok(Vec::new()) } } ``` ### Cargo.toml ```toml [package] name = "stylus_data_example" version = "0.1.7" edition = "2021" license = "MIT OR Apache-2.0" keywords = ["arbitrum", "ethereum", "stylus", "alloy"] [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" mini-alloc = "0.4.2" stylus-sdk = "0.6.0" hex = "0.4.3" [dev-dependencies] tokio = { version = "1.12.0", features = ["full"] } ethers = "2.0" eyre = "0.6.8" [features] export-abi = ["stylus-sdk/export-abi"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # Sending Ether We have three main ways to send Ether in Rust Stylus: using the `transfer_eth` method, using low level `call` method, and sending value while calling an external contract. It's important to note that the `transfer_eth` method in Rust Stylus invokes the recipient contract, which may subsequently call other contracts. All the gas is supplied to the recipient, which it may burn. Conversely, the transfer method in Solidity is capped at 2300 gas. In Rust Stylus, you can cap the gas by using the low-level call method with a specified gas. An example of this is provided in the code on bottom of the page. These two methods are exactly equivalent under the hood: > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust transfer_eth(recipient, value)?; call(Call::new_in(self).value(value), recipient, &[])?; ``` ## Where to Send Ether 1. **Externally Owned Account (EOA) Addresses**: Directly send Ether to an EOA address. 2. **Solidity Smart Contracts with Receive Function (No Calldata)**: Send Ether to a Solidity smart contract that has a `receive` function without providing any calldata. 3. **Solidity Smart Contracts with Fallback Function (With Calldata)**: Send Ether to a Solidity smart contract that has a `fallback` function by providing the necessary calldata. 4. **Smart Contracts with Payable Methods (both Solidity and Stylus)**: Send Ether to smart contracts that have defined payable methods. Payable methods are identified by the `payable` modifier in Solidity, and the `#[payable]` macro in Rust. Below you can find examples for each of these methods and how to define them in a Rust Stylus smart contract using the Stylus SDK: ### `src/lib.rs` ```rust // Only run this as a WASM if the export-abi feature is not set. #![cfg_attr(not(any(feature = "export-abi", test)), no_main)] extern crate alloc; use alloy_primitives::Address; use stylus_sdk::{ abi::Bytes, call::{call, transfer_eth, Call}, msg::{self}, prelude::*, }; sol_interface! { interface ITarget { function receiveEther() external payable; } } #[storage] #[entrypoint] pub struct SendEther {} #[public] impl SendEther { // Transfer Ether using the transfer_eth method // This can be used to send Ether to an EOA or a Solidity smart contract that has a receive() function implemented #[payable] pub fn send_via_transfer(to: Address) -> Result<(), Vec> { transfer_eth(to, msg::value())?; Ok(()) } // Transfer Ether using a low-level call // This can be used to send Ether to an EOA or a Solidity smart contract that has a receive() function implemented #[payable] pub fn send_via_call(&mut self, to: Address) -> Result<(), Vec> { call(Call::new_in(self).value(msg::value()), to, &[])?; Ok(()) } // Transfer Ether using a low-level call with a specified gas limit // This can be used to send Ether to an EOA or a Solidity smart contract that has a receive() function implemented #[payable] pub fn send_via_call_gas_limit(&mut self, to: Address, gas_amount: u64) -> Result<(), Vec> { call( Call::new_in(self).value(msg::value()).gas(gas_amount), to, &[], )?; Ok(()) } // Transfer Ether using a low-level call with calldata // This can be used to call a Solidity smart contract's fallback function and send Ether along with calldata #[payable] pub fn send_via_call_with_call_data( &mut self, to: Address, data: Bytes, ) -> Result<(), Vec> { call(Call::new_in(self).value(msg::value()), to, data.as_slice())?; Ok(()) } // Transfer Ether to another smart contract via a payable method on the target contract // The target contract can be either a Solidity smart contract or a Stylus contract that has a receiveEther function, which is a payable function #[payable] pub fn send_to_stylus_contract(&mut self, to: Address) -> Result<(), Vec> { let target = ITarget::new(to); let config = Call::new_in(self).value(msg::value()); target.receive_ether(config)?; Ok(()) } } ``` ### `Cargo.toml` ```toml [package] name = "stylus_sending_ether_example" version = "0.1.7" edition = "2021" license = "MIT OR Apache-2.0" keywords = ["arbitrum", "ethereum", "stylus", "alloy"] [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" mini-alloc = "0.4.2" stylus-sdk = "0.6.0" hex = "0.4.3" [dev-dependencies] tokio = { version = "1.12.0", features = ["full"] } ethers = "2.0" eyre = "0.6.8" [features] export-abi = ["stylus-sdk/export-abi"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # Variables In Solidity, there are 3 types of variables: local, state, and global. Local variables are not stored on the blockchain, while state variables are (and incur a much higher cost as a result). This is true of Arbitrum Stylus Rust smart contracts as well, although how they're defined is quite different. In Rust, **local variables** are just ordinary variables you assign with `let` or `let mut` statements. Local variables are far cheaper than state variables, even on the EVM, however, Stylus local variables are more than 100x cheaper to allocate in memory than their Solidity equivalents. Unlike Solidity, Rust was not built inherently with the blockchain in mind. It is a general purpose programming language. We therefore define specific *storage* types to explicitly denote values intended to be stored permanently as part of the contract's state. **State variables** cost the same to store as their Solidity equivalents. **Global variables** in Solidity, such as `msg.sender` and `block.timestamp`, are available as function calls pulled in from the `stylus_sdk` with their Rust equivalents being `msg::sender()` and `block::timestamp()`, respectively. These variables provide information about the blockchain or the active transaction. ## Learn more * [Rust Docs - Variables and Mutability](https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html) * [Stylus SDK Rust Docs - Storage](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/index.html) * [Stylus SDK Guide - Storage](https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#storage) * [Solidity docs - state variables](https://docs.soliditylang.org/en/v0.8.19/structure-of-a-contract.html#state-variables) * [Solidity docs - global variables](https://docs.soliditylang.org/en/v0.8.19/cheatsheet.html#global-variables) ### src/lib.rs > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust // Only run this as a WASM if the export-abi feature is not set. #![cfg_attr(not(any(feature = "export-abi", test)), no_main)] extern crate alloc; use stylus_sdk::alloy_primitives::{U16, U256}; use stylus_sdk::prelude::*; use stylus_sdk::storage::{StorageAddress, StorageBool, StorageU256}; use stylus_sdk::{block, console, msg}; #[storage] #[entrypoint] pub struct Contract { initialized: StorageBool, owner: StorageAddress, max_supply: StorageU256, } #[public] impl Contract { // State variables are initialized in an `init` function. pub fn init(&mut self) -> Result<(), Vec> { // We check if contract has been initialized before. // We return if so, we initialize if not. let initialized = self.initialized.get(); if initialized { return Ok(()); } self.initialized.set(true); // We set the contract owner to the caller, // which we get from the global msg module self.owner.set(msg::sender()); self.max_supply.set(U256::from(10_000)); Ok(()) } pub fn do_something() -> Result<(), Vec> { // Local variables are not saved to the blockchain // 16-bit Rust integer let _i = 456_u16; // 16-bit int inferred from U16 Alloy primitive let _j = U16::from(123); // Here are some global variables let _timestamp = block::timestamp(); let _amount = msg::value(); console!("Local variables: {_i}, {_j}"); console!("Global variables: {_timestamp}, {_amount}"); Ok(()) } } ``` ### Cargo.toml ```toml [package] name = "stylus_variable_example" version = "0.1.7" edition = "2021" license = "MIT OR Apache-2.0" keywords = ["arbitrum", "ethereum", "stylus", "alloy"] [dependencies] alloy-primitives = "=0.7.6" alloy-sol-types = "=0.7.6" mini-alloc = "0.4.2" stylus-sdk = "0.6.0" hex = "0.4.3" [dev-dependencies] tokio = { version = "1.12.0", features = ["full"] } ethers = "2.0" eyre = "0.6.8" [features] export-abi = ["stylus-sdk/export-abi"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "s" ``` --- > For a complete page index, fetch # VM affordances The Stylus Rust SDK contains several modules for interacting with the Virtual Machine (VM), which can be imported from `stylus_sdk`. Let's see an example: > **NOTE** > > This code has yet to be audited. Please use at your own risk. ```rust use stylus_sdk::{msg}; let callvalue = msg::value(); ``` This page lists the modules that are available, as well as the methods within those modules. ## block Allows you to inspect the current block: * `basefee`: gets the basefee of the current block * `chainid`: gets the unique chain identifier of the Arbitrum chain * `coinbase`: gets the coinbase of the current block, which on Arbitrum chains is the L1 batch poster's address * `gas_limit`: gets the gas limit of the current block * `number`: gets a bounded estimate of the L1 block number at which the sequencer sequenced the transaction. See [Block gas limit, numbers and time](https://docs.arbitrum.io/build-decentralized-apps/arbitrum-vs-ethereum/block-numbers-and-time) for more information on how this value is determined * `timestamp`: gets a bounded estimate of the Unix timestamp at which the sequencer sequenced the transaction. See [Block gas limit, numbers and time](https://docs.arbitrum.io/build-decentralized-apps/arbitrum-vs-ethereum/block-numbers-and-time) for more information on how this value is determined ```rust use stylus_sdk::{block}; let basefee = block::basefee(); let chainid = block::chainid(); let coinbase = block::coinbase(); let gas_limit = block::gas_limit(); let number = block::number(); let timestamp = block::timestamp(); ``` ## contract Allows you to inspect the contract itself: * `address`: gets the address of the current program * `args`: reads the invocation's calldata. The entrypoint macro uses this under the hood * `balance`: gets the balance of the current program * `output`: writes the contract's return data. The entrypoint macro uses this under the hood * `read_return_data`: copies the bytes of the last EVM call or deployment return result. Note: this function does not revert if out of bounds, but rather will copy the overlapping portion * `return_data_len`: returns the length of the last EVM call or deployment return result, or 0 if neither have happened during the program's execution ```rust use stylus_sdk::{contract}; let address = contract::address(); contract::args(); let balance = contract::balance(); contract::output(); contract::read_return_data(); contract::return_data_len(); ``` ## crypto Allows you to access VM-accelerated cryptographic functions: * `keccak`: efficiently computes the [keccak256](https://en.wikipedia.org/wiki/SHA-3) hash of the given preimage ```rust use stylus_sdk::{crypto}; use stylus_sdk::alloy_primitives::address; let preimage = address!("361594F5429D23ECE0A88E4fBE529E1c49D524d8"); let hash = crypto::keccak(&preimage); ``` ## evm Allows you to access affordances for the Ethereum Virtual Machine: * `gas_left`: gets the amount of gas remaining. See [Ink and Gas](https://docs.arbitrum.io/stylus/concepts/stylus-gas) for more information on Stylus's compute pricing * `ink_left`: gets the amount of ink remaining. See [Ink and Gas](https://docs.arbitrum.io/stylus/concepts/stylus-gas) for more information on Stylus's compute pricing * `log`: emits a typed alloy log * `pay_for_memory_grow`: this function exists to force the compiler to import this symbol. Calling it will unproductively consume gas * `raw_log`: emits an EVM log from its raw topics and data. Most users should prefer the alloy-typed [raw\_log](https://docs.rs/stylus-sdk/latest/stylus_sdk/evm/fn.raw_log.html) ```rust use stylus_sdk::{evm}; let gas_left = evm::gas_left(); let ink_left = evm::ink_left(); evm::log(...); evm::pay_for_memory_grow(); evm::raw_log(...); ``` Here's an example of how to emit a Transfer log: ```rust sol! { event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); } fn foo() { ... evm::log(Transfer { from: Address::ZERO, to: address, value, }); } ``` ## msg Allows you to inspect the current call * `reentrant`: whether the current call is reentrant * `sender`: gets the address of the account that called the program. For normal L2-to-L2 transactions the semantics are equivalent to that of the EVM's [CALLER](https://www.evm.codes/#33) opcode, including in cases arising from [DELEGATE\_CALL](https://www.evm.codes/#f4) * `value`: gets the ETH value in wei sent to the program ```rust use stylus_sdk::{msg}; let reentrant = msg::reentrant(); let sender = msg::sender(); let value = msg::value(); ``` ## tx Allows you to inspect the current transaction * `gas_price`: gets the gas price in wei per gas, which on Arbitrum chains equals the basefee * `gas_to_ink`: converts evm gas to ink. See [Ink and Gas](https://docs.arbitrum.io/stylus/concepts/stylus-gas) for more information on Stylus's compute-pricing model * `ink_price`: gets the price of ink in evm gas basis points. See [Ink and Gas](https://docs.arbitrum.io/stylus/concepts/stylus-gas) for more information on Stylus's compute-pricing model * `ink_to_gas`: converts ink to evm gas. See [Ink and Gas](https://docs.arbitrum.io/stylus/concepts/stylus-gas) for more information on Stylus's compute-pricing model * `origin`: gets the top-level sender of the transaction. The semantics are equivalent to that of the EVM's [ORIGIN](https://www.evm.codes/#32) opcode ```rust use stylus_sdk::{tx}; let gas_price = tx::gas_price(); let gas_to_ink = tx::gas_to_ink(); let ink_price = tx::ink_price(); let ink_to_gas = tx::ink_to_gas(); let origin = tx::origin(); ``` ## Learn More * [`Arbitrum documentation`](https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#evm-affordances) * [`Stylus SDK modules`](https://docs.rs/stylus-sdk/latest/stylus_sdk/index.html#modules) --- > For a complete page index, fetch # Hostio exports Hostio (Host I/O) exports are low-level functions that provide direct access to the Stylus VM runtime. These functions are WebAssembly imports that allow Stylus programs to interact with the blockchain environment, similar to how EVM opcodes do in Solidity. ## Overview Hostio functions are the foundational layer that powers all Stylus smart contract operations. While most developers will use the higher-level SDK abstractions, understanding hostio functions is valuable for: * Performance optimization through direct VM access * Implementing custom low-level operations * Understanding gas costs and execution flow * Debugging and troubleshooting contract behavior Info Most developers should use the high-level SDK wrappers instead of calling hostio functions directly. The SDK provides safe, ergonomic interfaces that handle memory management and error checking automatically. ## How hostio works Hostio functions are WebAssembly imports defined in the `vm_hooks` module. When a Stylus program is compiled to WASM, these functions are linked at runtime by the Arbitrum VM: ```rust #[link(wasm_import_module = "vm_hooks")] extern "C" { pub fn msg_sender(sender: *mut u8); pub fn block_number() -> u64; // ... more functions } ``` During execution, the Stylus VM intercepts calls to these functions and implements the actual functionality using the underlying ArbOS infrastructure. ## Function categories Hostio functions are organized into several categories based on their purpose. ### Account operations Query information about accounts on the blockchain. #### `account_balance` Gets the **ETH** balance of an account in wei. Equivalent to EVM's `BALANCE` opcode. ```rust pub fn account_balance(address: *const u8, dest: *mut u8); ``` **Parameters:** * `address`: Pointer to the 20-byte address * `dest`: Pointer to write the 32-byte balance value **Usage:** ```rust use stylus_sdk::alloy_primitives::{Address, U256}; unsafe { let addr = Address::from([0x11; 20]); let mut balance_bytes = [0u8; 32]; hostio::account_balance(addr.as_ptr(), balance_bytes.as_mut_ptr()); let balance = U256::from_be_bytes(balance_bytes); } ``` #### `account_code` Gets a subset of code from an account. Equivalent to EVM's `EXTCODECOPY` opcode. ```rust pub fn account_code( address: *const u8, offset: usize, size: usize, dest: *mut u8 ) -> usize; ``` **Returns:** Number of bytes actually written #### `account_code_size` Gets the size of code at an address. Equivalent to EVM's `EXTCODESIZE` opcode. ```rust pub fn account_code_size(address: *const u8) -> usize; ``` #### `account_codehash` Gets the code hash of an account. Equivalent to EVM's `EXTCODEHASH` opcode. ```rust pub fn account_codehash(address: *const u8, dest: *mut u8); ``` Note Empty accounts return the keccak256 hash of empty bytes: `c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470` ### Storage operations Interact with persistent contract storage. #### `storage_load_bytes32` Reads a 32-byte value from storage. Equivalent to EVM's `SLOAD` opcode. ```rust pub fn storage_load_bytes32(key: *const u8, dest: *mut u8); ``` **Parameters:** * `key`: Pointer to the 32-byte storage key * `dest`: Pointer to write the 32-byte value #### `storage_cache_bytes32` Writes a 32-byte value to the storage cache. Equivalent to EVM's `SSTORE` opcode. ```rust pub fn storage_cache_bytes32(key: *const u8, value: *const u8); ``` Warning Values are cached and must be persisted using `storage_flush_cache` before they're permanently written to storage. #### `storage_flush_cache` Persists cached storage values to the EVM state trie. Equivalent to multiple `SSTORE` operations. ```rust pub fn storage_flush_cache(clear: bool); ``` **Parameters:** * `clear`: Whether to drop the cache entirely after flushing **Storage caching benefits:** The Stylus VM implements storage caching for improved performance: * Repeated reads of the same key cost less gas * Writes are batched for efficiency * The SDK manages the cache automatically ### Block information Access information about the current block. #### `block_basefee` Gets the basefee of the current block. Equivalent to EVM's `BASEFEE` opcode. ```rust pub fn block_basefee(basefee: *mut u8); ``` #### `block_chainid` Gets the chain identifier. Equivalent to EVM's `CHAINID` opcode. ```rust pub fn chainid() -> u64; ``` #### `block_coinbase` Gets the coinbase (block producer address). On Arbitrum, this is the L1 batch poster's address. ```rust pub fn block_coinbase(coinbase: *mut u8); ``` #### `block_gas_limit` Gets the gas limit of the current block. Equivalent to EVM's `GASLIMIT` opcode. ```rust pub fn block_gas_limit() -> u64; ``` #### `block_number` Gets a bounded estimate of the L1 block number when the transaction was sequenced. ```rust pub fn block_number() -> u64; ``` Info See [Arbitrum block numbers and time](/arbitrum-essentials/arbitrum-vs-ethereum/block-numbers-and-time.md) for more information on how block numbers work on Arbitrum chains. #### `block_timestamp` Gets a bounded estimate of the Unix timestamp when the transaction was sequenced. ```rust pub fn block_timestamp() -> u64; ``` ### Transaction and message context Access information about the current transaction and call context. #### `msg_sender` Gets the address of the caller. Equivalent to EVM's `CALLER` opcode. ```rust pub fn msg_sender(sender: *mut u8); ``` **Parameters:** * `sender`: Pointer to write the 20-byte address Note For L1-to-L2 retryable ticket transactions, addresses are aliased. See [address aliasing documentation](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#address-aliasing). #### `msg_value` Gets the **ETH** value sent with the call in wei. Equivalent to EVM's `CALLVALUE` opcode. ```rust pub fn msg_value(value: *mut u8); ``` #### `msg_reentrant` Checks if the current call is reentrant. ```rust pub fn msg_reentrant() -> bool; ``` #### `tx_gas_price` Gets the gas price in wei per gas. On Arbitrum, this equals the basefee. Equivalent to EVM's `GASPRICE` opcode. ```rust pub fn tx_gas_price(gas_price: *mut u8); ``` #### `tx_origin` Gets the top-level sender of the transaction. Equivalent to EVM's `ORIGIN` opcode. ```rust pub fn tx_origin(origin: *mut u8); ``` #### `tx_ink_price` Gets the price of ink on the EVM gas basis in gas basis points. See [Ink and Gas](https://docs.arbitrum.io/stylus/concepts/gas-metering) for more information. ```rust pub fn tx_ink_price() -> u32; ``` ### Contract calls Make calls to other contracts. #### `call_contract` Calls another contract with an optional value and gas limit. Equivalent to EVM's `CALL` opcode. ```rust pub fn call_contract( contract: *const u8, calldata: *const u8, calldata_len: usize, value: *const u8, gas: u64, return_data_len: *mut usize ) -> u8; ``` **Parameters:** * `contract`: Pointer to 20-byte contract address * `calldata`: Pointer to calldata bytes * `calldata_len`: Length of calldata * `value`: Pointer to 32-byte value in wei (use 0 for no value) * `gas`: Gas to supply (use `u64::MAX` for all available gas) * `return_data_len`: Pointer to store the length of the return data **Returns:** `0` on success, non-zero on failure **Gas rules:** * Follows the 63/64 rule (at most 63/64 of available gas is forwarded) * Includes callvalue stipend when value is sent **Usage:** ```rust use stylus_sdk::call::RawCall; unsafe { let result = RawCall::new(self.vm()) .gas(100_000) .value(U256::from(1_000_000)) .call(contract_address, &calldata)?; } ``` #### `delegate_call_contract` Delegate calls another contract. Equivalent to EVM's `DELEGATECALL` opcode. ```rust pub fn delegate_call_contract( contract: *const u8, calldata: *const u8, calldata_len: usize, gas: u64, return_data_len: *mut usize ) -> u8; ``` Warning Delegate calls execute code in the context of the current contract. Be extremely careful when delegate calling to untrusted contracts, as they have full access to your storage. #### `static_call_contract` Static calls another contract (read-only). Equivalent to EVM's `STATICCALL` opcode. ```rust pub fn static_call_contract( contract: *const u8, calldata: *const u8, calldata_len: usize, gas: u64, return_data_len: *mut usize ) -> u8; ``` ### Contract deployment Deploy new contracts. #### `create1` Deploys a contract using CREATE. Equivalent to EVM's `CREATE` opcode. ```rust pub fn create1( code: *const u8, code_len: usize, endowment: *const u8, contract: *mut u8, revert_data_len: *mut usize ); ``` **Parameters:** * `code`: Pointer to initialization code (EVM bytecode) * `code_len`: Length of initialization code * `endowment`: Pointer to 32-byte value to send * `contract`: Pointer to write the deployed contract address (20 bytes) * `revert_data_len`: Pointer to store revert data length on failure **Deployment rules:** * Init code must be EVM bytecode * Deployed code can be Stylus (WASM) if it starts with `0xEFF000` header * The address is determined by the sender and the nonce * On failure, the address will be zero #### `create2` Deploys a contract using CREATE2. Equivalent to EVM's `CREATE2` opcode. ```rust pub fn create2( code: *const u8, code_len: usize, endowment: *const u8, salt: *const u8, contract: *mut u8, revert_data_len: *mut usize ); ``` **Parameters:** * `salt`: Pointer to 32-byte salt value **Address calculation:** * Address is deterministic based on sender, salt, and init code hash * Allows for pre-computed addresses ### Events and logging Emit events to the blockchain. #### `emit_log` Emits an EVM log with topics and data. Equivalent to EVM's `LOG0`-`LOG4` opcodes. ```rust pub fn emit_log(data: *const u8, len: usize, topics: usize); ``` **Parameters:** * `data`: Pointer to event data (first bytes should be 32-byte aligned topics) * `len`: Total length of data, including topics * `topics`: Number of topics (0-4) Warning Requesting more than 4 topics will cause a revert. **Higher-level usage:** ```rust sol! { event Transfer(address indexed from, address indexed to, uint256 value); } // Emit using the SDK self.vm().log(Transfer { from: sender, to: recipient, value: amount, }); ``` ### Gas and ink metering Monitor execution costs. #### `evm_gas_left` Gets the amount of gas remaining. Equivalent to EVM's `GAS` opcode. ```rust pub fn evm_gas_left() -> u64; ``` #### `evm_ink_left` Gets the amount of ink remaining—a Stylus-specific metering unit. ```rust pub fn evm_ink_left() -> u64; ``` Info Ink is Stylus's compute pricing unit. See [Ink and Gas](https://docs.arbitrum.io/stylus/concepts/gas-metering) for conversion between ink and gas. #### `pay_for_memory_grow` Pays for WASM memory growth. Automatically called when allocating new pages. ```rust pub fn pay_for_memory_grow(pages: u16); ``` Note The `entrypoint!` macro handles importing this hostio. Manual calls will unproductively consume gas. ### Cryptography Cryptographic operations. #### `native_keccak256` Efficiently computes the Keccak256 hash. Equivalent to EVM's `SHA3` opcode. ```rust pub fn native_keccak256(bytes: *const u8, len: usize, output: *mut u8); ``` **Parameters:** * `bytes`: Pointer to input data * `len`: Length of input data * `output`: Pointer to write 32-byte hash **Higher-level usage:** ```rust use stylus_sdk::crypto::keccak; let hash = keccak(b"hello world"); ``` ### Calldata operations Read and write calldata and return data. #### `read_args` Reads the program calldata. Equivalent to EVM's `CALLDATACOPY` opcode. ```rust pub fn read_args(dest: *mut u8); ``` Note This reads all of the calldata of the call. #### `read_return_data` Copies bytes from the last call or deployment return result. Equivalent to EVM's `RETURNDATACOPY` opcode. ```rust pub fn read_return_data(dest: *mut u8, offset: usize, size: usize) -> usize; ``` **Parameters:** * `dest`: Destination buffer * `offset`: Offset in return data to start copying from * `size`: Number of bytes to copy **Returns:** Number of bytes actually written **Behavior:** * Does not revert if out of bounds * Copies the overlapping portion only #### `return_data_size` Gets the length of the last return result. Equivalent to EVM's `RETURNDATASIZE` opcode. ```rust pub fn return_data_size() -> usize; ``` #### `write_result` Writes the final return data for the current call. ```rust pub fn write_result(data: *const u8, len: usize); ``` **Behavior:** * Does not cause the program to exit * If not called, the return data will be empty * The program exits naturally when the entrypoint returns #### `contract_address` Gets the address of the current program. Equivalent to EVM's `ADDRESS` opcode. ```rust pub fn contract_address(address: *mut u8); ``` ### Debug and console Debug-only functions for development. #### `log_txt` Prints UTF-8 text to the console. Only available in debug mode. ```rust pub fn log_txt(text: *const u8, len: usize); ``` #### `log_i32` / `log_i64` Prints integers to the console. Only available in debug mode. ```rust pub fn log_i32(value: i32); pub fn log_i64(value: i64); ``` #### `log_f32` / `log_f64` Prints floating-point numbers to the console. Only available in debug mode with floating point enabled. ```rust pub fn log_f32(value: f32); pub fn log_f64(value: f64); ``` **Higher-level usage:** ```rust use stylus_sdk::console; console!("Value: {}", value); // Prints in debug mode, no-op in production ``` ## Safety considerations All hostio functions are marked `unsafe` because they: 1. **Operate on raw pointers**: Require correct memory management 2. **Lack of bounds checking**: Can cause undefined behavior if pointers are invalid 3. **Have side effects**: Can modify contract state or make external calls 4. **May revert**: Some operations can cause the transaction to revert ### Safe usage patterns **Always validate inputs:** ```rust // Bad: unchecked pointer usage unsafe { hostio::msg_sender(ptr); // ptr might be invalid } // Good: use safe wrappers let sender = self.vm().msg_sender(); ``` **Use SDK wrappers:** ```rust // Bad: direct hostio call unsafe { let mut balance = [0u8; 32]; hostio::account_balance(addr.as_ptr(), balance.as_mut_ptr()); } // Good: use SDK wrapper use stylus_sdk::evm; let balance = evm::balance(addr); ``` **Handle return values:** ```rust // Check return status from calls let status = unsafe { hostio::call_contract( contract.as_ptr(), calldata.as_ptr(), calldata.len(), value.as_ptr(), gas, &mut return_len, ) }; if status != 0 { // Handle call failure } ``` ## Higher-level wrappers The Stylus SDK provides safe, ergonomic wrappers around hostio functions: ### Storage operations ```rust // Instead of direct hostio: unsafe { hostio::storage_load_bytes32(key.as_ptr(), dest.as_mut_ptr()); } // Use storage types: use stylus_sdk::storage::StorageU256; #[storage] pub struct Contract { value: StorageU256, } let value = self.value.get(); // Safe, ergonomic ``` ### Contract calls ```rust // Instead of direct hostio: unsafe { hostio::call_contract(/* many parameters */); } // Use RawCall or typed interfaces: use stylus_sdk::call::RawCall; let result = unsafe { RawCall::new(self.vm()) .gas(100_000) .call(contract, &calldata)? }; ``` ### VM context ```rust // Instead of direct hostio: unsafe { let mut sender = [0u8; 20]; hostio::msg_sender(sender.as_mut_ptr()); } // Use VM accessor: let sender = self.vm().msg_sender(); let value = self.vm().msg_value(); let timestamp = self.vm().block_timestamp(); ``` ## Feature flags Hostio behavior changes based on feature flags: ### `export-abi` When enabled, hostio functions are stubbed and return `unimplemented!()`—which is used for ABI generation. ### `stylus-test` When enabled, hostio functions panic with an error message. Use `TestVM` for testing instead. ### `debug` When enabled, console logging functions become available. In production, console functions are no-ops. ## Performance considerations ### Direct hostio vs SDK wrappers * **Direct hostio**: Slightly lower overhead, requires manual memory management * **SDK wrappers**: Minimal overhead (often zero-cost abstractions), much safer **Recommendation:** Use SDK wrappers unless profiling shows a specific performance bottleneck. ### Storage caching The Stylus VM automatically caches storage operations: ```rust // First read: full SLOAD cost let value1 = storage.value.get(); // Subsequent reads: reduced cost from cache let value2 = storage.value.get(); // Writes are cached until flush storage.value.set(new_value); // Cached // Cache is flushed automatically at call boundaries ``` ### Gas vs ink Stylus uses "ink" for fine-grained gas metering: * **Ink**: WASM execution cost in Stylus-specific units * **Gas**: Standard EVM gas units * Conversion happens automatically Most developers don't need to think about ink vs gas distinction. ## Common patterns ### Check-effects-interactions pattern ```rust #[public] impl MyContract { pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), Vec> { // Checks let sender = self.vm().msg_sender(); let balance = self.balances.get(sender); if balance < amount { return Err(b"Insufficient balance".to_vec()); } // Effects self.balances.setter(sender).set(balance - amount); self.balances.setter(to).set(self.balances.get(to) + amount); // Interactions (if any) Ok(()) } } ``` ### Efficient event logging ```rust sol! { event DataUpdated(bytes32 indexed key, uint256 value); } // SDK handles hostio::emit_log internally self.vm().log(DataUpdated { key: key_hash, value: new_value, }); ``` ### Gas-limited external calls ```rust use stylus_sdk::call::RawCall; // Limit gas to prevent griefing let result = unsafe { RawCall::new(self.vm()) .gas(50_000) // Fixed gas limit .call(untrusted_contract, &calldata) }; match result { Ok(data) => { /* process return data */ }, Err(_) => { /* handle failure gracefully */ }, } ``` ## Testing with hostio Hostio functions are not available in the test environment. Use `TestVM` instead: ```rust #[cfg(test)] mod tests { use super::*; use stylus_sdk::testing::*; #[test] fn test_function() { let vm = TestVM::default(); let mut contract = MyContract::from(&vm); // VM functions work in tests let sender = vm.msg_sender(); // Works // Direct hostio would panic // unsafe { hostio::msg_sender(...) } // Would panic } } ``` ## Resources * [Stylus VM specification](https://github.com/OffchainLabs/stylus) * [EVM opcodes reference](https://www.evm.codes/) * [Arbitrum block numbers and time](/arbitrum-essentials/arbitrum-vs-ethereum/block-numbers-and-time.md) * [Ink and gas metering](https://docs.arbitrum.io/stylus/concepts/gas-metering) * [stylus-sdk-rs source](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/stylus-sdk/src/hostio.rs) ## Best practices 1. **Use SDK wrappers**: Prefer high-level abstractions over direct hostio calls 2. **Validate inputs**: Always check pointers and sizes before unsafe operations 3. **Handle errors**: Check return values from call operations 4. **Test thoroughly**: Use `TestVM` for comprehensive testing 5. **Profile first**: Only optimize to direct hostio if profiling shows it's necessary 6. **Document unsafe code**: Always document why `unsafe` is necessary 7. **Minimize unsafe blocks**: Keep `unsafe` blocks as small as possible --- > For a complete page index, fetch # Minimal entrypoint contracts This guide explains the low-level mechanics of Stylus contract entrypoints, helping you understand what happens behind the `#[entrypoint]` and `#[public]` macros. This knowledge is useful for advanced use cases, debugging, and building custom contract frameworks. ## Overview A Stylus contract at its core consists of: 1. **`user_entrypoint` function**: The WASM export that Stylus calls 2. **Router implementation**: Routes function selectors to method implementations 3. **TopLevelStorage trait**: Marks the contract's root storage type 4. **ArbResult type**: Represents success/failure with encoded return data ## Understanding `ArbResult` `ArbResult` is the fundamental return type for Stylus contract methods: ```rust pub type ArbResult = Result, Vec>; ``` * `Ok(Vec)` - Success with ABI-encoded return data * `Err(Vec)` - Revert with ABI-encoded error data **Example:** ```rust use stylus_sdk::ArbResult; // Success with no return data fn no_return() -> ArbResult { Ok(Vec::new()) } // Success with encoded data fn return_value() -> ArbResult { let value: u32 = 42; Ok(value.to_le_bytes().to_vec()) } // Revert with error data fn revert_with_error() -> ArbResult { Err(b"InsufficientBalance".to_vec()) } ``` ## The `user_entrypoint` function The `user_entrypoint` function is the WASM export that Stylus calls when a transaction invokes the contract. The `#[entrypoint]` macro generates this function automatically. ### Generated structure When you use `#[entrypoint]`, the macro generates: ```rust #[no_mangle] pub extern "C" fn user_entrypoint(len: usize) -> usize { let host = stylus_sdk::host::VM { host: stylus_sdk::host::WasmVM{} }; // Reentrancy check (unless reentrant feature enabled) if host.msg_reentrant() { return 1; // revert } // Ensure pay_for_memory_grow is referenced // (costs 8700 ink, less than 1 gas) host.pay_for_memory_grow(0); // Read calldata let input = host.read_args(len); // Call the router let (data, status) = match router_entrypoint::(input, host.clone()) { Ok(data) => (data, 0), // Success Err(data) => (data, 1), // Revert }; // Persist storage changes host.flush_cache(false); // Write return data host.write_result(&data); status } ``` ### Key points * **Signature**: `extern "C" fn user_entrypoint(len: usize) -> usize` * **Input**: `len` is the length of calldata to read * **Output**: `0` for success, `1` for revert * **Side effects**: Reads calldata, writes return data, flushes storage cache ## The `Router` trait The `Router` trait defines how function calls are dispatched to method implementations. ### Trait definition ```rust pub trait Router where S: TopLevelStorage + BorrowMut + ValueDenier, I: ?Sized, { type Storage; /// Route a function call by selector fn route(storage: &mut S, selector: u32, input: &[u8]) -> Option; /// Handle receive (plain ETH transfers, no calldata) fn receive(storage: &mut S) -> Option>>; /// Handle fallback (unknown selectors or no receive) fn fallback(storage: &mut S, calldata: &[u8]) -> Option; /// Handle constructor fn constructor(storage: &mut S, calldata: &[u8]) -> Option; } ``` ### Routing logic The `router_entrypoint` function implements the routing logic: ```rust pub fn router_entrypoint(input: Vec, host: VM) -> ArbResult where R: Router, S: StorageType + TopLevelStorage + BorrowMut + ValueDenier, { let mut storage = unsafe { S::new(U256::ZERO, 0, host) }; // No calldata - try receive, then fallback if input.is_empty() { if let Some(res) = R::receive(&mut storage) { return res.map(|_| Vec::new()); } if let Some(res) = R::fallback(&mut storage, &[]) { return res; } return Err(Vec::new()); // No receive or fallback } // Extract selector (first 4 bytes) if input.len() >= 4 { let selector = u32::from_be_bytes(input[..4].try_into().unwrap()); // Check for constructor if selector == CONSTRUCTOR_SELECTOR { if let Some(res) = R::constructor(&mut storage, &input[4..]) { return res; } } // Try to route to a method else if let Some(res) = R::route(&mut storage, selector, &input[4..]) { return res; } } // Try fallback if let Some(res) = R::fallback(&mut storage, &input) { return res; } Err(Vec::new()) // Unknown selector and no fallback } ``` ## The `TopLevelStorage` trait The `TopLevelStorage` trait marks types that represent the contract's root storage. ### Trait definition ```rust pub unsafe trait TopLevelStorage {} ``` ### Purpose * Prevents storage aliasing during reentrancy * Lifetime tracks all EVM state changes during contract invocation * Must hold a reference when making external calls * Automatically implemented by `#[entrypoint]` ### Safety The trait is `unsafe` because: * Type must truly be top-level to prevent storage aliasing * Incorrectly implementing this trait can lead to undefined behavior ## Building a minimal contract Here's a minimal contract without using the high-level macros: ### Step 1: Define `Storage` ```rust #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; use alloc::vec::Vec; use stylus_sdk::{ abi::{Router, ArbResult}, storage::StorageType, host::VM, alloy_primitives::U256, }; // Mark as top-level storage (normally done by #[entrypoint]) pub struct MyContract; unsafe impl stylus_core::storage::TopLevelStorage for MyContract {} impl StorageType for MyContract { type Wraps<'a> = &'a Self where Self: 'a; type WrapsMut<'a> = &'a mut Self where Self: 'a; unsafe fn new(_slot: U256, _offset: u8, _host: VM) -> Self { MyContract } fn load<'s>(self) -> Self::Wraps<'s> { &self } fn load_mut<'s>(self) -> Self::WrapsMut<'s> { &mut self } } ``` ### Step 2: Implement `Router` ```rust impl Router for MyContract { type Storage = MyContract; fn route(_storage: &mut MyContract, selector: u32, _input: &[u8]) -> Option { // Simple example: one method with selector 0x12345678 match selector { 0x12345678 => Some(Ok(Vec::new())), _ => None, // Unknown selector } } fn receive(_storage: &mut MyContract) -> Option>> { None // No receive function } fn fallback(_storage: &mut MyContract, _calldata: &[u8]) -> Option { None // No fallback function } fn constructor(_storage: &mut MyContract, _calldata: &[u8]) -> Option { None // No constructor } } ``` ### Step 3: Define entrypoint ```rust #[no_mangle] pub extern "C" fn user_entrypoint(len: usize) -> usize { let host = VM { host: stylus_sdk::host::WasmVM{} }; // Reentrancy check if host.msg_reentrant() { return 1; } // Reference pay_for_memory_grow host.pay_for_memory_grow(0); // Read input let input = host.read_args(len); // Route the call let (data, status) = match stylus_sdk::abi::router_entrypoint::(input, host.clone()) { Ok(data) => (data, 0), Err(data) => (data, 1), }; // Flush storage host.flush_cache(false); // Write result host.write_result(&data); status } ``` ## Function selectors Function selectors are 4-byte identifiers computed from the function signature. ### Computing selectors ```rust use stylus_sdk::function_selector; // Manual computation const MY_FUNCTION: [u8; 4] = function_selector!("myFunction"); // With parameters const TRANSFER: [u8; 4] = function_selector!("transfer", Address, U256); // Constructor selector const CONSTRUCTOR_SELECTOR: u32 = u32::from_be_bytes(function_selector!("constructor")); ``` ### Using in `Router` ```rust impl Router for MyContract { type Storage = MyContract; fn route(_storage: &mut MyContract, selector: u32, input: &[u8]) -> Option { const GET_VALUE: u32 = u32::from_be_bytes(function_selector!("getValue")); const SET_VALUE: u32 = u32::from_be_bytes(function_selector!("setValue", U256)); match selector { GET_VALUE => { // Return encoded U256 value let value = U256::from(42); Some(Ok(value.to_be_bytes::<32>().to_vec())) } SET_VALUE => { // Decode input and set value if input.len() >= 32 { // Process set_value logic Some(Ok(Vec::new())) } else { Some(Err(Vec::new())) } } _ => None, } } fn receive(_storage: &mut MyContract) -> Option>> { None } fn fallback(_storage: &mut MyContract, _calldata: &[u8]) -> Option { None } fn constructor(_storage: &mut MyContract, _calldata: &[u8]) -> Option { None } } ``` ## Implementing special functions ### Receive function Handles plain ETH transfers (no calldata): ```rust fn receive(storage: &mut MyContract) -> Option>> { // Access msg_value via storage.vm().msg_value() // Must return Ok(()) for success Some(Ok(())) } ``` ### Fallback function Handles unknown selectors or when no receive is defined: ```rust fn fallback(storage: &mut MyContract, calldata: &[u8]) -> Option { // Can access full calldata // Return Some to handle, None to revert Some(Ok(Vec::new())) } ``` ### Constructor Called once during deployment with `CONSTRUCTOR_SELECTOR`: ```rust fn constructor(storage: &mut MyContract, calldata: &[u8]) -> Option { // Initialize contract state // calldata contains constructor parameters Some(Ok(Vec::new())) } ``` ## Complete minimal example Here's a complete working minimal contract: ```rust #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; use alloc::vec::Vec; use core::borrow::BorrowMut; use stylus_sdk::{ abi::Router, alloy_primitives::U256, host::VM, storage::StorageType, ArbResult, function_selector, }; use stylus_core::{storage::TopLevelStorage, ValueDenier}; // Contract storage pub struct MinimalContract; // Mark as top-level storage unsafe impl TopLevelStorage for MinimalContract {} // Implement StorageType impl StorageType for MinimalContract { type Wraps<'a> = &'a Self where Self: 'a; type WrapsMut<'a> = &'a mut Self where Self: 'a; unsafe fn new(_slot: U256, _offset: u8, _host: VM) -> Self { MinimalContract } fn load<'s>(self) -> Self::Wraps<'s> { &self } fn load_mut<'s>(self) -> Self::WrapsMut<'s> { &mut self } } // Implement ValueDenier (for non-payable check) impl ValueDenier for MinimalContract { fn deny_value(&self, _method_name: &str) -> Result<(), Vec> { Ok(()) // Allow all for simplicity } } // Implement BorrowMut impl BorrowMut for MinimalContract { fn borrow_mut(&mut self) -> &mut MinimalContract { self } } // Implement Router impl Router for MinimalContract { type Storage = MinimalContract; fn route(_storage: &mut MinimalContract, selector: u32, _input: &[u8]) -> Option { const HELLO: u32 = u32::from_be_bytes(function_selector!("hello")); match selector { HELLO => Some(Ok(Vec::new())), _ => None, } } fn receive(_storage: &mut MinimalContract) -> Option>> { None } fn fallback(_storage: &mut MinimalContract, _calldata: &[u8]) -> Option { Some(Ok(Vec::new())) // Accept all unknown calls } fn constructor(_storage: &mut MinimalContract, _calldata: &[u8]) -> Option { Some(Ok(Vec::new())) } } // Define user_entrypoint #[no_mangle] pub extern "C" fn user_entrypoint(len: usize) -> usize { let host = VM { host: stylus_sdk::host::WasmVM{} }; if host.msg_reentrant() { return 1; } host.pay_for_memory_grow(0); let input = host.read_args(len); let (data, status) = match stylus_sdk::abi::router_entrypoint::(input, host.clone()) { Ok(data) => (data, 0), Err(data) => (data, 1), }; host.flush_cache(false); host.write_result(&data); status } ``` ## Why use high-level macros? While minimal contracts are educational, the `#[entrypoint]` and `#[public]` macros provide: 1. **Automatic selector generation** from method names 2. **Type-safe parameter encoding/decoding** using Alloy types 3. **Solidity ABI export** for interoperability 4. **Storage trait implementations** with caching 5. **Error handling** with `Result` types 6. **Payable checks** for ETH-receiving functions 7. **Reentrancy protection** by default **Recommended approach:** ```rust // Use macros for production contracts #[storage] #[entrypoint] pub struct MyContract { value: StorageU256, } #[public] impl MyContract { pub fn get_value(&self) -> U256 { self.value.get() } pub fn set_value(&mut self, value: U256) { self.value.set(value); } } ``` This generates all the low-level code automatically while providing a clean, type-safe interface. ## Advanced use cases ### Custom routing logic Implement custom routing for multi-contract systems: ```rust impl Router for MultiContract { type Storage = MultiContract; fn route(storage: &mut MultiContract, selector: u32, input: &[u8]) -> Option { // Route to different modules based on selector range match selector { 0x00000000..=0x0fffffff => ModuleA::route(storage, selector, input), 0x10000000..=0x1fffffff => ModuleB::route(storage, selector, input), _ => None, } } // ... other methods } ``` ### Custom entrypoint logic Add custom logic before/after routing: ```rust #[no_mangle] pub extern "C" fn user_entrypoint(len: usize) -> usize { let host = VM { host: stylus_sdk::host::WasmVM{} }; // Custom pre-processing let start_gas = host.evm_gas_left(); // Standard entrypoint logic if host.msg_reentrant() { return 1; } host.pay_for_memory_grow(0); let input = host.read_args(len); let (data, status) = match stylus_sdk::abi::router_entrypoint::(input, host.clone()) { Ok(data) => (data, 0), Err(data) => (data, 1), }; // Custom post-processing let gas_used = start_gas - host.evm_gas_left(); // Log or handle gas usage host.flush_cache(false); host.write_result(&data); status } ``` ## Debugging tips ### Enable debug mode ```rust #[cfg(feature = "debug")] use stylus_sdk::console; fn route(storage: &mut MyContract, selector: u32, input: &[u8]) -> Option { #[cfg(feature = "debug")] console!("Selector: {:08x}", selector); // Routing logic... } ``` ### Check selector computation ```rust #[test] fn test_selectors() { use stylus_sdk::function_selector; let hello = u32::from_be_bytes(function_selector!("hello")); assert_eq!(hello, 0x19ff1d21); // Compare with Solidity: bytes4(keccak256("hello()")) } ``` ## See also * [Contracts](/stylus/fundamentals/contracts.md): High-level contract development * [Global Variables](/stylus/fundamentals/global-variables-and-functions.md): VM context methods * [Storage Types](/stylus/fundamentals/data-types/storage.md): Persistent storage --- > For a complete page index, fetch # Recommended libraries (Rust crates) ## Using public Rust crates Rust provides a package registry at [crates.io](https://crates.io/), which lets developers conveniently access a plethora of open source libraries to utilize as dependencies in their code. Stylus Rust contracts can take advantage of these crates to simplify their development workflow. While **crates.io** is a fantastic resource, many of these libraries were not designed with the constraints of a blockchain environment in mind. Some produce large binaries that exceed the decompressed WASM size limit (`MaxWasmSize`) for Stylus programs on Arbitrum — a chain-configurable ArbOS parameter that defaults to 128 KB, raised to 256 KB at ArbOS 60 and later. Many also take advantage of unsupported features such as: * Random numbers * Multi threading * Floating point numbers and operations Using the standard Rust library often bloats contract sizes beyond the maximum size. For this reason, libraries designated as `no_std` are typically much stronger candidates for usage as a smart contract dependency. **crates.io** has a special tag for marking crates as `no_std`; however, it's not universally used. Still, it can be a good starting point for locating supported libraries. See ["No standard library"](https://crates.io/categories/no-std) crates for more details. Library compatibility checklist A Rust library is compatible with Stylus if it meets all of the following: * **Single-threaded**: no `async`/`await`, no thread spawning, no concurrent execution primitives. * **No floating-point**: `f32` and `f64` are not supported by the Stylus VM. * **No randomness**: no entropy is available onchain; libraries that depend on `rand` or system entropy will not work. * **`no_std`-friendly**: prefer crates that compile without the standard library; the `std` lib pulls in OS-level features that are unavailable and inflates the binary. * **WASM-compatible target**: must build for `wasm32-unknown-unknown`. ## Curated crates To save developers time on smart contract development for common dependencies, we've curated a list of crates and utilities that we found helpful. Keep in mind that we have not audited this code, and you should always be mindful about pulling dependencies into your codebase, whether they've been audited or not. We provide this list for you to use at your discretion and risk. * [`alloy-primitives`](https://crates.io/crates/alloy-primitives): Core Ethereum primitive types (`U256`, `Address`, `B256`, and more). Re-exported by the Stylus SDK, which currently pins the 1.x line (`1.5.7`), and the foundation for nearly every Stylus contract * [`alloy-sol-types`](https://crates.io/crates/alloy-sol-types): Solidity type and ABI encoding/decoding via the `sol!` macro. Used by the Stylus SDK (same 1.x line) for typed calls, events, and errors * [`rust_decimal`](https://crates.io/crates/rust_decimal): Decimal number implementation written in pure Rust. Suitable for financial and fixed-precision calculations * [`special`](https://crates.io/crates/special): The package provides special functions, which are mathematical functions with special names due to their common usage, such as `sin`, `ln`, `tan`, etc. * [`hashbrown`](https://crates.io/crates/hashbrown): Rust port of Google's SwissTable hash map * [`time`](https://crates.io/crates/time): Date and time library * [`hex`](https://crates.io/crates/hex): Encoding and decoding data into/from hexadecimal representation We'll be adding more libraries to this list as we find them. Feel free to suggest an edit if you know of any great crates that would be generally useful here. --- > For a complete page index, fetch # Rust to Solidity differences Stylus introduces a new paradigm for writing smart contracts on Arbitrum using Rust and other WebAssembly-compatible languages. While Stylus contracts maintain full interoperability with Solidity contracts, there are important differences in how you structure and write code. This guide helps Solidity developers understand these differences. ## Language and syntax ### Contract structure **Solidity:** ```solidity contract MyContract { uint256 private value; address public owner; constructor(uint256 initialValue) { value = initialValue; owner = msg.sender; } function setValue(uint256 newValue) public { value = newValue; } } ``` **Stylus (Rust):** ```rust use stylus_sdk::prelude::*; use stylus_sdk::alloy_primitives::{Address, U256}; #[storage] #[entrypoint] pub struct MyContract { value: StorageU256, owner: StorageAddress, } #[public] impl MyContract { #[constructor] pub fn constructor(&mut self, initial_value: U256) { self.value.set(initial_value); self.owner.set(self.vm().msg_sender()); } pub fn set_value(&mut self, new_value: U256) { self.value.set(new_value); } } ``` ### Key structural differences 1. **Attributes over keywords**: Stylus uses Rust attributes (`#[storage]`, `#[entrypoint]`, `#[public]`) instead of Solidity keywords 2. **Explicit storage types**: Storage variables use special types like `StorageU256`, `StorageAddress` 3. **Getter/setter pattern**: Storage access requires explicit `.get()` and `.set()` calls 4. **Module system**: Rust uses `mod` and `use` for imports instead of `import` ## Function visibility and state mutability ### Visibility **Solidity:** ```solidity function publicFunc() public {} function externalFunc() external {} function internalFunc() internal {} function privateFunc() private {} ``` **Stylus:** ```rust #[public] impl MyContract { // Public external functions pub fn public_func(&self) {} // Internal functions (not in #[public] block) fn internal_func(&self) {} // Private functions fn private_func(&self) {} } ``` In Stylus: * Functions in `#[public]` blocks are externally callable * Regular `pub fn` outside `#[public]` blocks are internal * Non-pub functions are private to the module ### State mutability **Solidity:** ```solidity function viewFunc() public view returns (uint256) {} function pureFunc() public pure returns (uint256) {} function payableFunc() public payable {} ``` **Stylus:** ```rust #[public] impl MyContract { // View function (immutable reference) pub fn view_func(&self) -> U256 { self.value.get() } // Pure function (no self reference) pub fn pure_func(a: U256, b: U256) -> U256 { a + b } // Payable function #[payable] pub fn payable_func(&mut self) { // Can receive Ether } // Write function (mutable reference) pub fn write_func(&mut self) { self.value.set(U256::from(42)); } } ``` State mutability in Stylus is determined by: * `&self` → View (read-only) * `&mut self` → Write (can modify storage) * No `self` → Pure (no storage access) * `#[payable]` → Can receive Ether ## Constructors **Solidity:** ```solidity constructor(uint256 initialValue) { value = initialValue; } ``` **Stylus:** ```rust #[public] impl MyContract { #[constructor] pub fn constructor(&mut self, initial_value: U256) { self.value.set(initial_value); } } ``` Key differences: * Use `#[constructor]` attribute * Constructor name is always `constructor` * Can be marked `#[payable]` if needed * Called only once during deployment * Each contract struct can have only one constructor ## Modifiers Solidity modifiers don't exist in Stylus. Instead, use regular Rust patterns. **Solidity:** ```solidity modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } function sensitiveFunction() public onlyOwner { // Function logic } ``` **Stylus:** ```rust impl MyContract { fn only_owner(&self) -> Result<(), Vec> { if self.owner.get() != self.vm().msg_sender() { return Err(b"Not owner".to_vec()); } Ok(()) } } #[public] impl MyContract { pub fn sensitive_function(&mut self) -> Result<(), Vec> { self.only_owner()?; // Function logic Ok(()) } } ``` Or using custom errors: ```rust sol! { error Unauthorized(); } #[derive(SolidityError)] pub enum MyErrors { Unauthorized(Unauthorized), } impl MyContract { fn only_owner(&self) -> Result<(), MyErrors> { if self.owner.get() != self.vm().msg_sender() { return Err(MyErrors::Unauthorized(Unauthorized {})); } Ok(()) } } ``` ## Fallback and receive functions **Solidity:** ```solidity receive() external payable { // Handle plain Ether transfers } fallback() external payable { // Handle unmatched calls } ``` **Stylus:** ```rust #[public] impl MyContract { #[receive] #[payable] pub fn receive(&mut self) -> Result<(), Vec> { // Handle plain Ether transfers Ok(()) } #[fallback] #[payable] pub fn fallback(&mut self, calldata: &[u8]) -> ArbResult { // Handle unmatched calls Ok(Vec::new()) } } ``` Key differences: * Use `#[receive]` and `#[fallback]` attributes * Receive function takes no parameters * Fallback function receives calldata as a parameter * Both return `Result` types ## Events **Solidity:** ```solidity event Transfer(address indexed from, address indexed to, uint256 value); function transfer() public { emit Transfer(msg.sender, recipient, amount); } ``` **Stylus:** ```rust sol! { event Transfer(address indexed from, address indexed to, uint256 value); } #[public] impl MyContract { pub fn transfer(&mut self, recipient: Address, amount: U256) { self.vm().log(Transfer { from: self.vm().msg_sender(), to: recipient, value: amount, }); } } ``` Key differences: * Define events in `sol!` macro * Emit using `self.vm().log()` * Up to 3 parameters can be indexed * Can also use `raw_log()` for custom logging ## Error handling **Solidity:** ```solidity error InsufficientBalance(uint256 requested, uint256 available); function withdraw(uint256 amount) public { if (balance < amount) { revert InsufficientBalance(amount, balance); } } ``` **Stylus:** ```rust sol! { error InsufficientBalance(uint256 requested, uint256 available); } #[derive(SolidityError)] pub enum MyErrors { InsufficientBalance(InsufficientBalance), } #[public] impl MyContract { pub fn withdraw(&mut self, amount: U256) -> Result<(), MyErrors> { let balance = self.balance.get(); if balance < amount { return Err(MyErrors::InsufficientBalance(InsufficientBalance { requested: amount, available: balance, })); } Ok(()) } } ``` Key differences: * Define errors in `sol!` macro * Create error enum with `#[derive(SolidityError)]` * Return `Result` * Use Rust's `?` operator for error propagation ## Inheritance **Solidity:** ```solidity contract Base { function baseFoo() public virtual {} } contract Derived is Base { function baseFoo() public override {} } ``` **Stylus:** ```rust #[public] trait IBase { fn base_foo(&self); } #[storage] struct Base {} #[public] impl IBase for Base { fn base_foo(&self) { // Implementation } } #[storage] #[entrypoint] struct Derived { base: Base, } #[public] #[implements(IBase)] impl Derived {} #[public] impl IBase for Derived { fn base_foo(&self) { // Override implementation } } ``` Key differences: * Use Rust traits for interfaces * Composition through storage fields * Use `#[implements()]` to expose inherited interfaces * No `virtual` or `override` keywords ## Storage ### Storage slots **Solidity:** ```solidity uint256 public value; mapping(address => uint256) public balances; uint256[] public items; ``` **Stylus:** ```rust #[storage] pub struct MyContract { value: StorageU256, balances: StorageMap, items: StorageVec, } ``` ### Storage access **Solidity:** ```solidity value = 42; uint256 x = value; balances[user] = 100; ``` **Stylus:** ```rust self.value.set(U256::from(42)); let x = self.value.get(); self.balances.setter(user).set(U256::from(100)); ``` Key differences: * Explicit storage types (`Storage*`) * Must use `.get()` and `.set()` * Maps use `.setter()` for write access * Storage layout is compatible with Solidity ## Constants and immutables **Solidity:** ```solidity uint256 public constant MAX_SUPPLY = 1000000; address public immutable OWNER; constructor() { OWNER = msg.sender; } ``` **Stylus:** ```rust const MAX_SUPPLY: u64 = 1000000; #[storage] #[entrypoint] pub struct MyContract { owner: StorageAddress, // Set in constructor } #[public] impl MyContract { #[constructor] pub fn constructor(&mut self) { self.owner.set(self.vm().msg_sender()); } } ``` Key differences: * Use Rust `const` for constants * No direct equivalent to `immutable` (use storage set once in constructor) * Constants can be defined outside structs ## Type system ### Integer types | Solidity | Stylus (Rust) | Notes | | -------------------- | ---------------------- | ------------------------------------- | | `uint8` to `uint256` | `u8` to `u128`, `U256` | Native Rust types or Alloy primitives | | `int8` to `int256` | `i8` to `i128`, `I256` | Signed integers | | `address` | `Address` | 20-byte addresses | | `bytes` | `Bytes` | Dynamic bytes | | `bytesN` | `FixedBytes` | Fixed-size bytes | | `string` | `String` | UTF-8 strings | ### Arrays and mappings | Solidity | Stylus (Rust) | Notes | | ----------------------------- | ------------------------------------------------------------- | -------------- | | `uint256[]` | `Vec` (memory)
`StorageVec` (storage) | Dynamic arrays | | `uint256[5]` | `[U256; 5]` | Fixed arrays | | `mapping(address => uint256)` | `StorageMap` | Key-value maps | ## Global variables and functions ### Block and transaction properties | Solidity | Stylus (Rust) | | ----------------- | ----------------------------- | | `msg.sender` | `self.vm().msg_sender()` | | `msg.value` | `self.vm().msg_value()` | | `msg.data` | Access through calldata | | `tx.origin` | `self.vm().tx_origin()` | | `tx.gasprice` | `self.vm().tx_gas_price()` | | `block.number` | `self.vm().block_number()` | | `block.timestamp` | `self.vm().block_timestamp()` | | `block.basefee` | `self.vm().block_basefee()` | | `block.coinbase` | `self.vm().block_coinbase()` | ### Cryptographic functions | Solidity | Stylus (Rust) | | ----------------- | ---------------------------------- | | `keccak256(data)` | `self.vm().native_keccak256(data)` | | `sha256(data)` | Use external crate | | `ecrecover(...)` | Use `crypto::recover()` | ## External calls **Solidity:** ```solidity (bool success, bytes memory data) = address.call{value: amount}(data); ``` **Stylus:** ```rust use stylus_sdk::call::RawCall; let result = unsafe { RawCall::new(self.vm()) .value(amount) .call(address, &data) }; ``` Key differences: * Use `RawCall` for raw calls * Calls are `unsafe` in Rust * Use type-safe interfaces when possible via `sol_interface!` ## Contract deployment **Solidity:** ```solidity new MyContract{value: amount}(arg1, arg2); ``` **Stylus:** ```rust use stylus_sdk::deploy::RawDeploy; let contract_address = unsafe { RawDeploy::new() .salt(salt) .deploy(self.vm(), &bytecode, amount)? }; ``` ## Assembly **Solidity:** ```solidity assembly { let x := mload(0x40) sstore(0, x) } ``` **Stylus:** Stylus does not support inline assembly. Instead: * Use hostio functions for low-level operations * Use Rust's `unsafe` blocks when necessary * Direct memory manipulation through safe Rust APIs ## Features not in Stylus 1. **No inline assembly**: Use hostio or safe Rust instead 2. **No `selfdestruct`**: Deprecated in Ethereum, not available in Stylus 3. **No `delegatecall` from storage**: Available but requires careful use 4. **No modifier syntax**: Use regular functions 5. **No multiple inheritance complexity**: Use trait-based composition ## Features unique to Stylus 1. **Rust's type system**: Strong compile-time guarantees 2. **Zero-cost abstractions**: No overhead for safe code patterns 3. **Cargo ecosystem**: Access to thousands of Rust crates 4. **Memory safety**: Rust's borrow checker prevents common bugs 5. **Better performance**: Wasm execution can be more efficient 6. **Testing framework**: Use Rust's built-in testing with `TestVM` ## Memory and gas costs ### Memory management * **Solidity**: Automatic memory management with gas costs for allocation * **Stylus**: Manual control with Rust's ownership system, more efficient memory usage ### Gas efficiency Stylus programs typically use less gas than equivalent Solidity: * More efficient Wasm execution * Better compiler optimizations * Fine-grained control over allocations ## Development workflow ### Compilation **Solidity:** ```shell solc --bin --abi MyContract.sol ``` **Stylus:** ```shell cargo stylus build ``` ### Testing **Solidity:** ```javascript // Hardhat or Foundry tests ``` **Stylus:** ```rust #[cfg(test)] mod tests { use super::*; use stylus_sdk::testing::*; #[test] fn test_function() { let vm = TestVM::default(); let mut contract = MyContract::from(&vm); // Test logic } } ``` ### Deployment Both use similar deployment processes but Stylus requires an [activation step](/stylus/concepts/activation.md) for new programs. ## Interoperability Stylus and Solidity contracts can fully interact: * Stylus can call Solidity contracts * Solidity can call Stylus contracts * Same ABI encoding/decoding * Share storage layout compatibility Example calling Solidity from Stylus: ```rust sol_interface! { interface IToken { function transfer(address to, uint256 amount) external returns (bool); } } #[public] impl MyContract { pub fn call_token(&self, token: Address, recipient: Address, amount: U256) -> Result> { let token_contract = IToken::new(token); let result = token_contract.transfer(self.vm(), recipient, amount)?; Ok(result) } } ``` ## Best practices for transitioning 1. **Think in Rust patterns**: Don't translate Solidity directly, use idiomatic Rust 2. **Leverage the type system**: Use Rust's types to prevent bugs at compile time 3. **Use composition over inheritance**: Prefer traits and composition 4. **Handle errors explicitly**: Use `Result` types and the `?` operator 5. **Write tests in Rust**: Take advantage of `TestVM` for unit testing 6. **Read existing examples**: Study the stylus-sdk-rs examples directory 7. **Start small**: Convert simple contracts first to learn the patterns ## Resources * [Stylus SDK Documentation](https://docs.arbitrum.io/stylus/reference/stylus-sdk) * [stylus-sdk-rs Repository](https://github.com/OffchainLabs/stylus-sdk-rs) * [Example Contracts](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/examples) * [Rust Book](https://doc.rust-lang.org/book/) for learning Rust --- > For a complete page index, fetch # Gas optimization best practices Stylus contracts can offer significant gas savings compared to Solidity for compute-heavy operations, and following the optimization best practices below can reduce costs even further. Exact savings depend on the workload, so benchmark your own contract. ## Why Stylus is cheaper ![Gas Comparison](/assets/images/stylus-gas-comparison-bfeee7698665b66b3ff7be634b5951b0.png) *Figure: Stylus WASM executes natively, avoiding EVM interpretation overhead.* ### Performance comparison | Operation | Solidity (EVM) | Stylus (WASM) | Relative savings | | ------------------------------------- | ------------------------ | ---------------------- | ----------------------- | | Compute-heavy loops | High | Very low | \~50–100x | | Signature verification (`ecrecover`) | \~3,000 gas (precompile) | \~300 gas | \~10x | | Memory operations (`MLOAD`/`MSTORE`) | \~3 gas/word | \~0.3 gas/word | \~10x | | Keccak256 hashing | 30 gas + 6 gas/word | native `keccak` hostio | Varies (small per byte) | | Storage operations (`SLOAD`/`SSTORE`) | EVM cost | Same EVM cost | None (1x) | The EVM-side costs are fixed protocol prices: `ecrecover` = 3,000 gas, `MLOAD`/`MSTORE` = 3 gas/word, and `KECCAK256` = 30 gas + 6 gas per 32-byte word. The Stylus-side figures and the multipliers are directional — drawn from Offchain Labs' Stylus benchmarks — and vary with workload, input size, and ArbOS version. Benchmark your own contract to get numbers you can rely on. Note that Keccak256 is already cheap per byte on the EVM, so hashing is not a headline saving; Stylus' large wins come from compute-heavy logic, memory, and native cryptography. **Key insight**: [Storage operations](/stylus/advanced/hostio-exports.md#storage-operations) map to the same underlying EVM `SLOAD`/`SSTORE` costs in Stylus as in Solidity, so they are not where Stylus saves gas. Optimize by reducing storage access and maximizing compute efficiency. ## Storage optimization ### 1. Minimize storage reads ```rust // ❌ Bad: Multiple storage reads pub fn calculate_bad(&self, iterations: u32) -> U256 { let mut result = U256::ZERO; for i in 0..iterations { // Reads from storage every iteration! result += self.multiplier.get(); } result } // ✅ Good: Cache storage value pub fn calculate_good(&self, iterations: u32) -> U256 { // Read once, use many times let multiplier = self.multiplier.get(); let mut result = U256::ZERO; for i in 0..iterations { result += multiplier; } result } ``` **Gas impact**: Storage reads map to EVM `SLOAD` costs, where a cold slot (first access in a transaction, per EIP-2929) is far more expensive than a warm one. The SDK also caches storage, so repeated reads of the same slot within a single call are cheap. Caching the value in a local variable, as shown above, avoids repeated `SLOAD` work and can save significant gas in large loops. ### 2. Batch storage writes ```rust // ❌ Bad: Multiple separate writes pub fn update_user_bad(&mut self, addr: Address, amount: U256, active: bool) { self.balances.setter(addr).set(amount); self.last_update.setter(addr).set(U256::from(self.vm().block_timestamp())); self.is_active.setter(addr).set(active); } // ✅ Good: Combine into struct sol_storage! { pub struct UserData { uint256 balance; uint256 last_update; bool is_active; } pub struct OptimizedContract { mapping(address => UserData) users; } } pub fn update_user_good(&mut self, addr: Address, amount: U256, active: bool) { // Read host state before taking the storage setter to avoid borrowing // `self` both mutably (the setter) and immutably (`self.vm()`). let timestamp = U256::from(self.vm().block_timestamp()); let mut user = self.users.setter(addr); user.balance.set(amount); user.last_update.set(timestamp); user.is_active.set(active); // Grouped fields share contiguous slots instead of three unrelated slots } ``` ### 3. Use appropriate data types ```rust // ❌ Bad: Oversized types sol_storage! { pub struct Wasteful { StorageU256 tiny_counter; // Only needs u8 StorageU256 timestamp; // Only needs u64 StorageU256 percentage; // Only needs u16 } } // ✅ Good: Right-sized types sol_storage! { pub struct Efficient { StorageU8 tiny_counter; // Saves 31 bytes StorageU64 timestamp; // Saves 24 bytes StorageU16 percentage; // Saves 30 bytes } } ``` **Note**: While smaller types save storage space, they don't reduce gas for individual storage operations. The benefit comes from packing multiple small values in one slot (if your storage layout supports it). ### 4. Delete unused storage ```rust pub fn cleanup(&mut self, addr: Address) -> Result<(), Vec> { let balance = self.balances.get(addr); if balance != U256::ZERO { return Err(b"Balance not zero".to_vec()); } // ✅ Deleting storage refunds gas self.balances.delete(addr); self.metadata.delete(addr); Ok(()) } ``` **Gas refund**: Clearing a storage slot (setting it back to zero) triggers an `SSTORE` refund. Since EIP-3529 this refund is capped at 4,800 gas per cleared slot, and the total refund for a transaction cannot exceed one fifth (20%) of the gas the transaction used. ## Memory optimization ### 1. Avoid unnecessary clones ```rust use alloy_primitives::Bytes; // ❌ Bad: Unnecessary cloning pub fn process_data_bad(&self, data: Bytes) -> Bytes { let copy = data.clone(); // Expensive memory allocation copy } // ✅ Good: Use references pub fn process_data_good(&self, data: &Bytes) -> &Bytes { data // No clone needed } // ✅ Good: Move when possible pub fn consume_data(mut data: Bytes) -> Bytes { data.extend_from_slice(&[1, 2, 3]); data // Ownership moved, no clone } ``` ### 2. Use iterators efficiently ```rust // ❌ Bad: Collect into vector unnecessarily pub fn sum_bad(&self, values: Vec) -> U256 { let filtered: Vec = values .iter() .filter(|v| **v > U256::ZERO) .copied() .collect(); // Allocates new vector filtered.iter().sum() } // ✅ Good: Chain iterators pub fn sum_good(&self, values: Vec) -> U256 { values .iter() .filter(|v| **v > U256::ZERO) .sum() // No intermediate allocation } ``` ### 3. Reuse allocations ```rust // ✅ Reuse buffers for repeated operations pub fn process_batch(&mut self, items: Vec) -> Vec { let mut buffer = Vec::with_capacity(items.len()); for item in items { buffer.clear(); // Reuse allocation buffer.extend_from_slice(&item); // Process buffer... } buffer } ``` ## Computation optimization ### 1. Use Stylus for compute-heavy operations ```rust // ✅ Stylus excels at complex computation pub fn verify_merkle_proof( &self, leaf: [u8; 32], proof: Vec<[u8; 32]>, root: [u8; 32] ) -> bool { let mut computed_hash = leaf; // This loop is typically much cheaper in Stylus than Solidity for proof_element in proof { // keccak256 returns a B256; `.0` extracts the [u8; 32] array computed_hash = if computed_hash <= proof_element { keccak256([computed_hash, proof_element].concat()).0 } else { keccak256([proof_element, computed_hash].concat()).0 }; } computed_hash == root } ``` **Why it's faster**: Native WASM execution avoids EVM interpretation overhead, which makes compute-heavy loops cheaper. Benchmark to quantify the savings for your specific workload. ### 2. Optimize hot paths ```rust // ✅ Hint the compiler to inline small, frequently-called helpers. // `#[inline(always)]` is a hint, not a guarantee; measure before relying on it. #[inline(always)] pub fn is_valid_amount(&self, amount: U256) -> bool { amount > U256::ZERO && amount <= self.max_amount.get() } // Use in hot path pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), Vec> { if !self.is_valid_amount(amount) { return Err(b"Invalid amount".to_vec()); } // Transfer logic... Ok(()) } ``` ### 3. Avoid redundant checks ```rust // ❌ Bad: Redundant zero check pub fn add_to_balance_bad(&mut self, addr: Address, amount: U256) -> Result<(), Vec> { if amount == U256::ZERO { return Err(b"Amount must be positive".to_vec()); } let current = self.balances.get(addr); if current + amount <= current { // Redundant if amount > 0 return Err(b"Overflow".to_vec()); } self.balances.setter(addr).set(current + amount); Ok(()) } // ✅ Good: Single overflow check covers both pub fn add_to_balance_good(&mut self, addr: Address, amount: U256) -> Result<(), Vec> { let current = self.balances.get(addr); let new_balance = current .checked_add(amount) .ok_or(b"Overflow or invalid amount".to_vec())?; self.balances.setter(addr).set(new_balance); Ok(()) } ``` ## Function call optimization ### 1. Minimize cross-contract calls ```rust // The interface is declared with sol_interface!: // sol_interface! { // interface IOracle { // function getPrice(address token) external view returns (uint256); // function getDecimals(address token) external view returns (uint256); // function getTimestamp(address token) external view returns (uint256); // function getPriceData(address token) // external view returns (uint256, uint256, uint256); // } // } // ❌ Bad: Multiple external calls pub fn get_price_bad(&self, token: Address) -> Result> { let oracle = IOracle::new(self.oracle_address.get()); let price = oracle.get_price(self.vm(), Call::new(), token)?; let _decimals = oracle.get_decimals(self.vm(), Call::new(), token)?; // Second call let _timestamp = oracle.get_timestamp(self.vm(), Call::new(), token)?; // Third call Ok(price) } // ✅ Good: Batch external calls pub fn get_price_good(&self, token: Address) -> Result<(U256, U256, U256), Vec> { let oracle = IOracle::new(self.oracle_address.get()); // Single call returns all data Ok(oracle.get_price_data(self.vm(), Call::new(), token)?) } ``` **Gas impact**: Each external call has overhead. Batching reduces cost significantly. ### 2. Use internal functions ```rust // ✅ Extract common logic to internal functions impl MyContract { // Internal helper (no ABI encoding overhead) fn internal_validate(&self, addr: Address, amount: U256) -> Result<(), Vec> { if addr.is_zero() { return Err(b"Invalid address".to_vec()); } if amount == U256::ZERO { return Err(b"Invalid amount".to_vec()); } Ok(()) } } #[public] impl MyContract { // Public functions use internal helper pub fn deposit(&mut self, amount: U256) -> Result<(), Vec> { self.internal_validate(self.vm().msg_sender(), amount)?; // Deposit logic... Ok(()) } pub fn withdraw(&mut self, amount: U256) -> Result<(), Vec> { self.internal_validate(self.vm().msg_sender(), amount)?; // Withdraw logic... Ok(()) } } ``` ## Event optimization ### 1. Use indexed parameters wisely ```rust sol! { // ✅ Index frequently-queried fields (max 3 indexed) event Transfer( address indexed from, address indexed to, uint256 value // Not indexed - saves gas ); // ❌ Bad: Too many indexed parameters event TooManyIndexed( address indexed from, address indexed to, uint256 indexed amount, // Expensive to index uint256 indexed timestamp // 4th indexed param - not allowed! ); } ``` **Gas impact**: Each additional log topic (indexed parameter) adds to the cost of emitting the event, so only index fields you will actually filter by. The exact per-topic cost is set by EVM `LOG` opcode pricing; verify against current gas-schedule values if you need a precise figure. ### 2. Batch events when possible ```rust // ✅ Emit single event for batch operation sol! { event BatchTransfer( address indexed from, address[] to, uint256[] amounts ); } pub fn batch_transfer( &mut self, recipients: Vec
, amounts: Vec ) -> Result<(), Vec> { // Process transfers... // Single event instead of N events self.vm().log(BatchTransfer { from: self.vm().msg_sender(), to: recipients, amounts, }); Ok(()) } ``` ## Binary size optimization Smaller WASM binaries cost less to deploy and activate. ### 1. Optimize compilation flags ```toml # Cargo.toml [profile.release] opt-level = "z" # Optimize for size lto = true # Link-time optimization codegen-units = 1 # Better optimization strip = true # Remove debug symbols panic = "abort" # Smaller panic handling ``` ### 2. Avoid large dependencies ```rust // ❌ Bad: Heavy dependency for simple task use fancy_math_library::complex_sqrt; // Adds 50KB to binary pub fn calculate(&self, value: U256) -> U256 { complex_sqrt(value) // Using 1% of library } // ✅ Good: Implement simple operations yourself (sketch) pub fn simple_sqrt(&self, value: U256) -> U256 { // Custom implementation adds minimal binary size. // Provide a real algorithm (Newton's method or similar) here. unimplemented!("integer square root") } ``` ### 3. Check binary size and optimize the build ```shell # Compile and report the activated contract size cargo stylus check ``` `cargo stylus` does not expose `--optimize` flags. Control binary size through your Cargo release profile (see "Optimize compilation flags" above) and, if you need further shrinking, by running `wasm-opt` from [Binaryen](https://github.com/WebAssembly/binaryen) on the compiled `.wasm`. See [optimizing binaries](/stylus/how-tos/optimizing-binaries.md) for details. ## Gas measurement ### 1. Test behavior with the unit-test VM The `TestVM` from `stylus_sdk::testing` runs your contract logic off-chain so you can assert behavior quickly. It does not expose a gas meter (there is no `gas_left()` getter on `TestVM`), so use it to verify correctness, not to measure gas. ```rust #[cfg(test)] mod gas_tests { use super::*; use stylus_sdk::testing::*; #[test] fn update_user_persists() { let vm = TestVM::default(); let mut contract = OptimizedContract::from(&vm); let user = Address::from([0x11; 20]); contract.update_user_good(user, U256::from(100), true); let stored = contract.users.get(user); assert_eq!(stored.balance.get(), U256::from(100)); assert!(stored.is_active.get()); } } ``` ### 2. Measure gas on a live endpoint To compare the gas cost of two implementations, deploy each to a Stylus dev node and measure the gas used by real transactions (for example with `cast estimate` or by reading the gas used from the transaction receipt). On-chain measurement is the reliable way to compare optimization patterns; the unit-test VM cannot report gas. ## Optimization checklist Before deploying, verify you've: * [ ] Minimized storage reads and writes * [ ] Cached frequently-accessed storage values * [ ] Used appropriate data types * [ ] Deleted unused storage for gas refunds * [ ] Avoided unnecessary clones and allocations * [ ] Optimized hot code paths * [ ] Minimized cross-contract calls * [ ] Used indexed events sparingly * [ ] Optimized WASM binary size * [ ] Profiled gas usage for critical functions * [ ] Compared against Solidity baseline (if porting) ## Common optimizations summary | Pattern | Gas savings | Complexity | | ------------------------- | ----------------------------------- | ---------- | | Cache storage reads | High (avoids repeated `SLOAD`) | Low | | Delete unused storage | Medium (≤4,800 gas refund per slot) | Low | | Batch storage writes | Medium (varies) | Medium | | Use iterators vs. collect | Low-Medium | Low | | Minimize external calls | High | Medium | | Optimize binary size | High (deployment only) | Medium | | Right-size data types | Low-Medium | Low | ## Advanced optimization ### Custom memory allocators The Stylus SDK ships with `mini-alloc` enabled by default (the `mini-alloc` feature in the generated `Cargo.toml`), a small WASM-oriented allocator that is already a good fit for most contracts. Reach for a custom `#[global_allocator]` only if profiling shows allocation is a bottleneck. Note that `wee_alloc`, once a common choice for size-constrained WASM, is unmaintained (archived upstream) and is not recommended for new contracts. Prefer the SDK default unless you have a specific, measured reason to change it. ### Assembly optimization For critical paths, advanced developers can reach for WASM intrinsics from `core::arch::wasm32`. The following is pseudocode showing where such an optimization would live; the body is intentionally omitted because a real implementation depends on the specific operation you are optimizing: ```rust use core::arch::wasm32::*; // ✅ Advanced: use WASM intrinsics for critical operations. // Pseudocode — fill in a complete, measured implementation before using. pub fn optimized_hash(&self, data: &[u8]) -> [u8; 32] { // WASM-optimized hashing goes here. unimplemented!("provide a real implementation") } ``` ## Next steps * Apply [security best practices](/stylus/best-practices/security.md) * Study [deployment optimization](/stylus/how-tos/optimizing-binaries.md) * Learn about [caching strategies](/stylus/how-tos/caching-contracts.md) * Review [debugging techniques](/stylus/cli-tools/debugging-tx.md) --- > For a complete page index, fetch # Security best practices Writing secure smart contracts is critical - vulnerabilities can lead to loss of funds and user trust. This guide covers essential security patterns for Stylus development. Compile-time determinism Compiling Rust to WebAssembly is not guaranteed to be deterministic on its own — non-determinism comes from OS-level features (system clock, file system, networking). The Stylus VM does not expose these features at runtime, so they cannot affect contract execution. To keep **builds** reproducible, avoid build scripts and dependencies that read from the system clock or query the network at compile time, and pin your toolchain via `rust-toolchain.toml`. ## Core security principles ### 1. Input validation Always validate external inputs before using them in your contract logic. ```rust use stylus_sdk::{ alloy_primitives::{Address, U256}, prelude::*, storage::{StorageMap, StorageU256}, }; #[storage] #[entrypoint] pub struct MyContract { balances: StorageMap, } #[public] impl MyContract { // ❌ Bad: No validation pub fn transfer_bad(&mut self, recipient: Address, amount: U256) -> Result<(), Vec> { let sender = self.vm().msg_sender(); let sender_balance = self.balances.get(sender); let recipient_balance = self.balances.get(recipient); self.balances.setter(sender).set(sender_balance - amount); self.balances.setter(recipient).set(recipient_balance + amount); Ok(()) } // ✅ Good: Proper validation pub fn transfer_good(&mut self, recipient: Address, amount: U256) -> Result<(), Vec> { // Validate inputs if recipient.is_zero() { return Err("Invalid recipient".into()); } if amount == U256::ZERO { return Err("Amount must be positive".into()); } let sender = self.vm().msg_sender(); let sender_balance = self.balances.get(sender); // Check sufficient balance if sender_balance < amount { return Err("Insufficient balance".into()); } // Safe arithmetic let recipient_balance = self.balances.get(recipient); self.balances.setter(sender).set(sender_balance - amount); self.balances.setter(recipient).set(recipient_balance + amount); Ok(()) } } ``` ### 2. Access control Implement proper authorization checks for privileged operations. ```rust use stylus_sdk::{ alloy_primitives::{Address, U256}, prelude::*, }; sol_storage! { #[entrypoint] pub struct Ownable { address owner; } } #[public] impl Ownable { // Initialize owner in constructor-like pattern pub fn init(&mut self) -> Result<(), Vec> { let owner = self.owner.get(); if !owner.is_zero() { return Err("Already initialized".into()); } self.owner.set(self.vm().msg_sender()); Ok(()) } // Modifier pattern for owner-only functions fn only_owner(&self) -> Result<(), Vec> { if self.vm().msg_sender() != self.owner.get() { return Err("Not authorized".into()); } Ok(()) } pub fn sensitive_operation(&mut self) -> Result<(), Vec> { self.only_owner()?; // Perform privileged operation Ok(()) } pub fn transfer_ownership(&mut self, new_owner: Address) -> Result<(), Vec> { self.only_owner()?; if new_owner.is_zero() { return Err("Invalid new owner".into()); } self.owner.set(new_owner); Ok(()) } } ``` ### 3. Reentrancy protection Protect against reentrancy attacks using the checks-effects-interactions pattern. Info Don't rely on the old opt-in reentrancy guard. The `reentrant` feature flag and `deny_reentrant` entrypoint guard were deprecated in SDK 0.10.5: the high-level call functions automatically flush the storage cache before every external call, so any state you write before a call is observed by a reentrant call. That makes the guard redundant. Checks-effects-interactions — updating state before the external call — remains the correct way to write reentrancy-safe contracts. ```rust use stylus_sdk::{ alloy_primitives::{Address, U256}, call::transfer::transfer_eth, prelude::*, }; sol_storage! { #[entrypoint] pub struct Vault { mapping(address => uint256) balances; bool locked; // Optional application-level guard } } #[public] impl Vault { // ❌ Bad: Vulnerable to reentrancy pub fn withdraw_bad(&mut self, amount: U256) -> Result<(), Vec> { let sender = self.vm().msg_sender(); let balance = self.balances.get(sender); if balance < amount { return Err("Insufficient balance".into()); } // DANGER: External call before state update transfer_eth(self.vm(), sender, amount)?; // State updated after external call - vulnerable! self.balances.setter(sender).set(balance - amount); Ok(()) } // ✅ Good: Checks-Effects-Interactions pattern pub fn withdraw_good(&mut self, amount: U256) -> Result<(), Vec> { // Optional application-level guard if self.locked.get() { return Err("Reentrancy detected".into()); } self.locked.set(true); let sender = self.vm().msg_sender(); let balance = self.balances.get(sender); // Check: Validate conditions if balance < amount { return Err("Insufficient balance".into()); } // Effect: Update state BEFORE external call self.balances.setter(sender).set(balance - amount); // Interaction: External call last let result = transfer_eth(self.vm(), sender, amount); // Release lock self.locked.set(false); result } } ``` ### 4. Safe arithmetic While Rust prevents overflows in debug mode, use explicit checks for production. ```rust use stylus_sdk::{alloy_primitives::U256, prelude::*}; #[storage] #[entrypoint] pub struct SafeMath {} #[public] impl SafeMath { // ✅ Use checked arithmetic pub fn safe_add(&self, a: U256, b: U256) -> Result> { a.checked_add(b).ok_or("Arithmetic overflow".into()) } pub fn safe_mul(&self, a: U256, b: U256) -> Result> { a.checked_mul(b).ok_or("Arithmetic overflow".into()) } // ✅ Validate before operations pub fn calculate_fee(&self, amount: U256, basis_points: U256) -> Result> { if basis_points > U256::from(10000) { return Err("Invalid fee".into()); } amount .checked_mul(basis_points) .and_then(|v| v.checked_div(U256::from(10000))) .ok_or("Fee calculation failed".into()) } } ``` ## Common vulnerabilities ### Integer overflow/underflow **Risk**: Arithmetic operations that exceed type limits can cause unexpected behavior. **Prevention**: ```rust // ✅ Use checked operations let result = value.checked_add(amount).ok_or("Overflow")?; // ✅ Or use saturating operations when appropriate let capped_value = value.saturating_add(amount); ``` ### Unchecked external calls **Risk**: Failed external calls may be silently ignored. **Prevention**: ```rust // ❌ Bad: Ignoring call result let _ = external_contract.call(data); // ✅ Good: Handle all results external_contract.call(data).map_err(|e| "External call failed")?; ``` ### Front-running **Risk**: Transactions visible in mempool can be exploited by miners or bots. **Prevention**: ```rust // ✅ Use commit-reveal pattern for sensitive operations use stylus_sdk::{ alloy_primitives::{FixedBytes, U256}, crypto::keccak, prelude::*, }; sol_storage! { #[entrypoint] pub struct CommitReveal { mapping(address => bytes32) commits; mapping(address => uint256) reveal_times; } } #[public] impl CommitReveal { pub fn commit(&mut self, commitment: FixedBytes<32>) -> Result<(), Vec> { let sender = self.vm().msg_sender(); // block_timestamp() returns u64; convert before storing in a U256 map let reveal_at = U256::from(self.vm().block_timestamp()) + U256::from(100); self.commits.setter(sender).set(commitment); self.reveal_times.setter(sender).set(reveal_at); Ok(()) } pub fn reveal(&mut self, value: U256, salt: FixedBytes<32>) -> Result<(), Vec> { let sender = self.vm().msg_sender(); // Verify commit period passed let now = U256::from(self.vm().block_timestamp()); if now < self.reveal_times.get(sender) { return Err("Too early to reveal".into()); } // Verify commitment let mut preimage = Vec::new(); preimage.extend_from_slice(&value.to_be_bytes::<32>()); preimage.extend_from_slice(salt.as_slice()); let expected = keccak(&preimage); if expected != self.commits.get(sender) { return Err("Invalid reveal".into()); } // Process reveal... Ok(()) } } ``` ### Denial of Service (DoS) **Risk**: Unbounded loops or operations that can be griefed. **Prevention**: ```rust // ❌ Bad: Unbounded loop pub fn distribute_rewards_bad(&mut self, recipients: Vec
) -> Result<(), Vec> { for recipient in recipients { // Could run out of gas with too many recipients self.send_reward(recipient)?; } Ok(()) } // ✅ Good: Paginated or pull-based pattern pub fn distribute_rewards_good( &mut self, start_index: U256, count: U256 ) -> Result<(), Vec> { if count > U256::from(50) { return Err("Batch too large".into()); } let end = start_index + count; let mut i = start_index; while i < end { let idx = usize::try_from(i).map_err(|_| b"index overflow".to_vec())?; let recipient = self.recipients.get(idx).unwrap_or(Address::ZERO); if !recipient.is_zero() { self.send_reward(recipient)?; } i += U256::from(1); } Ok(()) } // ✅ Better: Pull-based (users claim their own rewards) pub fn claim_reward(&mut self) -> Result<(), Vec> { let sender = self.vm().msg_sender(); let reward = self.pending_rewards.get(sender); if reward == U256::ZERO { return Err("No rewards".into()); } self.pending_rewards.setter(sender).set(U256::ZERO); transfer_eth(self.vm(), sender, reward)?; Ok(()) } ``` ## Storage security ### Visibility and access patterns ```rust sol_storage! { #[entrypoint] pub struct SecureVault { // Public read, controlled write uint256 public_total; // Private storage - not visible off-chain without knowing slot mapping(address => uint256) private_balances; // Owner-controlled address owner; } } #[public] impl SecureVault { // ✅ Expose only what's necessary pub fn get_total(&self) -> U256 { self.public_total.get() } // ✅ Don't expose internal mappings directly pub fn get_balance(&self, account: Address) -> Result> { let caller = self.vm().msg_sender(); if caller != account && caller != self.owner.get() { return Err("Unauthorized".into()); } Ok(self.private_balances.get(account)) } } ``` ### Prevent storage collisions ```rust use stylus_sdk::{ alloy_primitives::{Address, U256}, prelude::*, storage::{StorageMap, StorageU256}, }; // ✅ Group related state in its own storage struct... #[storage] pub struct MyContractStorage { value: StorageU256, balances: StorageMap, } // ...then compose it into the entrypoint as a named field. // Each nested storage struct gets its own slot range, which avoids // collisions between logically separate pieces of state. #[storage] #[entrypoint] pub struct MyContract { inner: MyContractStorage, } ``` ## Error handling ### Informative error messages ```rust use stylus_sdk::{ alloy_primitives::{Address, U256}, alloy_sol_types::sol, prelude::*, }; // Each enum variant wraps a Solidity error type declared in a sol! block. sol! { error InsufficientBalance(uint256 available); error Unauthorized(address caller); error InvalidAmount(uint256 amount); error TransferFailed(address to, uint256 amount); } #[derive(SolidityError)] pub enum MyError { InsufficientBalance(InsufficientBalance), Unauthorized(Unauthorized), InvalidAmount(InvalidAmount), TransferFailed(TransferFailed), } #[public] impl MyContract { pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), MyError> { let sender = self.vm().msg_sender(); let balance = self.balances.get(sender); if balance < amount { return Err(MyError::InsufficientBalance(InsufficientBalance { available: balance, })); } if to.is_zero() { return Err(MyError::InvalidAmount(InvalidAmount { amount })); } // Transfer logic... Ok(()) } } ``` ### Fail securely ```rust // ✅ Fail closed, not open pub fn privileged_function(&mut self) -> Result<(), Vec> { // Default to denying access let is_authorized = self.check_authorization(self.vm().msg_sender()); // Explicit check required to proceed if !is_authorized { return Err("Access denied".into()); } // Privileged operation Ok(()) } ``` ## Testing for security ### Write comprehensive tests The `testing` module is gated behind the `stylus-test` feature, so add it as a dev-dependency in `Cargo.toml`: ```toml [dev-dependencies] stylus-sdk = { version = "0.10.7", features = ["stylus-test"] } ``` ```rust #[cfg(test)] mod tests { use super::*; use alloy_primitives::address; use stylus_sdk::testing::*; #[test] fn test_withdraw_balance_checks() { let vm = TestVM::default(); let mut contract = Vault::from(&vm); // Deposit funds vm.set_value(U256::from(100)); contract.deposit(); // A withdrawal within balance succeeds assert!(contract.withdraw_good(U256::from(50)).is_ok()); // A withdrawal exceeding the remaining balance fails assert!(contract.withdraw_good(U256::from(100)).is_err()); } #[test] fn test_access_control() { let vm = TestVM::default(); // Capture the default sender, which becomes the owner on init let owner = vm.msg_sender(); let mut contract = Ownable::from(&vm); // Initialize owner contract.init().unwrap(); // Non-owner should be rejected vm.set_sender(address!("0x0000000000000000000000000000000000000001")); assert!(contract.sensitive_operation().is_err()); // Owner should succeed vm.set_sender(owner); assert!(contract.sensitive_operation().is_ok()); } #[test] fn test_arithmetic_safety() { let vm = TestVM::default(); let contract = SafeMath::from(&vm); // Test overflow let max = U256::MAX; let result = contract.safe_add(max, U256::from(1)); assert!(result.is_err()); // Test valid operation let result = contract.safe_add(U256::from(1), U256::from(2)); assert_eq!(result.unwrap(), U256::from(3)); } } ``` ## Security checklist Before deploying your contract, verify: * [ ] All external inputs are validated * [ ] Access control is implemented for privileged functions * [ ] Reentrancy guards protect state-changing functions * [ ] Arithmetic operations use checked methods * [ ] External call results are handled * [ ] Error messages don't leak sensitive information * [ ] Storage visibility is appropriate * [ ] No unbounded loops or arrays * [ ] Critical functions have comprehensive tests * [ ] Code has been reviewed by another developer * [ ] Consider professional audit for high-value contracts ## Additional resources * [Stylus Security Audit](https://docs.arbitrum.io/stylus/reference/audit-reports) * [Rust Security Guidelines](https://anssi-fr.github.io/rust-guide/) * [Smart Contract Security Best Practices](https://consensys.github.io/smart-contract-best-practices/) * [OWASP Smart Contract Top 10](https://owasp.org/www-project-smart-contract-top-10/) ## Next steps * Review [gas optimization best practices](/stylus/best-practices/gas-optimization.md) * Study [error handling patterns](/stylus/fundamentals/contracts.md#error-handling) * Explore [testing strategies](/stylus/fundamentals/testing-contracts.md) --- > For a complete page index, fetch # Check and deploy This guide explains how to validate and deploy Stylus smart contracts using the `cargo stylus` CLI tool. The process involves two main steps: checking that your contract is valid, and deploying it to an Arbitrum chain. ## Prerequisites Before checking or deploying contracts, ensure you have: 1. **Rust toolchain** installed (see [rustup.rs](https://rustup.rs)) 2. **WebAssembly target** added: ```shell rustup target add wasm32-unknown-unknown ``` 3. **cargo-stylus CLI** installed: ```shell cargo install cargo-stylus ``` 4. **RPC endpoint** for the target chain (see [testnet information](https://docs.arbitrum.io/stylus/reference/testnet-information)) 5. **Funded account** with ETH for gas and activation fees ## Overview: The Two-Step Process Deploying a Stylus contract involves two distinct steps: 1. **Deployment**: Upload the compressed WASM bytecode to the chain, assigning it an address 2. **[Activation](/stylus/concepts/activation.md)**: Trigger onchain compilation to native code and cache it for fast execution The `cargo stylus check` command validates your contract before deployment, and `cargo stylus deploy` handles both steps automatically. ## Checking Contracts The `cargo stylus check` command validates that your contract can be deployed and activated without actually sending a transaction. ### What check does 1. **Compiles** your Rust code to WASM with `wasm32-unknown-unknown` target 2. **Compresses** the WASM using Brotli compression 3. **Validates** the WASM structure: * Required exports (`user_entrypoint`) * Allowed imports (only `vm_hooks`) * Memory constraints * Size limits (decompressed WASM within `MaxWasmSize`) 4. **Simulates activation** using `eth_call` to verify onchain compatibility 5. **Estimates the data fee** required for activation ### Basic usage ```shell # Check the current project against Arbitrum Sepolia (default) cargo stylus check ``` ### Common options ```shell # Check against a specific network cargo stylus check \ --endpoint="https://arb1.arbitrum.io/rpc" # Check a specific WASM file cargo stylus check \ --wasm-file=./target/wasm32-unknown-unknown/release/my_contract.wasm # Check with a specific contract address cargo stylus check \ --contract-address=0x1234567890123456789012345678901234567890 ``` ### Success output When your contract passes validation: ```shell Finished release [optimized] target(s) in 1.88s Reading WASM file at target/wasm32-unknown-unknown/release/my_contract.wasm Compressed WASM size: 3 KB Contract succeeded Stylus onchain activation checks with Stylus version: 1 wasm data fee: 0.0001 ETH (originally 0.00008 ETH with 20% bump) ``` ### Failure output If validation fails, you'll see detailed error information: ```shell Reading WASM file at target/wasm32-unknown-unknown/release/bad_contract.wasm Compressed WASM size: 55 B Stylus checks failed: contract predeployment check failed Caused by: binary exports reserved symbol stylus_ink_left Location: prover/src/binary.rs:493:9 ``` Common validation errors include: * **Missing entrypoint**: Contract lacks `#[entrypoint]` attribute * **Invalid exports**: Contract exports reserved symbols * **Size limit exceeded**: Decompressed WASM exceeds `MaxWasmSize` (the chain-configurable limit, 128 KB by default and 256 KB at ArbOS 60+) * **Invalid imports**: Contract imports functions outside `vm_hooks` * **Memory violations**: Incorrect memory handling or growth ## Deploying Contracts The `cargo stylus deploy` command compiles, deploys, and activates your contract in a single operation. ### What deploy does 1. **Compiles and checks** the contract (same as `cargo stylus check`) 2. **Deploys bytecode**: Sends transaction to upload compressed WASM to the chain 3. **Activates contract**: Calls `activateProgram` precompile to compile to native code 4. **Verifies success**: Confirms both transactions completed successfully ### Basic deployment ```shell # Deploy to Arbitrum Sepolia (default testnet) cargo stylus deploy \ --private-key-path=./key.txt ``` ### Deployment with gas estimation Before deploying, estimate the gas required: ```shell cargo stylus deploy \ --private-key-path=./key.txt \ --estimate-gas ``` Output: ```shell Compressed WASM size: 3 KB Deploying contract to address 0x457b1ba688e9854bdbed2f473f7510c476a3da09 Estimated gas: 12756792 wasm data fee: 0.0001 ETH ``` ### Full deployment Once the estimation looks correct, deploy for real: ```shell cargo stylus deploy \ --private-key-path=./key.txt ``` Output shows both transactions: ```shell Compressed WASM size: 3 KB Deploying contract to address 0x457b1ba688e9854bdbed2f473f7510c476a3da09 Estimated gas: 12756792 Submitting tx... Confirmed tx 0x42db...7311, gas used 11657164 Activating contract at address 0x457b1ba688e9854bdbed2f473f7510c476a3da09 Estimated gas: 14251759 Submitting tx... Confirmed tx 0x0bdb...3307, gas used 14204908 ``` ### Deployment options #### Network selection ```shell # Deploy to Arbitrum One (mainnet) cargo stylus deploy \ --endpoint="https://arb1.arbitrum.io/rpc" \ --private-key-path=./key.txt # Deploy to Arbitrum Sepolia (testnet) cargo stylus deploy \ --endpoint="https://sepolia-rollup.arbitrum.io/rpc" \ --private-key-path=./key.txt ``` #### Private key management ```shell # From file (recommended) cargo stylus deploy \ --private-key-path=./key.txt # From environment (WARNING: exposes key to shell history) cargo stylus deploy \ --private-key=$PRIVATE_KEY # From keystore file cargo stylus deploy \ --keystore-path=./keystore.json \ --keystore-password-path=./password.txt ``` #### Gas price control ```shell # Set custom gas price (in gwei) cargo stylus deploy \ --private-key-path=./key.txt \ --max-fee-per-gas-gwei=0.05 ``` #### Deploy without activation For advanced use cases, deploy bytecode without immediate activation: ```shell cargo stylus deploy \ --private-key-path=./key.txt \ --no-activate ``` Later, activate separately: ```shell cargo stylus activate \ --address=0x457b1ba688e9854bdbed2f473f7510c476a3da09 \ --private-key-path=./key.txt ``` #### Constructor arguments Deploy contracts with constructor arguments: ```shell cargo stylus deploy \ --private-key-path=./key.txt \ --constructor-args "Hello" "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" 42 ``` Send ETH to payable constructor: ```shell cargo stylus deploy \ --private-key-path=./key.txt \ --constructor-value=0.1 \ --constructor-args "InitialValue" ``` #### Reproducible builds For contract verification, use Docker-based reproducible builds: ```shell # Default: uses Docker for reproducibility cargo stylus deploy \ --private-key-path=./key.txt # Specify cargo-stylus version cargo stylus deploy \ --private-key-path=./key.txt \ --cargo-stylus-version=0.10.7 ``` Skip Docker for local development (non-reproducible): ```shell cargo stylus deploy \ --private-key-path=./key.txt \ --no-verify ``` ## Understanding Activation Activation is the process of compiling WASM to native machine code onchain. ### Why is activation required * **Performance**: Native code executes 10-100x faster than interpreted WASM * **Validation**: Ensures WASM is well-formed and follows all constraints * **Caching**: Compiled code is cached for all future contract calls ### Activation process 1. **Decompress**: Brotli-compressed WASM is decompressed 2. **Validate**: WASM structure is checked for correctness 3. **Compile**: WASM is compiled to native machine code 4. **Cache**: Compiled code is stored in the activation cache 5. **Charge fee**: Data fee based on WASM size is charged ### Data fee calculation The data fee depends on the size of your WASM: ```rust // From activation.rs pub async fn data_fee( code: impl Into, address: Address, config: &ActivationConfig, provider: &impl Provider, ) -> Result { let result = arbwasm .activateProgram(address) .call() .await?; let data_fee = result.dataFee; let bump = config.data_fee_bump_percent; // Default 20% let adjusted = bump_data_fee(data_fee, bump); Ok(adjusted) } ``` By default, the fee is bumped by 20% to account for gas price fluctuations. ### Activation errors Common activation failures: #### Missing entrypoint ```text Error: Contract could not be activated as it is missing an entrypoint. Please ensure that your contract has an #[entrypoint] defined on your main struct ``` **Solution**: Add `#[entrypoint]` to your main storage struct: ```rust #[entrypoint] #[storage] pub struct MyContract { // ... } ``` #### Insufficient funds ```text Error: not enough funds in account 0x... to pay for data fee balance 0.0001 ETH < 0.0005 ETH ``` **Solution**: Fund your account with more ETH. Get testnet ETH from faucets: * [Arbitrum Sepolia faucet](https://faucet.quicknode.com/arbitrum/sepolia) #### Invalid WASM ```text Error: contract activation failed: failed to parse contract Caused by: binary exports reserved symbol stylus_ink_left ``` **Solution**: Ensure you're using the latest `stylus-sdk` version and following SDK conventions. ## Deployment Workflows ### Development workflow For rapid iteration during development: ```shell # 1. Check frequently during development cargo stylus check # 2. Deploy to testnet with no-verify for speed cargo stylus deploy \ --endpoint="https://sepolia-rollup.arbitrum.io/rpc" \ --private-key-path=./key.txt \ --no-verify # 3. Test the deployed contract # (use your testing framework) # 4. Iterate and redeploy as needed ``` ### Production workflow For production deployments: ```shell # 1. Final check against mainnet cargo stylus check \ --endpoint="https://arb1.arbitrum.io/rpc" # 2. Estimate costs cargo stylus deploy \ --endpoint="https://arb1.arbitrum.io/rpc" \ --private-key-path=./key.txt \ --estimate-gas # 3. Deploy with reproducible build (for verification) cargo stylus deploy \ --endpoint="https://arb1.arbitrum.io/rpc" \ --private-key-path=./key.txt \ --cargo-stylus-version=0.10.7 # 4. Verify the deployed contract cargo stylus verify \ --endpoint="https://arb1.arbitrum.io/rpc" \ --deployment-tx=0x... ``` ### Multi-contract deployment Deploy multiple contracts from a workspace: ```shell # Check all contracts in workspace cargo stylus check # Deploy specific contract cargo stylus deploy \ --contract=my-token \ --private-key-path=./key.txt # Deploy all contracts (must have no-arg constructors) cargo stylus deploy \ --private-key-path=./key.txt ``` ## Checking Existing Deployments ### Check if the contract is activated ```rust // From check.rs let codehash = processed.codehash(); if Contract::exists(codehash, &provider).await? { return Ok(ContractStatus::Active { code: processed.code, }); } ``` Use `cargo stylus check` with `--contract-address` to verify an existing deployment: ```shell cargo stylus check \ --contract-address=0x457b1ba688e9854bdbed2f473f7510c476a3da09 ``` ### Re-activation If a contract is already deployed but not activated, activate it: ```shell cargo stylus activate \ --address=0x457b1ba688e9854bdbed2f473f7510c476a3da09 \ --private-key-path=./key.txt ``` ## Best practices ### 1. Always check before deploying ```shell # ✅ Good: Check first cargo stylus check cargo stylus deploy --private-key-path=./key.txt # ❌ Bad: Deploy without checking cargo stylus deploy --private-key-path=./key.txt ``` ### 2. Use gas estimation ```shell # ✅ Good: Estimate first cargo stylus deploy --private-key-path=./key.txt --estimate-gas # Review the output, then deploy for real cargo stylus deploy --private-key-path=./key.txt # ❌ Bad: Deploy without estimation cargo stylus deploy --private-key-path=./key.txt ``` ### 3. Secure private key handling ```shell # ✅ Good: Use key file echo $PRIVATE_KEY > /tmp/key.txt chmod 600 /tmp/key.txt cargo stylus deploy --private-key-path=/tmp/key.txt rm /tmp/key.txt # ⚠️ Risky: Expose key in command line cargo stylus deploy --private-key=$PRIVATE_KEY ``` ### 4. Test on testnet first ```shell # ✅ Good: Test on Sepolia first cargo stylus deploy \ --endpoint="https://sepolia-rollup.arbitrum.io/rpc" \ --private-key-path=./key.txt # After testing succeeds, deploy to mainnet cargo stylus deploy \ --endpoint="https://arb1.arbitrum.io/rpc" \ --private-key-path=./key.txt # ❌ Bad: Deploy directly to mainnet cargo stylus deploy \ --endpoint="https://arb1.arbitrum.io/rpc" \ --private-key-path=./key.txt ``` ### 5. Use reproducible builds for verification ```shell # ✅ Good: Reproducible build for mainnet cargo stylus deploy \ --endpoint="https://arb1.arbitrum.io/rpc" \ --private-key-path=./key.txt \ --cargo-stylus-version=0.10.7 # Then verify on Arbiscan cargo stylus verify \ --endpoint="https://arb1.arbitrum.io/rpc" \ --deployment-tx=0x... # ⚠️ OK for development: Skip Docker cargo stylus deploy \ --endpoint="https://sepolia-rollup.arbitrum.io/rpc" \ --private-key-path=./key.txt \ --no-verify ``` ### 6. Monitor contract size ```shell # Check compressed size cargo stylus check # If the decompressed WASM is close to the MaxWasmSize limit: # - Use #[no_std] # - Remove unused dependencies # - Enable aggressive optimizations # - Strip debug symbols ``` ### 7. Verify the data fee is reasonable ```shell # Check data fee before deploying cargo stylus deploy --private-key-path=./key.txt --estimate-gas # Output shows: # wasm data fee: 0.0001 ETH (originally 0.00008 ETH with 20% bump) # If fee seems high: # - Optimize WASM size # - Check network congestion # - Verify contract correctness ``` ## Troubleshooting ### Size limit errors **Error**: Decompressed WASM exceeds the size limit (`MaxWasmSize`) **Solutions**: 1. Use `#[no_std]` to eliminate the standard library: ```rust #![no_std] extern crate alloc; ``` 2. Remove unused dependencies from `Cargo.toml`: ```toml [dependencies] stylus-sdk = "0.10.7" # Remove unnecessary crates ``` 3. Enable size optimizations in `Cargo.toml`: ```toml [profile.release] opt-level = "z" lto = true strip = true ``` 4. Use `wasm-opt` for additional optimization: ```shell wasm-opt -Oz -o optimized.wasm input.wasm cargo stylus deploy --wasm-file=optimized.wasm --private-key-path=./key.txt ``` ### Activation failures **Error**: Transaction reverted during activation **Solutions**: 1. Verify entrypoint exists: ```rust #[entrypoint] #[storage] pub struct MyContract { /* ... */ } ``` 2. Check WASM validity: ```shell cargo stylus check --wasm-file=./target/wasm32-unknown-unknown/release/my_contract.wasm ``` 3. Ensure sufficient funds: ```shell # Check balance cast balance $YOUR_ADDRESS --rpc-url $RPC_URL # Get testnet ETH if needed # Visit faucet.quicknode.com/arbitrum/sepolia ``` ### RPC errors **Error**: Connection timeout or RPC error **Solutions**: 1. Verify endpoint URL: ```shell curl -X POST $RPC_URL \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' ``` 2. Try alternative endpoints: ```shell # Arbitrum One --endpoint="https://arb1.arbitrum.io/rpc" --endpoint="https://arbitrum-one.publicnode.com" # Arbitrum Sepolia --endpoint="https://sepolia-rollup.arbitrum.io/rpc" --endpoint="https://arbitrum-sepolia.blockpi.network/v1/rpc/public" ``` 3. Check network status: * [Arbitrum Status](https://arbiscan.io/) * [Chainlist](https://chainlist.org/) ### Build errors **Error**: Compilation fails **Solutions**: 1. Update dependencies: ```shell cargo update ``` 2. Clear build cache: ```shell cargo clean ``` 3. Verify Rust toolchain: ```shell rustup update rustup target add wasm32-unknown-unknown ``` 4. Check SDK version compatibility: ```toml [dependencies] stylus-sdk = "0.10.7" # Use latest stable version ``` ## Non-Rust WASM Deployment Deploy WASM from any language (C, C++, etc.): ```shell # Deploy raw WASM file cargo stylus deploy \ --wasm-file=./my_contract.wasm \ --private-key-path=./key.txt # Deploy WebAssembly Text (WAT) file cargo stylus deploy \ --wasm-file=./my_contract.wat \ --private-key-path=./key.txt ``` Example WAT file structure: ```wasm (module (memory 0 0) (export "memory" (memory 0)) (func (export "user_entrypoint") (param $args_len i32) (result i32) (i32.const 0) )) ``` ## Command Reference ### cargo stylus check **Syntax**: ```shell cargo stylus check [OPTIONS] ``` **Common options**: * `--endpoint=`: RPC endpoint (default: Arbitrum Sepolia) * `--wasm-file=`: Check specific WASM file * `--contract-address=
`: Target contract address ### `cargo stylus deploy` **Syntax**: ```shell cargo stylus deploy [OPTIONS] ``` **Common options**: * `--endpoint=`: RPC endpoint * `--private-key-path=`: Private key file * `--estimate-gas`: Only estimate gas * `--no-activate`: Deploy without activation * `--no-verify`: Skip Docker reproducible build * `--constructor-args `: Constructor arguments * `--constructor-value=`: ETH sent to constructor * `--max-fee-per-gas-gwei=`: Custom gas price ### `cargo stylus activate` **Syntax**: ```shell cargo stylus activate --address=
[OPTIONS] ``` **Options**: * `--address=
`: Deployed contract address (required) * `--private-key-path=`: Private key file * `--estimate-gas`: Only estimate gas ## Resources * [Stylus quickstart guide](https://docs.arbitrum.io/stylus/stylus-quickstart) * [Cargo Stylus repository](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus) * [Testnet information](https://docs.arbitrum.io/stylus/reference/testnet-information) * [Contract verification guide](https://docs.arbitrum.io/stylus/guides/verifying-contracts) * [Optimizing WASM size](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus/blob/main/OPTIMIZING_BINARIES.md) * [Valid WASM requirements](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus/blob/main/VALID_WASM.md) --- > For a complete page index, fetch # cargo-stylus command reference Complete reference for all `cargo-stylus` commands (v0.10.7). Every command supports the `--verbose` global flag for debug output. ## activate Activate an already deployed Stylus contract onchain. **Alias:** `a` **Usage:** ```shell cargo stylus activate \ --address \ --private-key \ --endpoint ``` **Options:** | Option | Description | Default | | ----------------------------------- | ------------------------------------------------- | ------- | | `--address
` | Deployed contract address to activate (required) | — | | `--estimate-gas` | Only estimate gas without sending a transaction | `false` | | `--data-fee-bump-percent ` | Percent to bump the estimated activation data fee | `20` | Also accepts [authentication options](#authentication-options) and [provider options](#provider-options). ## build Compile a Stylus contract to WASM. **Alias:** `b` **Usage:** ```shell cargo stylus build ``` **Options:** | Option | Description | Default | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------- | | `--features ` | Cargo features to enable when building | — | | `--source-files-for-project-hash ` | Paths to include in the project hash for verification. If omitted, all `.rs`, `Cargo.toml`, and `Cargo.lock` files are included | — | Also accepts [project options](#project-options). ## cache Manage contract caching using the Stylus CacheManager. Has three subcommands. ### cache bid Place a bid to cache a deployed and activated contract. **Alias:** `b` **Usage:** ```shell cargo stylus cache bid
``` **Positional arguments:** | Argument | Description | | ----------- | ----------------------------------------- | | `
` | Deployed and activated contract address | | `` | Bid amount in wei (a value of 0 is valid) | Also accepts [authentication options](#authentication-options) and [provider options](#provider-options). ### cache status Check a contract's cache status. **Alias:** `s` **Usage:** ```shell cargo stylus cache status --address
``` **Options:** | Option | Description | Default | | --------------------- | ------------------------- | ------- | | `--address
` | Contract address to check | — | Also accepts [provider options](#provider-options). ### cache suggest-bid Get the suggested minimum bid for caching a contract. **Usage:** ```shell cargo stylus cache suggest-bid
``` **Positional arguments:** | Argument | Description | | ----------- | ----------------------------------------- | | `
` | Contract address to get suggested bid for | Also accepts [provider options](#provider-options). ## cgen Generate C code bindings for a Stylus contract. **Usage:** ```shell cargo stylus cgen ``` **Positional arguments:** | Argument | Description | | -------------- | --------------------- | | `` | Input file path | | `` | Output directory path | ## check Verify that a contract compiles to valid WASM and passes onchain activation checks. **Alias:** `c` **Usage:** ```shell cargo stylus check --endpoint ``` **Options:** | Option | Description | Default | | ----------------------------------- | -------------------------------------------------------------- | ------- | | `--wasm-file ` | Prebuilt WASM file(s) to check instead of building from source | — | | `--wasm-file-address
` | Deployment address for the WASM file | random | | `--contract-address
` | Deployment address for the contract | random | | `--data-fee-bump-percent ` | Percent to bump the estimated activation data fee | `20` | Also accepts [build options](#build-options), [project options](#project-options), and [provider options](#provider-options). **Example:** ```shell cargo stylus check --endpoint https://sepolia-rollup.arbitrum.io/rpc ``` ## codehash-keepalive Request to keep a contract's codehash from expiring in the ArbOS codehash registry. **Alias:** `k` **Usage:** ```shell cargo stylus codehash-keepalive \ --codehash \ --private-key ``` **Options:** | Option | Description | Default | | ------------------- | ------------------------------------- | ------- | | `--codehash ` | The codehash to keep alive (required) | — | Also accepts [authentication options](#authentication-options) and [provider options](#provider-options). ## constructor Print the signature of a contract's constructor. **Usage:** ```shell cargo stylus constructor ``` Also accepts [project options](#project-options) and [reflection options](#reflection-options). ## deploy Deploy one or more Stylus contracts. By default, contracts are deployed through the Stylus deployer contract, which handles deployment, activation, and constructor initialization. **Alias:** `d` **Usage:** ```shell cargo stylus deploy \ --private-key \ --endpoint ``` **Options:** | Option | Description | Default | | ---------------------------------- | ------------------------------------------------------- | ----------------------- | | `--estimate-gas` | Only estimate gas, don't deploy | `false` | | `--no-verify` | Skip reproducible Docker container verification | `false` | | `--no-activate` | Skip activation after deployment | `false` | | `--cargo-stylus-version ` | cargo-stylus version for the Docker image | local version | | `--deployer-address
` | Address of the Stylus deployer contract | Stylus default deployer | | `--deployer-salt ` | Salt passed to the deployer for deterministic addresses | `0x0...0` | | `--constructor-args ...` | Constructor arguments (supports multiple values) | — | | `--constructor-value ` | Ether to send through the constructor (in ETH) | `0` | | `--constructor-signature ` | Constructor signature when using `--wasm-file` | — | | `--wasm-file ` | Deploy a prebuilt WASM file directly | — | Also accepts [authentication options](#authentication-options), [build options](#build-options), [project options](#project-options), and [provider options](#provider-options). **Example:** ```shell cargo stylus deploy \ --endpoint https://sepolia-rollup.arbitrum.io/rpc \ --private-key $PRIVATE_KEY \ --constructor-args "0xMyTokenName" "0xMTK" \ --estimate-gas ``` ## export-abi Export a Solidity ABI interface for a Stylus contract. **Usage:** ```shell cargo stylus export-abi ``` Also accepts [project options](#project-options) and [reflection options](#reflection-options). **Example:** ```shell cargo stylus export-abi > IMyContract.sol cargo stylus export-abi --json > abi.json ``` ## get-initcode Generate and print the initialization code (initcode) for a Stylus contract. **Alias:** `e` **Usage:** ```shell cargo stylus get-initcode ``` **Options:** | Option | Description | Default | | ----------------- | -------------------------------------- | ------- | | `--output ` | Output file for the generated hex code | stdout | Also accepts [build options](#build-options) and [project options](#project-options). ## init Initialize a Stylus project in an existing directory. **Usage:** ```shell cargo stylus init [PATH] ``` **Positional arguments:** | Argument | Description | Default | | -------- | ----------------------------------------------------------- | ------- | | `[PATH]` | Path to existing directory, cargo crate, or cargo workspace | `.` | **Options:** | Option | Description | Default | | ------------- | -------------------------------- | ------- | | `--contract` | Initialize as a Stylus contract | default | | `--workspace` | Initialize as a Stylus workspace | `false` | ## new Create a new Stylus project. **Usage:** ```shell cargo stylus new ``` **Positional arguments:** | Argument | Description | | ---------------- | -------------------------------- | | `` | Name or path for the new project | **Options:** | Option | Description | Default | | ------------- | ------------------------------ | ------- | | `--contract` | Create a new contract project | default | | `--workspace` | Create a new workspace project | `false` | **Example:** ```shell cargo stylus new my-stylus-contract ``` ## replay Replay a transaction using an external debugger (GDB, LLDB, or StylusDB). **Alias:** `r` **Usage:** ```shell cargo stylus replay --tx ``` **Options:** | Option | Description | Default | | ----------------------------- | --------------------------------------------------------- | ------- | | `--tx ` | Transaction hash to replay (required) | — | | `--project ` | Project path | `.` | | `--debugger ` | Debugger to use: `gdb`, `lldb`, `stylusdb`, or `auto` | `auto` | | `--features ` | Cargo features for building | — | | `--package ` | Specific package to build during replay | — | | `--contracts ` | Multi-contract debugging: `ADDRESS1:PATH1,ADDRESS2:PATH2` | — | | `--addr-solidity ` | Comma-separated Solidity contract addresses for display | — | | `--use-native-tracer` | Use the native tracer instead of JavaScript | `false` | Also accepts [project options](#project-options) and [provider options](#provider-options). **Example:** ```shell cargo stylus replay \ --tx 0xd4...85 \ --endpoint https://sepolia-rollup.arbitrum.io/rpc \ --debugger stylusdb ``` ## simulate Simulate a transaction without executing it onchain. **Alias:** `s` **Usage:** ```shell cargo stylus simulate --to
--data ``` **Options:** | Option | Description | Default | | --------------------- | ---------------------------------------------------- | ------- | | `--from
` | Sender address | — | | `--to
` | Target contract address | — | | `--gas ` | Gas limit | — | | `--gas-price ` | Gas price (in wei) | — | | `--value ` | Value to send (in wei) | — | | `--data ` | Calldata as hex string (with or without `0x` prefix) | — | | `--use-native-tracer` | Use the native tracer instead of JavaScript | `false` | Also accepts [provider options](#provider-options). ## trace Display a trace of a transaction. **Alias:** `t` **Usage:** ```shell cargo stylus trace --tx ``` **Options:** | Option | Description | Default | | --------------------- | ------------------------------------------- | ------- | | `--tx ` | Transaction hash to trace (required) | — | | `--project ` | Project path | `.` | | `--use-native-tracer` | Use the native tracer instead of JavaScript | `false` | Also accepts [provider options](#provider-options). ## usertrace Trace a transaction with StylusDB, capturing user function calls. Provides a higher-level view than `trace` by filtering for user-defined function calls. **Alias:** `ut` **Usage:** ```shell cargo stylus usertrace --tx ``` **Options:** | Option | Description | Default | | ------------------------------------- | ------------------------------------------------------- | ------- | | `--tx ` | Transaction hash to trace (required) | — | | `--project ` | Project path | `.` | | `--features ` | Cargo features for building | — | | `--package ` | Specific package to build during trace | — | | `--contracts ` | Multi-contract tracing: `ADDRESS1:PATH1,ADDRESS2:PATH2` | — | | `--verbose-usertrace` | Include `stylus_sdk` functions in the trace | `false` | | `--trace-external-usertrace ` | Comma-separated list of additional crates to trace | — | | `--enable-stylusdb-output` | Show StylusDB output (silenced by default) | `false` | | `--use-native-tracer` | Use the native tracer instead of JavaScript | `false` | Also accepts [project options](#project-options) and [provider options](#provider-options). ## verify Verify the deployment of a Stylus contract using reproducible builds. **Usage:** ```shell cargo stylus verify --deployment-tx ``` **Options:** | Option | Description | Default | | ---------------------------------- | ---------------------------------------------------- | ------------- | | `--deployment-tx ` | Deployment transaction hash(es) to verify (required) | — | | `--cargo-stylus-version ` | cargo-stylus version for the Docker image | local version | | `--no-verify` | Skip reproducible Docker container | `false` | | `--skip-clean` | Skip cleaning before verification | `false` | Also accepts [project options](#project-options) and [provider options](#provider-options). **Example:** ```shell cargo stylus verify \ --deployment-tx 0xd4...85 \ --endpoint https://sepolia-rollup.arbitrum.io/rpc ``` *** ## Common options These option groups are shared across multiple commands. Each command's documentation notes which groups it accepts. ### Provider options | Option | Description | Default | | ---------------------- | ------------------------- | ----------------------- | | `-e, --endpoint ` | Arbitrum RPC endpoint URL | `http://localhost:8547` | ### Authentication options | Option | Description | Default | | --------------------------------- | ---------------------------------------------------------- | ------- | | `--private-key ` | Private key as a hex string (exposes key to shell history) | — | | `--private-key-path ` | Path to a text file containing a hex-encoded private key | — | | `--keystore-path ` | Path to an Ethereum wallet keystore file (e.g., Clef) | — | | `--keystore-password-path ` | Path to the keystore password file | — | | `--max-fee-per-gas-gwei ` | Maximum fee per gas in gwei | — | ### Build options | Option | Description | Default | | ----------------------------------------- | ------------------------------------------------- | ------------------------------------- | | `--features ` | Cargo features to enable | — | | `--source-files-for-project-hash ` | Paths to include in project hash for verification | all `.rs`, `Cargo.toml`, `Cargo.lock` | ### Project options | Option | Description | Default | | ------------------- | ----------------------------------------------------- | --------------------- | | `--contract ` | Specific contract package(s) to target in a workspace | all default contracts | ### Reflection options | Option | Description | Default | | ---------------------------- | -------------------------------------------- | ------- | | `--output ` | Output file path | stdout | | `--json` | Write JSON ABI (requires `solc`) | `false` | | `--rust-features ` | Rust crate features to include in ABI export | — | *** ## Command aliases | Alias | Command | | ----- | ------------------ | | `a` | activate | | `b` | build | | `c` | check | | `d` | deploy | | `e` | get-initcode | | `k` | codehash-keepalive | | `r` | replay | | `s` | simulate | | `t` | trace | | `ut` | usertrace | *** For usage examples and workflow guides, see the [CLI tools overview](/stylus/cli-tools/overview.md). --- > For a complete page index, fetch # How to debug Stylus transactions Debugging smart contracts can be challenging, especially when dealing with complex transactions. The `cargo-stylus` crate simplifies the debugging process by providing multiple tools for replaying, tracing, and interactively debugging Stylus transactions. These tools enable you to set breakpoints, inspect state changes, and trace execution step by step. ## Overview Cargo Stylus provides several debugging capabilities: 1. **Trace transactions** (`cargo stylus trace`): Perform trace calls against Stylus transactions using `debug_traceTransaction` RPC. This provides low-level function-call data along with Ink consumption metrics. 2. **User function tracing** (`cargo stylus usertrace`): Generate human-readable function call trees showing your contract's execution flow without needing a full debugger session. 3. **Interactive debugging** (`cargo stylus replay`): Replay and debug Transaction execution using GDB, LLDB, or StylusDB. Set breakpoints, inspect variables, and step through code line by line. 4. **Multi-contract debugging**: Debug transactions that span multiple Stylus contracts simultaneously using StylusDB. ## Requirements * **Rust** (version 1.77 or higher) * **cargo-stylus** crate * **Debugger**: One of the following: * **GDB** (Linux) * **LLDB** (macOS) * **StylusDB** (macOS/Linux) - recommended for advanced debugging * **[Cast](https://book.getfoundry.sh/cast/)** (an Ethereum CLI tool) * **[Arbitrum RPC provider](#rpc-endpoint-compatibility)** with tracing endpoints enabled or a [local Stylus dev node](/run-arbitrum-node/run-nitro-dev-node.md) ## Installation and setup ### Install cargo-stylus ```shell cargo install cargo-stylus ``` ### Install a debugger **Linux (GDB):** ```shell sudo apt-get install gdb ``` **macOS (LLDB):** ```shell xcode-select --install ``` ### Install StylusDB (optional, recommended) StylusDB is a modern LLDB-based debugger built specifically for Stylus contracts. It provides enhanced features like multi-contract debugging, contract-specific breakpoints, and pretty-print formatting for Stylus types. **Option 1: Pre-built binaries (easiest)** Download platform-specific installers from the [StylusDB releases page](https://github.com/walnuthq/stylusdb/releases). **Option 2: Build from source** ```shell git clone https://github.com/walnuthq/stylusdb.git cd stylusdb && ./build.sh ``` StylusDB also requires Python 3 with the colorama package for trace visualization: ```shell python3 -m venv myvenv source ./myvenv/bin/activate pip3 install colorama ``` ## Deploy and send a transaction For this guide, we demonstrate debugging using the [stylus-hello-world](https://github.com/OffchainLabs/stylus-hello-world) smart contract. The `increment()` method in `src/lib.rs` looks like this: ```rust #[public] impl Counter { // ... /// Increments number and updates its value in storage. pub fn increment(&mut self) { let number = self.number.get(); self.set_number(number + U256::from(1)); } // ... } ``` ### Set environment variables Configure your RPC endpoint (must have **tracing enabled**) and private key: ```shell export RPC_URL=... export PRIV_KEY=... ``` ### Deploy your contract ```shell cargo stylus deploy --private-key=$PRIV_KEY --endpoint=$RPC_URL ``` You should see output similar to: ```shell contract size: 4.0 KB wasm size: 12.1 KB contract size: 4.0 KB deployed code at address: 0x2c8d8a1229252b07e73b35774ad91c0b973ecf71 wasm already activated! ``` ### Send a transaction Set the deployed contract address and send a transaction: ```shell export ADDR=0x2c8d8a1229252b07e73b35774ad91c0b973ecf71 cast send --rpc-url=$RPC_URL --private-key=$PRIV_KEY $ADDR "increment()" ``` Save the transaction hash for debugging: ```shell export TX_HASH=0x18b241841fa0a59e02d3c6d693750ff0080ad792204aac7e5d4ce9e20c466835 ``` ## Trace a transaction ### Basic trace (`cargo stylus trace`) The `trace` command calls `debug_traceTransaction` to get low-level execution data with ink consumption metrics: ```shell cargo stylus trace --tx=$TX_HASH --endpoint=$RPC_URL --use-native-tracer ``` **Options:** | Flag | Description | | --------------------------- | ------------------------------------------------------------------- | | `-e, --endpoint ` | RPC endpoint (default: `http://localhost:8547`) | | `-t, --tx ` | Transaction hash to trace | | `-p, --project ` | Project path (default: `.`) | | `--use-native-tracer` | Use native Stylus tracer instead of JavaScript tracer (recommended) | This produces a JSON trace showing functions called and [ink](/stylus/concepts/gas-metering.md#ink-and-gas) consumption: ```json [{"args":[0,0,0,4],"endInk":846200000,"name":"user_entrypoint","outs":[],"startInk":846200000},{"args":[],"endInk":846167558,"name":"msg_reentrant","outs":[0,0,0,0],"startInk":846175958},...] ``` ### User function tracing (`cargo stylus usertrace`) The `usertrace` command generates human-readable function call trees that show your contract's execution flow: ```shell cargo stylus usertrace --tx=$TX_HASH --endpoint=$RPC_URL ``` **Options for usertrace:** | Flag | Description | | --------------------------------------- | ------------------------------------- | | `--verbose-usertrace` | Include SDK calls in the trace output | | `--trace-external-usertrace="std,core"` | Include calls from external crates | **Example with SDK calls:** ```shell cargo stylus usertrace --tx=$TX_HASH --endpoint=$RPC_URL --verbose-usertrace ``` The trace output is saved to `/tmp/lldb_function_trace.json`. ## Interactive debugging ### Replay with GDB or LLDB The `replay` command allows you to debug transaction execution interactively: ```shell cargo stylus replay --tx=$TX_HASH --endpoint=$RPC_URL --use-native-tracer ``` Note The `--use-native-tracer` flag uses `stylusTracer` instead of `jsTracer`, which is required for tracing Stylus transactions on most RPC providers. See [RPC endpoint compatibility](#rpc-endpoint-compatibility) for details. The debugger loads and sets a breakpoint at the `user_entrypoint` function: ```shell Thread 1 "cargo-stylus" hit Breakpoint 1, stylus_hello_world::user_entrypoint (len=4) at src/lib.rs:38 38 #[entrypoint] (gdb) ``` **Set a breakpoint at the increment method:** ```shell (gdb) b stylus_hello_world::Counter::increment Breakpoint 2 at 0x7ffff7e4ee33: file src/lib.rs, line 69. ``` **Continue execution:** ```shell (gdb) c ``` **Inspect variables when the breakpoint is hit:** ```shell Thread 1 "cargo-stylus" hit Breakpoint 2, stylus_hello_world::Counter::increment (self=0x7fffffff9ae8) at src/lib.rs:69 69 let number = self.number.get(); (gdb) p number ``` For LLDB command equivalents, see the [LLDB to GDB command map](https://lldb.llvm.org/use/map.html). ### Replay with StylusDB StylusDB provides enhanced debugging capabilities, including multi-contract support and contract-specific breakpoints: ```shell cargo stylus replay --debugger stylusdb --tx=$TX_HASH --endpoint=$RPC_URL ``` #### StylusDB contract management commands | Command | Description | | ------------------------------------------------- | -------------------------------------------------- | | `stylus-contract add
` | Register a contract for debugging | | `stylus-contract list` | View registered contracts | | `stylus-contract context
` | Switch debugging context to a specific contract | | `stylus-contract breakpoint
` | Set a breakpoint on a specific contract's function | | `stylus-contract stack` | Display the current call stack | #### Standard LLDB commands in StylusDB | Command | Description | | -------------- | --------------------- | | `c` | Continue execution | | `n` | Step over (next line) | | `s` | Step into function | | `p ` | Print variable value | | `bt` | Show backtrace | | `b ` | Set breakpoint | ## Multi-contract debugging For transactions that involve multiple Stylus contracts, use StylusDB with the `--contracts` flag: ```shell cargo stylus replay --debugger stylusdb --tx=$TX_HASH \ --contracts 0xADDRESS1:/path/to/contract1,0xADDRESS2:/path/to/contract2 \ --endpoint=$RPC_URL ``` The format is `ADDRESS:PATH` pairs separated by commas, where: * `ADDRESS` is the deployed contract address * `PATH` is the path to the contract's source directory ### Handling Solidity contracts If your transaction interacts with both Stylus and Solidity contracts, mark the Solidity addresses with the `--addr-solidity` flag. The debugger displays contract addresses and function selectors for Solidity calls, but cannot provide source-level debugging for Solidity code: ```shell cargo stylus replay --debugger stylusdb --tx=$TX_HASH \ --contracts 0xSTYLUS_ADDR:/path/to/stylus/contract \ --addr-solidity 0xSOLIDITY_ADDR \ --endpoint=$RPC_URL ``` ## RPC endpoint compatibility Both `cargo stylus trace` and `cargo stylus replay` require an RPC endpoint that supports the `debug_traceTransaction` function. | Tracer type | Flag | Provider support | | -------------- | --------------------- | ------------------------------------------- | | `jsTracer` | (default) | Limited - most providers don't support this | | `stylusTracer` | `--use-native-tracer` | Broader support from RPC providers | Both tracers are available on local nodes, but `stylusTracer` is more efficient. See the [list of RPC providers](/for-devs/dev-tools-and-resources/chain-info.md#third-party-rpc-providers) for tracing support information. ## Limitations Cannot debug deployment or activation transactions `cargo stylus replay` and `cargo stylus trace` only work on regular Stylus contract calls. They cannot trace contract **deployment** transactions (initial bytecode upload) or **activation** transactions (calls to the `ArbWasm` precompile). For issues at deploy or activate time, run `cargo stylus check` against a local devnet and inspect the resulting error messages directly. ## Additional resources * [StylusDB GitHub repository](https://github.com/walnuthq/stylusdb) * [Stylus SDK debugger documentation](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/cargo-stylus/docs/StylusDebugger.md) * [GDB documentation](https://sourceware.org/gdb/) * [LLDB documentation](https://lldb.llvm.org/) --- > For a complete page index, fetch # Using Stylus CLI This guide will get you started with [`cargo stylus`](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus) a CLI toolkit that helps developers manage, compile, deploy, and optimize their Stylus contracts efficiently. This overview will help you discover and learn how to use `cargo stylus` tools. ### Installing `cargo stylus` `cargo stylus` is a plugin to the standard cargo tool for developing Rust programs. #### Prerequisites Rust toolchain Follow the instructions on [Rust Lang's installation page](https://www.rust-lang.org/tools/install) to install a complete Rust toolchain (v1.91 or newer) on your system. After installation, ensure you can access the programs `rustup`, `rustc`, and `cargo` from your preferred terminal application. Docker We will use the testnet and some `cargo stylus` commands, which require Docker to run. You can download Docker from [Docker's website](https://www.docker.com/products/docker-desktop). Foundry's Cast [Foundry's Cast](https://book.getfoundry.sh/cast/) is a command-line tool for interacting with your EVM contracts. Nitro devnode Stylus is available on Arbitrum Sepolia, but we'll use Nitro devnode, which has a pre-funded wallet, saving us the effort of wallet provisioning or running out of tokens to send transactions. Install your devnode ```shell git clone https://github.com/OffchainLabs/nitro-devnode.git cd nitro-devnode ``` Launch your devnode ```shell ./run-dev-node.sh ``` #### Installation In your terminal, run: ```shell cargo install --force cargo-stylus ``` Add WASM ([WebAssembly](https://webassembly.org/)) as a build target for the specific Rust toolchain you are using. The example below sets your default Rust toolchain to 1.91, as well as adding the WASM build target: ```shell rustup default 1.91 rustup target add wasm32-unknown-unknown --toolchain 1.91 ``` You can verify the cargo stylus installation by running `cargo stylus -V` in your terminal, returning something like:`stylus 0.10.7` ### Using `cargo stylus` #### `cargo stylus` commands reference For the complete list of `cargo stylus` commands, flags, defaults, and aliases, see the [commands reference](/stylus/cli-tools/commands-reference.md). #### How-tos | Topic | Description | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | [Learn how to optimize WASM binaries](/stylus/how-tos/optimizing-binaries.md) | The `cargo-stylus` tool allows you to optimize WebAssembly (WASM) binaries, ensuring that your contracts are as efficient as possible. | | [Debug Stylus transactions](/stylus/cli-tools/debugging-tx.md) | A guide to debugging transactions, helping you identify and fix issues. Gain insights into your Stylus contracts by debugging transactions. | | [Verify contracts](/stylus/cli-tools/verify-contracts.md) | Ensure that your Stylus contracts are correctly verified. Step-by-step instructions on how to verify your contracts using `cargo-stylus`. | | [Run a Stylus dev node](/run-arbitrum-node/run-local-full-chain-simulation.md) | Learn how to run a local Arbitrum dev node to test your Stylus contracts. | #### Additional resources #### [Troubleshooting](/stylus/troubleshooting-building-stylus.md): solve the most common issues. #### [cargo-stylus repository](https://github.com/OffchainLabs/cargo-stylus): consult cargo stylus' source code. --- > For a complete page index, fetch # How to verify Stylus contracts You can verify a Stylus contract in two ways: locally with `cargo stylus` or on [Arbiscan](https://arbiscan.io/), Arbitrum’s block explorer. This guide covers both methods. Info Stylus contract verification on Arbiscan is only supported for contracts deployed using `cargo-stylus` 0.5.0 or higher. ## Overview: Why verify contracts? Contract verification ensures that: * Your deployment is reproducible by anyone running the same environment * Users can independently verify that the deployed bytecode matches the published source code * The contract source code is publicly available on block explorers * Trust and transparency are established with your users ## Local verification Local verification uses the `cargo stylus` tool to verify contracts against your local codebase. ### Goals * Ensure Stylus contract deployments are reproducible by anyone on the same architecture * Sandbox the reproducible environment and standardize it * Guarantee that programs reproducibly deployed with cargo stylus version ≥ 0.4.2 are verifiable ### Opting out By default, `cargo stylus deploy` is reproducible as it runs in a Docker container. You can opt out by specifying `--no-verify`: ```shell cargo stylus deploy --no-verify ``` ### How it works When you deploy a contract, the deployment transaction's `data` field contains your compressed contract code. The `cargo stylus` tool compresses and encodes your contract's WASM in a standardized, reproducible way. Anyone with the same codebase can rebuild your contract and verify the output matches the onchain data. This is similar to [Solidity contract verification](https://docs.sourcify.dev/docs/how-to-verify/#how-does-verification-work) but for WASM-based contracts. ### Verification flow ![Contract verification flow](/img/stylus-contract-verification-flow.svg) ### Prerequisites for local verification 1. Docker is installed and running 2. The same codebase is used for deployment 3. `cargo-stylus` CLI installed (version ≥ 0.4.2) ### Running local verification Use the `cargo stylus verify` command with your deployment transaction hash: ```shell cargo stylus verify --deployment-tx 0xd4...85 ``` **Example output (successful verification):** ```text Reading deployment tx from chain... Wasm data hash: 0xf80a... Program succeeded Stylus onchain activation checks. Connecting to Docker... Reproducing wasm binary from local code... (This may take a while) Finished release build in X.XXs INFO: contract code matches the onchain code ✓ ``` **Example output (mismatch):** ```text Reading deployment tx from chain... ERROR: contract code does not match the onchain code ✗ ``` ### Troubleshooting local verification **Error: Docker not running** ```text Error: Cannot connect to Docker daemon ``` Solution: Start Docker Desktop or Docker daemon **Error: Mismatch** If verification fails, common causes: * Different Rust toolchain version * Different dependency versions * Modified source code * Different build flags Ensure you're using the same codebase and Rust version as the deployment. *** ## Arbiscan verification Arbiscan provides a web-based interface for contract verification, making your source code publicly visible on the block explorer. ### View verified contracts You can browse verified Stylus contracts on: * [Verified Stylus Contracts on Arbitrum One](https://arbiscan.io/contractsVerified?filter=stylus) * [Verified Stylus Contracts on Arbitrum Sepolia](https://sepolia.arbiscan.io/contractsVerified?filter=stylus) Example: [English Auction Stylus contract](https://sepolia.arbiscan.io/address/0xe85a046fd3ea22ceeb3caef3a0d38123eecbe3ca) (verified on Arbitrum Sepolia) ### Step 1: Navigate to the verification page You have two options: **Option A: Direct link** Visit [Arbiscan Verify Contract](https://arbiscan.io/verifyContract) directly if you have the contract address ready. **Option B: From the contract page** 1. Go to your contract's page on Arbiscan 2. Click the "Contract" tab 3. Click the "Verify and Publish" link ![Verify and Publish link on Arbiscan](/img/stylus-arbiscan-verification-1.png) ### Step 2: Enter contract details On the verification page, provide: 1. **Contract address**: The deployed contract address (e.g., `0xe85a...3ca`) 2. **Compiler type**: Select **"Stylus (Rust)"** from the dropdown 3. **cargo-stylus version**: Select the version you used for deployment (must be ≥ 0.5.0) ![Select Stylus Rust compiler type](/img/stylus-arbiscan-verification-2.png) Then click **Continue**. ### Step 3: Submit source code You can submit your source code in two ways: #### Option A: Single Rust file If your contract is a single `.rs` file: 1. Click **"Rust File Upload"** 2. Upload your `.rs` file #### Option B: Standard JSON input For multi-file projects: 1. Click **"Standard JSON Input"** 2. Prepare a Standard JSON Input file containing all of your contract's source files and metadata, following the fields requested by the Arbiscan form. 3. Upload the Standard JSON Input file Info The `cargo stylus` CLI does not currently emit this Standard JSON Input bundle. For CLI-based reproducible verification, use the local `cargo stylus verify` flow shown above; the Arbiscan Standard JSON Input is prepared through the explorer's verification form. ![Standard JSON input option](/img/stylus/standard-json-input.webp) Click **Verify and Publish** to complete verification. ### Step 4: Verification result If successful, you'll see: * ✅ Verification successful message * Your source code is now publicly visible on Arbiscan * The "Contract" tab shows your Rust source code ![Verification success message](/img/stylus/verification-success.webp) ### Handling previously verified contracts If your contract was already verified: 1. Arbiscan will detect this automatically 2. It will display: "Contract Source Code Already Verified" 3. You can view the existing verification in the Contract tab ![Already verified contract message](/img/stylus/already-verified.webp) ### Troubleshooting Arbiscan verification **Error: cargo-stylus version too old** Arbiscan requires cargo-stylus ≥ 0.5.0. Update your toolchain: ```shell cargo install cargo-stylus --force ``` **Error: Verification failed** Common causes: * Wrong cargo-stylus version selected * Source code doesn't match deployed bytecode * Missing dependencies in the JSON file Solution: Ensure you're using the exact source code from deployment and the correct cargo-stylus version. **Error: Contract not found** Ensure: * The contract address is correct * The contract is deployed on the selected network (Arbitrum One vs Sepolia) * The deployment transaction is confirmed *** ## Which verification method should I use? | Method | When to use | Benefits | | ------------------------- | --------------------------------------- | --------------------------------------------------------------------- | | **Local verification** | Quick verification during development | Fast, no external dependencies, proves reproducibility | | **Arbiscan verification** | Publishing verified contracts for users | Public source code visibility, block explorer integration, user trust | **Best practice**: Use both methods: 1. Verify locally after deployment to ensure reproducibility 2. Verify on Arbiscan to publish source code for your users *** ## Next steps * [Learn about deployment](/stylus/cli-tools/check-and-deploy.md) * [Explore CLI tools](/stylus/cli-tools/overview.md) * [View verified examples](https://github.com/OffchainLabs/stylus-by-example) --- > For a complete page index, fetch # Activation Stylus contracts undergo a two-step process to become executable on Arbitrum chains: **deployment** and **activation**. This guide explains both steps, the distinction between them, and how to manage the activation process. ## Overview Unlike traditional EVM contracts that become immediately executable after deployment, Stylus contracts require an additional activation step: 1. **Deployment**: Stores the compressed WASM bytecode onchain at a contract address 2. **Activation**: Converts the bytecode into an executable Stylus program by registering it with the [ArbWasm precompile](/arbitrum-essentials/precompiles/reference.md#common-precompiles) **Why two steps?** * **Gas optimization**: Activation involves one-time processing and caching that would be expensive to repeat on every call * **Code reuse**: Multiple contracts can share the same activated codehash, reducing activation costs * **Version management**: Allows the chain to track which Stylus protocol version a contract targets ## Deployment vs activation | Aspect | Deployment | Activation | | --------------------- | ----------------------------- | ------------------------------ | | **Purpose** | Store compressed WASM onchain | Register program with ArbWasm | | **Transaction count** | one transaction | one transaction (separate) | | **Cost type** | Standard EVM deployment gas | Data fee (WASM-specific cost) | | **When required** | Always - stores the code | Always - makes code executable | | **Reversible** | No | No (but can expire) | | **Who can call** | Anyone with funds | Anyone (after deployment) | | **Can be skipped** | No | No (unless already activated) | ### Contract state A Stylus contract can be in one of these states: ```rust pub enum ContractStatus { /// Contract already exists onchain and is activated Active { code: Vec }, /// Contract is deployed but not yet activated /// Ready to activate with the given data fee Ready { code: Vec, fee: U256 }, } ``` ## The Activation process ### Step 1: Build and process WASM Before deployment, your Rust contract is compiled and processed: ```shell cargo stylus check ``` This performs: 1. **Compile Rust to WASM**: Using `wasm32-unknown-unknown` target 2. **Process WASM binary**: * Remove dangling references * Add project hash metadata * Strip unnecessary custom sections 3. **Brotli compression**: Maximum compression (level 11) 4. **Add EOF prefix**: `EFF00000` (identifies Stylus programs) 5. **Size validation**: The *decompressed* WASM must fit within the chain's `MaxWasmSize` parameter (default 128 KB, raised to 256 KB at ArbOS 60+). This is a chain-configurable ArbOS parameter, not the 24 KB EVM contract-code limit that applies to Solidity. At ArbOS 60+, programs larger than the limit can be split into a root plus fragments. **WASM Processing Pipeline**: ![Activation Process](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAxAAAAAuCAIAAAA3A2jqAAAAAXNSR0IArs4c6QAAIABJREFUeJztnXl4TFcfx8+dfbInM9kTiex7RCIRSUiooA2CqCWldq2lKH0t9Sol6lVtaZS+aF9U31LaogixJjQLEkkIIbLInkxmMpkls96Z94/jvW4nqyyScD6Px3Pce+6Zc79+597f3c4X02q1AIFAIBAIBALROpTe7gACgUAgEAhEXwclTAgEAoFAIBDtQOvtDrzmFNwRo2eeAAALeybHmtH1dpCeZJCq3YJHkCHW5StHmQQvfdjUPR3qbyABO42znz6D1VXt1Eptfi4fvV3TRZxcTYxM6W3XQQlTz1JXoXD0N+ntXvQ+hTkijrVZ19upKVM4BSA9n9NdqtaWKwYOekNVLcltdA3Q0ihYF9sR8lQyOca1Y3VTv/oNxTlIwE5S/VQqFeFdT5jkTXgmP1vmUdpN/XojEegxy94xMjVtuxZKmHoWBouib4xE7jaQnj0BnfnmqspkU7urKZY+9Q2UEQnYaZj63SYdxlYDY1l3tfYmoqSADuiH3mFCIBAIBAKBaAeUMCEQCAQCgUC0A0qYEAgEAoFAINrhxQPjolxR5ZMGtj5KoQAAAFAYQ8Za9nYnEAgEAoFA9AlIb9hpNL7uhaYcZW92p89wJ9sNAJQwIRAIBAKBAOiRHAKBQCAQCET7oIQJgUAgEAgEoh1QwtSHaGwU7ty5KTFxR0bGzZ5o/8aN5NzcrI7UvHbt4v379wAASUmn+fz6nujMq0Gr1V69eoG8REeEr7/+/KUaPHHisEKhaG0tjuMCAV9nYVLS6cLCgjbaVKvViYk7XqobfZna2ur9+3ft2rVZIhHL5XKpVEJee//+vby87N7rXV9h9+6EXbs2X758rrUKRCyVlZWcPn38NRiM3U52dqbOcCMPZ/JIz8rK2L07YffuhAMHdgMAHjzI2blz059/noTBmZi4A65NS7vR8V+vrCzftWtzQcGDY8cOdt8+vQoKzhSkf5Oe/k36gxMPau/X1ubVtlZT+Ez46I9Hr7Z3umQfylaIWz3k6pD2VVqPdgYlTH0IqVSiUiljYuKOHft3fX0dAIDHq1Wr1QAAkahRJpNJpRK5XF5bWw3ry+VykagRAMDn18NqPF6tVqtVKBTND6w8Xm19fa1UKtZoNOXlpWKxCAAgFotwHIe/BQCQyWQ8Xm1paVFDA18qlSiVSgsLKz09fZGoUSwWyeVyWK2qqqKyslwm6wfzpOXn5yYm7lCpVPCfhAgAAKVSyefzSkqewlVarZbPr+fzeVBJgYCvVqurqirIOkulEgcHZyqVCgAgi4zjeEVFmVqtzsy8+euvhxsbheQ+8Pm8hgZ+XV0N0QeiLJGI6+vrNBpNWVlJbW11G6lYP+K//z0UEhIxffo8PT39I0f2ZWbeampqIvSk0+lcroVYLJJKJTpRyufz4P+LWCzSarWVleVKpZIstVKpFIka+fz6fhF7bZOXl7V48ccpKcnl5aVwXFdVVWg0GiLYiFiSy2U8Xi0cjPr6BkQLRNQBABoaBFArsVikVqv5/HocxysryzUaTRuB3dgohEqS1ebxaiUSca9q01GOH//PzZtXYFlnOJNHOgCgtLTIycl1xox5kybNkMvle/YkTJjwrr6+wRdfbAAA3Lt3e8aMeTNmzPPzC4T1+fx6GKI6UafVauHglcvlJ08ejYoaa2/v6ObmJZPJ4BG1oUGA43gv6dFRqu5WuY519Z3p6/SWE4VGYXPZCrFCg2ukPCmsIK2TSuokAAC1XN3E+5tfjUqmktQ8vwSSNchwJQ4AkIvkKplKKVGq5CpJrQQAoJKrFGKFtO55g031TaomlUwgAwDIG+UqmQoAgKtwUaUIhqioUgSb0mq1xFYAAEmtRFQu0uIvXF80uEZUIcLVOLkDAABciTfVNwlLhM0b6UbeoGlV+wUwehQKBYZh3377BZ1OLy4uXLNm81dfbXF19Xz0KM/IyIROp/v6DnZwcP7tt5/09Q19fQc3NjZYW9sFBg7duXPTwoUrjx07wOVaGBoazZ27FDZ74MA3IlHjw4d5CxZ8lJGRmpZ249mz4jVrNt+8ebWurlqpVLq4eISEhO/evc3MjMtm6zk4OAMAhELBd9/tTEj4ds2aRWPHTkxNvZKY+NP333/FZrOTkk4fPHiSzWb3tmDtcPHi6REjRmdlpQ8dOpwsQknJ0337vrSxsZPLn596VSrV/PmTx4+fmpeXtW5dwv79u8zNLX18Ani8WkJnOzuHAwe+cXFxFwj4hMjDhkUeObLfxcXD3d372bPioqInjx7dHzo0gtyNU6d+MjY28fUdHBo6IjHxCwCw8PCRPj4Bn322ysvLb9q0OXl5WWfOHM/Ozty79xiN1r9HpbW13a1b1+bMWUKhUIqKnojFIkdH58OH90E9r149/9ZbMRUVz549K6JQKFZWtkSUbt68OjHxaFHR4/T0FIVCrlAobG3t/f2HEFKHhUV9/fXnQUHDpk6d3fdjr22kUvGVK+cBAObmVufP/5abe8fd3cfHJ4AINqFQAGPJzIxDDMadO79nsVgAgMeP84moq66uLC19KhQKZs1afOdOWkXFM5VK2dDAd3f3rqmpWr9+e4uBnZt7Nz8/RyIRjxkzISfnDlSbw7G4du0Ch2O+atU/e1uhdqioKDMz46akJE+cOE1nOJNHOlG/tLSIxWIPHTo8Ly8rPHykh4ePh4fP77//3NQklUrFubl3DQ2NQkNHwMoJCWs9PHyfPi2YNGkGh2MOo278+KlbtyY4ODhVVpYtXLgyJ+e2sbEJnc44eHD3rl0HN21a+dFHGw4d2pOQkNh7qnSU2vxaliHLcaRj+lfpztHOonKRpE6iUWpMXU0dhztmfpsJMOAQ7mDsaEzeqvBCYXVWNcuM5RHrUZ5eLiwRyhvk/rP9s3/I5rhyeA95LGMWhUax8LMwcTDJOpBl7mUuqhCN+mLUuQ/PuYxxsR9mX5FZwXvAU0qUTtFO93++b+lnaRVgVZ1VrVaojWyN3Ma7pWxJMXEwEVWKwjeEp36eamhtWJ1TTXSgvqA+53COmasZ150rrhETHaAyqXe+u2NobaiWqVUyFdFI1NYoDOuqaQ8ZdIepb1FS8vSbb7ZGR49XqVTZ2RkMBpPJZKanp+C4eu7cpSNHjouMHPPBB6vv3bt96dKZhQtXfvzxpqSkP6KjJ1y7diE19co770y5cOE3AwNDfX2DixdPQztGgYBfVlayZs3miROnAQB8fAIcHV3odHpOzh0AwKhR7yxZ8kl2dsb9+/eiosbOnLlAo9HAzlhYWLm6egAAbGzsZ81aPGRIWElJYU7O7YULVw4ZMqzvez02Ngrr6mri4mZdvnxOR4Tk5LNz5ixZteqfLNaL866rq+f8+csnTHg3M/MmAGDy5PioqDFknf38BkNByCKfP//7ggUr5s1bFhYW5e09yN8/SCdbAgDExy9cuXJjRkaqmRk3IuIt+H/KZLJwHA8JieByLf38AhctWjVkSBhxt6//Mm3aHE9P35Ur51ZVVfj7B40eHePk5EroGRoaCavFxExdv357ZuZNeOdDByMjE6lUPGrUOzrx7OLisWDBR8bG/d75js3Wc3Z219c3yM29CwAIDx8VH7+AHGw6sUQMRsiFC8+jLjR0xJUr51av/mzGjPkpKckAgHHjJr333qKBA10XL/64urqixcCOjIw+efKoqSnHxMTs0qWzhNqmpmaNjcIJE6b1njAdJTn57JgxEywsrIuLC8nDWWekE+jrGxgbm1KpVLG4kc3WgwvZbH2ZrIlGoxsbmxoavkgOMAybN2/Zxx9vunTpLAAARl1+fo5KpaLR6NXVlXK5fOBA19jYGX5+g1kstrGxyYwZ81evXrBixacUSj84q7KMWEwTJsCAfZg9XOL0llPQ0qDqu9VsDtthuAOVQS1PLydvotVqn5x7ErY+LHhpsIG1QXFy8bA1w3zjfUtvlGrUmoB5AQNHDXSMcgz6MKjmXg0AwH6YffCyYH1zfcFTAZ1ND5gfwPXk5v+azzJlMU2YxcnFdD0604hpPdiaacRUSpRObzmVp5Vr1BoKnSKplhSeL7QaZBWyIsQ6wJroQ+H5wsBFgYPnD7YbZkfuQNHFokFzB4WuDqWxaeRGdG6PdZ3+fS37+uHl5ff++x/+858rQkMjra3t4uMXAgBoNBq88wyTZRqNrtVqWSy2Wq1WqZQsFpvD4RoYGCUnn92162B+fk5k5BgPD9/4+IWwvkaDk+8S//jj3qCg0GnT5j57VgSXMBhMtVodEhK+ZMlMJye3uXOX5efnqNUqnb6x2Wy1Wh0R8daSJTP9/AItLKxerTYvzY0bl2SyplOnfsrKyhAK/3arXKlUtpbwyWRNMIsyMjIGAJB1JurQ6QxC5IMHdyuVzx+lYRimUMhbbJZGo6tUqpSUyyUlhcuWrfvXvzZyONzExKObNq0kesJgMIlstV8zenRMY6Pw/v1s+EANLoR6ksFxHMdx4gRDp9MVCgUUMD5+QVrajU2bVnh6+hFSl5UVGxu3447ZX6DTGX5+g+vqqh88uGdiYmZkZKITbKRYwpoPRhqNDqOOQqHQ6QytVqtQyNlsfbiWuKqm0xnEJjqBraenN3XqbAaDSaVS6XQ6VHvv3mNr125bvXr+sWMX6PR2nNt7ERzHr169IBDU8/m8a9eSyMNZ53BH4OrqGRISDgAYNGjIjh0bJ0+OF4ka5XIZh2Oup6cPV+kgkzXp6ekDAGDU0Wj0QYOGTJnyXnz8QgaDoVO5rKzYxMS0urrCysqmZ3a6O7EebK3H1dNZSGVQNbimNKW0obgh5KOQW1/cwjBMo/77EUkDYNRRGVStVquWq+l6z+MERh2FTgGkI6uqSUVj0phGTAzDtFotnU33eteLxqBhVAxgIOc/OblHcgcvGFz+V/m1jdd8pvlYDbLyivPye8+vLK1MWqP7WI1Cp6gV6uYdkAvlxI9SaVSiESqz29z6IChh6nOYm1uOGzfp+vUkT0/f7dvXS6XihIS9/1+JEWeXceMmHTmyX6PB4+JmAQDeeWdKXl4WnU4fP37q999/RaPRQkMjx42LBQBwuRaOjs4bNiyTy2VxcbPc3b1PnDjs4OAkkYhdXF5cszY1Sc3NLalUakHBfR+fgG+/3R4WFtW8e/X1ddbWdnV1NXx+PYfDfTWadI7z53/bufPfJiamdnYODx/mkkUYOzZ2//4vBwxwUqleTDxWWPhoy5Y1AICVKzcSr3/q6AwTGrLIMTFxBw/uZjJZ4eEjQ0NHHDjwdWKidPnydS12yd7e8ejR/XK5vKamqqysZM+ehIYGAZf7Ws34deLE4by8LK1Wu3btturqin37vqytrWpe7ciRfTQaferU2URIjxw5bsuW1SqVytPT94cfEvPystzcvMlSOzu7v/K96Sl4vNpNm1ZSqbRFi1alp6fAheRgc3Fxh7G0YMGK3Ny7ZWUl5M3JURcbO/3zzz/RaPDFi1dfv36RnDBBmgc2hmHTp8/bseNTlUq1aNGq1NTLUO3U1Cvnzp0yN7fs47dJqqrKR4yIXrRopUKhWLz43XXrEg4c+AYOZ53DHayPYdjhw/v+/PMkh2O+bt02Z2e3RYve5XItyM/syGi12oSEdXK57P33PwTguZghIREpKcnbt683M+OuXPkpuX5hYUFeXlZi4k8bNiz78ssD8LFpP8XY3jj3cC6uwCW1En0r/ZqcGuEzoYmDCYZhPtN8rn56lcaiBcwN8Ij1SN2SqsE1QR8GVd+rBgAA7G+BV5RcVF9Qb2hjaDzg+ZUShmE+M3xubb+lUWsGLxqc82OOTCDzmuKVfSi7NreW6861HWpbmlKampDKNmMHLgy8uf3mrS9u8R/z//8/ANzGu2X9O4vGog0IH0DugKpJdee7O8YOxrgSJzcSuiq0e8XBiMS86J7QjHq34xNX7jxwXiSReThZx40dwmLpptvNOZ2cFezvZGNpej39YVSol0jcdOZK9nuxYUkpuW9HDgIA5D569kdylpW5yQczR15KzRsz3A8AkHzzfnSEL2yhskZw8MQNFpM+JsI3wNvxdHJWeJAb18ywC7vfKney3YaMd+l6OxlJAtdgs05vrlKp2rjO02q1Go0GvoMMP7YiXn9pviF5LSzr1OHxavPysrhciz17tv/44+84jhMtk0lNvWJubnnmzIlRo94eMmRYB3ek8LZg6LjO60CQdl7gPrTz7ZBFgKkPcWJQKpWffrp8+/a9zQUndH706P6ePQm7dx+GB0SygETLOI7LZLJffvmB2HzmzPnkd3VhTeJvCoXS6ZNTH1G1OQqFgslkwrJKpaLRaDqn8GPHDvr6Bnh7D9J5Yat5lBKN9NANj8I7wqBRxjR6V190qC6RNwi0lo5dfbOKPKhxHNdqtTQajSjoVCYkImulQxuBrVarMQyDv9WRplrkyW3hkLd6X0Cd4dzuXpBDtDmrVy9ISNjbYt7TjaFYXiBxcGWaWnS1NYlQ/UtucpNfYbf0CgCAq3EqjapRayg0igbXaLVaKu35uUCDazAKBoczrNZiCzU5NXUP6ryneVPpuhVwNY5hGIVKAQDAnyAX4MvgxFYt/gSxkLxWJwDIjXQInn60bIqnfzs3sDt/GZFx7+ni6VFPn9Ve/itfo9GUVvBE4iYAQA1PCACQK5Rw5PP4Ilgfw8Ctu0+USvVne34XiqS384rlClXuo7Id359TqdQAgG3fnV02a/Q7kf4AgD2Hk+vqG6tqG77/7zXiFxvFMiqVMi9u+Na9ZxQKlZW5sYE+UyiSqlTqhkYplIzohkQqlyuUFdWC2vrnr80TPelHtD0yiYMdhHyAaL4heS0s69RhMJhCYcPjx/lbt+4BALSYLcFXAe7c+WvYsMjAwKEvv0O9DFkEnUyFQqFERIxqUXBC54KCB+vXbycOo+TKRMtUKtXAwGDhwhXEH3K2RNQk/u7jl/Kdg3wqotPpzd+7dHPz4nItm5/Smkcp0UiPdbZvQR7UVCoVikAUdCAWtpEctBHYNBqN+K2ONNWX0RnO7e5FG9kSAGD48NGthdybEIowC4EZDIVKIacsFCqFGM6tZUsAAD2uHteD22LKQqVRYbZE/AS5AAAgb9XiTxAL/9axvwfAy2VLHYa6efNmWGqokbMpVWy9jn4SeeJ85tgRvnfvl7gOtCqvEpy+nPXz2XR/jwFbEv8YHuz+89l0AEBtvejSzbxgf2cAAMfE4NjpNHOO4d37JZZco/R7T9+JGnTq4h0XR0sqleJgy029XQAACPIdKJHK9/981d6GU14tOH89d+aEUAaDBgDgCcQXU/OkMqWJkb6Hs/WGXSdHh/l8+tWpqjrh4d9uOtmbFxRVE924kpZ/9PdbIonsv2fTw4PcnlXW//JnenhQR2/pV1VzbN07f8399ttvi0QiGxsbUS2dY9s/PudhsVheXn7e3oOav2tCxsbGzt8/yMHB6aW+PhBUyuxcO69DXFwcjuN2dnb15YBr1yN6UigUd3fvtut4evqamPShd2i6qOqkSZO0Wq21tTW/EushVVvD1nZA22H2yhBUyW2cWBRqJ2+QjBkz5sGDBxYWFvpMrlwGDEz63Nm0I4HdFfiVctsuC2hpaanP5PQdAT08fF7BlYyoXmnCobH1O3lqj4uLy8jIMDMzMzHiPuSVqiwF3d3BzsM0Yhra9MjDn56iieGs9jK3aucw2PmLCbFU/u2RZLVaEzXUUyyRV9QIyqsFd/KKx43wv5b+MD37aXm1wECPOWXsEFjfnGPUJFecv567+aNJP59Nq6husOQa1fAa130Qs+/Y1eHBHrvWz/jh15QP/nn4w5kjZ00K+yurkErF3p8cXlhaE+DtCBsxMdLzdLbJyEnFMMzD2RoAoMbxhdMivV1tsx+Uxo4OJLoBABg1zHv8qAAbS5PLt/Jr6xvHDvfr+N5JpdKkpCQmk6lQKBgMhlKppNPpcDof+E94qaFSqSgUCrwZCJfDTaqqqg4dOnTmzJlZMVu78kjutYHH4yUlZZIfRUGhCBnhkV2j0cC18BoRVsYw7OnTpzt37jx58uSa+Yd6e1f6EHw+PykpEwYelIsITqJApVJxHCdEhgUGg6HRaEpKSr788ssTJ07Mn/wlAG9olGo0mkuXLjGYVLJ6z19Q/b9oRKySpYZ/y2Syq1evpqWlDQ+eGD9zaW/vTS/QooA6UYcEbI3U1FSWoZp83IOiEf/UGdFQWHiobGpqSk1NzcjICB/6luvE/ne/vz/S+YTJxEhvxyfTFqz/obSi/pc/00MDXOfGRRQ9qxsf7hP7we41C97OLSh7+LRqneOLb6kiQzzPXMn+3HvKoV9TAn0ck28+aJIrfzr9V0bOU6FIamyot3TW6JVbj919UOLtaiuSyBRK9SAvh4LiaiJh4poahgW5XU3PLyytIXeGxaSrcc3eny4T3aDRqCZGegCAqKFeyzYfBQAseW9Ux/eOyWS6+vnBuIRHT/g3cTAlCjr/JPIne3v70aNH21j3gy8mXgFGRoYufs8TVh2hiAKhJERnuaur64QJE3pvD/oiBgYGhKrkWG0jSglV4XNqZ2fn2NhYI/1+dS3YrWAY5uPjw2BSNRqNTsi1JibcEFaGn5gFBwdHR0f39q70DhQK5uPZJQFpNNobK6C7u7uhGUYemOQR2vwwSBaQxWJhGBYUFBQTE/MYoCngXwVdelxNo1HXfRCzff/Zd6IGHf79ppO9uVgqf39KxPBgj+gIH1dHy8zcYnL98CA3hVINABg3ws/czHDXoaQDCfNMjfUdbLhX/sq/nVssV6jMTPTrBeIxEX56LKZMoXSyN7986wHcHMNA6u3HFTUCS67x0EEuF27k6vTH29WO6Eagz/Mci81iONpxrbjGL3WLlUaj2dradlqZ+fPnx8XFWVtbZyT1odukvQiTybK17fw9jFWrVsXExHC53LTzSM8XMJnMrqi6fPny2NjYN1xVDMNsbW07/c6yn5/frFmzgoKC4DvL3d27fkFXBZw9e3ZgYOCbKaClpWWnX/r28PBYsWLFsGHDFFLscW5yd3cN0QKd/0pOB7Uap9GoKpWaTqfBMrGweTW1GqdQsOYZjEKhYjKfRw+c87q1V4870g24pLa+8Yv9f348f+wAm5f4Br6PfCXXQbKyMi5fPrds2VoDgz56q6DPfs/VQfh8Hodj3uKqGzeSTU05/v6BxBIcxxsbhXCC5h6lz6rahlydbbDezIyDYVhS0ung4PBunM+ir30lp8OBA7vj4xfofDfQImVlJdnZmbGx04klcrk8Le3GyJFjdcrdSx/5So5MTU3V8eP/geUxYyZeunQGlidPnnnmzAk4S1NwcJi9vePp08dDQiKCg8PKy0tbLIvFolOnfrKwsI6OHi+Xy4hyt7z63We/kusWHp99zHvEcwh3IObGFFWK5EK5WqZmGDA47py8n/I8JnlkH8rWqDVW/lbO0c692d2e/kpOB5gYwTSFSJJozV5xh0toNGqL93uIbOn5zFQvmS3pdANSUs6bMyXipbKlPgvZc4ow2BKLRT//fHDhwhUGBoYtum4R/kdarVbHFU4mk9XUPJ8jh/CtU6lU0IWqV/f1ldKiVR+hm1DYsHnzah6vFqpdXl5KbEhYVikUitLSIvjGBuECRrbte50gXMxadH8jy6XRaODc5VqtViwWNTQI1Gq1SNQoFDaQGyT7lxFxCCepb2wUKpXKzZs/rqh41tTURPipkd3TCD9Esifa60FFxTM4Eomh3ZpxJLScIzbk8Wo/+WRRZmaqTvlNQCQSKhRyaA8Hp/SEZUtLG8I2zscnYOfOTZMmzfzjj/+Wlz9rsVxZWb53746goGF8ft316xfJ5d7exT4K4eymwTVMI2bQB0FZh7Kgc5xSqry28ZqkViIsFUprpYUXCrUaLdOQWZtXG/RBUOmN0sayxt7ufvv0yy9IX4qhAd1wo6iPcPHiGeg5NWRIGGGw5ecXWFlZdv/+PRsb++auW2T/ow0bdnz44QzCFe769YvZ2RmmppyJE6f/9tsx6Fu3evVn27b9w88v0N9/SIuz376WbNu2Vseqb/To8Vu3fgJ1i4ubJRDUp6enjB8/9euvPzc2NlWrVWvWbCZbVv3662GhsOHx4we7dx8uLS2CLmAaDU7Y9jk4OPX2XnYPT548Onz4O+hidvz4f5q7v3l4+EK5hg8fvX37egcHJ5VKuWTJPxYsmDJ2bGxm5k0XF49nz4qXL1/v5uYJALh27SLhX0b2Tzxx4jD8EhPqn5JyOSwsCvqpnTt3iuyeRvgh8ng1sA9Tp87ubZ26kydPHhFDm8Vit2gcqTPNLIVCWbLkk9Onf9EpvyHU1VXn5t51d/em0aiw7ODgZGlpTdjG2djYDxzoYmc3wNd38L17t1ssl5WVVFaW+foGAADS01OIckbGm5J6dhyFWPHk3JOmuqaQFSFw9gHHSEd5o5xKp9LZdA2uyfg6w9zLHI7oxvLG8rTysXvGAgCUEmXx5WIAgL6Ffm/vRPu8hnPAvN5AzymywdbgwSEWFtaRkdEtum6R/Y+KiwsJV7ji4ifnz59au3bbkiWfUKlUwrcuIyOVzdY3NDQePDikt/f11dHcqi89/QahG4PBtLKymTDhXQzDYmOns1isW7eu6VhWRUaOMTIylkjEJSVPCRcwHdu+14NLl84Q3nnk5YQfmaenL5QrIyM1Kmrs0qX/qK+vEwobBgxwmjt3qb2945w5S6ZOnV1QcB9uSPiX1dRUEXF48eKZxsaG5cvXLV++ztra1sbG/t133x840MXV1UOj0ei4pxF+iEQfekmbnoI8tKOjx7dhHEnA4ZhDuxWd8hsCm61nbGzKZLKIMvSPI2zjRKJGuBbHcaVS0WKZSqU0NTXB4wOF8qLcT6eq6lEuLL3ANmXDbIng4amHHpM8AAC5h3MNbQ0NrQxlfBkAIP9kvkqmUoqVAAA6m27mYsbQZ9Tk1rTefF8BJUz9DHjgo9MZ0dHj4+MXHjnyJ9k3iryRJw89AAAEZklEQVSQ7H8UH78wMfEn6IFKuMIRs6PSaDToW7dx486YmLgvvvhOKpUcPfp9r+5oL0C26vu7bm7Q1YvPrz927ODs2R/a2Q3Qsaz67LOPx42bFBHxllqtIlzAfvxxr5WVzbRpc1szmOuP0Gg0wjtPx/0tKmrspk0rAABwCZPJwnE1fJyk471Fo9GIZ74BAcFr127bsGFpU5OUiEPoP03ehDCxadE9DfohkvvwekChUFQqFXloczjm0DgyLCzq70Oe0txy7o3F0tImJCQc2rrB8oABAwEA0DbOx2eQmRm3uPgJAKC09KmPz6AWy25u3gqFXC6Xl5WVeHn5EeUendeqnxLz7xiVTJX+dTqxhF/Ir3tQ5zzGGQDAMmMxDBmCIkFDSQMAIGBeQODCwOwfsuEMk5Z+ljbBNnX3+4HvOMqU+xNEbtTcME5nIeG6RfY/WrLkE3JT7747Z+PGj1gs9pw5Swjfum3bEj//fE1DA3/y5Pje2MXe5cWHCDq+UdbWdlu2rFm0aBWfz9u3bycAQCCoJ1tWeXn57d37LwC0JiZm48bFQhcwsm1fb+9atzFu3KQDB76BLmYtur/B/HvLljXz5i0/enT/3bvpgweHQBNTmAHoTHlK+JfZ2NiT/RMHDnTZuPEjCoW6ceO/IiJGbdu2dvr0uXCT5u5pEKIPr1aSHiQkJOL48R91xnuLxpGRkWOg5RzMDN5ssFu3rkL3PSJmdLC1tXd3916+fHZISISHh0+LZRMT03nzlq1d+4Gt7YClS/9BLr/yPerTqOXq/F/z4XyVcqGcZcLSaDRpX6ap5eor/7jiPtHdc5InAKBQv5DGpskFcrYJ236YfWFSYV1+nbReem3jNQqNErg4sAM/1ct021dyrxn94iu5Fl2NOr4Q3nYmTmAtOqN1F332e662IWsCzaeI+yLEXHMtOvQR5l8t2vZ1F72oaovWY+Qy4dXVkVhqzTauxQabr2qtqY7Qx7+S+/+zIWobyhDLW7Oc61H64FdyHae16O1Iueu8Nl/JabVapeR55kDXoxPOJ/2Jjn0lh+4w9WNaPAd3fKGOYVyLzmhvOGRN4NmauAUFC6059OlYdL1+/lMtWo+Ry0Ry05FYas02rsUG22729QtdIpZaU4ZY3onPit9wWovejpQRBBiGMQ3b8uZ7beiHmSACgUAgEAjEqwUlTAgEAoFAIBDtQLrBiGGFhWb61W/QdIVtQUE3txEIBAKBQDznRcLk6GNk4+Lfq53pQ9AZ6N4bAoFAIBCI57xImKg0jG2AbqsgEAgEAoFA6ILuoyAQCAQCgUC0A0qYEAgEAoFAINoBJUwIBAKBQCAQ7YCm4epZMAwUpAt6uxe9j4O7Xre0Q6EgPV+AVO06GAYo1K7OUg0AMDSlFeeLGqpl3dGp/gQSsCuwDbphCDNYFBOJlX4apzt69ObCGdL+3JuYjsc1AoFAIBAIBEIH9EgOgUAgEAgEoh1QwoRAIBAIBALRDihhQiAQCAQCgWiH/wE8sKE+0jcJyQAAAABJRU5ErkJggg==) *Figure 1: WASM binary processing pipeline showing transformation from raw binary to deployment-ready compressed code.* ### Step 2: Deploy the contract Deployment creates a transaction that stores your processed WASM onchain: ```shell cargo stylus deploy \ --private-key-path=key.txt \ --endpoint="https://sepolia-rollup.arbitrum.io/rpc" ``` **What happens during deployment:** 1. **Generate deployment bytecode**: Create EVM initcode with embedded compressed WASM 2. **Estimate gas**: Calculate deployment transaction gas cost 3. **Send deployment transaction**: To the Stylus deployer contract 4. **Extract contract address**: From the transaction receipt **Deployment Bytecode Structure**: ```text EVM Initcode Prelude (43 bytes): ┌─────────────────────────────────────┐ │ 0x7f PUSH32 │ Push code length │ 0x80 DUP1 │ Duplicate length │ 0x60 PUSH1 │ Push prelude length │ 0x60 PUSH1 0x00 │ Push 0 │ 0x39 CODECOPY │ Copy code to memory │ 0x60 PUSH1 0x00 │ Push 0 │ 0xf3 RETURN │ Return code │ 0x00 │ Stylus version └─────────────────────────────────────┘ ↓ ``` ### Step 3: Calculate activation fee Before activating, the data fee must be calculated. Conceptually (illustrative pseudocode, not a runnable API): ```rust // Simulated via state overrides (no transaction sent) let data_fee = calculate_activation_fee(contract_address); // Apply bump percentage for safety (default: 20%) let final_fee = data_fee * (1 + bump_percent / 100); ``` **Data fee calculation**: * Uses state override simulation to estimate the fee * No actual transaction sent during estimation * Configurable bump percentage protects against variance (default: 20%) * The fee is paid in **ETH** when activating ### Step 4: Activate the contract Activation registers your contract with the ArbWasm precompile: ```shell # Automatic activation (default) cargo stylus deploy --private-key-path=key.txt # Or manual activation cargo stylus activate \ --address=0x1234... \ --private-key-path=key.txt ``` **What happens during activation:** 1. **Call ArbWasm precompile**: At address `0x0000000000000000000000000000000000000071` 2. **Send activation transaction**: ```solidity ArbWasm.activateProgram{value: dataFee}(contractAddress) ``` 3. **ArbWasm processes the code**: * Validates WASM formatting * Checks against the protocol version * Stores the activation metadata * Emits a `ProgramActivated` event 4. **Returns activation info**: ```solidity returns (uint16 version, uint256 actualDataFee) ``` ## Using `cargo-stylus` The `cargo-stylus` CLI maps directly onto the two steps above: * `cargo stylus deploy` handles both deployment and activation in one command (the default). * `cargo stylus deploy --no-activate` deploys without activating, so the program can be inspected or activated later. * `cargo stylus activate --address=
` activates a previously deployed contract. * `cargo stylus check` validates the contract and reports whether matching code is already activated, the estimated data fee, or any validation errors. For the full command syntax and options, see the [check and deploy guide](/stylus/cli-tools/check-and-deploy.md) and the [`cargo-stylus` command reference](/stylus/cli-tools/commands-reference.md). ## Deployment with constructors If your contract has a constructor, provide arguments during deployment: ```rust #[public] impl MyContract { #[constructor] pub fn constructor(&mut self, initial_value: U256, owner: Address) { self.value.set(initial_value); self.owner.set(owner); } } ``` **Deploy with constructor arguments**: ```shell cargo stylus deploy \ --private-key-path=wallet.txt \ --constructor-args 42 0x1234567890abcdef1234567890abcdef12345678 ``` **With payable constructor**: ```rust #[constructor] #[payable] pub fn constructor(&mut self) { let value = self.vm().msg_value(); self.initial_balance.set(value); } ``` ```shell cargo stylus deploy \ --private-key-path=wallet.txt \ --constructor-value=1000000000000000000 # 1 ETH in wei ``` ## The `ArbWasm` precompile Activation is handled by the ArbWasm precompile at address `0x0000000000000000000000000000000000000071`. ### Key functions #### `activateProgram` Activates a deployed Stylus contract: ```solidity function activateProgram( address program ) external payable returns (uint16 version, uint256 dataFee); ``` **Parameters**: * `program`: Contract address containing WASM bytecode **Payment**: * Must send `value` equal to the calculated data fee (in wei) **Returns**: * `version`: Stylus protocol version the program was activated against * `dataFee`: Actual fee paid for activation **Example (via cast)**: ```shell cast send 0x0000000000000000000000000000000000000071 \ "activateProgram(address)" \ 0x1234567890abcdef \ --value 100000000000000000 \ --private-key=$PRIVATE_KEY ``` #### `codehashVersion` Check if a codehash is activated and get its version: ```solidity function codehashVersion(bytes32 codehash) external view returns (uint16 version); ``` **Reverts if**: * The code is not activated * The program needs an upgrade * The program has expired #### `programTimeLeft` Get the remaining time before a program expires: ```solidity function programTimeLeft(address program) external view returns (uint64 timeLeft); ``` Returns the number of seconds until expiration (default: \~1 year from activation). #### `codehashKeepalive` Extend a program's expiration time: ```solidity function codehashKeepalive(bytes32 codehash) external payable; ``` Resets the expiration timer to prevent program deactivation. The call pays the activation data fee in `value` and returns no value. To read the time remaining before expiry, use the separate `programTimeLeft` getter (above), which returns the seconds left. ### `ArbWasm` errors Activation can fail with these errors: ```solidity error ProgramNotWasm(); // The deployed bytecode is not valid WASM error ProgramNotActivated(); // Contract exists but hasn't been activated error ProgramNeedsUpgrade(uint16 version, uint16 stylusVersion); // Program version incompatible with current Stylus version error ProgramExpired(uint64 ageInSeconds); // Program has expired and must be reactivated error ProgramInsufficientValue(uint256 have, uint256 want); // Sent data fee is less than required ``` ## Gas and Fee Optimization ### Estimating costs Before deploying, `cargo stylus deploy --estimate-gas` reports the deployment gas and `cargo stylus check` reports the estimated activation data fee. See the [check and deploy guide](/stylus/cli-tools/check-and-deploy.md) for details. ### Fee bump configuration Protect against fee variance with configurable bump percentage: ```shell # Default: 20% bump cargo stylus deploy --private-key-path=wallet.txt # Custom bump percentage # (Note: Use programmatically via stylus-tools library) ``` **In code** (illustrative, using the `stylus-tools` library): ```rust // Illustrative — types and field names are not a stable public API. let config = ActivationConfig { data_fee_bump_percent: 25, // 25% safety margin }; ``` ### Code reuse optimization If your contract's codehash matches an already-activated contract: ```shell cargo stylus check ``` **Output if already activated**: ```text Checking contract... ✓ Contract with this codehash is already activated! Version: 1 No activation needed - you can deploy without activating. ``` You can deploy the contract normally, and it will automatically use the existing activation. ### Contract caching After activation, contracts can be [cached](/stylus/how-tos/caching-contracts.md) for cheaper calls: ```solidity // ArbWasmCache precompile (0x0000000000000000000000000000000000000072) function cacheProgram(address program) external payable returns (uint256); ``` **Benefits**: * Reduces gas costs for subsequent contract calls * One-time caching fee * Shared across all contracts with the same codehash ## Advanced activation patterns ### Multi-contract deployment When deploying multiple instances of the same contract: ```shell # First deployment: full deploy + activate cargo stylus deploy --private-key-path=wallet.txt # Contract 1: 0xaaaa... (activated) # Subsequent deployments: deploy only (reuses activation) cargo stylus deploy --private-key-path=wallet.txt --no-activate # Contract 2: 0xbbbb... (uses existing activation) cargo stylus deploy --private-key-path=wallet.txt --no-activate # Contract 3: 0xcccc... (uses existing activation) ``` All three contracts share the same codehash and activation, saving on data fees. ### Programmatic deployment Using the `stylus-tools` library directly. The following is illustrative pseudocode that sketches the deploy/activate flow — it is not a compilable example, and the exact types and signatures vary by `stylus-tools` version: ```rust use stylus_tools::core::{ deployment::{deploy, DeploymentConfig}, activation::{activate_contract, ActivationConfig, data_fee}, check::{check_contract, ContractStatus}, }; use alloy::providers::{Provider, WalletProvider}; async fn deploy_and_activate( provider: &impl Provider + WalletProvider, ) -> Result> { let contract = /* build contract */; // Step 1: Check if already activated let config = CheckConfig::default(); match check_contract(&contract, None, &config, provider).await? { ContractStatus::Active { .. } => { println!("Already activated!"); // Deploy without activation } ContractStatus::Ready { code, fee } => { println!("Ready to activate. Data fee: {}", fee); // Continue with deployment + activation } } // Step 2: Deploy let deploy_config = DeploymentConfig { no_activate: false, ..Default::default() }; deploy(&contract, &deploy_config, provider).await?; // Contract address returned from deployment Ok(contract_address) } ``` ### Custom deployer contracts Use a custom deployer contract instead of the default: ```shell cargo stylus deploy \ --private-key-path=wallet.txt \ --deployer-address=0x... \ --deployer-salt=0x0000000000000000000000000000000000000000000000000000000000000001 ``` This is useful for: * CREATE2 deterministic addresses * Custom deployment logic * Factory patterns ## Contract lifecycle ### Activation lifecycle ```text Deployed → Activated → [Active] → [Keepalive] → [Expired] ↑ ↓ └──────────┘ (Periodic keepalive) ``` ### Expiration and `Keepalive` Programs expire after the chain's `ExpiryDays` parameter (default 365 days). Both the expiry period and the minimum age before a program can be kept alive (`KeepaliveDays`, default 31 days) are configurable ArbOS parameters: ```solidity // Check time remaining uint64 timeLeft = ArbWasm.programTimeLeft(contractAddress); // Extend expiration (pays the data fee in value; the program must be at least KeepaliveDays old) ArbWasm.codehashKeepalive{value: keepaliveFee}(codehash); ``` **Why expiration?** * Prevents abandoned contracts from consuming ArbOS resources * Encourages active maintenance * Allows protocol upgrades **Keepalive strategy**: * Monitor `programTimeLeft()` periodically * Call `codehashKeepalive()` before expiration * Automated scripts can handle this ### Reactivation after expiry If a program expires: ```shell # Reactivate the existing deployment cargo stylus activate --address=0x... ``` The contract code remains onchain; only the activation state is cleared. ## Troubleshooting ### Common activation errors #### "Program not activated" **Cause**: Trying to call a deployed but not activated contract **Solution**: ```shell cargo stylus activate --address=0x... ``` #### "Insufficient value" **Cause**: The data fee sent is less than required **Solution**: * Check current data fee: `cargo stylus check` * Increase fee bump percentage * Ensure sufficient **ETH** balance #### "Program not WASM" **Cause**: Deployed bytecode is not valid Stylus WASM **Solution**: * Verify you deployed the correct contract * Rebuild and redeploy: `cargo stylus deploy` #### "Program needs upgrade" **Cause**: Contract was activated against an old Stylus version **Solution**: * Recompile with the latest SDK * Redeploy and reactivate #### "Program expired" **Cause**: The contract hasn’t been kept alive and has expired **Solution**: ```shell # Reactivate the contract cargo stylus activate --address=0x... ``` ### Debugging and verifying activation To inspect status or debug a failed activation, `cargo stylus check` validates a contract (add `--verbose` for detail), and `cargo stylus check --address=0x...` reports whether an existing deployment is activated. For verbose deploy logging and `cast`-based status checks against the ArbWasm precompile, see the [debugging transactions guide](/stylus/cli-tools/debugging-tx.md) and the [check and deploy guide](/stylus/cli-tools/check-and-deploy.md). ## Best practices * **Check before deploying.** `cargo stylus check` catches validation errors and reports whether matching code is already activated, avoiding duplicate activations and wasted gas. * **Use automatic activation.** Unless you have a reason to split the steps, `cargo stylus deploy` deploys and activates in one command. * **Test on Arbitrum Sepolia first**, then deploy to mainnet. * **Monitor expiration** of production contracts with `programTimeLeft` and call `codehashKeepalive` before the program expires. * **Keep the SDK updated** — older versions may become incompatible with chain upgrades: ```toml [dependencies] stylus-sdk = "0.10.7" ``` For the full deployment walkthrough — project setup, gas estimation, reproducible builds, and verification — see the [check and deploy guide](/stylus/cli-tools/check-and-deploy.md). ## Summary * **Two-step process**: Deployment stores code, activation makes it executable * **cargo-stylus handles both**: Use `deploy` for automatic activation * **Data fee required**: Activation costs **ETH** (separate from deployment gas) * **Code reuse**: Identical contracts share activation, saving costs * **Expiration**: Programs expire after the chain's `ExpiryDays` parameter (default 365 days) unless kept alive * **ArbWasm precompile**: All activation goes through address `0x71` * **Check first**: Use the `cargo stylus check` to avoid duplicate activations ## See also * [Contracts](/stylus/fundamentals/contracts.md): Writing Stylus contracts * [Global Variables and Functions](/stylus/fundamentals/global-variables-and-functions.md): VM interface methods * [Check and deploy](/stylus/cli-tools/check-and-deploy.md): Full `cargo stylus` deployment walkthrough * [`cargo-stylus` command reference](/stylus/cli-tools/commands-reference.md): Complete CLI option reference --- > For a complete page index, fetch # Gas metering **Gas and ink** are the pricing primitives that are used to determine the cost of handling specific opcodes and host I/Os on Stylus. For an overview of specific opcode and host I/O costs, see [Gas and ink costs](/stylus/reference/opcode-hostio-pricing.md). ## Stylus gas costs Stylus introduces new pricing models for WASM programs. Intended for high-compute applications, Stylus makes the following more affordable: * Compute, which is generally **10-100x** cheaper depending on the program. This is primarily due to the efficiency of the WASM runtime relative to the EVM, and the quality of the code produced by Rust, C, and C++ compilers. Another factor that matters is the quality of the code itself. For example, highly optimized and audited C libraries that implement a particular cryptographic operation are usually deployable without modification and perform exceptionally well. The fee reduction may be smaller for highly optimized Solidity that makes heavy use of native precompiles vs. an unoptimized Stylus equivalent that doesn't do the same. * Memory, which is **100-500x** cheaper due to Stylus's novel exponential pricing mechanism intended to address Vitalik's concerns with the EVM's per-call, [quadratic memory pricing policy](https://notes.ethereum.org/@vbuterin/proposals_to_adjust_memory_gas_costs). For the first time ever, high-memory applications are possible on an EVM-equivalent chain. * Storage, for which the Rust SDK promotes better access patterns and type choices. Note that while the underlying [`SLOAD`](https://www.evm.codes/#54) and [`SSTORE`](https://www.evm.codes/#55) operations cost as they do in the EVM, the Rust SDK implements an optimal caching policy that minimizes their use. Exact savings depends on the program. * VM affordances, including common operations like `keccak` and reentrancy detection. No longer is it expensive to make safety the default. These models also change how costs scale: while the EVM reprices memory quadratically within each call, Stylus charges for memory by the 64 KB WASM page, and a program can use up to 8 MB of memory by default (a `PageLimit` of 128 pages, configurable by the chain owner). In practice, the gas cost of a compute- and memory-heavy Stylus call grows near-linearly with the size of its workload. For more benchmarks, see the [Stylus benchmarking projects](https://github.com/OffchainLabs/stylus-nanoGPT/blob/main/benchmark-compare/README.md). There are, however, minor overheads to using Stylus that may matter to your application: * The first time a WASM is deployed, it must be *activated*. Activation charges a configurable `ActivationGas` amount (`0` by default), plus a fixed computation charge of `1,659,168` gas, plus a dynamic data fee that scales with the size of the compiled program. Note that you do not have to activate future copies of the same program. For example, the same NFT template can be deployed many times without paying the activation cost more than once. * Calling a Stylus contract incurs a minimum init cost of `8,832` gas when the program is not cached, or `352` gas when it is cached, plus a dynamic init term proportional to the program's complexity. There will likely always be some amount of gas one pays to jump into WASM execution. This means that if a contract does next to nothing, it may be cheaper in Solidity. However if a contract starts doing interesting work, the dynamic fees will quickly make up for this fixed-cost overhead. All of these parameters are configurable by the chain owner and subject to change as pricing models mature and further optimizations are made. Since gas numbers will vary across updates, it may make more sense to clock the time it takes to perform an operation rather than going solely by the numbers reported in receipts. ## Ink and gas Because WASM opcodes are orders of magnitude faster than their EVM counterparts, almost every operation that Stylus does costs less than `1 gas`. “Fractional gas” isn’t an EVM concept, so the Stylus VM introduces a new unit of payment known as ink that’s orders of magnitude smaller. ```jsx 1 gas = 10,000 ink ``` ### Intuition To build intuition for why this is the case, consider the `ADD` instruction. #### In the EVM 1. Pay for gas, requiring multiple look-ups of an in-memory table 2. Consider tracing, even if disabled 3. Pop two items of the simulated stack 4. Add them together 5. Push the result #### In the Stylus VM 1. Execute a single x86 or ARM `ADD` instruction Note that unlike the EVM, which charges for gas before running each opcode, the Stylus VM strategically charges for many opcodes all at once. This cuts fees considerably, since the VM only rarely needs to execute gas charging logic. Additionally, gas charging happens *inside the program*, removing the need for an in-memory table. ### The ink price The ink price, which measures the amount of ink a single EVM gas buys, is configurable by the chain owner. By default, the exchange rate is `1:10000`, but this may be adjusted as the EVM and Stylus VM improve over time. For example, if the Stylus VM becomes 2x faster, instead of cutting the nominal cost of each operation, the ink price may instead be halved, allowing 1 EVM gas to buy twice as much ink. This provides an elegant mechanism for smoothly repricing resources between the two VMs as each makes independent progress. ### User experience It is important to note that users never need to worry about this notion of ink. Receipts will always be measured in gas, with the exchange rate applied automatically under the hood as the VMs pass execution back and forth. However, developers optimizing contracts may choose to measure performance in ink to pin down the exact cost of executing various routines. The `evm_ink_left()` host method exposes this value, and various methods throughout the Rust SDK optionally accept ink amounts too. ### See also * [Gas and ink costs](/stylus/reference/opcode-hostio-pricing.md): Detailed costs per opcode and host I/O * [Caching strategy](/stylus/how-tos/caching-contracts.md): Description of the Stylus caching strategy and the `CacheManager` contract --- > For a complete page index, fetch # VM and execution differences Arbitrum Nitro supports two execution environments: the traditional Ethereum Virtual Machine (EVM) for Solidity contracts and a WebAssembly (WASM) VM for Stylus contracts. While both environments are fully interoperable and share the same state, they differ significantly in their execution models, performance characteristics, and developer experience. ## Execution model ### EVM: Stack-based architecture The EVM uses a stack-based execution model: * **Operations**: Work with values on a stack (PUSH, POP, ADD, etc.) * **Opcodes**: 256 predefined opcodes with fixed gas costs * **Memory**: Linear, byte-addressable memory that grows dynamically * **Storage**: 256-bit word-based key-value store * **Call depth**: Limited to 1024 levels **Example EVM execution:** ```text PUSH1 0x02 // Push 2 onto stack PUSH1 0x03 // Push 3 onto stack ADD // Pop 2 values, push sum (5) ``` ### WASM: Stack-based architecture The Stylus WASM VM also uses a stack-based execution model, operating on a structured value stack with typed local variables: * **Operations**: Push and pop typed values on an operand stack, with named local variables * **Instructions**: Hundreds of WASM instructions with fine-grained metering * **Memory**: Linear memory with explicit grow operations * **Storage**: Same 256-bit storage as EVM (shared state) * **Call depth**: Same 1024 limit for compatibility **Example WASM execution:** ```wasm (local.get 0) ;; Push local variable 0 onto the stack (local.get 1) ;; Push local variable 1 onto the stack (i32.add) ;; Pop two values, push their sum (local.set 2) ;; Pop the result into local variable 2 ``` ## Memory model ### EVM memory * **Dynamic expansion**: Memory grows in 32-byte chunks * **Gas cost**: Quadratic growth (memory expansion gets expensive) * **Access pattern**: Byte-level addressing * **Limit**: Bounded in practice by the quadratic gas cost of expansion ### WASM memory * **Page-based**: Memory grows in 64 KB pages (WASM standard) * **Gas cost**: Linear cost per page through `pay_for_memory_grow` * **Access pattern**: Direct memory load/store instructions * **Limit**: Can grow much larger efficiently **Memory growth in Stylus:** ```rust // The #[entrypoint] attribute automatically handles pay_for_memory_grow #[entrypoint] pub struct MyContract { // Large data structures are more practical in WASM data: StorageVec, } // Nitro automatically inserts pay_for_memory_grow calls // when allocating new pages let large_vector = vec![0u8; 100_000]; // Efficient in WASM ``` Note The Stylus SDK's `#[entrypoint]` attribute includes a no-op call to `pay_for_memory_grow` to ensure the function is referenced. Nitro then automatically inserts actual calls when memory allocation occurs. ## Gas metering: Ink and gas ### EVM gas metering * **Unit**: Gas (standard Ethereum unit) * **Granularity**: Per opcode (e.g., ADD = 3 gas, SSTORE = 20,000 gas) * **Measurement**: Coarse-grained * **Refunds**: Available for storage deletions ### Stylus ink metering Stylus introduces "ink" as a fine-grained metering unit: * **Unit**: Ink (Stylus-specific, converted to gas) * **Granularity**: Per WASM instruction (more fine-grained) * **Measurement**: Precise tracking of WASM execution costs * **Conversion**: Ink → Gas conversion happens automatically **Ink to gas conversion:** ```rust // Check remaining ink let ink_left = self.vm().evm_ink_left(); // Check remaining gas let gas_left = self.vm().evm_gas_left(); // Get ink price (in gas basis points) let ink_price = self.vm().tx_ink_price(); // Conversion formula: // gas = ink * ink_price / 10000 ``` **Why ink?** 1. **Precision**: WASM instructions have varying costs that don't map cleanly to EVM gas 2. **Efficiency**: Fine-grained metering allows for more accurate pricing 3. **Performance**: Enables cheaper execution for compute-heavy operations 4. **Flexibility**: Ink prices can be adjusted without changing contract code **Gas cost comparison:** The two execution models price work differently. The EVM charges a fixed gas cost per opcode, so compute-heavy logic accumulates cost quickly. Stylus meters each WASM instruction in ink and converts ink to gas at execution time, which lets compute-intensive code run for less gas. Operations that touch shared state — `SLOAD`, `SSTORE`, external calls, value transfers, and event emission — cost the same in both environments because they use the same underlying mechanisms. For verified per-operation figures, see the [performance comparison](/stylus/best-practices/gas-optimization.md#performance-comparison) in the gas optimization guide. ## Instruction sets ### EVM opcodes * **Count**: \~140 opcodes * **Categories**: Arithmetic, logic, storage, flow control, system * **Size**: 1 byte per opcode * **Examples**: * `ADD`, `MUL`, `SUB`, `DIV` (arithmetic) * `SLOAD`, `SSTORE` (storage) * `CALL`, `DELEGATECALL` (calls) * `SHA3` (hashing) ### WASM instructions * **Count**: Hundreds of instructions * **Categories**: Numeric, memory, control flow, function calls * **Size**: Variable encoding (1-5 bytes) * **Examples**: * `i32.add`, `i64.mul`, `f64.div` (numeric) * `memory.grow`, `memory.size` (memory) * `call`, `call_indirect` (functions) * Hostio imports (system operations) **WASM advantages:** * More expressive instruction set * Better compiler optimization targets * Efficient handling of complex data structures * Native support for 32-bit and 64-bit operations ## Size limits ### EVM contracts * **Maximum size**: 24,576 bytes (24 KB) of deployed bytecode * **Limit reason**: Block gas limit and deployment costs * **Workaround**: Contract splitting, proxies ### Stylus contracts * **Size limit**: Bounds the **decompressed** WASM, not the EVM 24 KB bytecode limit. It is a chain-configurable ArbOS parameter (`MaxWasmSize`), defaulting to 128 KB and raised to 256 KB at ArbOS 60 and later * **On-chain storage**: WASM is stored compressed, so the on-chain bytecode is smaller than the decompressed ceiling * **Large programs**: At ArbOS 60 and later, programs that exceed a single code account are split into a root plus fragments * **Configurable**: A chain owner can adjust `MaxWasmSize` via `ArbOwner.setWasmMaxSize` **Size optimization:** ```rust // Stylus contracts benefit from: // 1. Rust's zero-cost abstractions // 2. Dead code elimination by wasm-opt // 3. Efficient WASM encoding #[no_std] // Opt out of standard library for smaller binaries extern crate alloc; // Only the code actually used is included use stylus_sdk::prelude::*; ``` ## Storage model Both EVM and WASM contracts use the **same storage system**: * **Format**: 256-bit key-value store * **Compatibility**: EVM and WASM contracts can share storage * **Costs**: SLOAD and SSTORE costs are identical * **Caching**: Stylus VM implements storage caching for efficiency ### Storage caching in Stylus ```rust use stylus_sdk::prelude::*; #[storage] pub struct Counter { count: StorageU256, } #[public] impl Counter { pub fn increment(&mut self) { // First read: full SLOAD cost let current = self.count.get(); // Write is cached self.count.set(current + U256::from(1)); // Additional reads in same call are cheaper (cached) let new_value = self.count.get(); // Cache is automatically flushed at call boundary } } ``` **Cache benefits:** 1. **Reduced gas costs**: Repeated reads are cheaper 2. **Better performance**: Fewer state trie accesses 3. **Automatic management**: SDK handles cache flushing 4. **Compatibility**: Refund logic matches EVM exactly ## Performance characteristics Compute-bound work — integer arithmetic, loops, memory copying, cryptography, and string and byte manipulation — generally runs for less gas on Stylus than on the EVM, because WASM instructions are metered finely in ink and compiled to native code rather than interpreted opcode by opcode. State-bound work is identical across the two environments. Storage reads and writes (`SLOAD`, `SSTORE`), storage refunds, contract calls, value transfers, and event emission all use the same underlying mechanisms and cost the same gas. Stylus adds storage caching within a call, so repeated reads of the same slot in one execution are cheaper. For verified per-operation gas figures, see the [performance comparison](/stylus/best-practices/gas-optimization.md#performance-comparison) in the gas optimization guide. ## Call semantics ### Interoperability Both environments support interoperability: ```rust // Stylus calling Solidity sol_interface! { interface IERC20 { function transfer(address to, uint256 amount) external returns (bool); } } #[public] impl MyContract { pub fn call_evm_contract(&self, token: Address) -> Result> { let erc20 = IERC20::new(token); let result = erc20.transfer(self.vm(), recipient, amount)?; Ok(result) } } ``` ```solidity // Solidity calling Stylus interface IStylusContract { function computeHash(bytes calldata data) external view returns (bytes32); } contract EvmContract { function useStylus(address stylusAddr, bytes calldata data) public view returns (bytes32) { return IStylusContract(stylusAddr).computeHash(data); } } ``` ### Call costs * **Same call overhead**: Both directions have similar base costs * **ABI encoding**: Identical for both * **Gas forwarding**: Follows 63/64 rule in both cases * **Return data**: Handled consistently ## Contract lifecycle ### Deployment **EVM contracts:** 1. Submit init code (constructor bytecode) 2. EVM executes init code 3. Returns runtime bytecode 4. Bytecode stored onchain **Stylus contracts:** 1. Compile Rust → WASM 2. Submit WASM code 3. **Activation step**: One-time compilation to native code 4. Activated programs cached for efficiency 5. WASM code stored onchain **Activation benefits:** ```shell # Deploy and activate a Stylus program cargo stylus deploy --private-key $PRIVATE_KEY # Activation happens once # Subsequent calls use cached native code ``` * **One-time cost**: Pay activation gas once * **Future savings**: All executions use optimized native code * **Upgradeability**: Re-activation needed for upgrades ### Execution flow **EVM contracts:** ```text Transaction → EVM → Opcode interpretation → State changes ``` **Stylus contracts:** ```text Transaction → WASM VM → Native code execution → State changes ↓ Hostio calls for state access ``` ## Developer experience ### EVM development **Languages**: Solidity, Vyper, Huff **Tools**: * Hardhat, Foundry for testing * Remix for quick development * Ethers.js/Web3.js for interaction **Debugging**: * Revert messages * Events for tracing * Stack traces limited ### Stylus development **Languages**: Rust, C, C++ (any WASM-compatible language) **Tools**: * `cargo stylus` for deployment * Standard Rust tooling (cargo, rustc) * `TestVM` for unit testing * Rust analyzer for IDE support **Debugging**: * Full Rust error messages * Compile-time safety checks * `console!` macro for debug builds * Stack traces in development **Development comparison:** | Aspect | EVM | Stylus | Notes | | --------------- | -------------- | ------------------- | ------------------------------------- | | Type safety | Runtime | Compile-time | Rust catches errors before deployment | | Memory safety | Manual | Automatic | Rust's borrow checker | | Testing | External tools | Built-in Rust tests | `#[test]` functions work natively | | Iteration speed | Slower | Faster | No need to redeploy for tests | | Learning curve | Moderate | Steeper | Rust has more concepts | | Maturity | Very mature | Growing | Solidity has more resources | ## Feature compatibility ### Supported features Both EVM and Stylus support: ✅ Contract calls and delegate calls ✅ Value transfers ✅ Event emission ✅ Storage operations ✅ Block and transaction properties ✅ Cryptographic functions (keccak256) ✅ Contract creation (CREATE, CREATE2) ✅ Revert and error handling ✅ Reentrancy guards ### EVM-specific features not in WASM ❌ Inline assembly (use hostio or Rust instead) ❌ `selfdestruct` (deprecated in Ethereum anyway) ❌ Solidity modifiers (use Rust functions) ❌ Multiple inheritance (use traits and composition) ### Stylus-specific features not in EVM ✅ Access to Rust ecosystem (crates) ✅ Efficient memory management ✅ Zero-cost abstractions ✅ Compile-time guarantees ✅ Native testing support ✅ Better optimization opportunities ## State sharing EVM and WASM contracts share the same blockchain state: ```rust // Stylus contract can read EVM contract storage #[storage] pub struct Bridge { evm_contract: StorageAddress, } #[public] impl Bridge { pub fn read_evm_storage(&self, key: U256) -> U256 { // Illustrative: Stylus and the EVM share one storage trie, so a Stylus // contract can read slots written by EVM contracts. Reading an arbitrary // slot uses the low-level `storage_load_bytes32` hostio (unsafe, pointer- // based) or the SDK storage types — not the simplified call shown here. storage_load_bytes32(key) } } ``` **Shared state:** * Account balances * Contract storage * Contract code * Transaction history * Block data ## Gas economics ### Cost structure **EVM contract execution:** ```text Total cost = Base transaction cost (21,000 gas) + Input data cost (~16 gas/byte) + Execution cost (opcode gas) + Storage cost (SLOAD/SSTORE) ``` **Stylus contract execution:** ```text Total cost = Base transaction cost (21,000 gas) + Input data cost (~16 gas/byte) + Execution cost (ink → gas conversion) + Storage cost (same as EVM) ``` ### When to use each **Use EVM (Solidity) when:** * Quick prototyping needed * Simple contracts with minimal computation * Team expertise in Solidity * Extensive storage operations (cost is equal) * Maximum ecosystem compatibility **Use Stylus (Rust) when:** * Compute-intensive operations * Complex algorithms or data structures * Need for memory safety guarantees * Existing Rust codebase to port * Optimizing for gas efficiency * Cryptographic operations * String/byte manipulation ## Best practices ### For EVM contracts 1. **Minimize storage operations**: Use memory when possible 2. **Optimize loops**: Keep iterations minimal 3. **Pack storage**: Use smaller types when possible 4. **Avoid complex math**: Basic operations only 5. **Use libraries**: Leverage audited code ### For Stylus contracts 1. **Leverage Rust's safety**: Let the compiler catch bugs 2. **Use iterators**: More efficient than manual loops 3. **Profile before optimizing**: Use cargo-stylus tools 4. **Test thoroughly**: Use Rust's built-in test framework 5. **Consider binary size**: Use `#[no_std]` if needed 6. **Batch operations**: Take advantage of cheap compute ### Hybrid approach Many projects can benefit from both: ```rust // Compute-heavy logic in Stylus #[public] impl ComputeEngine { pub fn complex_calculation(&self, data: Vec) -> Vec { // Efficient loops and data processing data.iter() .map(|x| expensive_computation(*x)) .collect() } } ``` ```solidity // Coordination and state management in Solidity contract Coordinator { IComputeEngine public engine; // Stylus contract function process(uint256[] calldata data) public { uint256[] memory results = engine.complex_calculation(data); // Store results, emit events, etc. } } ``` ## Migration considerations ### From Solidity to Stylus **What stays the same:** * Contract addresses * Storage layout * ABIs and interfaces * Gas for storage operations * Event signatures **What changes:** * Programming language (Solidity → Rust) * Execution engine (EVM → WASM) * Gas costs for compute (usually cheaper) * Development workflow * Testing approach **Migration strategy:** 1. Start with compute-heavy functions 2. Maintain same ABI for compatibility 3. Test extensively with existing contracts 4. Monitor gas costs in production 5. Gradually migrate more functionality ## Future developments ### EVM evolution * EIP improvements * New opcodes * Gas repricing * EOF (EVM Object Format) ### Stylus evolution * Support for more languages * SIMD instructions * Floating point operations * Larger contract size limits * Further gas optimizations * Enhanced debugging tools ## Resources * [Stylus documentation](https://docs.arbitrum.io/stylus) * [Ink and gas metering](https://docs.arbitrum.io/stylus/concepts/gas-metering) * [WASM specification](https://webassembly.github.io/spec/) * [EVM opcodes reference](https://www.evm.codes/) * [Stylus SDK repository](https://github.com/OffchainLabs/stylus-sdk-rs) ## Summary The WASM VM in Arbitrum Nitro represents a significant evolution in smart contract execution: **Key advantages of WASM:** * 10-100x cheaper for compute operations * More expressive programming languages * Better memory management * Compile-time safety guarantees * Access to mature language ecosystems **Key advantages of EVM:** * Mature tooling and ecosystem * Familiar to existing developers * No activation cost * Decades of collective knowledge Both execution environments coexist harmoniously on Arbitrum, allowing developers to choose the best tool for each use case while maintaining full interoperability. --- > For a complete page index, fetch # WebAssembly in Nitro WebAssembly (WASM) is a binary instruction format that enables high-performance execution of programs in the Nitro virtual machine. This guide explains how WASM works in the context of Arbitrum Nitro and Stylus smart contract development. ## What is WebAssembly? WebAssembly is a portable, size-efficient binary format designed for safe execution at near-native speeds. Key characteristics include: * **Binary format**: Compact representation that's faster to parse than text-based formats * **Stack-based VM**: Simple execution model with operand stack * **Sandboxed execution**: Memory-safe by design with explicit bounds checking * **Language-agnostic**: Can be targeted by many programming languages (Rust, C, C++, etc.) ## Why WebAssembly in Nitro? Nitro uses WebAssembly as its execution environment for several reasons: 1. **Performance**: WASM compiles to native machine code for fast execution 2. **Security**: Sandboxed environment prevents unauthorized access 3. **Portability**: Same bytecode runs identically across all nodes 4. **Language flexibility**: Developers can use Rust, C, C++, or any language that compiles to WASM 5. **Determinism**: Guaranteed identical execution across all validators ![WASM Execution Pipeline](/assets/images/stylus-wasm-compile-665d1e2a6740b15aa36df45ccab20021.svg) *Figure: WebAssembly execution pipeline in Arbitrum Nitro, from source code to native execution with access to blockchain state.* ## WASM compilation target Stylus contracts are compiled to the `wasm32-unknown-unknown` target, which means: * **32-bit addressing**: Uses 32-bit pointers and memory addresses * **Unknown OS**: No operating system dependencies * **Unknown environment**: Minimal runtime assumptions (no std by default) The `.cargo/config.toml` file in Stylus projects configures the WASM target: ```toml [target.wasm32-unknown-unknown] rustflags = [ "-C", "link-arg=-zstack-size=32768", # 32KB stack "-C", "target-feature=-reference-types", # Disable reference types "-C", "target-feature=+bulk-memory", # Enable bulk memory operations ] ``` ### Compilation flags * **Stack size**: Limited to 32KB to ensure bounded memory usage * **Bulk memory**: Enables efficient `memory.copy` and `memory.fill` operations * **No reference types**: Keeps the WASM simpler and more compatible ## WASM binary structure A Stylus WASM module consists of several sections: ![WASM Module Structure](/assets/images/stylus-wasm-deploy-e61123c2bd71fc63f2aca0d48cb9ff72.svg) *Figure: WASM module structure showing the main sections including exports, imports (hostio functions), memory, and code.* ### Exports Every Stylus contract exports a `user_entrypoint` function: ```rust #[no_mangle] pub extern "C" fn user_entrypoint(len: usize) -> usize { // Entry point for all contract calls // len: size of calldata in bytes // returns: size of output data in bytes } ``` This function is automatically generated by the `#[entrypoint]` macro and serves as the single entry point for all contract interactions. ### Imports WASM modules import low-level functions from the `vm_hooks` module: ```rust // Example hostio imports extern "C" { fn storage_load_bytes32(key: *const u8, dest: *mut u8); fn storage_store_bytes32(key: *const u8, value: *const u8); fn msg_sender(sender: *mut u8); fn block_timestamp() -> u64; // ... and many more } ``` These imported functions (called "hostio" functions) provide access to blockchain state and functionality. ### Memory WASM modules use linear memory, which is: * **Contiguous**: Single continuous address space starting at 0 * **Growable**: Can expand at runtime (in 64KB pages) * **Isolated**: Each contract has its own memory space Memory growth is explicitly metered: ```rust // Exported function that must exist #[no_mangle] pub extern "C" fn pay_for_memory_grow(pages: u16) { // Called before memory.grow to charge for new pages // Each page is 64KB } ``` ### Custom sections WASM supports custom sections for metadata: ```rust // Example: Add version information #[link_section = ".custom.stylus-version"] static VERSION: [u8; 5] = *b"0.1.0"; ``` Custom sections can store: * Contract version * Source code hashes * Compiler metadata * ABI information ## VM runtime Stylus executes WebAssembly on a modified version of the [Wasmer](https://github.com/wasmerio/wasmer) runtime. Wasmer is used unmodified at the runtime layer; Stylus extends the environment with **host I/O imports** ([documented in `hostio-exports.mdx`](/stylus/advanced/hostio-exports.md)) that let WASM programs call into native blockchain functions (storage, calls, logs, cryptography). For Arbitrum-specific adaptations to Wasmer (gas metering, deterministic fraud-proof support), see Offchain Labs' fork: [github.com/OffchainLabs/wasmer](https://github.com/OffchainLabs/wasmer). ## Compression and deployment Before deployment, Stylus contracts undergo compression: ### Brotli compression ```rust // From stylus-tools/src/utils/wasm.rs pub fn brotli_compress(wasm: impl Read, compression_level: u32) -> io::Result> { let mut compressed = Vec::new(); let mut encoder = brotli::CompressorWriter::new(&mut compressed, 4096, compression_level, 22); io::copy(&mut wasm, &mut encoder)?; encoder.flush()?; Ok(compressed) } ``` Using Brotli compression typically reduces the WASM size by 50-70%. For more on reducing binary size before compression, see [Optimizing binaries](/stylus/how-tos/optimizing-binaries.md). ### 0xEFF00000 prefix Compressed WASM is prefixed with `0xEFF00000` (4 bytes, defined in the SDK as `EOF_NO_DICT`) to identify it as a Stylus program: ```rust pub fn add_prefix(compressed_wasm: impl IntoIterator, prefix: &str) -> Vec { let prefix_bytes = hex::decode(prefix.strip_prefix("0x").unwrap_or(prefix)).unwrap(); prefix_bytes.into_iter().chain(compressed_wasm).collect() } ``` This prefix allows the Nitro VM to distinguish Stylus contracts from EVM bytecode. ## Contract activation After deployment, **activating** a contract is required before execution: ### Activation process 1. **Initial deployment**: Contract code is stored onchain (compressed) 2. **Activation call**: Special transaction invokes `activateProgram` 3. **Decompression**: Brotli-compressed WASM is decompressed 4. **Validation**: WASM is checked for: * Valid structure * Required exports (`user_entrypoint`) * Allowed imports (only `vm_hooks`) * Memory constraints 5. **Compilation**: WASM is compiled to native machine code 6. **Caching**: Compiled code is cached for future executions ### One-time cost Activation incurs a one-time gas cost but provides benefits: * **Fast execution**: Native code runs 10-100x faster than interpreted * **Persistent cache**: Compilation happens once, benefits all future calls * **Optimizations**: Native compiler applies target-specific optimizations ### Verification The activation process checks for the `pay_for_memory_grow` function to verify the correct `entrypoint` setup: ```rust // From activation.rs if !wasm::has_entrypoint(&wasm)? { bail!("WASM is missing the entrypoint export"); } ``` ## Development workflow ### 1. Write Rust code ```rust use stylus_sdk::{alloy_primitives::U256, prelude::*}; #[entrypoint] #[storage] pub struct Counter { count: StorageU256, } #[public] impl Counter { pub fn increment(&mut self) { let count = self.count.get() + U256::from(1); self.count.set(count); } } ``` ### 2. Compile to WASM ```shell cargo stylus build ``` This runs: ```shell cargo build \ --lib \ --locked \ --release \ --target wasm32-unknown-unknown \ --target-dir target/wasm32-unknown-unknown/release ``` ### 3. Optimize (optional) ```shell wasm-opt target/wasm32-unknown-unknown/release/my_contract.wasm \ -O3 \ --strip-debug \ -o optimized.wasm ``` Optimization can reduce the size by an additional 10-30%. ### 4. Deploy and activate ```shell # Deploy compressed WASM cargo stylus deploy --private-key=$PRIVATE_KEY # Activation happens automatically ``` ## Size limitations Nitro bounds the **decompressed** size of an activated Stylus program through the `MaxWasmSize` ArbOS parameter. This is the WASM size after Brotli decompression, not the compressed on-chain bytecode, and it is not the EVM contract-size limit (the 24 KB EIP-170 ceiling applies to Solidity contracts, not Stylus programs). `MaxWasmSize` is chain-configurable by the chain owner via `ArbOwner.SetWasmMaxSize`. The values below are the protocol defaults: | ArbOS version | Default `MaxWasmSize` (decompressed) | | --------------- | ------------------------------------ | | Before ArbOS 60 | 128 KB | | ArbOS 60+ | 256 KB | From ArbOS 60 onward, large programs can also be stored as a root plus fragments (default `MaxFragmentCount` of 4): the compressed payload is split across multiple code accounts, reassembled, and decompressed at activation, with the decompressed total still bounded by `MaxWasmSize`. To stay within these limits: * Use `#![no_std]` to avoid standard library bloat * Strip debug symbols with `--strip-debug` * Enable aggressive optimization (`-O3`) * Minimize dependencies * Use compact data structures ## Memory model ![WASM Memory Model](/assets/images/stylus-wasm-execute-4f8ee9ae33741230fa86111baf9899d8.svg) *Figure: WASM linear memory model showing the fixed 32KB stack and growable heap organized in 64KB pages.* ### Linear memory layout ```text 0x00000000 ┌─────────────────┐ │ Stack │ 32 KB fixed size 0x00008000 ├─────────────────┤ │ Heap/Data │ Grows upward │ │ │ (Available) │ │ │ 0xFFFFFFFF └─────────────────┘ ``` ### Memory operations ```rust // Bulk memory operations (enabled by target config) unsafe { // Fast memory copy core::ptr::copy_nonoverlapping(src, dst, len); // Fast memory fill core::ptr::write_bytes(ptr, value, len); } ``` The `bulk-memory` feature flag enables efficient WASM instructions like `memory.copy` and `memory.fill`. ## Advanced: WASM instructions Stylus uses WASM MVP (Minimum Viable Product) instructions plus bulk-memory operations: ### Arithmetic * `i32.add`, `i32.sub`, `i32.mul`, `i32.div_s`, `i32.div_u` * `i64.add`, `i64.sub`, `i64.mul`, `i64.div_s`, `i64.div_u` ### Memory access * `i32.load`, `i32.store` (32-bit load/store) * `i64.load`, `i64.store` (64-bit load/store) * `memory.grow` (expand memory) * `memory.copy` (bulk copy, requires flag) * `memory.fill` (bulk fill, requires flag) ### Control flow * `call`, `call_indirect` (function calls) * `if`, `else`, `block`, `loop` (structured control flow) * `br`, `br_if` (branching) ### Not supported * ❌ Floating point operations (f32, f64) * ❌ SIMD operations * ❌ Reference types * ❌ Multiple memories * ❌ Threads ## Best practices ### 1. Minimize binary size ```rust // Use #![no_std] when possible #![no_std] extern crate alloc; // Avoid large dependencies // Prefer: alloy-primitives // Avoid: serde_json, regex (unless necessary) ``` ### 2. Optimize memory usage ```rust // Stack allocate when possible let small_buffer = [0u8; 32]; // Heap allocate only when necessary let large_buffer = vec![0u8; 1024]; ``` ### 3. Profile before optimizing ```shell # Check binary size ls -lh target/wasm32-unknown-unknown/release/*.wasm # Analyze with twiggy cargo install twiggy twiggy top target/wasm32-unknown-unknown/release/my_contract.wasm ``` ### 4. Test locally ```shell # Use cargo-stylus for local testing cargo stylus check cargo stylus export-abi ``` ### 5. Validate before deployment ```rust // Ensure entrypoint exists #[entrypoint] #[storage] pub struct MyContract { /* ... */ } // Verify required exports #[no_mangle] pub extern "C" fn pay_for_memory_grow(pages: u16) { // Generated automatically by SDK } ``` ## Resources * [WebAssembly specification](https://webassembly.github.io/spec/) * [Rust WASM target documentation](https://doc.rust-lang.org/rustc/platform-support/wasm32-unknown-unknown.html) * [Stylus SDK repository](https://github.com/OffchainLabs/stylus-sdk-rs) * [Cargo Stylus CLI tool](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus) * [WASM binary toolkit (wabt)](https://github.com/WebAssembly/wabt) * [Binaryen optimization tools](https://github.com/WebAssembly/binaryen) --- > For a complete page index, fetch # Choose your learning path Not sure where to start with Stylus? This guide helps you find the optimal learning path based on your background and goals. ## Quick path selector Path 1: New to Rust **Best for**: Developers experienced with other languages but new to Rust. ### Prerequisites * Basic programming knowledge * Familiarity with concepts like variables, functions, and control flow * Optional: Smart contract development experience ### Recommended journey 1. **Learn Rust basics** (1-2 weeks) * Work through [The Rust Book](https://doc.rust-lang.org/book/) chapters 1-10 * Focus on: ownership, borrowing, structs, enums, error handling * Practice with [Rustlings](https://github.com/rust-lang/rustlings) 2. **Start with Stylus** * Complete the [Quickstart](/stylus/quickstart.md) * Understand [project structure](/stylus/fundamentals/project-structure.md) * Deploy your first contract 3. **Build fundamentals** (1 week) * Study [data types](/stylus/fundamentals/data-types/primitives.md) * Learn [storage patterns](/stylus/fundamentals/data-types/storage.md) * Practice [writing tests](/stylus/fundamentals/testing-contracts.md) 4. **Explore advanced topics** (ongoing) * Review [best practices](/stylus/best-practices/security.md) * Study [gas optimization](/stylus/best-practices/gas-optimization.md) * Build real projects ### Key resources * [The Rust Book](https://doc.rust-lang.org/book/) * [Stylus Quickstart](/stylus/quickstart.md) * [Stylus by Example](https://stylus-by-example.org/) *** Path 2: Solidity developer **Best for**: Experienced Solidity developers transitioning to Stylus. ### Prerequisites * Strong Solidity knowledge * Understanding of EVM and smart contract security * No Rust experience required ### Recommended journey 1. **Understand the differences** (1 day) * Read [Rust to Solidity differences](/stylus/advanced/rust-to-solidity-differences.md) * Review [VM differences](/stylus/concepts/vm-differences.md) * Understand [type conversions](/stylus/fundamentals/data-types/conversions-between-types.md) 2. **Quick start** * Complete the [Quickstart](/stylus/quickstart.md) * Compare with familiar Solidity patterns * Deploy a simple contract 3. **Learn Rust patterns** * Study [ownership and borrowing](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html) * Learn [error handling](/stylus/fundamentals/data-types/primitives.md) * Understand [storage macros](/stylus/fundamentals/data-types/storage.md) 4. **Port existing contracts** (ongoing) * Start with simple contracts * Apply [security best practices](/stylus/best-practices/security.md) * Optimize for [gas efficiency](/stylus/best-practices/gas-optimization.md) ### Common migration patterns | Solidity Pattern | Stylus Equivalent | Guide | | ----------------------------- | --------------------------- | -------------------------------------------------------------------------- | | `mapping(address => uint)` | `StorageMap` | [Storage types](/stylus/fundamentals/data-types/storage.md) | | `require(condition, "error")` | `Result` + `?` | [Error handling](/stylus/fundamentals/contracts.md#error-handling) | | `msg.sender` | `self.vm().msg_sender()` | [Global functions](/stylus/fundamentals/global-variables-and-functions.md) | | `constructor` | `#[constructor]` method | [Contracts](/stylus/fundamentals/contracts.md) | | Events | `self.vm().log(Event{})` | [Global functions](/stylus/fundamentals/global-variables-and-functions.md) | ### Key resources * [Rust to Solidity Differences](/stylus/advanced/rust-to-solidity-differences.md) * [Stylus by Example](https://stylus-by-example.org/) * [Solidity to Rust Cheat Sheet](https://docs.arbitrum.io/stylus) *** Path 3: Smart contract developer (non-Solidity) **Best for**: Developers experienced with other smart contract platforms (e.g., Move, Cairo, Clarity, etc.). ### Prerequisites * Smart contract development experience * Understanding of blockchain concepts * Basic programming knowledge ### Recommended journey 1. **Jump right in** * Complete the [Quickstart](/stylus/quickstart.md) * Explore [project structure](/stylus/fundamentals/project-structure.md) * Deploy and interact with a contract 2. **Understand the execution model** * Study [WebAssembly concepts](/stylus/concepts/webassembly.md) * Review [VM differences](/stylus/concepts/vm-differences.md) * Learn about [activation](/stylus/concepts/activation.md) 3. **Master Rust for smart contracts** (1 week) * Focus on [storage patterns](/stylus/fundamentals/data-types/storage.md) * Learn [contract structure](/stylus/fundamentals/contracts.md) * Understand [testing approaches](/stylus/fundamentals/testing-contracts.md) 4. **Advanced development** (ongoing) * Apply platform-specific optimizations * Implement cross-contract calls * Build production applications ### Key resources * [Quickstart](/stylus/quickstart.md) * [WebAssembly Concepts](/stylus/concepts/webassembly.md) * [Advanced Topics](/stylus/advanced/rust-to-solidity-differences.md) *** Path 4: Rust developer new to Web3 **Best for**: Experienced Rust developers entering blockchain development. ### Prerequisites * Strong Rust knowledge * Understanding of ownership, traits, and async programming * No blockchain experience required ### Recommended journey 1. **Learn blockchain basics** * Understand [smart contracts](https://ethereum.org/en/developers/docs/smart-contracts/) * Learn about [gas and transactions](https://ethereum.org/en/developers/docs/gas/) * Study [EVM basics](https://ethereum.org/en/developers/docs/evm/) 2. **Quick start with Stylus** (1 day) * Complete the [Quickstart](/stylus/quickstart.md) * Your Rust skills transfer directly! * Deploy your first contract 3. **Web3-specific patterns** * Learn [storage patterns](/stylus/fundamentals/data-types/storage.md) * Understand [global variables](/stylus/fundamentals/global-variables-and-functions.md) * Study [security considerations](/stylus/best-practices/security.md) 4. **Build applications** (ongoing) * Start with simple DeFi primitives * Implement events and logs * Optimize for [gas efficiency](/stylus/best-practices/gas-optimization.md) ### Rust skills that transfer well * **Ownership model**: Natural fit for secure contract design * **Type safety**: Prevents common smart contract bugs * **Error handling**: `Result` maps perfectly to contract errors * **Testing**: Your testing skills apply directly * **Performance optimization**: Critical for gas efficiency ### Key resources * [Ethereum Smart Contracts](https://ethereum.org/en/developers/docs/smart-contracts/) * [Stylus Quickstart](/stylus/quickstart.md) * [Storage Patterns](/stylus/fundamentals/data-types/storage.md) *** ## General learning resources Regardless of your path, these resources are valuable: ### Official documentation * [Stylus Rust SDK Docs](https://docs.rs/stylus-sdk/latest/stylus_sdk/) * [Cargo Stylus CLI](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus) * [Stylus by Example](https://stylus-by-example.org/) ### Community and support * [Arbitrum Discord](https://discord.gg/arbitrum) * [Developer Forums](https://forum.arbitrum.foundation/) ### Practice projects 1. **Token contract**: Implement the **ERC-20** token standard 2. **NFT contract**: Build an **ERC-721** compatible NFT 3. **Simple DeFi**: Create a basic AMM or lending protocol 4. **DAO governance**: Implement a voting and proposal system ## Next steps Ready to start? Begin with these essential guides: 1. [Prerequisites and setup](/stylus/fundamentals/prerequisites.md) 2. [Quickstart: Deploy your first contract](/stylus/quickstart.md) 3. [Project structure](/stylus/fundamentals/project-structure.md) Good luck on your Stylus journey! --- > For a complete page index, fetch # Stylus contracts Stylus smart contracts are fully compatible with Solidity contracts on Arbitrum chains. They compile to WebAssembly and share the same EVM state trie as Solidity contracts, enabling interoperability. ## Contract basics A Stylus contract consists of three main components: 1. **Storage Definition**: Defines the contract's persistent state 2. **Entrypoint**: Marks the main contract struct that handles incoming calls 3. **Public Methods**: Functions exposed to external callers via the `#[public]` macro ### Minimal contract Here's the simplest possible Stylus contract: ```rust #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; use stylus_sdk::prelude::*; #[storage] #[entrypoint] pub struct HelloWorld; #[public] impl HelloWorld { pub fn greet(&self) -> String { "Hello, Stylus!".to_string() } } ``` This contract: * Uses `#[storage]` to define the contract struct (empty in this case) * Uses `#[entrypoint]` to mark it as the contract's entry point * Uses `#[public]` to expose the `greet` method * Exposes a single read-only method callable from Solidity or other contracts ## Storage definition Stylus contracts use the `sol_storage!` macro or `#[storage]` attribute to define persistent storage that maps directly to Solidity storage slots. ### Using `sol_storage!` (Solidity-style) The `sol_storage!` macro lets you define storage using Solidity syntax: ```rust use stylus_sdk::prelude::*; use alloy_primitives::{Address, U256}; sol_storage! { #[entrypoint] pub struct Counter { uint256 count; address owner; mapping(address => uint256) balances; } } ``` This creates a contract with: * A `count` field of type `StorageU256` * An `owner` field of type `StorageAddress` * A `balances` mapping from `Address` to `StorageU256` ### Using `#[storage]` (Rust-style) Alternatively, use the `#[storage]` attribute with explicit storage types: ```rust use stylus_sdk::prelude::*; use stylus_sdk::storage::{StorageU256, StorageAddress, StorageMap}; use alloy_primitives::{Address, U256}; #[storage] #[entrypoint] pub struct Counter { count: StorageU256, owner: StorageAddress, balances: StorageMap, } ``` Both approaches produce identical storage layouts and are fully interoperable with Solidity contracts using the same storage structure. ## The `#[entrypoint]` macro The `#[entrypoint]` macro marks a struct as the contract's main entry point. It automatically implements the `TopLevelStorage` trait, which enables: * Routing incoming calls to public methods * Managing contract storage * Flushing the storage cache automatically on cross-contract calls, which protects against reentrancy by default; follow the checks-effects-interactions pattern for additional safety **Key requirements:** * Exactly one struct per contract must have `#[entrypoint]` * The struct must also have `#[storage]` or be defined in `sol_storage!` * The entrypoint struct represents the contract's root storage **Example:** ```rust sol_storage! { #[entrypoint] pub struct MyContract { uint256 value; } } ``` The `#[entrypoint]` macro generates: 1. An implementation of `TopLevelStorage` for the struct 2. A `user_entrypoint` function that Stylus calls when the contract receives a transaction 3. Method routing logic to dispatch calls to `#[public]` methods ## Public methods with `#[public]` The `#[public]` macro exposes Rust methods as external contract functions callable from Solidity, other Stylus contracts, or external callers. ### Basic public methods ```rust use stylus_sdk::prelude::*; use alloy_primitives::U256; sol_storage! { #[entrypoint] pub struct Calculator { uint256 result; } } #[public] impl Calculator { // View function (read-only) pub fn get_result(&self) -> U256 { self.result.get() } // Write function (mutates state) pub fn set_result(&mut self, value: U256) { self.result.set(value); } // Pure function (no state access) pub fn add(a: U256, b: U256) -> U256 { a + b } } ``` ### State mutability The SDK automatically infers state mutability from the method signature: | Signature | Mutability | Solidity Equivalent | Description | | ----------- | ---------- | ------------------- | --------------------- | | `&self` | `view` | `view` | Read contract state | | `&mut self` | Write | (default) | Modify contract state | | Neither | `pure` | `pure` | No state access | **Examples:** ```rust #[public] impl MyContract { // View: can read state, cannot modify pub fn balance_of(&self, account: Address) -> U256 { self.balances.get(account) } // Write: can read and modify state pub fn transfer(&mut self, to: Address, amount: U256) { let sender = self.vm().msg_sender(); let balance = self.balances.get(sender); self.balances.setter(sender).set(balance - amount); let to_balance = self.balances.get(to); self.balances.setter(to).set(to_balance + amount); } // Pure: no state access at all pub fn calculate_fee(amount: U256) -> U256 { amount * U256::from(3) / U256::from(100) } } ``` ## Constructor The `#[constructor]` attribute marks a function that runs once during contract deployment. ### Basic constructor ```rust use stylus_sdk::prelude::*; use alloy_primitives::{Address, U256}; sol_storage! { #[entrypoint] pub struct Token { address owner; uint256 total_supply; } } #[public] impl Token { #[constructor] pub fn constructor(&mut self, initial_supply: U256) { let deployer = self.vm().tx_origin(); self.owner.set(deployer); self.total_supply.set(initial_supply); } pub fn owner(&self) -> Address { self.owner.get() } } ``` ### Constructor features **Payable Constructor:** ```rust #[public] impl Token { #[constructor] #[payable] pub fn constructor(&mut self, initial_supply: U256) { // Contract can receive ETH during deployment let received = self.vm().msg_value(); self.owner.set(self.vm().tx_origin()); self.total_supply.set(initial_supply); } } ``` **Important Notes:** * The constructor name can be anything (doesn't have to be `constructor`) * Only one constructor per contract * Constructor runs exactly once when the contract is deployed * Use `tx_origin()` instead of `msg_sender()` when deploying via a factory contract ## Method attributes ### `#[payable]` Marks a function as able to receive ETH: ```rust #[public] impl PaymentProcessor { #[payable] pub fn deposit(&mut self) -> U256 { let sender = self.vm().msg_sender(); let amount = self.vm().msg_value(); let current = self.balances.get(sender); self.balances.setter(sender).set(current + amount); amount } // Non-payable function will revert if ETH is sent pub fn withdraw(&mut self, amount: U256) { // Will revert if msg.value > 0 let sender = self.vm().msg_sender(); let balance = self.balances.get(sender); self.balances.setter(sender).set(balance - amount); } } ``` **Important:** Without `#[payable]`, sending ETH to a function causes a revert. ### `#[receive]` Handles plain ETH transfers without calldata (equivalent to Solidity's `receive()` function): ```rust use alloy_sol_types::sol; sol! { event EtherReceived(address indexed sender, uint256 amount); } #[public] impl Wallet { #[receive] #[payable] pub fn receive(&mut self) -> Result<(), Vec> { let sender = self.vm().msg_sender(); let amount = self.vm().msg_value(); let balance = self.balances.get(sender); self.balances.setter(sender).set(balance + amount); self.vm().log(EtherReceived { sender, amount }); Ok(()) } } ``` **Notes:** * Must be combined with `#[payable]` * Called when the contract receives ETH without calldata * Only one `#[receive]` function per contract * Must have signature: `fn name(&mut self) -> Result<(), Vec>` ### `#[fallback]` Handles calls to non-existent functions or as a fallback for ETH transfers: ```rust use alloy_sol_types::sol; sol! { event FallbackCalled(address indexed sender, bytes4 selector, uint256 value); } #[public] impl Contract { #[fallback] #[payable] pub fn fallback(&mut self, calldata: &[u8]) -> ArbResult { let sender = self.vm().msg_sender(); let value = self.vm().msg_value(); // Extract function selector if present let selector = if calldata.len() >= 4 { [calldata[0], calldata[1], calldata[2], calldata[3]] } else { [0; 4] }; self.vm().log(FallbackCalled { sender, selector: selector.into(), value, }); Ok(vec![]) } } ``` **Fallback is called when:** 1. A function call doesn't match any existing function signature 2. Plain ETH transfer when no `#[receive]` function exists 3. The contract receives calldata, but no function matches **Notes:** * Must have signature: `fn name(&mut self, calldata: &[u8]) -> ArbResult` * Can optionally include `#[payable]` to accept ETH * Only one `#[fallback]` function per contract ### `#[selector]` Customizes the Solidity function selector: ```rust #[public] impl Token { // Use a custom name in the ABI #[selector(name = "balanceOf")] pub fn get_balance(&self, account: Address) -> U256 { self.balances.get(account) } // Explicitly set the 4-byte selector #[selector(bytes = "0x70a08231")] pub fn balance_of_custom(&self, account: Address) -> U256 { self.balances.get(account) } } ``` This is useful for: * Matching existing Solidity interfaces exactly * Avoiding naming conflicts * Implementing multiple methods with the same name but different selectors ## Contract trait-based composition Stylus supports code reuse via trait-based composition. Define reusable functionality as traits and implement them on your contract: ```rust use stylus_sdk::prelude::*; use alloy_primitives::{Address, U256}; // Define interface traits #[public] trait IOwnable { fn owner(&self) -> Address; fn transfer_ownership(&mut self, new_owner: Address) -> bool; } #[public] trait IErc20 { fn name(&self) -> String; fn symbol(&self) -> String; fn balance_of(&self, account: Address) -> U256; fn transfer(&mut self, to: Address, value: U256) -> bool; } // Define storage components #[storage] struct Ownable { owner: StorageAddress, } #[storage] struct Erc20 { balances: StorageMap, } // Compose into main contract #[storage] #[entrypoint] struct MyToken { ownable: Ownable, erc20: Erc20, } // Declare which interfaces this contract implements #[public] #[implements(IOwnable, IErc20)] impl MyToken {} // Implement each trait #[public] impl IOwnable for MyToken { fn owner(&self) -> Address { self.ownable.owner.get() } fn transfer_ownership(&mut self, new_owner: Address) -> bool { let caller = self.vm().msg_sender(); if caller != self.ownable.owner.get() { return false; } self.ownable.owner.set(new_owner); true } } #[public] impl IErc20 for MyToken { fn name(&self) -> String { "MyToken".into() } fn symbol(&self) -> String { "MTK".into() } fn balance_of(&self, account: Address) -> U256 { self.erc20.balances.get(account) } fn transfer(&mut self, to: Address, value: U256) -> bool { let from = self.vm().msg_sender(); let from_balance = self.erc20.balances.get(from); if from_balance < value { return false; } self.erc20.balances.setter(from).set(from_balance - value); let to_balance = self.erc20.balances.get(to); self.erc20.balances.setter(to).set(to_balance + value); true } } ``` **Benefits:** * Clear separation of concerns * Explicit interface declarations * Type-safe composition * Easy to test components independently * Compatible with Solidity interface standards ### Accessing VM context All public methods can access the blockchain context via the `self.vm()` property: ```rust #[public] impl MyContract { pub fn get_caller_info(&self) -> (Address, U256, U256) { let vm = self.vm(); ( vm.msg_sender(), // Caller's address vm.msg_value(), // ETH sent with call vm.block_number(), // Current block number ) } } ``` See the [Global Variables and Functions](/stylus/fundamentals/global-variables-and-functions.md) documentation for a complete list of available VM methods. ## Events Events allow contracts to log data to the blockchain, enabling offchain monitoring and indexing. ### Defining events Use the `sol!` macro to define events with Solidity-compatible signatures: ```rust use alloy_sol_types::sol; use alloy_primitives::{Address, U256}; sol! { // Up to 3 parameters can be indexed event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); event DataUpdated(string indexed key, bytes data); } ``` **Indexed parameters:** * Allow filtering events by that parameter * Limited to 3 indexed parameters per event * Indexed parameters are stored in log topics, not data ### Emitting events Use `self.vm().log()` to emit events: ```rust #[public] impl Token { pub fn transfer(&mut self, to: Address, value: U256) -> bool { let from = self.vm().msg_sender(); // Transfer logic... let from_balance = self.balances.get(from); if from_balance < value { return false; } self.balances.setter(from).set(from_balance - value); let to_balance = self.balances.get(to); self.balances.setter(to).set(to_balance + value); // Emit event self.vm().log(Transfer { from, to, value }); true } } ``` ### Raw log emission For advanced use cases, emit raw logs directly: ```rust use alloy_primitives::FixedBytes; #[public] impl Contract { pub fn emit_raw_log(&self) { let user = Address::from([0x22; 20]); let balance = U256::from(1000); // Topics (up to 4, must be FixedBytes<32>) let topics = &[user.into_word()]; // Data (arbitrary bytes) let mut data: Vec = vec![]; data.extend_from_slice(&balance.to_be_bytes::<32>()); self.vm().raw_log(topics, &data).unwrap(); } } ``` ## External contract calls Stylus contracts can call other contracts (Solidity or Stylus) using typed interfaces or raw calls. ### Defining contract interfaces Use `sol_interface!` to define interfaces for external contracts: ```rust use stylus_sdk::prelude::*; sol_interface! { interface IToken { function balanceOf(address account) external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); } interface IOracle { function getPrice() external view returns (uint256); } } ``` ### Calling external contracts #### View calls (read-only) ```rust use stylus_sdk::prelude::*; // brings `Call` into scope #[public] impl MyContract { pub fn get_token_balance(&self, token: IToken, account: Address) -> U256 { // Call::new() for view calls (no state mutation) let config = Call::new(); token.balance_of(self.vm(), config, account).unwrap() } } ``` #### Mutating calls ```rust #[public] impl MyContract { pub fn transfer_tokens(&mut self, token: IToken, to: Address, amount: U256) -> bool { // Call::new_mutating(self) for state-changing calls let config = Call::new_mutating(self); token.transfer(self.vm(), config, to, amount).unwrap() } } ``` #### Payable calls ```rust #[public] impl MyContract { #[payable] pub fn forward_payment(&mut self, recipient: IPaymentProcessor) -> Result<(), Vec> { // Forward received ETH to another contract let value = self.vm().msg_value(); let config = Call::new_payable(self, value); recipient.process_payment(self.vm(), config)?; Ok(()) } } ``` #### Configuring gas ```rust #[public] impl MyContract { pub fn call_with_limited_gas(&mut self, token: IToken, to: Address) -> bool { let config = Call::new_mutating(self) .gas(self.vm().evm_gas_left() / 2); // Use half remaining gas token.transfer(self.vm(), config, to, U256::from(100)).unwrap() } } ``` ### Low-Level calls For maximum flexibility, use raw calls: ```rust use stylus_sdk::call::{call, static_call, RawCall}; use stylus_sdk::prelude::*; // brings `Call` into scope #[public] impl MyContract { // Low-level call (state-changing) pub fn execute_call(&mut self, target: Address, calldata: Vec) -> Result, Vec> { let config = Call::new_mutating(self) .gas(self.vm().evm_gas_left()); Ok(call(self.vm(), config, target, &calldata)?) } // Static call (read-only) pub fn execute_static_call(&self, target: Address, calldata: Vec) -> Result, Vec> { Ok(static_call(self.vm(), Call::new(), target, &calldata)?) } // Unsafe raw call with advanced options pub fn execute_raw_call(&mut self, target: Address, calldata: Vec) -> Result, Vec> { unsafe { RawCall::new_delegate(self.vm()) .gas(2100) .limit_return_data(0, 32) .flush_storage_cache() .call(target, &calldata) } } } ``` **Call Types:** * `call()`: State-changing call to another contract * `static_call()`: Read-only call (equivalent to Solidity `staticcall`) * `RawCall`: Low-level unsafe calls with fine-grained control ## Error handling Stylus contracts can define and return custom errors using Solidity-compatible error types. ### Defining errors ```rust use alloy_sol_types::sol; sol! { error Unauthorized(); error InsufficientBalance(address from, uint256 have, uint256 want); error InvalidAddress(address addr); } #[derive(SolidityError)] pub enum TokenError { Unauthorized(Unauthorized), InsufficientBalance(InsufficientBalance), InvalidAddress(InvalidAddress), } ``` ### Using errors in methods ```rust #[public] impl Token { pub fn transfer(&mut self, to: Address, amount: U256) -> Result { let from = self.vm().msg_sender(); if to == Address::ZERO { return Err(TokenError::InvalidAddress(InvalidAddress { addr: to })); } let balance = self.balances.get(from); if balance < amount { return Err(TokenError::InsufficientBalance(InsufficientBalance { from, have: balance, want: amount, })); } self.balances.setter(from).set(balance - amount); let to_balance = self.balances.get(to); self.balances.setter(to).set(to_balance + amount); Ok(true) } } ``` **Error handling notes:** * Errors automatically encode as Solidity-compatible error data * Use `Result` where `E` implements `SolidityError` * Error data includes the error signature and parameters * Compatible with Solidity `try/catch` blocks ## Complete example Here's a complete **ERC-20**-style token contract demonstrating all major features: ```rust #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; use alloy_primitives::{Address, U256}; use alloy_sol_types::sol; use stylus_sdk::prelude::*; // Define events sol! { event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); } // Define errors sol! { error InsufficientBalance(address from, uint256 have, uint256 want); error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want); error Unauthorized(); } #[derive(SolidityError)] pub enum TokenError { InsufficientBalance(InsufficientBalance), InsufficientAllowance(InsufficientAllowance), Unauthorized(Unauthorized), } // Define storage sol_storage! { #[entrypoint] pub struct SimpleToken { mapping(address => uint256) balances; mapping(address => mapping(address => uint256)) allowances; uint256 total_supply; address owner; } } #[public] impl SimpleToken { // Constructor #[constructor] pub fn constructor(&mut self, initial_supply: U256) { let deployer = self.vm().tx_origin(); self.owner.set(deployer); self.balances.setter(deployer).set(initial_supply); self.total_supply.set(initial_supply); self.vm().log(Transfer { from: Address::ZERO, to: deployer, value: initial_supply, }); } // View functions pub fn balance_of(&self, account: Address) -> U256 { self.balances.get(account) } pub fn allowance(&self, owner: Address, spender: Address) -> U256 { self.allowances.getter(owner).get(spender) } pub fn total_supply(&self) -> U256 { self.total_supply.get() } pub fn owner(&self) -> Address { self.owner.get() } // Write functions pub fn transfer(&mut self, to: Address, value: U256) -> Result { let from = self.vm().msg_sender(); self._transfer(from, to, value)?; Ok(true) } pub fn approve(&mut self, spender: Address, value: U256) -> bool { let owner = self.vm().msg_sender(); self.allowances.setter(owner).setter(spender).set(value); self.vm().log(Approval { owner, spender, value }); true } pub fn transfer_from( &mut self, from: Address, to: Address, value: U256 ) -> Result { let spender = self.vm().msg_sender(); // Check allowance let current_allowance = self.allowances.getter(from).get(spender); if current_allowance < value { return Err(TokenError::InsufficientAllowance(InsufficientAllowance { owner: from, spender, have: current_allowance, want: value, })); } // Update allowance self.allowances.setter(from).setter(spender).set(current_allowance - value); // Transfer self._transfer(from, to, value)?; Ok(true) } // Owner-only functions pub fn mint(&mut self, to: Address, value: U256) -> Result<(), TokenError> { if self.vm().msg_sender() != self.owner.get() { return Err(TokenError::Unauthorized(Unauthorized {})); } let to_balance = self.balances.get(to); self.balances.setter(to).set(to_balance + value); self.total_supply.set(self.total_supply.get() + value); self.vm().log(Transfer { from: Address::ZERO, to, value, }); Ok(()) } // Internal helper function fn _transfer(&mut self, from: Address, to: Address, value: U256) -> Result<(), TokenError> { let from_balance = self.balances.get(from); if from_balance < value { return Err(TokenError::InsufficientBalance(InsufficientBalance { from, have: from_balance, want: value, })); } self.balances.setter(from).set(from_balance - value); let to_balance = self.balances.get(to); self.balances.setter(to).set(to_balance + value); self.vm().log(Transfer { from, to, value }); Ok(()) } } ``` ## Best practices ### 1. Use appropriate state mutability ```rust // Good: Read-only functions use &self pub fn get_balance(&self, account: Address) -> U256 { self.balances.get(account) } // Good: State-changing functions use &mut self pub fn set_balance(&mut self, account: Address, balance: U256) { self.balances.setter(account).set(balance); } ``` ### 2. Validate inputs early ```rust pub fn transfer(&mut self, to: Address, amount: U256) -> Result { // Validate inputs first if to == Address::ZERO { return Err(TokenError::InvalidAddress(InvalidAddress { addr: to })); } if amount == U256::ZERO { return Ok(true); // Nothing to transfer } // Then proceed with logic let from = self.vm().msg_sender(); // ... } ``` ### 3. Use custom errors ```rust // Good: Descriptive custom errors pub fn withdraw(&mut self, amount: U256) -> Result<(), VaultError> { let balance = self.balances.get(self.vm().msg_sender()); if balance < amount { return Err(VaultError::InsufficientBalance(InsufficientBalance { have: balance, want: amount, })); } // ... } // Avoid: Generic Vec errors pub fn withdraw(&mut self, amount: U256) -> Result<(), Vec> { // Less informative } ``` ### 4. Emit events for state changes ```rust pub fn update_value(&mut self, new_value: U256) { let old_value = self.value.get(); self.value.set(new_value); // Always emit events for important state changes self.vm().log(ValueUpdated { old_value, new_value, }); } ``` ### 5. Access control patterns ```rust // Good: Clear access control checks pub fn admin_function(&mut self) -> Result<(), TokenError> { if self.vm().msg_sender() != self.owner.get() { return Err(TokenError::Unauthorized(Unauthorized {})); } // Admin logic... Ok(()) } // Consider: Reusable modifier-like helper impl Token { fn only_owner(&self) -> Result<(), TokenError> { if self.vm().msg_sender() != self.owner.get() { return Err(TokenError::Unauthorized(Unauthorized {})); } Ok(()) } pub fn admin_function(&mut self) -> Result<(), TokenError> { self.only_owner()?; // Admin logic... Ok(()) } } ``` ### 6. Gas-efficient storage access ```rust // Good: Read once, use multiple times pub fn complex_calculation(&self, account: Address) -> U256 { let balance = self.balances.get(account); // Read once let result = balance * U256::from(2) + balance / U256::from(10); result } // Avoid: Multiple reads of same storage slot pub fn inefficient_calculation(&self, account: Address) -> U256 { self.balances.get(account) * U256::from(2) + self.balances.get(account) / U256::from(10) } ``` ### 7. Check effects interactions pattern ```rust // Good: Check-Effects-Interactions pattern pub fn withdraw(&mut self, amount: U256) -> Result<(), VaultError> { let caller = self.vm().msg_sender(); // Checks let balance = self.balances.get(caller); if balance < amount { return Err(VaultError::InsufficientBalance(InsufficientBalance { have: balance, want: amount, })); } // Effects (update state BEFORE external calls) self.balances.setter(caller).set(balance - amount); // Interactions (external calls last) // transfer_eth(self.vm(), caller, amount)?; Ok(()) } ``` ### 8. Use type-safe interfaces for external calls ```rust // Good: Use sol_interface! for type safety sol_interface! { interface IToken { function transfer(address to, uint256 amount) external returns (bool); } } pub fn call_token(&mut self, token: IToken, to: Address, amount: U256) -> bool { let config = Call::new_mutating(self); token.transfer(self.vm(), config, to, amount).unwrap() } // Avoid: Raw calls unless necessary pub fn raw_call(&mut self, token: Address, to: Address, amount: U256) -> Vec { // Less type-safe, more error-prone let config = Call::new_mutating(self); let calldata = /* manually construct */; call(self.vm(), config, token, &calldata).unwrap() } ``` ## Delegate calls Delegate calls allow a contract to execute code from another contract while maintaining its own context. When Contract A executes a delegate call to Contract B, B's code runs using Contract A's storage, `msg.sender`, and `msg.value`. This means that any state changes affect Contract A, and the original sender and transaction value are preserved. This pattern is essential for building upgradeable contracts, proxy patterns, and modular smart contract systems. ### Using the low-level `delegate_call` function The `delegate_call` function is a low-level operation similar to `call` and `static_call`. It is considered unsafe because it relies on an external contract to ensure safety. ```rust pub unsafe fn delegate_call( host: &H, context: impl MutatingCallContext, to: Address, data: &[u8], ) -> Result, Error> ``` **Example usage:** ```rust use stylus_sdk::call::delegate_call; use stylus_sdk::prelude::*; // brings `Call` into scope pub fn low_level_delegate_call( &mut self, calldata: Vec, target: Address, ) -> Result, DelegateCallErrors> { unsafe { let config = Call::new_mutating(self); let result = delegate_call(self.vm(), config, target, &calldata) .map_err(|_| DelegateCallErrors::DelegateCallFailed(DelegateCallFailed {}))?; Ok(result) } } ``` ### Using `RawCall` with `new_delegate()` For scenarios that require untyped calls with additional configuration options, `RawCall` provides a fluent interface. You can set up a delegate call by chaining optional configuration methods. ```rust use stylus_sdk::call::RawCall; pub fn raw_delegate_call( &mut self, calldata: Vec, target: Address, ) -> Result, Vec> { let data = unsafe { RawCall::new_delegate(self.vm()) // Configure a delegate call .gas(2100) // Supply 2100 gas .limit_return_data(0, 32) // Only read the first 32 bytes back .call(target, &calldata)? }; Ok(data) } ``` ### Safety considerations Warning Delegate calls are inherently unsafe and should be used with caution. * **Trust requirement**: The calling contract must trust the external contract to uphold safety requirements * **Storage modification**: The external contract can arbitrarily change the calling contract's storage * **Ether spending**: The external contract may spend ether or perform other critical operations on behalf of the caller * **Cache clearing**: While the `delegate_call` function clears any cached values, it cannot prevent unsafe actions by the external contract ### Complete delegate call example ```rust #![cfg_attr(not(feature = "export-abi"), no_main)] extern crate alloc; use alloy_sol_types::sol; use stylus_sdk::{ alloy_primitives::Address, call::{delegate_call, RawCall}, prelude::*, }; #[storage] #[entrypoint] pub struct DelegateExample; sol! { error DelegateCallFailed(); } #[derive(SolidityError)] pub enum DelegateCallErrors { DelegateCallFailed(DelegateCallFailed), } #[public] impl DelegateExample { // Low-level delegate call pub fn low_level_delegate_call( &mut self, calldata: Vec, target: Address, ) -> Result, DelegateCallErrors> { unsafe { let config = Call::new_mutating(self); let result = delegate_call(self.vm(), config, target, &calldata) .map_err(|_| DelegateCallErrors::DelegateCallFailed(DelegateCallFailed {}))?; Ok(result) } } // RawCall delegate call with configuration pub fn raw_delegate_call( &mut self, calldata: Vec, target: Address, ) -> Result, Vec> { let data = unsafe { RawCall::new_delegate(self.vm()) .gas(2100) .limit_return_data(0, 32) .call(target, &calldata)? }; Ok(data) } } ``` ## Sending ether Stylus provides multiple ways to send ether from a contract. Unlike Solidity's `transfer` method, which is capped at 2300 gas, Stylus's `transfer_eth` forwards all gas to the recipient. You can cap gas using the low-level `call` method if needed. ### Methods for sending ether | Method | Description | Gas behavior | | ------------------------ | --------------------------------------- | ------------------------------- | | `transfer_eth()` | Simple ether transfer | Forwards all gas | | `call()` with `.value()` | Low-level call with value | Forwards all gas (configurable) | | Payable external calls | Call payable methods on other contracts | Forwards all gas (configurable) | ### Using `transfer_eth()` The simplest way to send ether: ```rust use stylus_sdk::call::transfer::transfer_eth; #[public] impl SendEther { #[payable] pub fn send_via_transfer(&self, to: Address) -> Result<(), Vec> { transfer_eth(self.vm(), to, self.vm().msg_value())?; Ok(()) } } ``` ### Using a low-level `call()` with a value For more control over the transfer: ```rust use stylus_sdk::call::call; use stylus_sdk::prelude::*; // brings `Call` into scope #[public] impl SendEther { #[payable] pub fn send_via_call(&mut self, to: Address) -> Result<(), Vec> { let value = self.vm().msg_value(); let context = Call::new_payable(self, value); call(self.vm(), context, to, &[])?; Ok(()) } } ``` These two approaches are equivalent under the hood: ```rust // These are equivalent: transfer_eth(self.vm(), recipient, value)?; let context = Call::new_payable(self, value); call(self.vm(), context, recipient, &[])?; ``` ### Sending with a gas limit To cap the gas forwarded to the recipient (similar to Solidity's `transfer`): ```rust #[payable] pub fn send_via_call_gas_limit(&mut self, to: Address, gas_amount: u64) -> Result<(), Vec> { let value = self.vm().msg_value(); let context = Call::new_payable(self, value).gas(gas_amount); call(self.vm(), context, to, &[])?; Ok(()) } ``` ### Sending with calldata To trigger a fallback function on the receiving contract: ```rust use stylus_sdk::abi::Bytes; #[payable] pub fn send_via_call_with_calldata( &mut self, to: Address, data: Bytes, ) -> Result<(), Vec> { let value = self.vm().msg_value(); let context = Call::new_payable(self, value); call(self.vm(), context, to, &data)?; Ok(()) } ``` ### Sending to payable contract methods Use typed interfaces to call payable methods on other contracts: ```rust sol_interface! { interface ITarget { function receiveEther() external payable; } } #[public] impl SendEther { #[payable] pub fn send_to_contract(&mut self, to: Address) -> Result<(), Vec> { let target = ITarget::new(to); let value = self.vm().msg_value(); let context = Call::new_payable(self, value); target.receive_ether(self.vm(), context)?; Ok(()) } } ``` ### Where can you send **ETH** 1. **Externally owned account (EOA) addresses**: Directly send ether to any EOA address 2. **Solidity contracts with the `receive()` function**: Send ether without calldata to contracts implementing `receive()` 3. **Solidity contracts with the `fallback()` function**: Send ether with calldata to contracts implementing `fallback()` 4. **Contracts with payable methods**: Call any payable method on Solidity or Stylus contracts ### Complete sending ether example ```rust #![cfg_attr(not(any(feature = "export-abi", test)), no_main)] extern crate alloc; use alloy_primitives::Address; use stylus_sdk::{ abi::Bytes, call::{call, transfer::transfer_eth}, prelude::*, }; sol_interface! { interface ITarget { function receiveEther() external payable; } } #[storage] #[entrypoint] pub struct SendEther; #[public] impl SendEther { // Simple transfer #[payable] pub fn send_via_transfer(&mut self, to: Address) -> Result<(), Vec> { let value = self.vm().msg_value(); transfer_eth(self.vm(), to, value)?; Ok(()) } // Low-level call #[payable] pub fn send_via_call(&mut self, to: Address) -> Result<(), Vec> { let value = self.vm().msg_value(); let context = Call::new_payable(self, value); call(self.vm(), context, to, &[])?; Ok(()) } // With gas limit #[payable] pub fn send_via_call_gas_limit(&mut self, to: Address, gas_amount: u64) -> Result<(), Vec> { let value = self.vm().msg_value(); let context = Call::new_payable(self, value).gas(gas_amount); call(self.vm(), context, to, &[])?; Ok(()) } // With calldata (triggers fallback) #[payable] pub fn send_via_call_with_calldata( &mut self, to: Address, data: Bytes, ) -> Result<(), Vec> { let value = self.vm().msg_value(); let context = Call::new_payable(self, value); call(self.vm(), context, to, &data)?; Ok(()) } // To payable contract method #[payable] pub fn send_to_contract(&mut self, to: Address) -> Result<(), Vec> { let target = ITarget::new(to); let value = self.vm().msg_value(); let context = Call::new_payable(self, value); target.receive_ether(self.vm(), context)?; Ok(()) } } ``` ## Factory contract deployment (coming soon) The factory pattern allows a contract to deploy other contracts programmatically. This is useful for creating contract instances on demand, such as deploying new token contracts or creating user-specific vaults. Note Advanced deployment patterns documentation is in development. This section will cover: * Deploying contracts from within a contract * Passing constructor arguments * Deterministic deployment with CREATE2 * Handling deployment failures **Constructor considerations for factory-deployed contracts:** When a contract is deployed via a factory contract (rather than directly by an EOA), the `msg_sender()` in the constructor will be the factory contract's address, not the original deployer. If you need the original deployer's address, use `tx_origin()` instead: ```rust #[public] impl FactoryDeployedContract { #[constructor] pub fn constructor(&mut self) { // msg_sender() = factory contract address // tx_origin() = original transaction sender (EOA) let original_deployer = self.vm().tx_origin(); self.owner.set(original_deployer); } } ``` ## Function modifiers and access control patterns Unlike Solidity, Rust does not have built-in modifier syntax. However, you can achieve similar functionality using helper functions that return `Result<(), Error>` combined with the `?` operator. ### Basic modifier pattern Create helper functions that perform checks and return early on failure: ```rust sol! { error Unauthorized(); error Paused(); } #[derive(SolidityError)] pub enum ContractError { Unauthorized(Unauthorized), Paused(Paused), } #[public] impl MyContract { // Modifier-like helper function fn only_owner(&self) -> Result<(), ContractError> { if self.vm().msg_sender() != self.owner.get() { return Err(ContractError::Unauthorized(Unauthorized {})); } Ok(()) } // Using the "modifier" with the ? operator pub fn admin_function(&mut self) -> Result<(), ContractError> { self.only_owner()?; // Returns early if check fails // Admin logic here... Ok(()) } } ``` ### Multiple guard functions You can combine multiple checks by chaining helper functions: ```rust #[public] impl MyContract { fn only_owner(&self) -> Result<(), ContractError> { if self.vm().msg_sender() != self.owner.get() { return Err(ContractError::Unauthorized(Unauthorized {})); } Ok(()) } fn when_not_paused(&self) -> Result<(), ContractError> { if self.paused.get() { return Err(ContractError::Paused(Paused {})); } Ok(()) } fn only_after(&self, timestamp: u64) -> Result<(), ContractError> { if self.vm().block_timestamp() < timestamp { return Err(ContractError::TooEarly(TooEarly {})); } Ok(()) } // Combining multiple "modifiers" pub fn protected_action(&mut self) -> Result<(), ContractError> { self.only_owner()?; self.when_not_paused()?; self.only_after(self.unlock_time.get())?; // Protected logic here... Ok(()) } } ``` ### Reusable access control module For larger projects, encapsulate access control in a reusable module: ```rust // Access control helpers impl MyContract { fn require_role(&self, role: FixedBytes<32>, account: Address) -> Result<(), ContractError> { if !self.has_role(role, account) { return Err(ContractError::MissingRole(MissingRole { role, account })); } Ok(()) } fn has_role(&self, role: FixedBytes<32>, account: Address) -> bool { self.roles.getter(role).get(account) } // Grant role (admin only) pub fn grant_role( &mut self, role: FixedBytes<32>, account: Address, ) -> Result<(), ContractError> { self.require_role(self.admin_role(), self.vm().msg_sender())?; self.roles.setter(role).setter(account).set(true); Ok(()) } } ``` ### Complete access control example ```rust #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; use alloy_primitives::{Address, U256}; use alloy_sol_types::sol; use stylus_sdk::prelude::*; sol! { error Unauthorized(); error Paused(); error InvalidAmount(); } #[derive(SolidityError)] pub enum VaultError { Unauthorized(Unauthorized), Paused(Paused), InvalidAmount(InvalidAmount), } sol_storage! { #[entrypoint] pub struct Vault { address owner; bool paused; mapping(address => uint256) balances; } } #[public] impl Vault { // Modifier-like helpers fn only_owner(&self) -> Result<(), VaultError> { if self.vm().msg_sender() != self.owner.get() { return Err(VaultError::Unauthorized(Unauthorized {})); } Ok(()) } fn when_not_paused(&self) -> Result<(), VaultError> { if self.paused.get() { return Err(VaultError::Paused(Paused {})); } Ok(()) } fn valid_amount(&self, amount: U256) -> Result<(), VaultError> { if amount == U256::ZERO { return Err(VaultError::InvalidAmount(InvalidAmount {})); } Ok(()) } // Public functions using modifiers #[payable] pub fn deposit(&mut self) -> Result<(), VaultError> { self.when_not_paused()?; let sender = self.vm().msg_sender(); let amount = self.vm().msg_value(); self.valid_amount(amount)?; let current = self.balances.get(sender); self.balances.setter(sender).set(current + amount); Ok(()) } pub fn pause(&mut self) -> Result<(), VaultError> { self.only_owner()?; self.paused.set(true); Ok(()) } pub fn unpause(&mut self) -> Result<(), VaultError> { self.only_owner()?; self.paused.set(false); Ok(()) } } ``` ## See also * [Primitives](/stylus/fundamentals/data-types/primitives.md): Basic data types * [Compound Types](/stylus/fundamentals/data-types/compound-types.md): Arrays, structs, tuples * [Storage Types](/stylus/fundamentals/data-types/storage.md): Persistent storage * [Global Variables and Functions](/stylus/fundamentals/global-variables-and-functions.md): VM context methods * [Minimal entrypoint contracts](/stylus/advanced/minimal-entrypoint-contracts.md): Hand-craft an entrypoint without macros --- > For a complete page index, fetch # Stylus compound types Compound types allow you to group multiple values in Stylus contracts. The SDK provides full support for tuples, structs, arrays, and vectors with automatic ABI encoding/decoding and Solidity type mappings. ## Tuples Tuples group multiple values of different types together. They map directly to Solidity tuples. ### Basic tuples ```rust use alloy_primitives::{Address, U256, Bytes}; use stylus_sdk::prelude::*; #[public] impl MyContract { // Return multiple values as a tuple pub fn get_data(&self) -> (U256, Address, bool) { (U256::from(100), Address::ZERO, true) } // Accept tuple as parameter pub fn process_tuple(&mut self, data: (U256, U256, U256)) -> U256 { let (a, b, c) = data; a + b + c } // Nested tuples pub fn nested(&self) -> ((U256, U256), bool) { ((U256::from(1), U256::from(2)), true) } } ``` ### Tuple destructuring ```rust use alloy_primitives::U256; use stylus_sdk::prelude::*; #[public] impl MyContract { pub fn calculate(&self) -> (U256, U256) { let values = (U256::from(100), U256::from(200)); // Destructure the tuple let (first, second) = values; // Return new tuple (first * U256::from(2), second * U256::from(2)) } // Pattern matching with tuples pub fn match_tuple(&self, data: (bool, U256)) -> U256 { match data { (true, value) => value * U256::from(2), (false, value) => value, } } } ``` ### Tuple type mappings | Rust Type | Solidity Type | ABI Signature | | ---------------------- | ---------------------------- | ---------------------------- | | `(U256,)` | `(uint256)` | `"(uint256)"` | | `(U256, Address)` | `(uint256, address)` | `"(uint256,address)"` | | `(bool, U256, Bytes)` | `(bool, uint256, bytes)` | `"(bool,uint256,bytes)"` | | `((U256, U256), bool)` | `((uint256, uint256), bool)` | `"((uint256,uint256),bool)"` | **Tuple Limitations**: * Tuples support up to 24 elements * Tuples are always returned as `memory` in Solidity * An empty tuple `()` represents no return value ## Structs Structs define custom data types with named fields. Use the `sol!` macro to define Solidity-compatible structs. ### Defining structs with `sol!` ```rust use alloy_primitives::{Address, U256}; use alloy_sol_types::sol; use stylus_sdk::prelude::*; sol! { #[derive(Debug, AbiType)] struct User { address account; uint256 balance; string name; } #[derive(Debug, AbiType)] struct Token { string name; string symbol; uint8 decimals; } } #[public] impl MyContract { pub fn get_user(&self) -> User { User { account: Address::ZERO, balance: U256::from(1000), name: "Alice".to_string(), } } pub fn process_user(&mut self, user: User) -> U256 { // Access struct fields user.balance } pub fn get_token_info(&self) -> Token { Token { name: "MyToken".to_string(), symbol: "MTK".to_string(), decimals: 18, } } } ``` ### Nested structs Structs can contain other structs, enabling complex data structures: ```rust use alloy_primitives::Address; use alloy_sol_types::sol; use stylus_sdk::prelude::*; sol! { #[derive(Debug, AbiType)] struct Dog { string name; string breed; } #[derive(Debug, AbiType)] struct User { address account; string name; Dog[] dogs; } } #[public] impl MyContract { pub fn create_user(&self) -> User { let dogs = vec![ Dog { name: "Rex".to_string(), breed: "Labrador".to_string(), }, Dog { name: "Max".to_string(), breed: "Beagle".to_string(), }, ]; User { account: Address::ZERO, name: "Alice".to_string(), dogs, } } pub fn get_dog_count(&self, user: User) -> u256 { user.dogs.len() as u256 } } ``` ### Struct best practices 1. **Always use `#[derive(AbiType)]`** for structs that are to be used in contract interfaces: ```rust sol! { #[derive(Debug, AbiType)] struct MyData { uint256 value; address owner; } } ``` 2. **Add `Debug` derive** for easier debugging: ```rust sol! { #[derive(Debug, AbiType)] struct Config { bool enabled; uint256 timeout; } } ``` 3. **Use descriptive field names** that match Solidity conventions: ```rust sol! { #[derive(Debug, AbiType)] struct VestingSchedule { address beneficiary; uint256 startTime; uint256 cliffDuration; uint256 totalAmount; } } ``` ## Arrays Arrays are fixed-size collections of elements. Stylus supports both Rust arrays and Solidity-style arrays. ### Fixed-size arrays ```rust use alloy_primitives::U256; use stylus_sdk::prelude::*; #[public] impl MyContract { // Return a fixed-size array pub fn get_numbers(&self) -> [U256; 5] { [ U256::from(1), U256::from(2), U256::from(3), U256::from(4), U256::from(5), ] } // Accept fixed-size array as parameter pub fn sum_array(&self, numbers: [U256; 5]) -> U256 { numbers.iter().fold(U256::ZERO, |acc, &x| acc + x) } // Nested arrays pub fn matrix(&self) -> [[u32; 2]; 3] { [[1, 2], [3, 4], [5, 6]] } } ``` ### Array operations ```rust use alloy_primitives::{Address, U256}; use stylus_sdk::prelude::*; #[public] impl MyContract { // Iterate over array pub fn process_addresses(&self, addresses: [Address; 10]) -> U256 { let mut count = U256::ZERO; for addr in addresses.iter() { if *addr != Address::ZERO { count += U256::from(1); } } count } // Array of booleans pub fn check_flags(&self, flags: [bool; 8]) -> bool { flags.iter().all(|&f| f) } } ``` ### Array type mappings | Rust Type | Solidity Type | Description | | --------------------- | -------------- | ------------------------- | | `[U256; 5]` | `uint256[5]` | 5-element uint256 array | | `[bool; 10]` | `bool[10]` | 10-element bool array | | `[Address; 3]` | `address[3]` | 3-element address array | | `[[u32; 2]; 4]` | `uint32[2][4]` | Nested array (4x2 matrix) | | `[FixedBytes<32>; 2]` | `bytes32[2]` | 2-element bytes32 array | ## Vectors Vectors are dynamic arrays that can grow or shrink at runtime. They map to Solidity dynamic arrays. ### Basic vector usage ```rust use alloy_primitives::{Address, U256, Bytes}; use stylus_sdk::prelude::*; #[public] impl MyContract { // Return a vector pub fn get_numbers(&self) -> Vec { vec![U256::from(1), U256::from(2), U256::from(3)] } // Accept vector as parameter pub fn sum_vec(&self, numbers: Vec) -> U256 { numbers.iter().fold(U256::ZERO, |acc, x| acc + *x) } // Vector of addresses pub fn get_addresses(&self) -> Vec
{ vec![Address::ZERO, Address::ZERO] } // Vector of bytes pub fn get_data_list(&self) -> Vec { vec![ Bytes::from(vec![1, 2, 3]), Bytes::from(vec![4, 5, 6]), ] } } ``` ### Vector operations ```rust use alloy_primitives::U256; use stylus_sdk::prelude::*; #[public] impl MyContract { // Filter vector pub fn filter_even(&self, numbers: Vec) -> Vec { numbers .into_iter() .filter(|n| n.byte(0) % 2 == 0) .collect() } // Map over vector pub fn double_values(&self, numbers: Vec) -> Vec { numbers .into_iter() .map(|n| n * U256::from(2)) .collect() } // Find in vector pub fn contains_value(&self, numbers: Vec, target: U256) -> bool { numbers.contains(&target) } // Get vector length pub fn get_length(&self, items: Vec) -> U256 { U256::from(items.len()) } } ``` ### Vectors of structs ```rust use alloy_primitives::Address; use alloy_sol_types::sol; use stylus_sdk::prelude::*; sol! { #[derive(Debug, AbiType)] struct Transaction { address from; address to; uint256 amount; } } #[public] impl MyContract { pub fn get_transactions(&self) -> Vec { vec![ Transaction { from: Address::ZERO, to: Address::ZERO, amount: U256::from(100), }, Transaction { from: Address::ZERO, to: Address::ZERO, amount: U256::from(200), }, ] } pub fn total_amount(&self, txs: Vec) -> U256 { txs.iter() .fold(U256::ZERO, |acc, tx| acc + tx.amount) } } ``` ### Vector type mappings | Rust Type | Solidity Type | ABI Signature | Storage | | --------------- | ------------- | --------------------- | ------- | | `Vec` | `uint256[]` | `"uint256[] memory"` | Dynamic | | `Vec
` | `address[]` | `"address[] memory"` | Dynamic | | `Vec` | `bool[]` | `"bool[] memory"` | Dynamic | | `Vec` | `bytes[]` | `"bytes[] memory"` | Dynamic | | `Vec` | `MyStruct[]` | `"MyStruct[] memory"` | Dynamic | **Important Notes**: * Vectors are **always returned as `memory`** in Solidity, never as `calldata` * `Vec` maps to `uint8[]`, not `bytes` (use `Bytes` for Solidity `bytes`) * Vectors have dynamic size and consume more gas than fixed arrays ## Bytes Types The SDK provides `Bytes` for dynamic byte arrays and `FixedBytes` for fixed-size byte arrays. ### Dynamic bytes (`Bytes`) ```rust use alloy_primitives::Bytes; use stylus_sdk::prelude::*; #[public] impl MyContract { // Return dynamic bytes pub fn get_data(&self) -> Bytes { Bytes::from(vec![1, 2, 3, 4, 5]) } // Process bytes pub fn get_length(&self, data: Bytes) -> usize { data.len() } // Concatenate bytes pub fn concat(&self, a: Bytes, b: Bytes) -> Bytes { let mut result = a.to_vec(); result.extend_from_slice(&b); Bytes::from(result) } } ``` ### Fixed bytes (`FixedBytes`) ```rust use alloy_primitives::FixedBytes; use stylus_sdk::prelude::*; #[public] impl MyContract { // bytes32 (common for hashes) pub fn get_hash(&self) -> FixedBytes<32> { FixedBytes::<32>::ZERO } // bytes4 (common for selectors) pub fn get_selector(&self) -> FixedBytes<4> { FixedBytes::from([0x12, 0x34, 0x56, 0x78]) } // bytes16 pub fn get_uuid(&self) -> FixedBytes<16> { FixedBytes::<16>::from([ 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, ]) } } ``` ## Complete examples ### Example 1: Complex data structures ```rust #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; use alloc::string::String; use alloy_primitives::{Address, U256}; use alloy_sol_types::sol; use stylus_sdk::prelude::*; sol! { #[derive(Debug, AbiType)] struct Token { string name; string symbol; uint8 decimals; uint256 totalSupply; } #[derive(Debug, AbiType)] struct Balance { address owner; uint256 amount; } } sol_storage! { #[entrypoint] pub struct CompoundExample { uint256 counter; } } #[public] impl CompoundExample { // Return tuple pub fn get_info(&self) -> (String, U256, bool) { ("Example".to_string(), U256::from(42), true) } // Return struct pub fn get_token(&self) -> Token { Token { name: "MyToken".to_string(), symbol: "MTK".to_string(), decimals: 18, totalSupply: U256::from(1000000), } } // Return vector of structs pub fn get_balances(&self) -> Vec { vec![ Balance { owner: Address::ZERO, amount: U256::from(100), }, Balance { owner: Address::ZERO, amount: U256::from(200), }, ] } // Accept array pub fn process_array(&self, data: [U256; 5]) -> U256 { data.iter().sum() } // Accept vector and struct pub fn batch_transfer(&mut self, recipients: Vec) -> U256 { recipients.iter().map(|b| b.amount).sum() } } ``` ### Example 2: Nested data structures ```rust #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; use alloc::string::String; use alloy_primitives::{Address, U256}; use alloy_sol_types::sol; use stylus_sdk::prelude::*; sol! { #[derive(Debug, AbiType)] struct Dog { string name; string breed; } #[derive(Debug, AbiType)] struct User { address account; string name; Dog[] dogs; } } sol_storage! { #[entrypoint] pub struct NestedExample {} } #[public] impl NestedExample { pub fn create_user(&self) -> User { User { account: Address::ZERO, name: "Alice".to_string(), dogs: vec![ Dog { name: "Rex".to_string(), breed: "Labrador".to_string(), }, Dog { name: "Max".to_string(), breed: "Beagle".to_string(), }, ], } } pub fn get_dog_names(&self, user: User) -> Vec { user.dogs.into_iter().map(|dog| dog.name).collect() } pub fn count_dogs(&self, users: Vec) -> U256 { let total: usize = users.iter().map(|u| u.dogs.len()).sum(); U256::from(total) } } ``` ## Best practices ### 1. Choose the right type ```rust // Use tuples for simple groupings pub fn get_basics(&self) -> (U256, Address, bool) { /* ... */ } // Use structs for complex data with named fields sol! { #[derive(Debug, AbiType)] struct UserProfile { address account; string name; uint256 balance; bool active; } } // Use arrays for fixed-size collections pub fn get_top_five(&self) -> [U256; 5] { /* ... */ } // Use vectors for dynamic collections pub fn get_all_users(&self) -> Vec
{ /* ... */ } ``` ### 2. Memory efficiency ```rust use alloy_primitives::U256; // Prefer fixed arrays when size is known pub fn fixed_data(&self) -> [U256; 10] { // More gas-efficient [U256::ZERO; 10] } // Use vectors only when size varies pub fn dynamic_data(&self, count: usize) -> Vec { vec![U256::ZERO; count] } ``` ### 3. Struct naming ```rust use alloy_sol_types::sol; sol! { // Good: Clear, descriptive names #[derive(Debug, AbiType)] struct TokenMetadata { string name; string symbol; uint8 decimals; } // Avoid: Ambiguous names #[derive(Debug, AbiType)] struct Data { uint256 x; uint256 y; } } ``` ### 4. Vector vs array ```rust use alloy_primitives::{Address, U256}; // Use fixed arrays for known sizes pub fn get_admins(&self) -> [Address; 3] { // Three admin addresses [Address::ZERO; 3] } // Use vectors for variable sizes pub fn get_users(&self) -> Vec
{ // Unknown number of users vec![] } ``` ### 5. Nested structures ```rust use alloy_sol_types::sol; sol! { // Good: Reasonable nesting depth #[derive(Debug, AbiType)] struct User { address account; Profile profile; } #[derive(Debug, AbiType)] struct Profile { string name; uint256 age; } // Avoid: Excessive nesting (gas inefficient) #[derive(Debug, AbiType)] struct DeepNesting { Level1 l1; } #[derive(Debug, AbiType)] struct Level1 { Level2 l2; } #[derive(Debug, AbiType)] struct Level2 { Level3 l3; } #[derive(Debug, AbiType)] struct Level3 { uint256 value; } } ``` ## Type conversion and helpers ### Converting between types ```rust use alloy_primitives::{U256, Bytes}; // Vec to Bytes let vec: Vec = vec![1, 2, 3]; let bytes = Bytes::from(vec); // Bytes to Vec let bytes = Bytes::from(vec![1, 2, 3]); let vec: Vec = bytes.to_vec(); // Array to Vec let arr: [U256; 3] = [U256::from(1), U256::from(2), U256::from(3)]; let vec: Vec = arr.to_vec(); // Vec to array (if size matches) let vec = vec![U256::from(1), U256::from(2), U256::from(3)]; let arr: [U256; 3] = vec.try_into().unwrap(); ``` ### Working with iterators ```rust use alloy_primitives::U256; // Map over vector let numbers = vec![U256::from(1), U256::from(2), U256::from(3)]; let doubled: Vec = numbers.iter().map(|n| n * U256::from(2)).collect(); // Filter vector let evens: Vec = numbers.into_iter().filter(|n| n.byte(0) % 2 == 0).collect(); // Fold/reduce let sum = numbers.iter().fold(U256::ZERO, |acc, n| acc + n); ``` ## Common patterns ### Batch operations ```rust use alloy_primitives::{Address, U256}; use alloy_sol_types::sol; use stylus_sdk::prelude::*; sol! { #[derive(Debug, AbiType)] struct Transfer { address to; uint256 amount; } } #[public] impl MyContract { pub fn batch_transfer(&mut self, transfers: Vec) -> U256 { let mut total = U256::ZERO; for transfer in transfers { // Process each transfer total += transfer.amount; } total } } ``` ### Pagination ```rust use alloy_primitives::U256; use stylus_sdk::prelude::*; #[public] impl MyContract { pub fn get_page(&self, items: Vec, page: usize, size: usize) -> Vec { let start = page * size; let end = start + size; items.get(start..end.min(items.len())) .unwrap_or(&[]) .to_vec() } } ``` ## See also * [Primitives](/stylus/fundamentals/data-types/primitives.md): Basic types (bool, integers, address, strings) * [Storage Types](/stylus/fundamentals/data-types/storage.md): Persistent storage for compound types * [Type Conversions](/stylus/fundamentals/data-types/conversions-between-types.md): Converting between types --- > For a complete page index, fetch # Conversions between types Stylus smart contracts often need to convert between different type representations: Rust native types, Alloy primitives, and Solidity types. The Stylus SDK provides comprehensive conversion mechanisms through the `AbiType` trait and various standard Rust conversion traits. ## Understanding type relationships The Stylus SDK establishes a bidirectional relationship between Rust and Solidity types: * **Alloy provides**: Solidity types → Rust types mapping via `SolType` * **Stylus SDK provides**: Rust types → Solidity types mapping via `AbiType` Together, these create a complete two-way type system for interoperability. ### Key type mappings | Rust/Alloy type | Solidity type | Notes | | --------------------------------- | ------------------------- | ---------------------------------------------- | | `bool` | `bool` | Native boolean | | `u8`, `u16`, `u32`, `u64`, `u128` | `uint8` through `uint128` | Native unsigned integers | | `i8`, `i16`, `i32`, `i64`, `i128` | `int8` through `int128` | Native signed integers | | `Uint` | `uintBITS` | Arbitrary-sized unsigned integers (8-256 bits) | | `Signed` | `intBITS` | Arbitrary-sized signed integers (8-256 bits) | | `U256` | `uint256` | 256-bit unsigned integer (most common) | | `Address` | `address` | 20-byte Ethereum address | | `FixedBytes` | `bytesN` | Fixed-size byte array | | `Bytes` | `bytes` | Dynamic byte array | | `String` | `string` | Dynamic UTF-8 string | | `Vec` | `T[]` | Dynamic array | | `[T; N]` | `T[N]` | Fixed-size array | | `(T1, T2, ...)` | `(T1, T2, ...)` | Tuple types | **Important**: The SDK treats `Vec` as Solidity `uint8[]`. For Solidity `bytes`, use `alloy_primitives::Bytes`. ## Converting numeric types ### Creating integers from literals ```rust use stylus_sdk::alloy_primitives::{U256, I256, U8, I8}; // From integer literals let small: U8 = U8::from(1); let large: U256 = U256::from(255); // For signed integers let positive: I8 = I8::unchecked_from(127); let negative: I8 = I8::unchecked_from(-1); let signed_large: I256 = I256::unchecked_from(0xff_u64); ``` ### Parsing from strings ```rust use stylus_sdk::alloy_primitives::I256; // Parse decimal strings let a = I256::try_from(20003000).unwrap(); let b = "100".parse::().unwrap(); // Parse hexadecimal strings let c = "-0x138f".parse::().unwrap(); // Underscores are ignored for readability let d = "1_000_000".parse::().unwrap(); // Arithmetic works as expected let result = a * b + c - d; ``` ### Integer constants ```rust use stylus_sdk::alloy_primitives::I256; let max = I256::MAX; // Maximum value let min = I256::MIN; // Minimum value let zero = I256::ZERO; // Zero let minus_one = I256::MINUS_ONE; // -1 ``` ### Converting between integer sizes ```rust use stylus_sdk::alloy_primitives::{Uint, Signed, U256}; // Between Alloy integer types (same bit-width) let uint_value = Uint::<128, 2>::from(999); let u128_value: u128 = uint_value.try_into() .map_err(|_| "conversion error") .unwrap(); // Between different bit-widths let small = Uint::<8, 1>::from(100); let large = U256::from(small); ``` The SDK uses the `ConvertInt` trait internally to enable conversions between Alloy’s `Uint` types and Rust’s native integer types, such as `u8`, `u16`, `u32`, `u64`, and `u128`. ## Converting addresses ### Creating addresses ```rust use stylus_sdk::alloy_primitives::{Address, address}; // From a 20-byte array let addr1 = Address::from([0x11; 20]); // Using the address! macro with checksummed string let addr2 = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); // From a byte slice let bytes: [u8; 20] = [0xd8, 0xda, 0x6b, 0xf2, /* ... */]; let addr3 = Address::from(bytes); ``` ### Converting addresses to bytes ```rust use stylus_sdk::alloy_primitives::Address; let addr = address!("d8da6bf26964af9d7eed9e03e53415d37aa96045"); // Get reference to underlying bytes let bytes_ref: &[u8] = addr.as_ref(); // Use in byte concatenation let data = [addr.as_ref(), other_data].concat(); ``` ## Converting byte types ### Fixed-size bytes ```rust use stylus_sdk::alloy_primitives::FixedBytes; // Create from array let fixed = FixedBytes::<32>::new([0u8; 32]); // Create from slice let slice: &[u8] = &[1, 2, 3, 4]; let fixed = FixedBytes::<4>::from_slice(slice); // Convert to slice let bytes_ref: &[u8] = fixed.as_ref(); ``` ### Dynamic bytes ```rust use stylus_sdk::abi::Bytes; // Create from Vec let bytes = Bytes::from(vec![1, 2, 3, 4]); // Create empty let empty = Bytes::new(); // Get reference to underlying data let data: &[u8] = bytes.as_ref(); // Convert to Vec let vec: Vec = bytes.to_vec(); ``` ### Byte array conversions ```rust use stylus_sdk::alloy_primitives::U256; // Convert U256 to big-endian bytes let value = U256::from(12345); let bytes_vec: Vec = value.to_be_bytes_vec(); let bytes_array: [u8; 32] = value.to_be_bytes(); // Convert from big-endian bytes let from_slice = U256::try_from_be_slice(&bytes_vec).unwrap(); ``` ## Converting strings ```rust use alloc::string::{String, ToString}; // String conversions let rust_string = "hello".to_string(); let bytes = rust_string.as_bytes(); // For Solidity string parameters in functions // the String type is automatically handled by AbiType pub fn process_string(&self, text: String) -> String { text } ``` ## Converting collections ### Dynamic arrays (Vec) ```rust use stylus_sdk::alloy_primitives::U256; use alloc::vec::Vec; // Vec is used directly as Solidity dynamic arrays let numbers: Vec = vec![ U256::from(1), U256::from(2), U256::from(3), ]; // For Vec, note this maps to uint8[], not bytes let uint8_array: Vec = vec![1, 2, 3]; ``` ### Fixed-size arrays ```rust use stylus_sdk::alloy_primitives::U256; // Fixed arrays map directly to Solidity fixed arrays let fixed: [U256; 3] = [ U256::from(1), U256::from(2), U256::from(3), ]; // Nested arrays let nested: [[u32; 2]; 4] = [[1, 2], [3, 4], [5, 6], [7, 8]]; ``` ## ABI encoding and decoding ### Encoding types ```rust use stylus_sdk::abi::{encode, encode_params}; use stylus_sdk::alloy_primitives::{Address, U256}; use alloy_sol_types::{sol_data::*, SolType}; // Encode a single value let value = U256::from(100); let encoded = encode(&value); // Encode tuple of parameters type TransferParams = (Address, Uint<256>); let params = (address, amount); let encoded = TransferParams::abi_encode_params(¶ms); ``` ### Decoding types ```rust use stylus_sdk::abi::decode_params; use stylus_sdk::alloy_primitives::{Address, U256}; use alloy_sol_types::{sol_data::*, SolType}; // Define the expected type structure type TransferParams = (Address, Uint<256>); // Decode from bytes let decoded: (Address, U256) = TransferParams::abi_decode_params(&encoded_data) .map_err(|_| "decode error")?; ``` ### Packed encoding Packed encoding is useful for hashing and signature verification: ```rust use stylus_sdk::alloy_primitives::{Address, U256}; use alloy_sol_types::{sol_data::*, SolType}; // Method 1: Using SolType::abi_encode_packed type DataTypes = (Address, Uint<256>, String, Bytes, Uint<256>); let data = (target, value, func, bytes, timestamp); let packed = DataTypes::abi_encode_packed(&data); // Method 2: Manual concatenation let packed_manual = [ target.as_ref(), &value.to_be_bytes_vec(), func.as_bytes(), bytes.as_ref(), ×tamp.to_be_bytes_vec(), ].concat(); ``` ## Error type conversions Stylus error types can be converted using the `Into` trait: ```rust use stylus_sdk::prelude::*; sol! { error InvalidParam(); error NotFound(); } #[derive(SolidityError)] pub enum MyError { InvalidParam(InvalidParam), NotFound(NotFound), } pub fn check_value(&self, value: U256) -> Result<(), MyError> { if value == U256::ZERO { return Err(InvalidParam {}.into()); } Ok(()) } ``` ## Storage type conversions Storage types require special handling for persistence: ```rust use stylus_sdk::prelude::*; use stylus_sdk::alloy_primitives::U256; #[storage] pub struct Counter { count: StorageU256, } #[public] impl Counter { // Get value from storage pub fn get_count(&self) -> U256 { self.count.get() } // Set value in storage pub fn set_count(&mut self, value: U256) { self.count.set(value); } // Increment using arithmetic pub fn increment(&mut self) { let current = self.count.get(); self.count.set(current + U256::from(1)); } } ``` ## Best practices 1. **Use `try_from` for fallible conversions**: When converting between types where overflow is possible, use `try_from` instead of panicking conversions. 2. **Prefer native types when appropriate**: Use Rust's native `bool`, `u8`-`u128`, and `i8`-`i128` types when they match your needs exactly. They're more efficient and ergonomic. 3. **Be explicit about byte types**: Remember that `Vec` maps to `uint8[]`, not `bytes`. Use `Bytes` from `stylus_sdk::abi` for Solidity `bytes` type. 4. **Use the address! macro**: For hardcoded addresses, use the `address!` macro which performs compile-time validation and checksumming. 5. **Handle conversion errors**: Always handle potential errors from `try_from`, `try_into`, and `parse` operations rather than using unwrap in production code. 6. **Consider packed encoding for hashing**: When preparing data for hashing or signature verification, packed encoding produces more compact representations. ## Reference For complete implementation details, see: * `[/stylus-sdk/src/abi/mod.rs](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/stylus-sdk/src/abi/mod.rs)`: AbiType trait and encoding functions * `[/stylus-sdk/src/abi/ints.rs](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/stylus-sdk/src/abi/ints.rs)`: Integer type conversions * `[/stylus-sdk/src/abi/impls.rs](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/stylus-sdk/src/abi/impls.rs)`: Implementations for standard types * `[/stylus-sdk/src/storage/traits.rs](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/stylus-sdk/src/abi/traits.rs)`: Storage type conversion traits ### External references Stylus uses the [Alloy](https://github.com/alloy-rs) library for EVM primitives, so primitive conversion rules follow Alloy's conventions: * [`alloy-primitives` docs.rs](https://docs.rs/alloy-primitives/latest/alloy_primitives/) — full API reference for `U256`, `Address`, `FixedBytes`, and related types. * [`alloy-rs/examples` — `big-numbers/conversion.rs`](https://github.com/alloy-rs/examples/blob/main/examples/big-numbers/examples/conversion.rs) — worked examples converting between `U256` and Rust native integer types. --- > For a complete page index, fetch # Stylus primitives The Stylus SDK provides full support for Rust primitive types with automatic ABI encoding/decoding and Solidity type mappings. These primitives can be used in contract method signatures, storage, and as function parameters. ## Boolean (`bool`) Booleans in Rust map directly to Solidity's `bool` type. ### Usage in contract methods ```rust use stylus_sdk::prelude::*; #[public] impl MyContract { pub fn is_valid(&self) -> bool { true } pub fn toggle(&mut self, flag: bool) { // Use the boolean value if flag { // Do something } } } ``` ### Solidity mapping * **Rust type**: `bool` * **Solidity type**: `bool` * **Storage size**: 1 byte * **ABI signature**: `"bool"` ## Integers The Stylus SDK supports both signed and unsigned integers with various bit sizes. All integer types from Rust's standard library and alloy-primitives are supported. ### Unsigned integers #### Standard Rust unsigned integers ```rust use stylus_sdk::prelude::*; #[public] impl MyContract { // u8: 8-bit unsigned integer pub fn get_byte(&self) -> u8 { 255 } // u16: 16-bit unsigned integer pub fn get_short(&self) -> u16 { 65535 } // u32: 32-bit unsigned integer pub fn get_int(&self) -> u32 { 4294967295 } // u64: 64-bit unsigned integer pub fn get_long(&self) -> u64 { 18446744073709551615 } // u128: 128-bit unsigned integer pub fn get_u128(&self) -> u128 { 340282366920938463463374607431768211455 } } ``` #### Alloy unsigned integers For larger integers and full compatibility with Solidity's uint types, use `alloy_primitives::Uint`: ```rust use alloy_primitives::{U256, Uint}; use stylus_sdk::prelude::*; #[public] impl MyContract { // U256: 256-bit unsigned integer (most common in Solidity) pub fn get_balance(&self) -> U256 { U256::from(1000000) } // Any bit size from 8 to 256 (in multiples of 8) pub fn get_u160(&self) -> Uint<160, 3> { Uint::<160, 3>::from(999) } pub fn get_u96(&self) -> Uint<96, 2> { Uint::<96, 2>::from(123456) } } ``` ### Signed integers #### Standard Rust signed integers ```rust use stylus_sdk::prelude::*; #[public] impl MyContract { // i8: 8-bit signed integer pub fn get_signed_byte(&self) -> i8 { -128 } // i16: 16-bit signed integer pub fn get_signed_short(&self) -> i16 { -32768 } // i32: 32-bit signed integer pub fn get_signed_int(&self) -> i32 { -2147483648 } // i64: 64-bit signed integer pub fn get_signed_long(&self) -> i64 { -9223372036854775808 } // i128: 128-bit signed integer pub fn get_signed_i128(&self) -> i128 { -170141183460469231731687303715884105728 } } ``` #### Alloy signed integers ```rust use alloy_primitives::{Signed, I256}; use stylus_sdk::prelude::*; #[public] impl MyContract { // I256: 256-bit signed integer pub fn get_signed_balance(&self) -> I256 { I256::try_from(-1000).unwrap() } // Any bit size from 8 to 256 (in multiples of 8) pub fn get_i160(&self) -> Signed<160, 3> { Signed::<160, 3>::try_from(-999).unwrap() } } ``` ### Integer type mappings | Rust Type | Solidity Type | Bit Size | ABI Signature | | ------------------------- | ------------- | -------- | ------------- | | `u8` | `uint8` | 8 bits | `"uint8"` | | `u16` | `uint16` | 16 bits | `"uint16"` | | `u32` | `uint32` | 32 bits | `"uint32"` | | `u64` | `uint64` | 64 bits | `"uint64"` | | `u128` | `uint128` | 128 bits | `"uint128"` | | `Uint<160, 3>` | `uint160` | 160 bits | `"uint160"` | | `U256` / `Uint<256, 4>` | `uint256` | 256 bits | `"uint256"` | | `i8` | `int8` | 8 bits | `"int8"` | | `i16` | `int16` | 16 bits | `"int16"` | | `i32` | `int32` | 32 bits | `"int32"` | | `i64` | `int64` | 64 bits | `"int64"` | | `i128` | `int128` | 128 bits | `"int128"` | | `Signed<160, 3>` | `int160` | 160 bits | `"int160"` | | `I256` / `Signed<256, 4>` | `int256` | 256 bits | `"int256"` | **Note**: All Solidity uint/int types from `uint8`/`int8` to `uint256`/`int256` (in 8-bit increments) are supported through `Uint` and `Signed`. ## Address Ethereum addresses are represented by the `Address` type from `alloy_primitives`. ### Basic usage ```rust use alloy_primitives::Address; use stylus_sdk::prelude::*; #[public] impl MyContract { pub fn get_owner(&self) -> Address { Address::ZERO } pub fn is_owner(&self, account: Address) -> bool { account == self.vm().msg_sender() } pub fn transfer_ownership(&mut self, new_owner: Address) { // Address validation and logic if new_owner == Address::ZERO { // Handle error } } } ``` ### Address constants ```rust use alloy_primitives::Address; // Zero address (0x0000000000000000000000000000000000000000) let zero = Address::ZERO; // Parse from string let addr = Address::parse_checksummed("0x1234567890123456789012345678901234567890", None).unwrap(); // Create from bytes let bytes: [u8; 20] = [0; 20]; let addr = Address::from(bytes); ``` ### Solidity mapping * **Rust type**: `Address` (from `alloy_primitives`) * **Solidity type**: `address` * **Storage size**: 20 bytes (160 bits) * **ABI signature**: `"address"` ## String Rust `String` types map to Solidity `string` type. ### String literals ```rust use stylus_sdk::prelude::*; use alloc::string::String; #[public] impl MyContract { pub fn get_name(&self) -> String { String::from("MyToken") } pub fn greet(&self, name: String) -> String { format!("Hello, {}!", name) } } ``` ### Solidity mapping * **Rust type**: `String` (from `alloc::string`) * **Solidity type**: `string` * **Storage**: Dynamic (heap-allocated) * **ABI signature**: `"string"` * **ABI export**: * As argument: `"string calldata"` * As return: `"string memory"` **Note**: Strings in Solidity are UTF-8 encoded byte arrays. When using strings in Stylus: * Use `alloc::string::String` for owned strings * Strings are dynamically sized and stored in memory/calldata * For storage, use `StorageString` (see [Storage Types](/stylus/fundamentals/data-types/storage.md)) ## Bytes The SDK provides two types for working with byte data: ### 1. Dynamic bytes (`Bytes`) For variable-length byte arrays (Solidity `bytes`): ```rust use alloy_primitives::Bytes; use stylus_sdk::prelude::*; #[public] impl MyContract { pub fn get_data(&self) -> Bytes { Bytes::from(vec![1, 2, 3, 4]) } pub fn process_data(&mut self, data: Bytes) -> usize { data.len() } } ``` ### 2. Fixed bytes (`FixedBytes`) For fixed-length byte arrays (Solidity `bytesN`): ```rust use alloy_primitives::FixedBytes; use stylus_sdk::prelude::*; #[public] impl MyContract { // bytes32 (common for hashes) pub fn get_hash(&self) -> FixedBytes<32> { FixedBytes::<32>::ZERO } // bytes2 pub fn get_signature(&self) -> FixedBytes<2> { FixedBytes::new([0x12, 0x34]) } // Any size from 1 to 32 pub fn get_bytes8(&self) -> FixedBytes<8> { FixedBytes::<8>::from([1, 2, 3, 4, 5, 6, 7, 8]) } } ``` ### Common FixedBytes aliases ```rust use alloy_primitives::{B256, B160, B128}; // B256 is FixedBytes<32> (bytes32) let hash: B256 = B256::ZERO; // B160 is FixedBytes<20> (bytes20) let data: B160 = B160::ZERO; // B128 is FixedBytes<16> (bytes16) let value: B128 = B128::ZERO; ``` ### Bytes type mappings | Rust Type | Solidity Type | Description | | ------------------------- | ------------- | -------------------------------- | | `Bytes` | `bytes` | Dynamic byte array | | `Vec` | `uint8[]` | Array of bytes (NOT `bytes`!) | | `FixedBytes` | `bytesN` | Fixed-size byte array (N = 1-32) | | `B256` / `FixedBytes<32>` | `bytes32` | 32-byte array (hashes) | | `B160` / `FixedBytes<20>` | `bytes20` | 20-byte array | | `B128` / `FixedBytes<16>` | `bytes16` | 16-byte array | **Important Distinction**: * `Vec` maps to Solidity `uint8[]` (array of unsigned integers) * `Bytes` maps to Solidity `bytes` (dynamic byte array) * For Solidity `bytes`, always use `alloy_primitives::Bytes` ### Bytes ABI encoding ```rust // Bytes type // ABI signature: "bytes" // As argument: "bytes calldata" // As return: "bytes memory" // FixedBytes type // ABI signature: "bytesN" where N is 1-32 // Example: FixedBytes<32> -> "bytes32" ``` ## Hex string literals When working with hex data, you can use hex literals: ```rust use alloy_primitives::{hex, Address, FixedBytes, Bytes}; // Hex bytes let data = hex!("deadbeef"); // Address from hex let addr = Address::from(hex!("1234567890123456789012345678901234567890")); // FixedBytes from hex let hash = FixedBytes::<32>::from(hex!( "0000000000000000000000000000000000000000000000000000000000000000" )); // Dynamic Bytes from hex let bytes = Bytes::from(hex!("aabbccdd")); ``` ## Complete example Here's a comprehensive example showing all primitive types: ```rust #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; use alloc::string::String; use alloy_primitives::{Address, Bytes, FixedBytes, U256}; use stylus_sdk::prelude::*; sol_storage! { #[entrypoint] pub struct PrimitiveExample { bool initialized; uint256 count; address owner; } } #[public] impl PrimitiveExample { // Boolean pub fn is_initialized(&self) -> bool { self.initialized.get() } // Unsigned integers (native Rust) pub fn get_u8(&self) -> u8 { 255 } pub fn get_u256(&self) -> U256 { self.count.get() } // Signed integers pub fn get_signed(&self) -> i32 { -42 } // Address pub fn get_owner(&self) -> Address { self.owner.get() } pub fn set_owner(&mut self, new_owner: Address) { self.owner.set(new_owner); } // String pub fn get_name(&self) -> String { String::from("PrimitiveExample") } // Dynamic bytes pub fn get_data(&self) -> Bytes { Bytes::from(vec![1, 2, 3, 4]) } // Fixed bytes pub fn get_hash(&self) -> FixedBytes<32> { FixedBytes::<32>::ZERO } // Multiple parameters pub fn complex_function( &mut self, flag: bool, amount: U256, recipient: Address, data: Bytes ) -> bool { // Function logic true } } ``` ## Best practices 1. **Use U256 for token amounts**: Solidity commonly uses `uint256` for token balances and amounts. ```rust use alloy_primitives::U256; pub fn transfer(&mut self, amount: U256) { // amount is uint256 in Solidity } ``` 2. **Use Address for account addresses**: Always use `alloy_primitives::Address` for Ethereum addresses. ```rust use alloy_primitives::Address; pub fn get_balance(&self, account: Address) -> U256 { // Query balance } ``` 3. **Use Bytes for dynamic byte data**: For Solidity `bytes`, use `alloy_primitives::Bytes`, not `Vec`. ```rust use alloy_primitives::Bytes; pub fn process(&self, data: Bytes) { // data maps to Solidity bytes } ``` 4. **Use FixedBytes for hashes and signatures**: For fixed-size byte data like hashes. ```rust use alloy_primitives::FixedBytes; pub fn verify(&self, hash: FixedBytes<32>) -> bool { // hash maps to Solidity bytes32 true } ``` 5. **Check for zero addresses**: Always validate addresses before use. ```rust use alloy_primitives::Address; pub fn set_admin(&mut self, admin: Address) { if admin == Address::ZERO { // Handle error } } ``` ## Type conversion ### Between integer types ```rust use alloy_primitives::U256; // Native to U256 let amount: u64 = 1000; let big_amount = U256::from(amount); // U256 to native (with bounds checking) let big_value = U256::from(1000); let small_value: u64 = big_value.to::(); ``` ### Address conversions ```rust use alloy_primitives::Address; // From bytes let bytes: [u8; 20] = [0; 20]; let addr = Address::from(bytes); // To bytes let addr = Address::ZERO; let bytes: [u8; 20] = addr.into(); ``` ## See also * [Compound Types](/stylus/fundamentals/data-types/compound-types.md) - Arrays, tuples, structs * [Storage Types](/stylus/fundamentals/data-types/storage.md) - Persistent storage for primitives * [Type Conversions](/stylus/fundamentals/data-types/conversions-between-types.md) - Converting between types --- > For a complete page index, fetch # Stylus Rust SDK storage Persistent storage in Stylus contracts provides access to the EVM State Trie, the same key-value storage used by Solidity contracts. The SDK provides type-safe storage access through dedicated storage types that prevent aliasing errors at compile time using Rust's borrow checker. ## Overview Stylus contracts share the same persistent storage as Solidity contracts: * Both Stylus and Solidity access the same EVM State Trie * Storage is fully interoperable between Stylus and Solidity contracts * Stylus provides compile-time safety through Rust's type system * Storage operations are cached for gas efficiency The Stylus SDK provides a comprehensive hierarchy of storage types: ![Stylus storage types](/img/stylus-storage-types-hierarchy.svg) *Storage type hierarchy showing primitives, collections, and custom struct options.* ### Storage declaration Use the `sol_storage!` macro to define contract storage with Solidity-compatible layout: ```rust use stylus_sdk::prelude::*; sol_storage! { #[entrypoint] pub struct MyContract { uint256 count; address owner; bool initialized; } } ``` Alternatively, use the `#[storage]` attribute for Rust-style declarations: ```rust use stylus_sdk::prelude::*; use stylus_sdk::storage::*; #[storage] #[entrypoint] pub struct MyContract { count: StorageU256, owner: StorageAddress, initialized: StorageBool, } ``` ## Storage primitives Storage primitives are persistent versions of basic types. ### Boolean storage (`StorageBool`) Store boolean values in persistent storage: ```rust use stylus_sdk::prelude::*; sol_storage! { #[entrypoint] pub struct Contract { bool is_initialized; bool is_paused; } } #[public] impl Contract { pub fn initialize(&mut self) { self.is_initialized.set(true); } pub fn is_initialized(&self) -> bool { self.is_initialized.get() } pub fn toggle_pause(&mut self) { let current = self.is_paused.get(); self.is_paused.set(!current); } } ``` ### Integer storage Store unsigned and signed integers with various bit sizes: ```rust use stylus_sdk::prelude::*; use alloy_primitives::U256; sol_storage! { #[entrypoint] pub struct Counter { uint256 count; uint64 timestamp; int256 balance; } } #[public] impl Counter { // Unsigned integer operations pub fn increment(&mut self) { let current = self.count.get(); self.count.set(current + U256::from(1)); } pub fn add(&mut self, value: U256) { let current = self.count.get(); self.count.set(current + value); } pub fn get_count(&self) -> U256 { self.count.get() } // Timestamp storage pub fn set_timestamp(&mut self, ts: u64) { self.timestamp.set(ts); } } ``` #### Available storage integer types | Storage Type | Primitive Type | Bit Size | Solidity Type | | ------------- | -------------- | -------- | ------------- | | `StorageU8` | `U8` | 8 bits | `uint8` | | `StorageU16` | `U16` | 16 bits | `uint16` | | `StorageU32` | `U32` | 32 bits | `uint32` | | `StorageU64` | `U64` | 64 bits | `uint64` | | `StorageU128` | `U128` | 128 bits | `uint128` | | `StorageU256` | `U256` | 256 bits | `uint256` | | `StorageI8` | `I8` | 8 bits | `int8` | | `StorageI16` | `I16` | 16 bits | `int16` | | `StorageI32` | `I32` | 32 bits | `int32` | | `StorageI64` | `I64` | 64 bits | `int64` | | `StorageI128` | `I128` | 128 bits | `int128` | | `StorageI256` | `I256` | 256 bits | `int256` | #### Integer update operations `StorageUint` types provide convenient update methods: ```rust use stylus_sdk::prelude::*; use alloy_primitives::U256; sol_storage! { #[entrypoint] pub struct Contract { uint256 balance; } } #[public] impl Contract { // Wrapping operations (overflow wraps around) pub fn add_wrapping(&mut self, value: U256) -> U256 { self.balance.update_wrap_add(value) } pub fn sub_wrapping(&mut self, value: U256) -> U256 { self.balance.update_wrap_sub(value) } pub fn mul_wrapping(&mut self, value: U256) -> U256 { self.balance.update_wrap_mul(value) } // Checked operations (return None on overflow) pub fn add_checked(&mut self, value: U256) -> Option { self.balance.update_check_add(value) } pub fn sub_checked(&mut self, value: U256) -> Option { self.balance.update_check_sub(value) } } ``` ### Address storage (`StorageAddress`) Store Ethereum addresses: ```rust use stylus_sdk::prelude::*; use alloy_primitives::Address; sol_storage! { #[entrypoint] pub struct Ownership { address owner; address pending_owner; } } #[public] impl Ownership { pub fn get_owner(&self) -> Address { self.owner.get() } pub fn transfer_ownership(&mut self, new_owner: Address) { // Validate address if new_owner == Address::ZERO { // Handle error return; } let current_owner = self.owner.get(); if self.vm().msg_sender() != current_owner { // Not authorized return; } self.pending_owner.set(new_owner); } pub fn accept_ownership(&mut self) { let caller = self.vm().msg_sender(); if caller != self.pending_owner.get() { return; } self.owner.set(caller); self.pending_owner.set(Address::ZERO); } } ``` ### Fixed bytes storage Store fixed-size byte arrays: ```rust use stylus_sdk::prelude::*; use alloy_primitives::FixedBytes; sol_storage! { #[entrypoint] pub struct Hashes { bytes32 merkle_root; bytes32 commitment; bytes4 selector; } } #[public] impl Hashes { pub fn set_merkle_root(&mut self, root: FixedBytes<32>) { self.merkle_root.set(root); } pub fn get_merkle_root(&self) -> FixedBytes<32> { self.merkle_root.get() } pub fn verify_hash(&self, proof: FixedBytes<32>) -> bool { self.merkle_root.get() == proof } } ``` #### Available fixed-byte storage types | Storage Type | Bytes | Bits | Solidity Type | | ------------- | ----- | -------- | ------------- | | `StorageB8` | 1 | 8 bits | `bytes1` | | `StorageB16` | 2 | 16 bits | `bytes2` | | `StorageB32` | 4 | 32 bits | `bytes4` | | `StorageB64` | 8 | 64 bits | `bytes8` | | `StorageB128` | 16 | 128 bits | `bytes16` | | `StorageB160` | 20 | 160 bits | `bytes20` | | `StorageB224` | 28 | 224 bits | `bytes28` | | `StorageB256` | 32 | 256 bits | `bytes32` | ## Storage collections Storage collections provide persistent arrays, vectors, and maps. \`.set()\` vs \`.setter()\` — when to use each * **Primitives** (`StorageUint`, `StorageAddress`, `StorageBool`, …): call `.set(value)` directly to write. * **Collections** (`StorageMap`, `StorageVec`, `StorageArray`): call `.setter(key)` to obtain a `StorageGuardMut` handle, then `.set(value)`, `.get()`, or `.erase()` on it. The guard enforces Rust's borrow rules and prevents storage aliasing at compile time. * If you both read and write the same element, bind the `.setter(...)` result to a `let` so you avoid two slot lookups. ### StorageVec (dynamic array) Dynamic arrays that can grow and shrink: ```rust use stylus_sdk::prelude::*; use alloy_primitives::{Address, U256}; sol_storage! { #[entrypoint] pub struct TokenList { address[] holders; uint256[] balances; } } #[public] impl TokenList { // Add element pub fn add_holder(&mut self, holder: Address) { self.holders.push(holder); } // Get element pub fn get_holder(&self, index: U256) -> Address { self.holders.get(index).unwrap() } // Get length pub fn holder_count(&self) -> U256 { U256::from(self.holders.len()) } // Set element pub fn set_balance(&mut self, index: U256, balance: U256) { self.balances.setter(index).unwrap().set(balance); } // Iterate over elements pub fn total_balance(&self) -> U256 { let mut total = U256::ZERO; for i in 0..self.balances.len() { total += self.balances.get(U256::from(i)).unwrap(); } total } // Remove element (erase to zero) pub fn remove_holder(&mut self, index: U256) { self.holders.setter(index).unwrap().erase(); } // Clear all elements pub fn clear_holders(&mut self) { self.holders.erase(); } } ``` #### StorageVec methods ```rust // Length operations fn len(&self) -> usize fn is_empty(&self) -> bool // Access operations fn get(&self, index: impl TryInto) -> Option fn getter(&self, index: impl TryInto) -> Option> fn setter(&mut self, index: impl TryInto) -> Option> // Mutation operations fn push(&mut self, value: T) fn grow(&mut self) -> StorageGuardMut<'_, T> // Add new element and return mutable reference fn erase(&mut self) // Clear all elements ``` ### StorageArray (fixed array) Fixed-size arrays with compile-time known length: ```rust use stylus_sdk::prelude::*; use alloy_primitives::U256; sol_storage! { #[entrypoint] pub struct FixedData { uint256[10] values; address[5] admins; } } #[public] impl FixedData { // Get element pub fn get_value(&self, index: U256) -> U256 { self.values.get(index).unwrap() } // Set element pub fn set_value(&mut self, index: U256, value: U256) { self.values.setter(index).unwrap().set(value); } // Get array length (compile-time constant) pub fn array_size(&self) -> U256 { U256::from(self.values.len()) } // Iterate over array pub fn sum_values(&self) -> U256 { let mut sum = U256::ZERO; for i in 0..self.values.len() { sum += self.values.get(U256::from(i)).unwrap(); } sum } } ``` ### StorageMap (mapping) Key-value storage, equivalent to Solidity `mapping`: ```rust use stylus_sdk::prelude::*; use alloy_primitives::{Address, U256}; sol_storage! { #[entrypoint] pub struct Token { mapping(address => uint256) balances; mapping(address => mapping(address => uint256)) allowances; } } #[public] impl Token { // Get value (returns zero if not set) pub fn balance_of(&self, account: Address) -> U256 { self.balances.get(account) } // Set value pub fn set_balance(&mut self, account: Address, amount: U256) { self.balances.setter(account).set(amount); } // Insert value (same as set) pub fn mint(&mut self, account: Address, amount: U256) { let current = self.balances.get(account); self.balances.insert(account, current + amount); } // Delete value (reset to zero) pub fn burn(&mut self, account: Address, amount: U256) { let current = self.balances.get(account); if current >= amount { self.balances.setter(account).set(current - amount); } } // Nested mapping pub fn allowance(&self, owner: Address, spender: Address) -> U256 { self.allowances.get(owner).get(spender) } pub fn approve(&mut self, spender: Address, amount: U256) { let owner = self.vm().msg_sender(); self.allowances .setter(owner) .setter(spender) .set(amount); } } ``` #### StorageMap methods ```rust // Read operations fn get(&self, key: K) -> V // Returns zero-value if not present fn getter(&self, key: K) -> StorageGuard<'_, V> // Write operations fn setter(&mut self, key: K) -> StorageGuardMut<'_, V> fn insert(&mut self, key: K, value: V) fn replace(&mut self, key: K, value: V) -> V // Returns old value fn take(&mut self, key: K) -> V // Returns value and deletes fn delete(&mut self, key: K) // Erases entry ``` #### Supported map key types Any type implementing `StorageKey` can be used as a map key: * `Address` * `U256`, `U160`, and other `Uint` types * `FixedBytes` * `Signed` types * `bool` ```rust use stylus_sdk::prelude::*; use alloy_primitives::{Address, U256, FixedBytes}; sol_storage! { #[entrypoint] pub struct MultiMap { mapping(address => uint256) by_address; mapping(uint256 => address) by_id; mapping(bytes32 => bool) by_hash; mapping(bool => uint256) by_flag; } } ``` ### StorageString and StorageBytes Dynamic string and byte storage: ```rust use stylus_sdk::prelude::*; use alloc::string::String; sol_storage! { #[entrypoint] pub struct Metadata { string name; string symbol; bytes data; } } #[public] impl Metadata { // String operations pub fn get_name(&self) -> String { self.name.get_string() } pub fn set_name(&mut self, name: String) { self.name.set_str(name); } pub fn name_length(&self) -> usize { self.name.len() } pub fn clear_name(&mut self) { self.name.erase(); } // Bytes operations pub fn get_data(&self) -> Vec { self.data.get_bytes() } pub fn set_data(&mut self, data: Vec) { self.data.set_bytes(data); } pub fn data_length(&self) -> usize { self.data.len() } } ``` ## Storage structs Define custom storage types with nested structures: ```rust use stylus_sdk::prelude::*; use alloy_primitives::{Address, U256}; // Storage struct definition #[storage] pub struct UserInfo { balance: StorageU256, is_active: StorageBool, timestamp: StorageU64, } sol_storage! { #[entrypoint] pub struct UserRegistry { mapping(address => UserInfo) users; uint256 total_users; } } #[public] impl UserRegistry { pub fn register_user(&mut self, user: Address) { let mut user_info = self.users.setter(user); user_info.balance.set(U256::ZERO); user_info.is_active.set(true); user_info.timestamp.set(self.vm().block_timestamp()); let count = self.total_users.get(); self.total_users.set(count + U256::from(1)); } pub fn get_balance(&self, user: Address) -> U256 { self.users.get(user).balance.get() } pub fn update_balance(&mut self, user: Address, amount: U256) { self.users.setter(user).balance.set(amount); } pub fn is_active(&self, user: Address) -> bool { self.users.get(user).is_active.get() } } ``` ### Nested storage structs ```rust use stylus_sdk::prelude::*; use alloy_primitives::Address; #[storage] pub struct Dog { name: StorageString, breed: StorageString, } #[storage] pub struct User { name: StorageString, dogs: StorageVec, } sol_storage! { #[entrypoint] pub struct Registry { mapping(address => User) users; } } #[public] impl Registry { pub fn add_dog(&mut self, owner: Address, name: String, breed: String) { let mut user = self.users.setter(owner); let mut dog = user.dogs.grow(); dog.name.set_str(name); dog.breed.set_str(breed); } pub fn get_dog_count(&self, owner: Address) -> usize { self.users.get(owner).dogs.len() } pub fn get_dog_name(&self, owner: Address, index: usize) -> String { self.users .get(owner) .dogs .get(index) .unwrap() .name .get_string() } } ``` ## Storage patterns ### Initialization pattern ```rust use stylus_sdk::prelude::*; use alloy_primitives::{Address, U256}; sol_storage! { #[entrypoint] pub struct Contract { bool initialized; address owner; uint256 value; } } #[public] impl Contract { #[constructor] pub fn constructor(&mut self, initial_value: U256) { self.owner.set(self.vm().msg_sender()); self.value.set(initial_value); self.initialized.set(true); } fn only_initialized(&self) { if !self.initialized.get() { // Revert: not initialized } } pub fn get_value(&self) -> U256 { self.only_initialized(); self.value.get() } } ``` ### Counter pattern ```rust use stylus_sdk::prelude::*; use alloy_primitives::U256; sol_storage! { #[entrypoint] pub struct Counter { uint256 count; mapping(address => uint256) user_counts; } } #[public] impl Counter { pub fn increment(&mut self) { let current = self.count.get(); self.count.set(current + U256::from(1)); } pub fn increment_by(&mut self, amount: U256) { let current = self.count.get(); self.count.set(current + amount); } pub fn increment_user(&mut self) { let user = self.vm().msg_sender(); let current = self.user_counts.get(user); self.user_counts.insert(user, current + U256::from(1)); } pub fn get_count(&self) -> U256 { self.count.get() } pub fn get_user_count(&self, user: Address) -> U256 { self.user_counts.get(user) } } ``` ### Access control pattern ```rust use stylus_sdk::prelude::*; use alloy_primitives::Address; sol_storage! { #[entrypoint] pub struct AccessControl { address owner; mapping(address => bool) admins; mapping(address => bool) users; } } #[public] impl AccessControl { #[constructor] pub fn constructor(&mut self) { let sender = self.vm().msg_sender(); self.owner.set(sender); self.admins.insert(sender, true); } fn only_owner(&self) { if self.vm().msg_sender() != self.owner.get() { // Revert: not owner } } fn only_admin(&self) { let sender = self.vm().msg_sender(); if !self.admins.get(sender) { // Revert: not admin } } pub fn add_admin(&mut self, admin: Address) { self.only_owner(); self.admins.insert(admin, true); } pub fn remove_admin(&mut self, admin: Address) { self.only_owner(); self.admins.delete(admin); } pub fn add_user(&mut self, user: Address) { self.only_admin(); self.users.insert(user, true); } pub fn is_admin(&self, account: Address) -> bool { self.admins.get(account) } pub fn is_user(&self, account: Address) -> bool { self.users.get(account) } } ``` ### Registry pattern ```rust use stylus_sdk::prelude::*; use alloy_primitives::{Address, U256}; #[storage] pub struct Record { owner: StorageAddress, created_at: StorageU64, updated_at: StorageU64, active: StorageBool, } sol_storage! { #[entrypoint] pub struct Registry { mapping(bytes32 => Record) records; mapping(address => bytes32[]) user_records; uint256 total_records; } } #[public] impl Registry { pub fn create_record(&mut self, id: FixedBytes<32>) { let now = self.vm().block_timestamp(); let owner = self.vm().msg_sender(); let mut record = self.records.setter(id); record.owner.set(owner); record.created_at.set(now); record.updated_at.set(now); record.active.set(true); // Add to user's record list self.user_records.setter(owner).push(id); // Increment total let total = self.total_records.get(); self.total_records.set(total + U256::from(1)); } pub fn get_record_owner(&self, id: FixedBytes<32>) -> Address { self.records.get(id).owner.get() } pub fn is_active(&self, id: FixedBytes<32>) -> bool { self.records.get(id).active.get() } pub fn deactivate(&mut self, id: FixedBytes<32>) { let owner = self.records.get(id).owner.get(); if owner != self.vm().msg_sender() { // Not authorized return; } self.records.setter(id).active.set(false); } } ``` ## Best practices ### 1. Use appropriate storage types ```rust // Good: Use StorageU256 for counters sol_storage! { pub struct Counter { uint256 count; } } // Good: Use StorageMap for lookups sol_storage! { pub struct Balances { mapping(address => uint256) balances; } } // Good: Use StorageVec for dynamic lists sol_storage! { pub struct Users { address[] user_list; } } ``` ### 2. Minimize storage operations ```rust // Bad: Multiple storage reads pub fn bad_example(&self) -> U256 { let a = self.value.get(); let b = self.value.get(); // Unnecessary read a + b } // Good: Single storage read pub fn good_example(&self) -> U256 { let value = self.value.get(); value + value } ``` ### 3. Use batch operations ```rust // Good: Batch updates in a single transaction pub fn update_multiple(&mut self, values: Vec) { for (i, value) in values.iter().enumerate() { self.data.setter(U256::from(i)).unwrap().set(*value); } } ``` ### 4. Check before deleting ```rust // Good: Verify before deletion pub fn remove_user(&mut self, user: Address) { if self.users.get(user) { self.users.delete(user); // Update related storage } } ``` ### 5. Use erase for gas refunds ```rust // Good: Clear storage for gas refunds pub fn clear_data(&mut self) { self.data.erase(); // Refunds gas } ``` ## Storage slots and layout Stylus uses the same storage layout as Solidity: * Each storage slot is **32 bytes** (256 bits) * Variables are **packed** when possible to save space * Arrays and mappings use **computed slots** via hashing ![Stylus storage slots](/img/stylus-storage-slots-layout.png) *Storage slot layout showing how variables are packed into 32-byte slots and how mapping values are computed using keccak256 hashing.* ```rust sol_storage! { pub struct Packed { uint128 a; // Slot 0 (first 16 bytes) uint128 b; // Slot 0 (last 16 bytes) uint256 c; // Slot 1 (full 32 bytes) bool d; // Slot 2 (1 byte) address e; // Slot 2 (20 bytes, packed with d) } } ``` ### Custom storage slots You can specify custom storage slots for specific use cases: ```rust use stylus_sdk::prelude::*; use alloy_primitives::U256; #[storage] #[entrypoint] pub struct CustomSlots { // Default slot allocation value: StorageU256, // Custom slot (advanced usage) // Note: Requires manual slot management } ``` ## Complete example Here's a comprehensive example demonstrating various storage types: ```rust #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; use alloc::string::String; use alloy_primitives::{Address, FixedBytes, U256}; use stylus_sdk::prelude::*; #[storage] pub struct TokenMetadata { name: StorageString, symbol: StorageString, decimals: StorageU8, } sol_storage! { #[entrypoint] pub struct Token { // Primitives uint256 total_supply; bool paused; address owner; // Collections mapping(address => uint256) balances; mapping(address => mapping(address => uint256)) allowances; address[] holders; // Nested struct TokenMetadata metadata; } } #[public] impl Token { #[constructor] pub fn constructor(&mut self, name: String, symbol: String) { self.owner.set(self.vm().msg_sender()); self.metadata.name.set_str(name); self.metadata.symbol.set_str(symbol); self.metadata.decimals.set(18); self.paused.set(false); } pub fn total_supply(&self) -> U256 { self.total_supply.get() } pub fn balance_of(&self, account: Address) -> U256 { self.balances.get(account) } pub fn transfer(&mut self, to: Address, amount: U256) -> bool { if self.paused.get() { return false; } let from = self.vm().msg_sender(); let from_balance = self.balances.get(from); if from_balance < amount { return false; } self.balances.insert(from, from_balance - amount); let to_balance = self.balances.get(to); self.balances.insert(to, to_balance + amount); true } pub fn approve(&mut self, spender: Address, amount: U256) -> bool { let owner = self.vm().msg_sender(); self.allowances.setter(owner).insert(spender, amount); true } pub fn allowance(&self, owner: Address, spender: Address) -> U256 { self.allowances.get(owner).get(spender) } pub fn pause(&mut self) { if self.vm().msg_sender() != self.owner.get() { return; } self.paused.set(true); } pub fn unpause(&mut self) { if self.vm().msg_sender() != self.owner.get() { return; } self.paused.set(false); } pub fn name(&self) -> String { self.metadata.name.get_string() } pub fn symbol(&self) -> String { self.metadata.symbol.get_string() } pub fn decimals(&self) -> u8 { self.metadata.decimals.get() } } ``` ## See also * [Primitives](/stylus/fundamentals/data-types/primitives.md): Basic types used in storage * [Compound Types](/stylus/fundamentals/data-types/compound-types.md): Complex types in storage * [Type Conversions](/stylus/fundamentals/data-types/conversions-between-types.md): Converting between types --- > For a complete page index, fetch # Global variables and functions Stylus contracts access blockchain context and utilities through the VM (Virtual Machine) interface via `self.vm()`. This provides access to message context, block information, transaction details, account data, cryptographic functions, and gas metering. ## Accessing the VM All public contract methods have access to the VM context through `self.vm()`: ```rust use stylus_sdk::prelude::*; use alloy_primitives::{Address, U256}; #[public] impl MyContract { pub fn get_context_info(&self) -> (Address, U256, u64) { let vm = self.vm(); ( vm.msg_sender(), // Caller's address vm.msg_value(), // ETH sent with call vm.block_number(), // Current block number ) } } ``` The VM provides methods organized into several categories: ## Message Context (`msg`) Methods for accessing information about the current call. ### `msg_sender()` Gets the address of the account that called the program. ```rust pub fn msg_sender(&self) -> Address ``` **Equivalent to**: Solidity's `msg.sender` **Example:** ```rust #[public] impl Token { pub fn transfer(&mut self, to: Address, amount: U256) -> bool { let from = self.vm().msg_sender(); // Transfer from the caller to recipient self._transfer(from, to, amount) } } ``` **Important Notes:** * For normal L2-to-L2 transactions, behaves like EVM's `CALLER` opcode * For L1-to-L2 retryable ticket transactions, the top-level sender's address will be [aliased](/how-arbitrum-works/deep-dives/l1-to-l2-messaging.md#address-aliasing) * In delegate calls, returns the original caller (not the delegating contract) ### `msg_value()` Gets the ETH value in wei sent to the program. ```rust pub fn msg_value(&self) -> U256 ``` **Equivalent to**: Solidity's `msg.value` **Example:** ```rust #[public] impl PaymentContract { #[payable] pub fn deposit(&mut self) -> U256 { let amount = self.vm().msg_value(); let sender = self.vm().msg_sender(); let balance = self.balances.get(sender); self.balances.setter(sender).set(balance + amount); amount } } ``` **Note:** Only functions marked with `#[payable]` can receive ETH. Non-payable functions will revert if `msg_value() > 0`. ### `msg_reentrant()` Checks whether the current call is reentrant. ```rust pub fn msg_reentrant(&self) -> bool ``` **Example:** ```rust #[public] impl Vault { pub fn withdraw(&mut self, amount: U256) { if self.vm().msg_reentrant() { // Handle reentrancy panic!("Reentrant call detected"); } // Withdrawal logic... } } ``` **Note:** By default, Stylus contracts prevent reentrancy unless the `reentrant` feature is enabled. ## Transaction Context (`tx`) Methods for accessing information about the current transaction. ### `tx_origin()` Gets the top-level sender of the transaction. ```rust pub fn tx_origin(&self) -> Address ``` **Equivalent to**: Solidity's `tx.origin` **Example:** ```rust #[public] impl Factory { #[constructor] pub fn constructor(&mut self) { // Use tx_origin when deploying via a factory let deployer = self.vm().tx_origin(); self.owner.set(deployer); } } ``` **Important:** Returns the original EOA (Externally Owned Account) that initiated the transaction, even through multiple contract calls. When deploying via a factory pattern, `msg_sender()` returns the factory address — use `tx_origin()` to get the actual deployer. See [Using `msg_sender()` instead of `tx_origin()`](/stylus/how-tos/using-constructors.md#using-msg_sender-instead-of-tx_origin) for details. ### `tx_gas_price()` Gets the gas price in wei per gas, which on Arbitrum chains equals the basefee. ```rust pub fn tx_gas_price(&self) -> U256 ``` **Equivalent to**: Solidity's `tx.gasprice` **Example:** ```rust #[public] impl Analytics { pub fn record_gas_price(&mut self) { let price = self.vm().tx_gas_price(); self.gas_prices.push(price); } } ``` ### `tx_ink_price()` Gets the price of ink in EVM gas basis points. ```rust pub fn tx_ink_price(&self) -> u32 ``` **Description:** Stylus uses "ink" as its unit of computation. This method returns the conversion rate from ink to gas. See [Ink and Gas](https://docs.arbitrum.io/stylus/concepts/gas-metering) for more information. **Example:** ```rust #[public] impl Contract { pub fn get_ink_price(&self) -> u32 { self.vm().tx_ink_price() } } ``` ## Block Context (`block`) Methods for accessing information about the current block. ### `block_number()` Gets a bounded estimate of the L1 block number at which the Sequencer sequenced the transaction. ```rust pub fn block_number(&self) -> u64 ``` **Equivalent to**: Solidity's `block.number` **Example:** ```rust #[public] impl TimeLock { pub fn lock_until(&mut self, blocks: u64) { let unlock_block = self.vm().block_number() + blocks; self.unlock_block.set(U256::from(unlock_block)); } pub fn can_unlock(&self) -> bool { let current = self.vm().block_number(); let unlock = self.unlock_block.get().try_into().unwrap_or(u64::MAX); current >= unlock } } ``` **Note:** See [Block Numbers and Time](/arbitrum-essentials/arbitrum-vs-ethereum/block-numbers-and-time.md) for more information on how this value is determined on Arbitrum. ### `block_timestamp()` Gets a bounded estimate of the Unix timestamp at which the Sequencer sequenced the transaction. ```rust pub fn block_timestamp(&self) -> u64 ``` **Equivalent to**: Solidity's `block.timestamp` **Example:** ```rust #[public] impl Auction { pub fn place_bid(&mut self, amount: U256) { let now = self.vm().block_timestamp(); let deadline = self.deadline.get().try_into().unwrap_or(0); if now > deadline { panic!("Auction ended"); } // Process bid... } } ``` **Note:** See [Block Numbers and Time](/arbitrum-essentials/arbitrum-vs-ethereum/block-numbers-and-time.md) for more information on how this value is determined on Arbitrum. ### `block_basefee()` Gets the basefee of the current block. ```rust pub fn block_basefee(&self) -> U256 ``` **Equivalent to**: Solidity's `block.basefee` **Example:** ```rust #[public] impl FeeTracker { pub fn current_basefee(&self) -> U256 { self.vm().block_basefee() } } ``` ### `block_coinbase()` Gets the coinbase of the current block. ```rust pub fn block_coinbase(&self) -> Address ``` **Equivalent to**: Solidity's `block.coinbase` **Important:** On Arbitrum chains, this is the L1 batch poster's address, which differs from Ethereum where the validator determines the coinbase. **Example:** ```rust #[public] impl Contract { pub fn get_batch_poster(&self) -> Address { self.vm().block_coinbase() } } ``` ### `block_gas_limit()` Gets the gas limit of the current block. ```rust pub fn block_gas_limit(&self) -> u64 ``` **Equivalent to**: Solidity's `block.gaslimit` **Example:** ```rust #[public] impl Contract { pub fn check_gas_limit(&self) -> bool { let limit = self.vm().block_gas_limit(); limit > 30_000_000 } } ``` ## Chain Context Methods for accessing chain-specific information. ### `chain_id()` Gets the unique chain identifier of the Arbitrum chain. ```rust pub fn chain_id(&self) -> u64 ``` **Equivalent to**: Solidity's `block.chainid` **Example:** ```rust #[public] impl MultiChain { pub fn verify_chain(&self, expected_chain: u64) -> bool { self.vm().chain_id() == expected_chain } } ``` **Common Arbitrum Chain IDs:** * Arbitrum One: 42161 * Arbitrum Nova: 42170 * Arbitrum Sepolia (testnet): 421614 ## Account Information Methods for querying account details. ### `contract_address()` Gets the address of the current program. ```rust pub fn contract_address(&self) -> Address ``` **Equivalent to**: Solidity's `address(this)` **Example:** ```rust #[public] impl Contract { pub fn this_address(&self) -> Address { self.vm().contract_address() } pub fn this_balance(&self) -> U256 { let addr = self.vm().contract_address(); self.vm().balance(addr) } } ``` ### `balance(address)` Gets the ETH balance in wei of the account at the given address. ```rust pub fn balance(&self, account: Address) -> U256 ``` **Equivalent to**: Solidity's `address.balance` **Example:** ```rust #[public] impl BalanceChecker { pub fn get_balance(&self, account: Address) -> U256 { self.vm().balance(account) } pub fn has_sufficient_balance(&self, account: Address, required: U256) -> bool { self.vm().balance(account) >= required } } ``` ### `code(address)` Gets the code from the account at the given address. ```rust pub fn code(&self, account: Address) -> Vec ``` **Equivalent to**: Solidity's `address.code` (similar to `EXTCODECOPY` opcode) **Example:** ```rust #[public] impl Contract { pub fn is_contract(&self, account: Address) -> bool { self.vm().code(account).len() > 0 } } ``` ### `code_size(address)` Gets the size of the code in bytes at the given address. ```rust pub fn code_size(&self, account: Address) -> usize ``` **Equivalent to**: Solidity's `EXTCODESIZE` opcode **Example:** ```rust #[public] impl Contract { pub fn get_code_size(&self, account: Address) -> usize { self.vm().code_size(account) } } ``` ### `code_hash(address)` Gets the code hash of the account at the given address. ```rust pub fn code_hash(&self, account: Address) -> B256 ``` **Equivalent to**: Solidity's `EXTCODEHASH` opcode **Example:** ```rust #[public] impl Contract { pub fn verify_code(&self, account: Address, expected_hash: B256) -> bool { self.vm().code_hash(account) == expected_hash } } ``` **Note:** The code hash of an account without code will be the empty hash: `keccak("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`. ## Gas and Metering Methods for accessing gas and ink metering information. ### `evm_gas_left()` Gets the amount of gas left after paying for the cost of this hostio. ```rust pub fn evm_gas_left(&self) -> u64 ``` **Equivalent to**: Solidity's `gasleft()` **Example:** ```rust use stylus_sdk::call::Call; #[public] impl Contract { pub fn complex_operation(&mut self, target: ITarget) { let gas_before = self.vm().evm_gas_left(); // Use half the remaining gas for external call let config = Call::new_mutating(self) .gas(gas_before / 2); target.do_work(self.vm(), config); } } ``` ### `evm_ink_left()` Gets the amount of ink remaining after paying for the cost of this hostio. ```rust pub fn evm_ink_left(&self) -> u64 ``` **Description:** Returns remaining computation units in "ink". See [Ink and Gas](https://docs.arbitrum.io/stylus/concepts/gas-metering) for more information on Stylus's compute pricing. **Example:** ```rust #[public] impl Contract { pub fn check_ink(&self) -> u64 { self.vm().evm_ink_left() } } ``` ### `ink_to_gas(ink)` Computes the units of gas per a specified amount of ink. ```rust pub fn ink_to_gas(&self, ink: u64) -> u64 ``` **Example:** ```rust #[public] impl Contract { pub fn convert_ink_to_gas(&self, ink: u64) -> u64 { self.vm().ink_to_gas(ink) } } ``` ### `gas_to_ink(gas)` Computes the units of ink per a specified amount of gas. ```rust pub fn gas_to_ink(&self, gas: u64) -> u64 ``` **Example:** ```rust #[public] impl Contract { pub fn convert_gas_to_ink(&self, gas: u64) -> u64 { self.vm().gas_to_ink(gas) } } ``` ## Cryptographic Functions The SDK provides cryptographic utilities through the `crypto` module and VM methods. ### `keccak()` Efficiently computes the keccak256 hash of the given preimage. ```rust use stylus_sdk::crypto; pub fn keccak>(bytes: T) -> B256 ``` **Equivalent to**: Solidity's `keccak256()` **Example:** ```rust use stylus_sdk::crypto; use alloy_primitives::{Address, FixedBytes, U256}; #[public] impl Contract { pub fn hash_data(&self, data: Vec) -> FixedBytes<32> { crypto::keccak(data) } pub fn verify_hash(&self, data: Vec, expected: FixedBytes<32>) -> bool { crypto::keccak(data) == expected } // Hash multiple values together pub fn hash_packed(&self, addr: Address, amount: U256) -> FixedBytes<32> { let packed = [ addr.as_ref(), &amount.to_be_bytes_vec(), ].concat(); crypto::keccak(packed) } } ``` ### `native_keccak256()` VM method for computing keccak256 hash (alternative to `crypto::keccak`). ```rust pub fn native_keccak256(&self, input: &[u8]) -> B256 ``` **Example:** ```rust #[public] impl Contract { pub fn hash_via_vm(&self, data: Vec) -> B256 { self.vm().native_keccak256(&data) } } ``` **Note:** `crypto::keccak()` is the recommended approach as it's more ergonomic. ## Event Logging Methods for emitting events to the blockchain. ### `log(event)` Emits a typed Solidity event. ```rust pub fn log(&self, event: T) ``` **Example:** ```rust use alloy_sol_types::sol; sol! { event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); } #[public] impl Token { pub fn transfer(&mut self, to: Address, value: U256) -> bool { let from = self.vm().msg_sender(); // Transfer logic... self._transfer(from, to, value); // Emit event self.vm().log(Transfer { from, to, value }); true } } ``` ### `raw_log(topics, data)` Emits a raw log with custom topics and data. ```rust pub fn raw_log(&self, topics: &[B256], data: &[u8]) -> Result<(), &'static str> ``` **Example:** ```rust use alloy_primitives::{B256, FixedBytes}; #[public] impl Contract { pub fn emit_custom_log(&self) { let topic = B256::from([1u8; 32]); let topics = &[topic]; let data = b"custom data"; self.vm().raw_log(topics, data).unwrap(); } } ``` **Note:** Maximum of 4 topics allowed. The first topic is typically the event signature hash. ## Storage Operations Methods for interacting with contract storage. ### `storage_load_bytes32(key)` Reads a 32-byte value from permanent storage. ```rust pub fn storage_load_bytes32(&self, key: U256) -> B256 ``` **Equivalent to**: Solidity's `SLOAD` opcode **Note:** Storage is cached for efficiency. Use the SDK's storage types instead of direct storage access. ### `flush_cache(clear)` Persists dirty values in the storage cache to the EVM state trie. ```rust pub fn flush_cache(&self, clear: bool) ``` **Parameters:** * `clear`: If `true`, drops the cache entirely after flushing **Note:** Typically handled automatically by the SDK. Manual cache flushing is rarely needed. ## Memory Management ### `pay_for_memory_grow(pages)` Pays for memory growth in WASM pages. ```rust pub fn pay_for_memory_grow(&self, pages: u16) ``` **Note:** The `#[entrypoint]` macro handles this automatically. Manual calls are not recommended and will unproductively consume gas. ## Complete Example Here's a comprehensive example using various global variables and functions: ```rust #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; use alloy_primitives::{Address, U256, FixedBytes}; use alloy_sol_types::sol; use stylus_sdk::{crypto, prelude::*}; sol! { event Action( address indexed caller, uint256 value, uint256 timestamp, bytes32 data_hash ); } sol_storage! { #[entrypoint] pub struct ContextExample { mapping(address => uint256) balances; uint256 total_deposits; uint256 creation_block; address owner; } } #[public] impl ContextExample { #[constructor] pub fn constructor(&mut self) { // Use tx_origin for factory deployments self.owner.set(self.vm().tx_origin()); self.creation_block.set(U256::from(self.vm().block_number())); } #[payable] pub fn deposit(&mut self, data: Vec) { // Message context let caller = self.vm().msg_sender(); let amount = self.vm().msg_value(); // Block context let timestamp = self.vm().block_timestamp(); let block_num = self.vm().block_number(); // Require minimum value if amount < U256::from(1000) { panic!("Insufficient deposit"); } // Update balances let balance = self.balances.get(caller); self.balances.setter(caller).set(balance + amount); self.total_deposits.set(self.total_deposits.get() + amount); // Hash the data let data_hash = crypto::keccak(&data); // Emit event self.vm().log(Action { caller, value: amount, timestamp: U256::from(timestamp), data_hash, }); } pub fn get_contract_info(&self) -> (Address, U256, u64, u64) { ( self.vm().contract_address(), // Contract's address self.vm().balance(self.vm().contract_address()), // Contract's balance self.vm().chain_id(), // Chain ID self.vm().block_number(), // Current block ) } pub fn verify_signature(&self, message: Vec, expected_hash: FixedBytes<32>) -> bool { let hash = crypto::keccak(message); hash == expected_hash } pub fn is_owner(&self, account: Address) -> bool { account == self.owner.get() } pub fn time_since_creation(&self) -> u64 { let current_block = self.vm().block_number(); let creation_block: u64 = self.creation_block.get().try_into().unwrap_or(0); current_block.saturating_sub(creation_block) } } ``` ## Summary of available methods The following tables provide a quick reference for all VM methods and utility functions available in Stylus contracts. ### Message context | Method | Parameters | Return type | Description | Solidity equivalent | | ----------------- | ---------- | ----------- | ------------------------------------- | ------------------- | | `msg_sender()` | None | `Address` | Caller's address | `msg.sender` | | `msg_value()` | None | `U256` | ETH sent with call (in wei) | `msg.value` | | `msg_reentrant()` | None | `bool` | Whether the current call is reentrant | N/A | ### Transaction context | Method | Parameters | Return type | Description | Solidity equivalent | | ---------------- | ---------- | ----------- | --------------------------------- | ------------------- | | `tx_origin()` | None | `Address` | Original transaction sender (EOA) | `tx.origin` | | `tx_gas_price()` | None | `U256` | Gas price in wei per gas | `tx.gasprice` | | `tx_ink_price()` | None | `u32` | Ink price in EVM gas basis points | N/A | ### Block context | Method | Parameters | Return type | Description | Solidity equivalent | | ------------------- | ---------- | ----------- | --------------------------- | ------------------- | | `block_number()` | None | `u64` | Current block number | `block.number` | | `block_timestamp()` | None | `u64` | Block timestamp (Unix time) | `block.timestamp` | | `block_basefee()` | None | `U256` | Base fee of current block | `block.basefee` | | `block_coinbase()` | None | `Address` | Batch poster address | `block.coinbase` | | `block_gas_limit()` | None | `u64` | Gas limit of current block | `block.gaslimit` | ### Chain context | Method | Parameters | Return type | Description | Solidity equivalent | | ------------ | ---------- | ----------- | ----------------------- | ------------------- | | `chain_id()` | None | `u64` | Unique chain identifier | `block.chainid` | ### Account information | Method | Parameters | Return type | Description | Solidity equivalent | | -------------------- | ------------------ | ----------- | ------------------------------ | ------------------- | | `contract_address()` | None | `Address` | This contract's address | `address(this)` | | `balance()` | `account: Address` | `U256` | ETH balance in wei | `address.balance` | | `code()` | `account: Address` | `Vec` | Account bytecode | `address.code` | | `code_size()` | `account: Address` | `usize` | Size of account code in bytes | `EXTCODESIZE` | | `code_hash()` | `account: Address` | `B256` | Keccak256 hash of account code | `EXTCODEHASH` | ### Gas and metering | Method | Parameters | Return type | Description | Solidity equivalent | | ---------------- | ---------- | ----------- | ----------------------------- | ------------------- | | `evm_gas_left()` | None | `u64` | Remaining gas after this call | `gasleft()` | | `evm_ink_left()` | None | `u64` | Remaining ink after this call | N/A | | `ink_to_gas()` | `ink: u64` | `u64` | Convert ink units to gas | N/A | | `gas_to_ink()` | `gas: u64` | `u64` | Convert gas units to ink | N/A | ### Cryptographic functions | Method | Parameters | Return type | Description | Solidity equivalent | | -------------------- | ------------------------- | ----------- | ----------------------------- | ------------------- | | `crypto::keccak()` | `bytes: impl AsRef<[u8]>` | `B256` | Compute keccak256 hash | `keccak256()` | | `native_keccak256()` | `input: &[u8]` | `B256` | Compute keccak256 hash via VM | `keccak256()` | ### Event logging | Method | Parameters | Return type | Description | Solidity equivalent | | ----------- | -------------------------------- | -------------------------- | --------------------------------- | --------------------- | | `log()` | `event: T` where `T: SolEvent` | None | Emit a typed Solidity event | `emit Event()` | | `raw_log()` | `topics: &[B256]`, `data: &[u8]` | `Result<(), &'static str>` | Emit a raw log with custom topics | `log0`-`log4` opcodes | ## See Also * [Contracts](/stylus/fundamentals/contracts.md) - Contract structure and methods * [Primitives](/stylus/fundamentals/data-types/primitives.md) - Basic data types * [Storage Types](/stylus/fundamentals/data-types/storage.md) - Persistent storage --- > For a complete page index, fetch # Prerequisites and setup This guide will help you set up everything you need to develop Stylus contracts. ## System requirements * **Operating System**: macOS, Linux, or Windows (WSL2) * **RAM**: Minimum 8GB, 16GB recommended * **Disk Space**: At least 20GB free ## Install Rust ### Installing Rust You'll use Rust to write your smart contracts. Stylus compiles Rust code to WASM, which runs on the Stylus virtual machine alongside the EVM. The Rust toolchain includes `rustc` (compiler), `cargo` (package manager), and `rustup` (version manager). **macOS/Linux:** ```shell curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` **Windows:** Download and run [rustup-init.exe](https://rustup.rs/). **Configure for WASM:** ```shell rustup target add wasm32-unknown-unknown ``` **Verify:** ```shell rustc --version cargo --version ``` ## Install cargo-stylus CLI ### Installing cargo-stylus The Stylus CLI is your toolkit for speeding up the development process. It handles project scaffolding, building contracts to WASM, checking onchain activation, and deploying your contracts. Windows users need WSL `cargo-stylus` requires a Unix environment. On Windows, install [WSL 2](https://learn.microsoft.com/en-us/windows/wsl/install) and run all `cargo stylus` commands from the WSL terminal — native Windows command prompts are not supported. The `cargo-stylus` CLI tool helps you create, build, and deploy Stylus contracts. **Install:** ```shell cargo install cargo-stylus ``` **Verify:** ```shell cargo stylus --version ``` **Update:** ```shell cargo install cargo-stylus --force ``` ## Install Docker (for local testnet) ### Installing Docker for local testing The Nitro devnode (and Docker) lets you spin up a local devnet to test your contracts before deploying them to an actual network. This gives you a fast feedback loop without spending testnet funds. Running a Nitro devnode also solves the issue of having tokens to deploy and execute contracts, as the devchain gives you pre-funded accounts. **macOS:** Download [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/). **Linux:** ```shell curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh ``` **Windows:** Download [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) (requires WSL2). **Start Nitro devnode:** ```shell git clone https://github.com/OffchainLabs/nitro-devnode.git cd nitro-devnode ./run-dev-node.sh ``` **Verify:** ```shell curl -X POST http://localhost:8547 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' ``` ## Install Foundry ### Installing Foundry [Foundry](https://book.getfoundry.sh/) is a toolkit for Ethereum development that includes `cast`, a command-line tool for interacting with smart contracts, sending transactions, and querying chain data. **Install:** ```shell curl -L https://foundry.paradigm.xyz | bash foundryup ``` **Verify:** ```shell cast --version ``` **Update:** ```shell foundryup ``` ## Verify installation ```shell # Check Rust version rustc --version # Check cargo-stylus cargo stylus --version # Check Docker docker --version # Check Foundry's cast cast --version ``` Expected output: ```text rustc 1.91.0 (or higher) cargo-stylus 0.10.7 (or higher) Docker version 24.0.0 (or higher) cast 1.0.0 (or higher) ``` ## Next steps * [Choose the learning path that works for you](/stylus/fundamentals/choose-your-path.md) * [A gentle introduction to Stylus](/stylus/gentle-introduction.md) * [Create your first project](/stylus/quickstart.md) * [Understand project structure](/stylus/fundamentals/project-structure.md) * [Learn about contracts](/stylus/fundamentals/contracts.md) --- > For a complete page index, fetch # Structure of a Stylus Rust project Contracts in Rust are similar to contracts in Solidity. Each contract can contain declarations of State Variables, Functions, Function Modifiers, Events, Errors, Struct Types, and Enum Types. In addition, Rust contracts can import third-party packages from [crates.io](https://crates.io) as dependencies and use them for advanced functionality. ## Project layout In the most basic example, this is how a Rust contract will be organized. The simplest way to get going with a new project is to follow the [Quickstart](https://docs.arbitrum.io/stylus/quickstart) guide, or if you've already installed all dependencies, just run `cargo stylus new ` from your terminal to begin a new project. Once installed, your project will include the following required files: ```shell - src - lib.rs - main.rs - Cargo.toml - Stylus.toml - rust-toolchain.toml ``` `src/lib.rs` is the root module of your contract's code. Here, you can import utilities or methods from internal or external modules, define the data layout of your contract's state variables, and define your contract's public API. This module must define a root data struct with the `#[entrypoint]` macro and provide an impl block annotated with `#[public]` to define public or external methods. See [First App](https://stylus-by-example.org/basic_examples/first_app) for an example of this. These macros are used to maintain [Solidity ABI](https://docs.soliditylang.org/en/v0.8.19/abi-spec.html#basic-design) compatibility to ensure that Rust contracts work with existing Solidity libraries and tooling. `src/main.rs` is typically auto-generated by [cargo-stylus](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus) and does not usually need to be modified. Its purpose is to assist with the generation of [JSON describing](https://docs.soliditylang.org/en/v0.8.19/abi-spec.html#json) your contract's public interface, for use with automated tooling and frontend frameworks. `Cargo.toml` is a standard file that Rust projects use to define a package's name, repository location, etc, as well as import dependencies and define feature and build flags. From here, you can define required dependencies such as the [Stylus SDK](https://crates.io/crates/stylus-sdk) itself or import third-party packages from [crates.io](https://crates.io). See [First Steps with Cargo](https://doc.rust-lang.org/cargo/getting-started/first-steps.html) if you are new to Rust. For a complete list of Stylus-specific configuration options, see the [Configuration reference](/stylus/reference/stylus-toml-reference.md). `Stylus.toml` marks the package as a Stylus contract. Without this file, `cargo stylus` does not recognize the package during workspace builds and deployments. It also stores deployment metadata such as network endpoints and deployer addresses. `rust-toolchain.toml` is used by public blockchain explorers, like [Arbiscan](https://arbiscan.io/), to assist with source code verification. To ensure that source code can be compiled deterministically, we use this file to include relevant metadata like what version of Rust was used. It can also be used to pin the project to a specific Rust version that it's compatible with. Your contract may also include other dot files (such as `.gitignore`, `.env`, etc), markdown files for docs, or additional subfolders. ## State variables Like Solidity, Rust contracts are able to define *state variables*. These are variables which are stored on the chain's *state trie*, which is essentially the chain's database. They differ from standard Rust variables in that they must implement the `Storage` trait from the Stylus SDK. This trait is used to layout the data in the trie in a Solidity-compatible fashion. The Stylus SDK provides Storage types for all Solidity primitives out-of-the-box, such as `StorageAddress`, `StorageU256`, etc. See [storage module](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/index.html#structs) for more information. When working with state variables, you can either use Rust-style syntax or Solidity-style syntax to define your data schema. The `#[storage]` macro is used to define Rust-style state variables while `sol_storage!` macro is used for Solidity-style state variables. Both styles may have more than one struct but must annotate a single struct as the root struct with `#[entrypoint]` macro. Below are examples of each. **Rust-style Schema** ```rust use stylus_sdk::{prelude::*, storage::{StorageU256, StorageAddress}}; #[storage] #[entrypoint] pub struct MyContract { owner: StorageAddress, version: StorageU256, } ``` **Solidity-style Schema** ```rust use stylus_sdk::{prelude::*}; sol_storage! { #[entrypoint] pub struct MyContract { address owner; version: uint256, } } ``` To read from state or write to it, getters and setters are used: ```rust let new_count = self.count.get() + U256::from(1); self.count.set(new_count); ``` See [Storage Data Types](https://stylus-by-example.org/basic_examples/storage_data_types) for more examples of this. ## Functions Contract functions are defined by providing an `impl` block for your contract's `#[entrypoint]` struct and annotating that block with `#[public]` to make the functions part of the contract's public API. The first parameter of each function is `&self`, which references the struct annotated with `#[entrypoint]`, it's used for reading state variables. By default, methods are view-only and cannot mutate state. To make a function mutable and able to alter state, `&mut self` must be used. Internal methods can be defined on a separate impl block for the struct that is not annotated with `#[public]`. Internal methods can access state. ```rust // Defines the public, external methods for your contract // This impl block must be for the #[entrypoint] struct defined prior #[public] impl Counter { // By annotating first arg with &self, this indicates a view function pub fn get(&self) -> U256 { self.count.get() } // By annotating with &mut self, this is a mutable public function pub fn set_count(&mut self, count: U256) { self.count.set(count); } } // Internal methods (NOT part of public API) impl Counter { fn add(a: U256, b: U256) -> U256 { a + b } } ``` ## Modules Modules are a way to organize code into logical units. While your contract must have a `lib.rs` which defines your entrypoint struct, you can also define utility functions, structs, enums, etc., in modules and import them to use in your contract's methods. For example, with this file structure: ```text - src - lib.rs - main.rs - utils - mod.rs - Cargo.toml - rust-toolchain.toml ``` In `lib.rs`: ```rust // import module mod utils; // ..other code const score = utils::check_score(); ``` See [Defining modules](https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html) in the Rust book for more info on modules and how to use them. ## Importing packages Rust has a robust package manager for managing dependencies and importing third-party libraries to use in your smart contracts. These packages (called crates in Rust) are located at [crates.io](https://crates.io). To make use of a dependency in your code, you'll need to complete these steps: Add the package name and version to your `Cargo.toml`: ```toml # Cargo.toml [package] # ...package info here [dependencies] rust_decimal = "1.36.0" ``` Import the package into your contract: ```rust // lib.rs use rust_decimal_macros::dec; ``` Use imported types in your contract: ```rust // create a fixed point Decimal value let price = dec!(72.00); ``` Not all Rust crates are compatible with Stylus since they need to be compiled to WASM and used in a blockchain context, which is more limited than a desktop application. For instance, the `rand` crate is not usable, as there is no onchain randomness available to smart contracts. In addition, contracts cannot access functions that use networking or filesystem access features. There is also a need to be mindful of the size of the crates you import, since the default contract size limit is 24KB (compressed). Crates that do not use the standard library (`no_std` crates) tend to work best. See [Using public Rust crates](/stylus/advanced/recommended-libraries.md#using-public-rust-crates) for more details on using public Rust crates as well as a curated list of crates that tend to work well for smart contract development. ## Events Events are used to publicly log values to the EVM. They can be useful for users to understand what occurred during a transaction while inspecting a transaction on a public explorer, like [Arbiscan](https://arbiscan.io/). ```rust sol! { event HighestBidIncreased(address bidder, uint256 amount); } #[public] impl AuctionContract { pub fn bid(&mut self) { // ... self.vm().log(HighestBidIncreased { bidder: Address::from([0x11; 20]), amount: U256::from(42), }); } } ``` ## Errors Errors allow you to define descriptive names for failure situations. These can be useful for debugging or providing users with helpful information for why a transaction may have failed. ```rust sol! { error NotEnoughFunds(uint256 request, uint256 available); } #[derive(SolidityError)] pub enum TokenErrors { NotEnoughFunds(NotEnoughFunds), } #[public] impl Token { pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), TokenErrors> { let balance = self.balances.get(self.vm().msg_sender()); if (balance < amount) { return Err(TokenErrors::NotEnoughFunds(NotEnoughFunds { request: amount, available: balance, })); } // .. other code here } } ``` --- > For a complete page index, fetch # Testing smart contracts with Stylus ## Introduction The Stylus SDK provides a robust testing framework that allows developers to write and run tests for their contracts directly in Rust without deploying to a blockchain. This guide will walk you through the process of writing and running tests for Stylus contracts using the built-in testing framework. The Stylus testing framework allows you to: * Simulate a complete Ethereum environment for your tests without the need for running a test node * Test contract storage operations and state transitions * Mock transaction context and block information * Test contract-to-contract interactions with mocked calls * Verify contract logic without deployment costs or delays * Simulate various user scenarios and edge cases ### Prerequisites Before you begin, make sure you have: * Basic familiarity with Rust and smart contract development * Understanding of unit testing concepts * Rust toolchain: follow the instructions on [Rust Lang's installation page](https://www.rust-lang.org/tools/install) to install a complete Rust toolchain (v1.91 or newer) on your system. After installation, ensure you can access the programs `rustup`, `rustc`, and `cargo` from your preferred terminal application. ## The Stylus Testing Framework The Stylus SDK includes `testing`, a module that provides all the tools you need to test your contracts. This module includes: * **TestVM**: A mock implementation of the Stylus VM that can simulate all host functions * **TestVMBuilder**: A builder pattern to conveniently configure the test VM * Built-in utilities for mocking calls, storage, and other EVM operations ### Key Components Here are the components you'll use when testing your Stylus contracts: * **TestVM**: The core component that simulates the Stylus execution environment * **Storage accessors**: For testing contract state changes * **Call mocking**: For simulating interactions with other contracts * **Block context**: For testing time-dependent logic ## Example Smart Contract: Cupcake Vending Machine Let's look at a Rust-based cupcake vending machine smart contract. This contract follows two simple rules: 1. The vending machine will distribute a cupcake to anyone who hasn't received one in the last 5 seconds 2. The vending machine tracks each user's cupcake balance Note You can find all the code in this tutorial as a Rust workspace in the [Quickstart repo](https://github.com/OffchainLabs/stylus-quickstart-vending-machine) Cupcake Vending Machine Contract ```rust #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; /// Import items from the SDK. The prelude contains common traits and macros. use stylus_sdk::alloy_primitives::{Address, U256}; use stylus_sdk::console; use stylus_sdk::prelude::*; use alloy_sol_types::sol; // Define the event using the sol! macro sol! { event CupcakeDistributed(address indexed recipient, uint256 indexed timestamp, uint256 new_balance); } sol_storage! { #[entrypoint] pub struct VendingMachine { mapping(address => uint256) cupcake_balances; mapping(address => uint256) cupcake_distribution_times; } } #[public] impl VendingMachine { pub fn give_cupcake_to(&mut self, user_address: Address) -> Result> { // Get the last distribution time for the user. let last_distribution = self.cupcake_distribution_times.get(user_address); // Calculate the earliest next time the user can receive a cupcake. let five_seconds_from_last_distribution = last_distribution + U256::from(5); // Get the current block timestamp using the VM pattern let current_time = self.vm().block_timestamp(); // Check if the user can receive a cupcake. let user_can_receive_cupcake = five_seconds_from_last_distribution <= U256::from(current_time); if user_can_receive_cupcake { // Increment the user's cupcake balance. let mut balance_accessor = self.cupcake_balances.setter(user_address); let balance = balance_accessor.get() + U256::from(1); balance_accessor.set(balance); // Get current timestamp using the VM pattern BEFORE creating the mutable borrow let new_distribution_time = self.vm().block_timestamp(); // Update the distribution time to the current time. let mut time_accessor = self.cupcake_distribution_times.setter(user_address); time_accessor.set(U256::from(new_distribution_time)); // Emit the CupcakeDistributed event self.vm().log(CupcakeDistributed { recipient: user_address, timestamp: U256::from(new_distribution_time), new_balance: balance, }); return Ok(true); } else { // User must wait before receiving another cupcake. console!( "HTTP 429: Too Many Cupcakes (you must wait at least 5 seconds between cupcakes)" ); return Ok(false); } } pub fn get_cupcake_balance_for(&self, user_address: Address) -> Result> { Ok(self.cupcake_balances.get(user_address)) } } ``` ## Writing Tests for the Vending Machine Now, let's write tests for our vending machine contract using the Stylus testing framework. We'll create tests that verify: 1. Users can get an initial cupcake 2. Users must wait 5 seconds between cupcakes 3. Cupcake balances are tracked correctly 4. The contract state updates properly ### Test Structure Create a test file using standard Rust test patterns. Here's the basic structure: ```rust // Import necessary dependencies #[cfg(test)] mod test { use super::*; use alloy_primitives::address; use stylus_sdk::testing::*; #[test] fn test_give_cupcake_to() { // Set up test environment let vm = TestVM::default(); // Initialize your contract let mut contract = VendingMachine::from(&vm); // Test logic goes here... } ``` ### Using the TestVM The `TestVM` simulates the execution environment for your contract, removing the need to run your tests against a test node. The `TestVM` allows you to control aspects like: * Block timestamp and number * Account balances * Transaction value and sender * Storage state Let's create a test suite that covers all aspects of our contract, we'll go over the code features one by one: Test Vending Machine Contract ```rust #[cfg(test)] mod test { use super::*; use alloy_primitives::address; use stylus_sdk::testing::*; #[test] fn test_give_cupcake_to() { let vm = TestVM::default(); let mut contract = VendingMachine::from(&vm); let user = address!("0xCDC41bff86a62716f050622325CC17a317f99404"); assert_eq!(contract.get_cupcake_balance_for(user).unwrap(), U256::ZERO); vm.set_block_timestamp(vm.block_timestamp() + 6); // Give a cupcake and verify it succeeds assert!(contract.give_cupcake_to(user).unwrap()); // Check balance is now 1 assert_eq!( contract.get_cupcake_balance_for(user).unwrap(), U256::from(1) ); // Try to give another cupcake immediately - should fail due to time restriction assert!(!contract.give_cupcake_to(user).unwrap()); // Balance should still be 1 assert_eq!( contract.get_cupcake_balance_for(user).unwrap(), U256::from(1) ); // Advance block timestamp by 6 seconds vm.set_block_timestamp(vm.block_timestamp() + 6); // Now giving a cupcake should succeed assert!(contract.give_cupcake_to(user).unwrap()); // Balance should now be 2 assert_eq!( contract.get_cupcake_balance_for(user).unwrap(), U256::from(2) ); } } ``` ### TestVM: advanced use This test shows how you can use advanced configuration and usage of the TestVM by creating and configuring a TestVM with custom parameters: * Setting blockchain state (timestamps, block numbers) * Interacting with contract methods * Taking and inspecting VM state snapshots * Mocking external contract calls * Testing time-dependent contract behavior * Testing logs Advanced TestVM Configuration #### 1: TestVM Setup and Configuration ```rust #[test] fn test_advanced_testvm_configuration() { // Create a TestVM with custom configuration using the builder pattern // This approach allows for fluent, readable test setup let vm: TestVM = TestVMBuilder::new() // Set the transaction sender address (msg.sender in Solidity) .sender(address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) // Set the address where our contract is deployed .contract_address(address!("0x5FbDB2315678afecb367f032d93F642f64180aa3")) // Set the ETH value sent with the transaction (msg.value in Solidity) .value(U256::from(1)) .build(); // Configure additional blockchain state parameters directly on the VM instance // This demonstrates how to set parameters after VM creation vm.set_block_number(12345678); // Note: The chain ID is set to 42161 (Arbitrum One) by default in the TestVM // We don't need to set it explicitly as it's already configured in the VM state ``` #### 2: Contract Initialization and User Setup ```rust // Initialize our VendingMachine contract with the configured VM // The `from` method connects our contract to the test environment let mut contract = VendingMachine::from(&vm); // Define a user address that will interact with our contract // This represents an external user's Ethereum address let user = address!("0xCDC41bff86a62716f050622325CC17a317f99404"); // 3: Initial State Verification // ------------------------------------ // Verify the user starts with zero cupcakes // This confirms our contract's initial state is as expected assert_eq!(contract.get_cupcake_balance_for(user).unwrap(), U256::ZERO); // Set the initial block timestamp by advancing it by 10 seconds // This ensures we're past any time-based restrictions vm.set_block_timestamp(vm.block_timestamp() + 10); ``` #### 4: Contract Interaction ```rust // Give a cupcake to the user and verify the operation succeeds // The contract should return true when a cupcake is successfully given assert!(contract.give_cupcake_to(user).unwrap()); // Verify the user now has exactly one cupcake // This confirms our contract correctly updated its storage assert_eq!( contract.get_cupcake_balance_for(user).unwrap(), U256::from(1) ); ``` #### 5: VM State Inspection ```rust // Take a snapshot of the current VM state for inspection // This captures all storage, balances, and blockchain parameters let snapshot = vm.snapshot(); // Inspect various aspects of the VM state to verify configuration // Chain ID should be Arbitrum One (42161) which is the default assert_eq!(snapshot.chain_id, 42161); // Message value should match what we configured (1 wei) assert_eq!(snapshot.msg_value, U256::from(1)); ``` #### 6: Mocking External Contract Calls ```rust // Define an external contract we might want to interact with let external_contract = address!("0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199"); // Define example call data we would send to that contract let call_data = vec![0xab, 0xcd, 0xef]; // Define the expected response from that contract let expected_response = vec![0x12, 0x34, 0x56]; // Mock the external call so it returns our expected response // This allows testing contract interactions without deploying external contracts // mock_call takes 4 args: (to, data, value, Ok(return)|Err(revert)) vm.mock_call(external_contract, call_data, U256::ZERO, Ok(expected_response)); ``` #### 7: Time-Dependent Behavior Testing ```rust // Set a specific block timestamp // This simulates the passage of time on the blockchain vm.set_block_timestamp(1006); // Try giving another cupcake after the time restriction has passed // The contract should allow this since enough time has elapsed assert!(contract.give_cupcake_to(user).unwrap()); // Verify the user now has two cupcakes // This confirms our contract correctly handles time-based restrictions assert_eq!( contract.get_cupcake_balance_for(user).unwrap(), U256::from(2) ); ``` #### 8: Testing Event Logs ```rust // Test that events are emitted when cupcakes are distributed let vm_logs = TestVM::new(); let mut contract_logs = VendingMachine::from(&vm_logs); let user_logs = address!("0xCDC41bff86a62716f050622325CC17a317f99404"); // Set initial timestamp to ensure we can give a cupcake vm_logs.set_block_timestamp(100); // Give a cupcake to the user - this should emit an event let result = contract_logs.give_cupcake_to(user_logs).unwrap(); assert!(result, "Should successfully give first cupcake"); // Get all emitted logs from the VM let logs = vm_logs.get_emitted_logs(); // Verify that exactly one event was emitted assert_eq!(logs.len(), 1, "Should emit exactly one CupcakeDistributed event"); // Calculate the expected event signature for CupcakeDistributed // Event signature: CupcakeDistributed(address indexed recipient, uint256 indexed timestamp, uint256 new_balance) // The signature is calculated as: keccak256("CupcakeDistributed(address,uint256,uint256)") use alloy_primitives::hex; use stylus_sdk::alloy_primitives::B256; let event_signature: B256 = hex!("c12a96437276bfca30ffd7a90b5e9d233c71c97f759c3b76b886f29e87989bb2").into(); // Check the first topic (event signature) assert_eq!( logs[0].0[0], event_signature, "First topic should be the event signature" ); // Extract the indexed recipient address from the second topic let recipient_topic = logs[0].0[1]; let recipient_bytes: [u8; 32] = recipient_topic.into(); // Indexed addresses are padded to 32 bytes - extract the last 20 bytes let mut recipient_address = [0u8; 20]; recipient_address.copy_from_slice(&recipient_bytes[12..32]); // Verify the recipient address matches our user assert_eq!( Address::from(recipient_address), user_logs, "Event recipient should match the user who received the cupcake" ); // The new_balance is not indexed, so it's in the data field let log_data = &logs[0].1; // For a single uint256, it should be 32 bytes assert_eq!(log_data.len(), 32, "Event data should contain one uint256 (32 bytes)"); // Convert the data bytes to U256 let mut balance_bytes = [0u8; 32]; balance_bytes.copy_from_slice(&log_data[0..32]); let new_balance = U256::from_be_bytes(balance_bytes); // Verify the balance is 1 (first cupcake) assert_eq!( new_balance, U256::from(1), "Event should show new balance of 1 cupcake" ); } ``` Here is a `cargo.toml` file to add the required dependencies: cargo.toml ```toml [package] name = "stylus-cupcake-example" version = "0.1.7" edition = "2024" license = "MIT OR Apache-2.0" keywords = ["arbitrum", "ethereum", "stylus", "alloy"] [dependencies] alloy-primitives = "1.5.7" alloy-sol-types = "1.5.7" stylus-sdk = "0.10.7" # Enable the in-memory TestVM only for tests, so it never ships in your contract. [dev-dependencies] stylus-sdk = { version = "0.10.7", features = ["stylus-test"] } [features] default = ["mini-alloc"] export-abi = ["stylus-sdk/export-abi"] debug = ["stylus-sdk/debug"] mini-alloc = ["stylus-sdk/mini-alloc"] [[bin]] name = "stylus-cupcake-example" path = "src/main.rs" [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" # If you need to reduce the binary size, it is advisable to try other # optimization levels, such as "s" and "z" opt-level = 3 ``` You can find the example above in the [stylus-quickstart-vending-machine git repository](https://github.com/OffchainLabs/stylus-quickstart-vending-machine). ## Running Tests Unit tests run with the standard Rust test command: ```shell cargo test ``` To run a specific test: ```shell cargo test test_give_cupcake ``` ## Testing Best Practices 1. **Test Isolation** * Create a new `TestVM` instance for each test * Avoid relying on state from previous tests 2. **Comprehensive Coverage** * Test both success and error conditions * Test edge cases and boundary conditions * Verify all public functions and important state transitions 3. **Clear Assertions** * Use descriptive error messages in assertions * Make assertions that verify the actual behavior you care about 4. **Realistic Scenarios** * Test real-world usage patterns * Include tests for authorization and access control 5. **Gas and Resource Efficiency** * For complex contracts, consider testing gas usage patterns * Look for storage optimization opportunities ## Migrating from Global Accessors to VM Accessors As of Stylus SDK 0.8.0, there's a shift away from global host function invocations to using the `.vm()` method. This is a safer approach that makes testing easier. For example: ```rust // Old style (deprecated) let timestamp = block::timestamp(); // New style (preferred) let timestamp = self.vm().block_timestamp(); ``` To make your contracts more testable, make sure they access host methods through the `HostAccess` trait with the `.vm()` method. --- > For a complete page index, fetch # A gentle introduction to Stylus ## In a nutshell: * Stylus lets you write smart contracts in programming languages that compile to WASM, such as Rust, C, C++, and others, allowing you to use their ecosystem of libraries and tools. Language and tooling support exists for Rust. You can try the SDK and CLI with the [quickstart](/stylus/quickstart.md). * Solidity contracts and Stylus contracts are interoperable. In Solidity, you can call a Rust program and vice versa, thanks to a second, coequal WASM virtual machine. * Stylus contracts offer reduced gas costs for memory and compute-intensive operations because WASM programs execute more efficiently than EVM bytecode for these workloads. ## What's Stylus? Stylus is an upgrade to Arbitrum Nitro [(ArbOS 32)](/run-arbitrum-node/arbos-releases/arbos32.md), the tech stack powering Arbitrum One, Arbitrum Nova, and Arbitrum chains. This upgrade adds a second, coequal virtual machine to the EVM, where EVM contracts continue to behave exactly as they would in Ethereum. This paradigm is called MultiVM because it is additive to existing functionality. ![Stylus gives you MultiVM](/img/stylus-multivm.jpg) This second virtual machine executes WebAssembly (WASM) rather than EVM bytecode. WASM is a binary format used in web standards and browsers for efficient computation. Its design is to be portable and human-readable, with sandboxed execution environments for security. Working with WASM is nothing new for Arbitrum chains. Ever since the [Nitro upgrade](https://medium.com/offchainlabs/arbitrum-nitro-one-small-step-for-l2-one-giant-leap-for-ethereum-bc9108047450), WASM has been a fundamental component of Arbitrum's fraud proofs. With a WASM VM, any programming language compilable to WASM is within Stylus's scope. In practice, some compilers are better suited to smart contract development than others. Rust has the first-class, fully supported SDK, and C and C++ are also supported. Any other language that compiles to WASM is theoretically possible, but support for those is experimental rather than officially maintained. Languages that include their own runtimes, like Python and JavaScript, are more complex for Stylus to support, although not impossible. WASM programs tend to be more efficient than EVM bytecode for memory-intensive applications. This efficiency comes from mature compiler toolchains for languages like Rust and C, which have benefited from decades of optimization work. The WASM runtime also executes faster than the EVM interpreter. Third-party contributions in the form of libraries for new and existing languages are welcome. ## How Stylus works Bringing a Stylus program to life involves four stages: coding, activation, execution, and proving. The following sections describe each step. ### Coding You write your smart contract in any programming language that compiles to WASM. Rust has the most developed support with an open-source SDK for smart contract development. C and C++ are also supported, so that you can deploy existing contracts in those languages onchain with minimal modifications. The [Stylus SDK for Rust](/stylus/reference/rust-sdk-guide.md) provides the development framework and language features for smart contract development. It also provides access to EVM-specific functionality used by smart contract developers. ### Activation Once you've written your contract, compile it to WASM using the Stylus CLI (or another compiler, like Clang for C/C++). Then you post the compiled WASM onchain. To make your contract callable, it must undergo an [activation process](/stylus/concepts/activation.md). During activation, the WASM compiles down to a node's native machine code (e.g., ARM or x86). This step also includes safety checks, such as [gas metering](/stylus/concepts/gas-metering.md), depth-checking, and memory charging, to ensure your program runs safely and can be fraud-proven. Stylus measures computational costs using ink instead of gas. Ink works like gas but is thousands of times smaller. WASM executes faster than the EVM, so a single EVM operation takes as long as thousands of WASM operations. A finer-grained unit makes pricing more precise. Note Stylus contracts need to be reactivated once per year (365 days) or after any Stylus upgrade. You can do this using [`cargo-stylus`](/stylus/cli-tools/commands-reference.md) or the [ArbWasm precompile](/arbitrum-essentials/precompiles/reference.md#common-precompiles). If a contract isn't reactivated, it becomes uncallable. The 365-day expiry and a minimum age of about 31 days before a contract can be kept alive are both configurable chain parameters; these are the current defaults. ### Execution When your Stylus program runs, it executes in a fork of [Wasmer](https://wasmer.io/), a [WebAssembly runtime](/stylus/concepts/webassembly.md). Because Wasmer compiles to native machine code, it executes faster than Geth's EVM bytecode interpreter. This performance difference reduces gas costs for compute-intensive operations. EVM contracts continue to work exactly as before. When a contract is called, the system checks whether it's an EVM contract or a WASM program and routes it to the appropriate runtime. Solidity and WASM contracts can call each other, so the language a contract was written in doesn't affect interoperability. ### Proving Stylus builds on Nitro's fraud-proving technology. In normal operation, execution compiles to native code for speed. But if there's a dispute, the execution history compiles to WASM so validators can run interactive fraud proofs on Ethereum. What makes Stylus possible: Nitro can already replay and verify disputes using WASM. Stylus extends this capability to verify not just execution history, but also the WASM programs you deploy. The result is a system where any program compiled to WASM can be deterministically fraud-proven. For more details, see the [Nitro architecture documentation](/how-arbitrum-works/inside-arbitrum-nitro.md). ## Use cases Stylus can integrate into existing Solidity projects by calling a Stylus contract to optimize specific parts of your app, or you can build an entire app with Stylus. Developers can also port existing applications written in Rust, C, or C++ to run onchain with minimal modifications. Here are some use cases where Stylus may be a good fit: * Onchain verification with zero-knowledge proofs: Reduce gas costs for onchain verification using zero-knowledge proving systems. See [case study](https://blog.arbitrum.io/renegade-stylus-case-study/) for an example implementation. * DeFi instruments: Implement custom pricing curves for AMMs, synthetic assets, options, and futures with onchain computation. You can extend existing protocols (such as Uniswap V4 hooks) or build your own. * Memory and compute-intensive applications: Build onchain games, generative art, or other applications that benefit from reduced memory costs — Stylus programs can use up to 8 MB of memory, and costs grow near-linearly with the size of the workload. You can write the entire application in Stylus or optimize specific parts of existing Solidity contracts. * Cryptographic applications: Implement applications that require advanced cryptography or other compute-heavy operations that would be cost-prohibitive in Solidity. ## When Stylus saves gas (and when it doesn't) Stylus's gas advantage is concentrated in **computation**. Storage operations cost roughly the same as in the EVM, so storage-heavy contracts gain little by porting. Quick rule of thumb * **Use Stylus** for compute-heavy logic: cryptography, math-heavy DeFi, ZK verification, in-memory algorithms, ports of existing Rust/C/C++ libraries. * **Stick with Solidity** when the contract mostly reads and writes storage with little computation in between — you won't see a meaningful gas reduction. * **Hybrid approach**: keep your storage layout in Solidity and call out to a Stylus contract for the compute-heavy hot paths. ## Getting started 1. Follow the [quickstart](/stylus/quickstart.md) to deploy your first Stylus contract, and explore the [Rust SDK](/stylus/reference/overview.md) documentation. 2. Join the Stylus Developer [Telegram](https://t.me/arbitrum_stylus) group and [Arbitrum Discord](https://discord.gg/arbitrum) for community support. 3. Browse the [Awesome Stylus](https://github.com/OffchainLabs/awesome-stylus) repository for community-contributed projects, examples, and tools. 4. Subscribe to the [Stylus Saturdays](https://stylus-saturdays.com/) newsletter for tutorials and technical content. --- > For a complete page index, fetch # Caching contracts with Stylus Stylus is designed for fast computation and efficiency. However, the initialization process when entering a contract can be resource-intensive and time-consuming. This initialization process, if repeated frequently, may lead to inefficiencies. To address this, we have implemented a caching strategy. By storing frequently accessed contracts in memory, we can avoid repeated initializations. This approach saves resources and time, significantly enhancing the speed and efficiency of contract execution. ## CacheManager contract The core component of our caching strategy is the [`CacheManager` contract](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/chain/CacheManager.sol). This smart contract manages the cache, interacts with precompiles, and determines which contracts should be cached. The `CacheManager` can hold approximately 4,000 contracts in memory. The `CacheManager` defines how contracts remain in the cache and how they compete with other contracts for cache space. Its primary purpose is to reduce high initialization costs, ensuring efficient contract activation and usage. The contract includes methods for adding and removing cache entries, querying the status of cached contracts, and managing the lifecycle of cached data. ### Key features The `CacheManager` plays a crucial role in our caching strategy by keeping a specific set of contracts in memory rather than retrieving them from disk. This significantly reduces the activation time for frequently accessed contracts. The `CacheManager` contract is an onchain contract that accepts bids for inserting contract code into the cache. It then calls a precompile that loads or unloads the contracts in the `ArbOS` cache, which follows the onchain cache but operates locally in the client and marks the contract as in or out of the cache in the `ArbOS` state. The cache operates through an auction system in which app developers submit bids to have their contracts inserted into the cache. If the cache is at capacity, lower bids are evicted to make space for higher bids. The cache maintains a minimum heap of bids for `codeHashes`, with bids encoded as `bid << 64 + index`, where `index` represents the position in the list of all bids. When an insertion exceeds the cache's maximum size, items are popped off the minimum heap and deleted until there is enough space to insert the new item. Contracts with equal bids will be popped in a random order, while the smallest bid is evicted first. To ensure that developers periodically pay to maintain their position in the cache, we use a global decay parameter computed by `decay = block.timestamp * _decay`. This inflates the value of bids over time, making newer bids more valuable. ### Cache access and costs During activation, we compute the contract's initialization costs for both non-cached and cached initialization. These costs take into account factors such as the number of functions, types, code size, data length, and memory usage. It's important to note that accessing an uncached contract does not automatically add it to the `CacheManager`'s cache. Only explicit calls to the `CacheManager` contract will add a contract to the cache. If a contract is removed from the cache, calling the contract becomes more expensive unless it is re-added. To see how much gas contract initialization would cost, you need to call `programInitGas(address)` from the [ArbWasm precompile](/arbitrum-essentials/precompiles/reference.md#common-precompiles). This function returns both the initialization cost when the contract is cached and when it is not. ### How to use the CacheManager API This section provides a practical guide for interacting with the `CacheManager` contract API, either directly or through the `cargo stylus` command-line tool. ## Step 1: Determine the minimum bid Before placing a bid, it's important to know the minimum bid required to cache the Stylys contract. This can be done using the `getMinBid` function, or using the `cargo stylus cache suggest-bid` command. ### Method 1: Direct smart contract call ```solidity uint192 minBid = cacheManager.getMinBid(contractAddress); ``` ### Method 2: Cargo stylus command Here, the \[`contractAddress`] is the address of the Stylus contract you want to cache. ```shell cargo stylus cache suggest-bid [contractAddress] ``` ## Step 2: Place a bid You can place a bid using either of the following methods: ### Method 1: Direct smart contract call Here, `bidAmount` is the amount you want to bid, and `contractAddress` is the address of the Stylus contract you're bidding for. ```solidity cacheManager.placeBid{value: bidAmount}(contractAddress); ``` ### Method 2: Cargo stylus command You can place a bid using the `cargo stylus cache bid` command: ```shell cargo stylus cache bid <--private-key-path |--private-key |--keystore-path > [contractAddress] [bidAmount] ``` * `[contractAddress]`: The address of the Stylus contract you want to cache. * `[bidAmount]`: The amount you want to bid. If not specified, the default bid is 0. If you specify a bid amount using `cargo stylus`, it will automatically validate that the bid is greater than or equal to the result of the `getMinBid` function. If the bid is insufficient, the command will fail, ensuring that only valid bids are placed. ## Step 3: Check cache status To check if a specific address is cached, you can use the `cargo stylus status` command: ```shell cargo stylus cache status --address=[contractAddress] ``` ### Additional information * **Pausing Bids**: The `CacheManager` contract has an `isPaused` state that can be toggled by the owner to prevent or allow new bids. * **Checking Cache Size**: You can monitor the current cache size and decay rate using the `getCacheSize` and `getDecayRate` functions respectively. By following these steps, you can effectively interact with the `CacheManager` contract, either directly through smart contract calls or using the `cargo stylus` command-line tool. This ensures that your bids meet the necessary requirements for caching programs on the network, optimizing your contracts for faster and more efficient execution. --- > For a complete page index, fetch # Deploying non-Rust WASM contracts While Rust provides the best developer experience for Stylus, any language that compiles to WebAssembly can be deployed. This guide explains how to deploy WASM contracts written in C, C++, or even pure WebAssembly Text (WAT). ## Overview Stylus accepts any valid WebAssembly module that meets its requirements. You can: * Write contracts in **C or C++** using the Stylus C SDK * Use **WebAssembly Text (WAT)** for direct bytecode control * Compile from **any language** that targets `wasm32-unknown-unknown` * Deploy **pre-compiled WASM** binaries directly The key is using the `--wasm-file` flag with `cargo stylus` commands to bypass Rust compilation. ## Why use non-Rust languages? Different languages excel at different tasks: | Language | Best For | Use Cases | | ------------------ | -------------------------------------- | -------------------------------------------------- | | **C/C++** | Low-level control, cryptography | Hash functions, signature verification, algorithms | | **WAT** | Learning, debugging, minimal contracts | Simple logic, educational examples | | **AssemblyScript** | TypeScript developers | Web3 integration with familiar syntax | | **Other** | Specific requirements | Domain-specific computations | ### When to choose non-Rust * **Existing codebase**: Port existing C/C++ cryptography libraries * **Performance-critical**: Hand-optimized assembly-like control * **Minimal size**: Ultra-compact contracts for specific operations * **Team expertise**: Leverage existing C/C++ knowledge ### When to stick with Rust * **Full-featured contracts**: Complex DeFi, NFTs, governance * **Type safety**: Strong guarantees and tooling * **Ecosystem**: Rich library support and examples * **Productivity**: Higher-level abstractions and macros ## WASM requirements All WASM modules deployed to Stylus must meet these requirements: ### Required exports ```wasm (export "user_entrypoint" (func $user_entrypoint)) (export "memory" (memory 0)) ``` The `user_entrypoint` function: * **Signature**: `(param i32) (result i32)` * **Parameter**: Length of input calldata in bytes * **Returns**: Length of output data in bytes ### Allowed imports Only functions from the `vm_hooks` module are permitted: ```wasm (import "vm_hooks" "msg_sender" (func $msg_sender (param i32))) (import "vm_hooks" "storage_load_bytes32" (func $storage_load (param i32 i32))) (import "vm_hooks" "storage_store_bytes32" (func $storage_store (param i32 i32))) ``` See the [hostio exports documentation](/stylus/advanced/hostio-exports.md) for the complete list of available VM hooks. ### Memory requirements * Linear memory must be exported as `"memory"` * Memory growth must be explicitly paid for * Initial memory size should be minimal (often `0 0`) * Gas costs limit maximum memory ### Compilation target * **Target triple**: `wasm32-unknown-unknown` * **No standard library**: WASM runs in a sandboxed environment * **No floating point**: Not yet supported by Stylus * **No SIMD**: Not yet supported by Stylus * **No reference types**: Disabled for compatibility ## WebAssembly Text (WAT) WAT provides direct control over WASM bytecode using a human-readable text format. ### Minimal contract The simplest valid Stylus contract: ```wasm (module ;; Export linear memory (memory 0 0) (export "memory" (memory 0)) ;; Required entrypoint ;; Takes calldata length, returns output length (func (export "user_entrypoint") (param $args_len i32) (result i32) (i32.const 0) ;; Return 0 bytes )) ``` Save as `minimal.wat` and deploy: ```shell cargo stylus deploy --wasm-file=minimal.wat --private-key-path=./key.txt ``` ### Echo contract Returns input data unchanged: ```wasm (module (memory 1 1) (export "memory" (memory 0)) ;; Import VM hook to read calldata (import "vm_hooks" "read_args" (func $read_args (param i32))) (func (export "user_entrypoint") (param $args_len i32) (result i32) ;; Read calldata into memory at offset 0 (call $read_args (i32.const 0)) ;; Return the same length (echo) (local.get $args_len) )) ``` ### Storage counter Increment a value in storage: ```wasm (module (memory 1 1) (export "memory" (memory 0)) ;; Import storage operations (import "vm_hooks" "storage_load_bytes32" (func $storage_load (param i32 i32))) (import "vm_hooks" "storage_store_bytes32" (func $storage_store (param i32 i32))) (func (export "user_entrypoint") (param $args_len i32) (result i32) ;; Load current value from storage slot 0 (call $storage_load (i32.const 0) ;; key pointer (i32.const 32)) ;; value destination ;; Increment the value at memory[32] (i32.store (i32.const 32) (i32.add (i32.load (i32.const 32)) (i32.const 1))) ;; Store back to storage (call $storage_store (i32.const 0) ;; key pointer (i32.const 32)) ;; value pointer ;; Return 0 bytes of output (i32.const 0) )) ``` ### Checking WAT contracts Validate before deploying: ```shell cargo stylus check --wasm-file=counter.wat ``` Output shows validation results: ```text Reading WASM file at counter.wat Compressed WASM size: 142 B Contract succeeded Stylus onchain activation checks with Stylus version: 2 ``` ## C/C++ development The [Stylus C SDK](https://github.com/OffchainLabs/stylus-sdk-c) enables C/C++ smart contract development. ### Installation Install the C SDK: ```shell git clone https://github.com/OffchainLabs/stylus-sdk-c.git cd stylus-sdk-c ``` Install dependencies: ```shell # macOS brew install llvm binaryen wabt # Ubuntu/Debian sudo apt-get install clang lld wasm-ld binaryen wabt ``` ### Project structure Basic C project layout: ```text my-contract/ ├── Makefile ├── src/ │ └── main.c └── include/ └── stylus_sdk.h ``` ### Simple C contract ```c // main.c #include "stylus_sdk.h" // Storage slot for counter static uint8_t counter_slot[32] = {0}; // Stylus calls user_entrypoint, not main. It receives the calldata length // and returns the output length. int user_entrypoint(int args_len) { // Load counter from storage uint8_t value[32]; storage_load_bytes32(counter_slot, value); // Increment value[31]++; // Store back storage_store_bytes32(counter_slot, value); return 0; // No output } ``` ### C SDK features The C SDK provides: ```c // Account operations void msg_sender(uint8_t *sender); void tx_origin(uint8_t *origin); void contract_address(uint8_t *addr); // Storage operations void storage_load_bytes32(uint8_t *key, uint8_t *dest); void storage_store_bytes32(uint8_t *key, uint8_t *value); // Block information uint64_t block_timestamp(void); uint64_t block_number(void); void block_basefee(uint8_t *basefee); // Call operations void call_contract( uint8_t *contract, uint8_t *calldata, uint32_t calldata_len, uint8_t *value, uint32_t gas, uint8_t *return_data_len ); // And many more... ``` ### Building C contracts Create a Makefile: ```makefile CLANG = clang WASM_LD = wasm-ld WASM_OPT = wasm-opt CFLAGS = -target wasm32 -nostdlib -O3 LDFLAGS = -no-entry --export=user_entrypoint --export=memory SRC = src/main.c OUT = build/contract.wasm OUT_OPT = build/contract-opt.wasm all: $(OUT_OPT) $(OUT): $(SRC) mkdir -p build $(CLANG) $(CFLAGS) -c $(SRC) -o build/main.o $(WASM_LD) $(LDFLAGS) build/main.o -o $(OUT) $(OUT_OPT): $(OUT) $(WASM_OPT) -Oz $(OUT) -o $(OUT_OPT) clean: rm -rf build deploy: $(OUT_OPT) cargo stylus deploy --wasm-file=$(OUT_OPT) \ --private-key-path=$$PRIVATE_KEY_PATH check: $(OUT_OPT) cargo stylus check --wasm-file=$(OUT_OPT) ``` Build and deploy: ```shell make make check make deploy ``` ### C cryptography example Verifying a signature: ```c #include "stylus_sdk.h" #include // Verify ECDSA signature int verify_signature( uint8_t *message_hash, uint8_t *signature, uint8_t *public_key ) { uint8_t recovered[65]; // Recover signer from signature if (ecrecover(message_hash, signature, recovered) != 0) { return -1; // Recovery failed } // Compare with expected public key if (memcmp(recovered + 1, public_key, 64) == 0) { return 0; // Valid signature } return -1; // Invalid signature } int user_entrypoint(int args_len) { uint8_t msg_hash[32]; uint8_t sig[65]; uint8_t pubkey[64]; // Read inputs from calldata read_args(0); memcpy(msg_hash, memory, 32); memcpy(sig, memory + 32, 65); memcpy(pubkey, memory + 97, 64); // Verify int result = verify_signature(msg_hash, sig, pubkey); // Write result memory[0] = (result == 0) ? 1 : 0; write_result(memory, 1); return 0; } ``` ## AssemblyScript contracts AssemblyScript is a TypeScript-like language that compiles to WebAssembly. ### Installation ```shell npm install -g assemblyscript npm install @assemblyscript/loader ``` ### Simple AssemblyScript contract ```typescript // contract.ts // Import Stylus VM hooks @external("vm_hooks", "msg_sender") declare function msg_sender(ptr: usize): void; @external("vm_hooks", "storage_load_bytes32") declare function storage_load(key: usize, dest: usize): void; @external("vm_hooks", "storage_store_bytes32") declare function storage_store(key: usize, value: usize): void; // Storage key const COUNTER_KEY: StaticArray = [0, 0, 0, 0, /* ... 32 zeros ... */]; // Entrypoint export function user_entrypoint(args_len: i32): i32 { // Load counter let value = new StaticArray(32); storage_load( changetype(COUNTER_KEY), changetype(value) ); // Increment value[31]++; // Store storage_store( changetype(COUNTER_KEY), changetype(value) ); return 0; // No output } ``` ### Compile AssemblyScript ```shell asc contract.ts \ --target release \ --exportRuntime \ --exportTable \ -o contract.wasm ``` ### Deploy the AssemblyScript contract ```shell cargo stylus deploy \ --wasm-file=contract.wasm \ --private-key-path=./key.txt ``` ## Example: writing a contract in Zig [Zig](https://ziglang.org/) is a systems language often described as a spiritual successor to C: it adds memory-safety guardrails, produces small binaries, and ships with a C compiler so existing C projects can adopt it incrementally. Because Zig compiles to WebAssembly, you can use it to write Stylus contracts that fit comfortably within the 24 KB Brotli-compressed limit and meet Stylus gas-metering requirements. Programs written in Zig have gas costs comparable to C. Info This walkthrough targets **Zig 0.11.0**. Some APIs used below (for example `std.mem.readIntSliceLittle` and the `std.heap.WasmAllocator` internals) changed or were removed in later Zig releases, so pin to 0.11.0 when following along. ### Requirements * Download and install [Zig 0.11.0](https://ziglang.org/downloads). * Install [Rust](https://www.rust-lang.org/tools/install), which the [Stylus CLI tool](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus) needs to deploy your program. This example also uses Rust to run a script that calls the Zig contract using the [ethers-rs](https://github.com/gakonst/ethers-rs) library. Once Rust is installed, install the Stylus CLI tool: ```shell RUSTFLAGS="-C link-args=-rdynamic" cargo install --force cargo-stylus ``` ### A minimal Zig entrypoint Clone the example repository: ```shell git clone https://github.com/offchainlabs/zig-on-stylus && cd zig-on-stylus ``` Then delete everything inside `main.zig` — you'll fill it out from scratch. A Stylus contract needs a special entrypoint function that takes the length of its input arguments (`len`) and returns a status code `i32` of either `0` or `1`. It also needs `memory_grow`, a function injected into every Stylus contract as an external import to allocate memory. These imports are called `vm_hooks` (or host I/Os), and they give the contract access to the host EVM environment. You don't need the Zig standard library yet. Replace everything in `main.zig` with: ```zig pub extern "vm_hooks" fn memory_grow(len: u32) void; export fn mark_unused() void { memory_grow(0); @panic(""); } // The main entrypoint to use for execution of the Stylus WASM program. export fn user_entrypoint(len: usize) i32 { _ = len; return 0; } ``` Build the Zig library to a freestanding WASM file for onchain deployment: ```shell zig build-lib ./src/main.zig -target wasm32-freestanding -dynamic --export=user_entrypoint -OReleaseSmall --export=mark_unused ``` Deploy it with the Stylus CLI tool. This example deploys to Arbitrum Sepolia; you can also target a [local Stylus devnode](/run-arbitrum-node/run-local-full-chain-simulation.md) at `http://localhost:8547`: ```shell cargo stylus deploy \ --private-key= \ --wasm-file=main.wasm \ --endpoint="https://sepolia-rollup.arbitrum.io/rpc" ``` The tool sends two transactions: one to deploy the contract code onchain, and one to activate it. ```text Uncompressed WASM size: 112 B Compressed WASM size to be deployed onchain: 103 B ``` The Zig program is tiny when compiled to WASM. Call the contract with any Ethereum tooling — here, the `cast` CLI from [Foundry](https://github.com/foundry-rs/foundry): ```shell export ADDR= cast call --rpc-url 'https://sepolia-rollup.arbitrum.io/rpc' $ADDR '0x' ``` Calling the contract returns `0`, as programmed: ```text 0x ``` ### Reading input and writing output data To do anything useful, a contract needs to read input and write output. The Stylus runtime provides two host I/Os for this: ```zig pub extern "vm_hooks" fn read_args(dest: *u8) void; pub extern "vm_hooks" fn write_result(data: *const u8, len: usize) void; ``` Add these near the top of `main.zig`. `read_args` takes a pointer to a byte slice where the input arguments are written. The slice length must equal the length of the program args received in `user_entrypoint`. Write a helper that wraps this host I/O and returns a Zig byte slice: ```zig // Allocates a Zig byte slice of length=`len` and reads a Stylus contract's // calldata using the read_args hostio function. pub fn input(len: usize) ![]u8 { var input = try allocator.alloc(u8, len); read_args(@ptrCast(*u8, input)); return input; } ``` Next, a helper that outputs bytes to the caller: ```zig // Outputs data as bytes via the write_result hostio to the Stylus contract's caller. pub fn output(data: []u8) void { write_result(@ptrCast(*u8, data), data.len); } ``` Put them together to echo the input back to the caller: ```zig // The main entrypoint to use for execution of the Stylus WASM program. // It echoes the input arguments to the caller. export fn user_entrypoint(len: usize) i32 { var in = input(len) catch return 1; output(in); return 0; } ``` Rebuilding now fails because there's no allocator: ```text src/main.zig:21:20: error: use of undeclared identifier 'allocator' var data = try allocator.alloc(u8, len); ^~~~~~~~~ ``` Zig requires you to provide an allocator explicitly. The standard library ships one built for WASM programs, where memory grows in 64 KB increments. Add this to the top of `main.zig`: ```zig const std = @import("std"); const allocator = std.heap.WasmAllocator; ``` The code compiles, but `cargo stylus check --wasm-file=main.wasm` fails: ```text Caused by: missing import memory_grow ``` The standard-library `WasmAllocator` needs to call our `memory_grow` host I/O under the hood. Fix this by copying the `WasmAllocator.zig` file from the standard library and changing a single line to use `memory_grow`. You can find this modified file as `WasmAllocator.zig` in the [zig-on-stylus repository](https://github.com/offchainlabs/zig-on-stylus). Use it like so: ```zig const std = @import("std"); const WasmAllocator = @import("WasmAllocator.zig"); // Uses our custom WasmAllocator, a simple modification over the wasm allocator // from the Zig standard library as of Zig 0.11.0. pub const allocator = std.mem.Allocator{ .ptr = undefined, .vtable = &WasmAllocator.vtable, }; ``` Rebuild and run `cargo stylus check` again — it now succeeds: ```text Uncompressed WASM size: 514 B Compressed WASM size to be deployed onchain: 341 B Connecting to Stylus RPC endpoint: https://sepolia-rollup.arbitrum.io/rpc Stylus program with same WASM code is already activated onchain ``` Deploy it: ```shell cargo stylus deploy \ --private-key= \ --wasm-file=main.wasm \ --endpoint="https://sepolia-rollup.arbitrum.io/rpc" ``` Now calling the contract echoes back whatever input you send. Send it `0x123456`: ```shell export ADDR= cast call --rpc-url 'https://sepolia-rollup.arbitrum.io/rpc' $ADDR '0x123456' 0x123456 ``` ### Prime number checker For something fancier, implement a primality checker using the [Sieve of Eratosthenes](https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes). Given a number, the contract outputs `1` if it's prime or `0` otherwise. This example leverages Zig's [comptime](https://kristoff.it/blog/what-is-zig-comptime/) keyword, which tells the compiler to evaluate code at compile time. Here, it defines a slice of booleans up to a fixed limit at compile time, marking which numbers are prime. ```zig fn sieve_of_erathosthenes(comptime limit: usize, nth: u16) bool { var prime = [_]bool{true} ** limit; prime[0] = false; prime[1] = false; var i: usize = 2; while (i * i < limit) : (i += 1) { if (prime[i]) { var j = i * i; while (j < limit) : (j += i) prime[j] = false; } } return prime[nth]; } ``` Checking whether a number `N` is prime is just reading index `N` of the `prime` slice. Integrate it into `user_entrypoint`: ```zig // The main entrypoint to use for execution of the Stylus WASM program. export fn user_entrypoint(len: usize) i32 { // Expects the input is a u16 encoded as little endian bytes. var in = input(len) catch return 1; var check_nth_prime = std.mem.readIntSliceLittle(u16, in); const limit: u16 = 10_000; if (check_nth_prime > limit) { @panic("input is greater than limit of 10,000 primes"); } // Checks if the number is prime and returns a boolean using the output function. var is_prime = sieve_of_erathosthenes(limit, check_nth_prime); var out = in[0..1]; if (is_prime) { out[0] = 1; } else { out[0] = 0; } output(out); return 0; } ``` Check and deploy: ```text Uncompressed WASM size: 10.8 KB Compressed WASM size to be deployed onchain: 525 B ``` The uncompressed size is large because of the boolean array, but it compresses well since the values are mostly zeros. ### Calling the Zig contract from Rust The repository includes a `rust-example` that uses [ethers-rs](https://github.com/gakonst/ethers-rs) to call the prime-sieve contract. Run it with: ```shell export STYLUS_PROGRAM_ADDRESS= cargo run ``` You'll see output like: ```text Checking if 2 is_prime = true, took: 404.146917ms Checking if 3 is_prime = true, took: 154.802083ms Checking if 4 is_prime = false, took: 123.239583ms Checking if 5 is_prime = true, took: 109.248709ms Checking if 6 is_prime = false, took: 113.086625ms Checking if 32 is_prime = false, took: 280.19975ms Checking if 53 is_prime = true, took: 123.667958ms ``` The host I/Os shown here aren't the only ones — see [stylus-sdk-c](https://github.com/OffchainLabs/stylus-sdk-c) (`hostio.h`) for the full list, including affordances for the EVM, storage access, and calling other Arbitrum contracts. ## Deployment workflow ### 1. Prepare your WASM Ensure your WASM module meets requirements: ```shell # Check WASM structure with wasm-objdump wasm-objdump -x contract.wasm | grep -A 5 "Export\|Import" # Should show: # Export[0]: # - func[0] # - memory[0] # Import[0]: # - module="vm_hooks" func=... ``` ### 2. Optimize the WASM Reduce size with wasm-opt: ```shell wasm-opt -Oz contract.wasm -o contract-opt.wasm ``` ### 3. Check before deploying Validate the contract: ```shell cargo stylus check --wasm-file=contract-opt.wasm ``` ### 4. Deploy Deploy to testnet: ```shell cargo stylus deploy \ --wasm-file=contract-opt.wasm \ --private-key-path=./key.txt \ --endpoint="https://sepolia-rollup.arbitrum.io/rpc" ``` ### 5. Verify deployment Check deployment succeeded: ```shell # Output shows: Compressed WASM size: 245 B Deploying contract to address 0x... Confirmed tx 0x... Activating contract at address 0x... Confirmed tx 0x... ``` ## Best practices ### 1. Minimize binary size ```shell # ✅ Good: Optimize aggressively wasm-opt -Oz input.wasm -o output.wasm # Use wasm-strip to remove symbols wasm-strip output.wasm # Check final size ls -lh output.wasm ``` ### 2. Test with cargo stylus check ```shell # ✅ Good: Always check before deploying cargo stylus check --wasm-file=contract.wasm # Test on testnet first cargo stylus deploy \ --wasm-file=contract.wasm \ --private-key-path=./key.txt \ --endpoint="https://sepolia-rollup.arbitrum.io/rpc" ``` ### 3. Use standard memory layout ```c // ✅ Good: Predictable memory layout uint8_t calldata[1024]; // 0-1023: Input data uint8_t storage[32]; // 1024-1055: Storage scratch uint8_t output[256]; // 1056-1311: Output buffer // ❌ Bad: Unpredictable allocations uint8_t *data = malloc(size); // No malloc in WASM! ``` ### 4. Handle calldata properly ```wasm ;; ✅ Good: Read calldata into memory (call $read_args (i32.const 0)) ;; Process the data (call $process_calldata (local.get $args_len)) ;; ❌ Bad: Assume calldata location (i32.load (i32.const 0)) ;; Calldata not automatically loaded ``` ### 5. Export all required functions ```wasm ;; ✅ Good: Export entrypoint and memory (export "user_entrypoint" (func $main)) (export "memory" (memory 0)) ;; ❌ Bad: Missing exports (export "main" (func $main)) ;; Wrong name! ``` ### 6. Use VM hooks correctly ```c // ✅ Good: Proper VM hook usage uint8_t sender[20]; msg_sender(sender); // ✅ Good: Check return values uint8_t success; call_contract(addr, data, len, value, gas, &success); if (!success) { revert("Call failed"); } // ❌ Bad: Ignoring errors call_contract(addr, data, len, value, gas, NULL); ``` ### 7. Mind the size limit ```shell # Check compressed size cargo stylus check --wasm-file=contract.wasm # Should show: # Compressed WASM size: < 24 KB # If too large: # - Remove debug symbols # - Enable aggressive optimization # - Minimize code and data sections ``` ## Troubleshooting ### Missing entrypoint **Error**: `WASM is missing the entrypoint export` **Solution**: Ensure `user_entrypoint` is exported: ```wasm ;; WAT (func (export "user_entrypoint") (param i32) (result i32) ;; Implementation ) // C int user_entrypoint(int argc) __attribute__((export_name("user_entrypoint"))); ``` ### Invalid imports **Error**: `contract imports unauthorized function` **Solution**: Only import from `vm_hooks`: ```wasm ;; ✅ Allowed (import "vm_hooks" "msg_sender" (func $msg_sender (param i32))) ;; ❌ Not allowed (import "env" "print" (func $print (param i32))) ``` ### Memory not exported **Error**: `WASM must export memory` **Solution**: Export linear memory: ```wasm ;; WAT (memory 1 1) (export "memory" (memory 0)) // C Makefile LDFLAGS = -no-entry --export=user_entrypoint --export=memory ``` ### Size too large **Error**: `Compressed WASM exceeds 24KB` **Solutions**: 1. Optimize with wasm-opt: ```shell wasm-opt -Oz input.wasm -o output.wasm ``` 2. Strip symbols: ```shell wasm-strip output.wasm ``` 3. Remove unused code: ```c // Use static/inline for internal functions static inline void helper(void) { } ``` 4. Minimize data section: ```c // ✅ Good: Minimal data const uint8_t PREFIX[4] = {0xEF, 0xF0, 0x00, 0x00}; // ❌ Bad: Large data const char *STRINGS[1000] = { /* ... */ }; ``` ### Compilation errors **Error**: Clang fails to compile **Solutions**: 1. Target wasm32: ```shell clang -target wasm32 -nostdlib -c main.c ``` 2. Disable standard library: ```c // Don't use stdio, stdlib, etc. // Use SDK-provided functions ``` 3. Check imports/exports: ```shell wasm-objdump -x contract.wasm ``` ### Runtime errors **Error**: Contract reverts unexpectedly **Solutions**: 1. Check gas usage: ```shell cargo stylus deploy --estimate-gas --wasm-file=contract.wasm ``` 2. Add debug output (testnet only): ```c emit_log(error_msg, sizeof(error_msg)); ``` 3. Test with minimal input: ```shell # Call with empty calldata cast call $CONTRACT "0x" ``` ## Examples repository Official examples for different languages: * [**Stylus C SDK**](https://github.com/OffchainLabs/stylus-sdk-c) - C/C++ examples * [**Stylus Bf SDK**](https://github.com/OffchainLabs/stylus-sdk-bf) - Brainfuck educational examples * [**Awesome Stylus**](https://github.com/OffchainLabs/awesome-stylus) - Community examples ## Language support matrix | Language | Status | SDK | Best Use Case | | ------------------ | --------------- | -------------------------------------------------------------- | --------------------------- | | **Rust** | ✅ Production | [stylus-sdk-rs](https://github.com/OffchainLabs/stylus-sdk-rs) | Full-featured contracts | | **C/C++** | ✅ Production | [stylus-sdk-c](https://github.com/OffchainLabs/stylus-sdk-c) | Cryptography, algorithms | | **WAT** | ✅ Supported | Manual | Minimal contracts, learning | | **AssemblyScript** | 🔶 Community | Custom | TypeScript developers | | **Go** | 🔶 Experimental | TinyGo | Custom applications | | **Zig** | 🔶 Experimental | [zig-on-stylus](https://github.com/offchainlabs/zig-on-stylus) | Systems programming | ## Advanced: custom languages To support a new language: 1. **Compile to wasm32-unknown-unknown** ```shell your-compiler --target=wasm32-unknown-unknown input.src -o output.wasm ``` 2. **Export required functions** ```text - user_entrypoint(i32) -> i32 - memory ``` 3. **Import only vm\_hooks** ```text - vm_hooks:msg_sender - vm_hooks:storage_* - etc. ``` 4. **Test and deploy** ```shell cargo stylus check --wasm-file=output.wasm cargo stylus deploy --wasm-file=output.wasm --private-key-path=./key.txt ``` ## Resources * [Stylus C SDK](https://github.com/OffchainLabs/stylus-sdk-c) * [WebAssembly specification](https://webassembly.github.io/spec/) * [WAT format reference](https://webassembly.github.io/spec/core/text/) * [Cargo Stylus CLI](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus) * [Awesome Stylus](https://github.com/OffchainLabs/awesome-stylus) * [WASM binary toolkit (wabt)](https://github.com/WebAssembly/wabt) * [Binaryen optimization tools](https://github.com/WebAssembly/binaryen) ## Sources * [GitHub - OffchainLabs/stylus-sdk-c: C/C++ Smart Contracts on Arbitrum](https://github.com/OffchainLabs/stylus-sdk-c) * [GitHub - OffchainLabs/awesome-stylus](https://github.com/OffchainLabs/awesome-stylus) * [GitHub - OffchainLabs/stylus-sdk-rs: Rust Smart Contracts on Arbitrum](https://github.com/OffchainLabs/stylus-sdk-rs) --- > For a complete page index, fetch # Exporting ABIs Stylus contracts written in Rust can automatically generate Solidity Application Binary Interfaces (ABIs) that enable interoperability with existing Ethereum tools, front-end libraries, and other smart contracts. ## What is an ABI? An Application Binary Interface (ABI) defines how to interact with a smart contract: * **Function signatures**: Names, parameters, and return types * **Events**: Event definitions and indexed parameters * **Errors**: Custom error types and parameters * **Constructor**: Initialization parameters ABIs enable: * Front-end libraries (ethers.js, web3.js, viem) to interact with contracts * Solidity contracts to call Rust contracts * Rust contracts to call Solidity contracts * Block explorers to decode transactions * Development tools to provide type-safe interfaces ## Overview: ABI generation Stylus contracts generate ABIs through: 1. **`#[public]` macro**: Annotates public functions 2. **`export-abi` feature**: Enables ABI generation code 3. **`cargo stylus export-abi`**: CLI command to generate output 4. **Solidity interface**: Generated Solidity interface file 5. **JSON format**: Optional JSON ABI for tool integration The process is automatic—annotate your functions with `#[public]` and run the export command. ## Basic usage ### Export Solidity interface Generate a Solidity interface for your contract: ```shell cargo stylus export-abi ``` Output: ```solidity /** * This file was automatically generated by Stylus and represents a Rust program. * For more information, please see [The Stylus SDK](https://github.com/OffchainLabs/stylus-sdk-rs). */ // SPDX-License-Identifier: MIT-OR-APACHE-2.0 pragma solidity ^0.8.23; interface IMyContract { function getValue() external view returns (uint256); function setValue(uint256 new_value) external; error Unauthorized(address); } ``` ### Export to file Save the interface to a file: ```shell cargo stylus export-abi > IMyContract.sol ``` Or specify output path: ```shell cargo stylus export-abi --output=./interfaces/IMyContract.sol ``` ### Export JSON ABI Generate JSON format ABI (requires `solc` installed): ```shell cargo stylus export-abi --json > abi.json ``` The JSON output is produced by `solc`, so it includes `solc`'s header lines before the ABI array: ```text ======= :IMyContract ======= Contract JSON ABI [{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"Unauthorized","type":"error"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"new_value","type":"uint256"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}] ``` Strip the two header lines before feeding the array to front-end tooling. ## Writing ABI-compatible contracts ### Basic contract structure Contracts must use the [`#[public]` macro](/stylus/fundamentals/contracts.md#public-methods-with-public) to generate ABIs: ```rust use stylus_sdk::{alloy_primitives::U256, prelude::*}; sol_storage! { #[entrypoint] pub struct Counter { uint256 count; } } #[public] impl Counter { // This function will be included in the ABI pub fn get_count(&self) -> U256 { self.count.get() } // This function will also be included pub fn increment(&mut self) { let count = self.count.get() + U256::from(1); self.count.set(count); } } ``` Generated interface: ```solidity interface ICounter { function getCount() external view returns (uint256); function increment() external; } ``` ### Function visibility mapping Rust function signatures map to Solidity visibility: ```rust #[public] impl MyContract { // Immutable reference → view function pub fn read_value(&self) -> U256 { self.value.get() } // Mutable reference → non-view function pub fn write_value(&mut self, new_value: U256) { self.value.set(new_value); } // Pure computation (no self) → pure function pub fn compute(a: U256, b: U256) -> U256 { a + b } } ``` Generated Solidity: ```solidity interface IMyContract { function readValue() external view returns (uint256); function writeValue(uint256 new_value) external; function compute(uint256 a, uint256 b) external pure returns (uint256); } ``` ### Type mapping Rust types map to Solidity types automatically: | Rust Type | Solidity Type | Example | | ------------------------- | ------------------------------------- | ---------------------- | | `U256` | `uint256` | Token amounts | | `U128`, `u128` | `uint128` | Medium integers | | `u64`, `u32`, `u16`, `u8` | `uint64`, `uint32`, `uint16`, `uint8` | Small integers | | `I256` | `int256` | Signed integers | | `I128`, `i128` | `int128` | Medium signed integers | | `i64`, `i32`, `i16`, `i8` | `int64`, `int32`, `int16`, `int8` | Small signed integers | | `Address` | `address` | Account addresses | | `bool` | `bool` | Boolean values | | `FixedBytes` | `bytesN` | Fixed-size byte arrays | | `Bytes` | `bytes` | Dynamic byte arrays | | `String` | `string` | UTF-8 strings | | `Vec` | `T[]` | Dynamic arrays | | `[T; N]` | `T[N]` | Fixed-size arrays | Example: ```rust #[public] impl MyContract { pub fn process( owner: Address, amount: U256, data: Bytes, flags: Vec, ) -> Result { // Implementation } } ``` Generates: ```solidity interface IMyContract { function process( address owner, uint256 amount, bytes calldata data, bool[] calldata flags ) external returns (string memory); } ``` ### Custom errors Define custom errors with parameters: ```rust use stylus_sdk::alloy_sol_types::sol; use stylus_sdk::prelude::*; sol! { error InsufficientBalance(address account, uint256 requested, uint256 available); error Unauthorized(address caller); error InvalidAmount(); } #[derive(SolidityError)] pub enum TokenError { InsufficientBalance(InsufficientBalance), Unauthorized(Unauthorized), InvalidAmount(InvalidAmount), } #[public] impl Token { pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), TokenError> { let sender = self.vm().msg_sender(); let balance = self.balances.get(sender); if balance < amount { return Err(TokenError::InsufficientBalance(InsufficientBalance { account: sender, requested: amount, available: balance, })); } // Transfer logic Ok(()) } } ``` Generated interface includes errors. Note that the exported Solidity drops the error parameter names, keeping only their types: ```solidity interface IToken { function transfer(address to, uint256 amount) external; error InsufficientBalance(address, uint256, uint256); error Unauthorized(address); error InvalidAmount(); } ``` ### Events Events are automatically included in the ABI: ```rust use stylus_sdk::alloy_sol_types::sol; use stylus_sdk::prelude::*; sol! { event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); } #[public] impl Token { pub fn transfer(&mut self, to: Address, value: U256) -> bool { // Transfer logic self.vm().log(Transfer { from: self.vm().msg_sender(), to, value, }); true } } ``` Generated interface: ```solidity interface IToken { function transfer(address to, uint256 value) external returns (bool); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); } ``` ## Trait implementation Export ABIs for trait implementations: ### Define a trait ```rust // ierc20.rs use stylus_sdk::prelude::*; #[public] pub trait IErc20 { fn name(&self) -> String; fn symbol(&self) -> String; fn decimals(&self) -> u8; fn total_supply(&self) -> U256; fn balance_of(&self, owner: Address) -> U256; fn transfer(&mut self, to: Address, value: U256) -> Result; } ``` ### Implement the trait ```rust // lib.rs use stylus_sdk::prelude::*; sol_storage! { #[entrypoint] struct MyToken { // Storage fields } } #[public] #[implements(IErc20)] impl MyToken { // Additional functions beyond the trait pub fn mint(&mut self, to: Address, value: U256) { // Mint logic } } #[public] impl IErc20 for MyToken { fn name(&self) -> String { "My Token".to_string() } fn symbol(&self) -> String { "MTK".to_string() } fn decimals(&self) -> u8 { 18 } fn total_supply(&self) -> U256 { self.total_supply.get() } fn balance_of(&self, owner: Address) -> U256 { self.balances.get(owner) } fn transfer(&mut self, to: Address, value: U256) -> Result { // Transfer logic Ok(true) } } ``` Generated interface with inheritance: ```solidity interface IMyToken is IIErc20 { function mint(address to, uint256 value) external; } interface IIErc20 { function name() external view returns (string memory); function symbol() external view returns (string memory); function decimals() external view returns (uint8); function totalSupply() external view returns (uint256); function balanceOf(address owner) external view returns (uint256); function transfer(address to, uint256 value) external returns (bool); } ``` ## Constructor signatures Export constructor signatures for deployment: ```rust sol_storage! { #[entrypoint] struct MyContract { address owner; uint256 initial_value; } } #[public] impl MyContract { #[constructor] pub fn new(&mut self, owner: Address, initial_value: U256) { self.owner.set(owner); self.initial_value.set(initial_value); } // Other methods... } ``` Export constructor signature with the top-level `constructor` command (the constructor is not part of `export-abi` output): ```shell cargo stylus constructor ``` Output: ```text constructor(address owner, uint256 initial_value) ``` For payable constructors: ```rust #[public] impl MyContract { #[constructor] #[payable] pub fn new(owner: Address) { self.owner.set(owner); // self.vm().msg_value() is available } } ``` Output: ```text constructor(address owner) payable ``` ## Export configuration ### Rust features Export ABI with specific Rust features enabled: ```shell cargo stylus export-abi --rust-features=feature1,feature2 ``` This is useful when your contract has conditional compilation: ```rust #[cfg(feature = "advanced")] #[public] impl MyContract { pub fn advanced_function(&self) -> U256 { // Advanced logic } } ``` ## Integration with front-end ### Using ethers.js ```typescript import { ethers } from 'ethers'; import MyContractABI from './abi.json'; const provider = new ethers.JsonRpcProvider('https://arb1.arbitrum.io/rpc'); const contract = new ethers.Contract('0x1234567890123456789012345678901234567890', MyContractABI, provider); // Call view function const value = await contract.getValue(); console.log('Value:', value.toString()); // Call state-changing function (requires signer) const signer = provider.getSigner(); const contractWithSigner = contract.connect(signer); const tx = await contractWithSigner.setValue(42); await tx.wait(); ``` ### Using viem ```typescript import { createPublicClient, http } from 'viem'; import { arbitrum } from 'viem/chains'; import MyContractABI from './abi.json'; const client = createPublicClient({ chain: arbitrum, transport: http(), }); // Read contract const value = await client.readContract({ address: '0x1234567890123456789012345678901234567890', abi: MyContractABI, functionName: 'getValue', }); // Write contract const hash = await client.writeContract({ address: '0x1234567890123456789012345678901234567890', abi: MyContractABI, functionName: 'setValue', args: [42n], }); ``` ### Using wagmi/RainbowKit ```typescript import { useContractRead, useContractWrite } from 'wagmi'; import MyContractABI from './abi.json'; function MyComponent() { // Read contract const { data: value } = useContractRead({ address: '0x1234567890123456789012345678901234567890', abi: MyContractABI, functionName: 'getValue', }); // Write contract const { write } = useContractWrite({ address: '0x1234567890123456789012345678901234567890', abi: MyContractABI, functionName: 'setValue', }); return (

Current value: {value?.toString()}

); } ``` ## Solidity integration Use exported interfaces in Solidity contracts: ### Import the interface ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import "./IMyContract.sol"; contract SolidityContract { IMyContract public stylusContract; constructor(address _stylusContract) { stylusContract = IMyContract(_stylusContract); } function interactWithStylus() external { // Read from Stylus contract uint256 value = stylusContract.getValue(); // Write to Stylus contract stylusContract.setValue(value + 1); } } ``` ### Cross-language composition Combine Solidity and Rust contracts: ```solidity contract Router { IToken public token; IStaking public staking; constructor(address _token, address _staking) { token = IToken(_token); // Rust contract staking = IStaking(_staking); // Rust contract } function stakeTokens(uint256 amount) external { // Transfer tokens (Rust contract) require( token.transferFrom(msg.sender, address(this), amount), "Transfer failed" ); // Stake tokens (Rust contract) token.approve(address(staking), amount); staking.stake(msg.sender, amount); } } ``` ## How it works ### The `export-abi` feature The `export-abi` feature enables ABI generation: ```toml # Cargo.toml [features] export-abi = ["stylus-sdk/export-abi"] [lib] crate-type = ["lib", "cdylib"] ``` When enabled, the SDK generates: 1. A `GenerateAbi` trait implementation 2. A CLI entry point for running ABI export 3. Formatting logic for Solidity interface generation ### Main function Your contract needs a main function for ABI export: ```rust // main.rs #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] #[cfg(not(any(test, feature = "export-abi")))] #[no_mangle] pub extern "C" fn main() {} #[cfg(feature = "export-abi")] fn main() { my_contract::print_from_args(); } ``` This is the main function: * Runs only when the `export-abi` feature is enabled * Executes the ABI generation logic * Outputs the Solidity interface to stdout ### The #\[public] macro The `#[public]` macro generates ABI code: ```rust // The macro generates an implementation roughly like this: impl GenerateAbi for MyContract { const NAME: &'static str = "MyContract"; fn fmt_abi(f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "interface I{} {{", Self::NAME)?; // Generated function signatures write!(f, "\n function getValue() external view returns (uint256);")?; writeln!(f, "\n}}")?; Ok(()) } fn fmt_constructor_signature(f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { // Emits the `constructor(...)` line printed by `cargo stylus constructor` write!(f, "constructor()") } } ``` Key transformations: * `snake_case` → `camelCase` function names * Rust types → Solidity types * `&self` → `view`, `&mut self` → non-view * `Result` → return type `T`, error `E` ## Best practices ### 1. Always export ABIs for integration ```shell # ✅ Good: Generate and version control ABIs cargo stylus export-abi > interfaces/IMyContract.sol git add interfaces/IMyContract.sol git commit -m "Update contract ABI" # ❌ Bad: Rely on manual interface definitions ``` ### 2. Use semantic function names ```rust // ✅ Good: Clear, descriptive names #[public] impl Token { pub fn get_balance(&self, account: Address) -> U256 { } pub fn transfer_from(&mut self, from: Address, to: Address, amount: U256) { } } // ❌ Bad: Unclear abbreviations #[public] impl Token { pub fn bal(&self, acc: Address) -> U256 { } pub fn xfer(&mut self, f: Address, t: Address, amt: U256) { } } ``` ### 3. Document complex functions ```rust #[public] impl Staking { /// Stakes tokens for a specified duration /// /// # Arguments /// * `amount` - Amount of tokens to stake /// * `duration` - Lock duration in seconds /// /// # Returns /// The unique stake ID pub fn stake(&mut self, amount: U256, duration: u64) -> U256 { // Implementation } } ``` ### 4. Export JSON for tooling ```shell # ✅ Good: Generate both formats cargo stylus export-abi > IMyContract.sol cargo stylus export-abi --json > abi.json # Share with front-end team cp abi.json ../frontend/src/abis/ ``` ### 5. Version control constructor changes When adding or modifying constructors, regenerate and commit: ```shell cargo stylus constructor > CONSTRUCTOR.txt git add CONSTRUCTOR.txt git commit -m "Update constructor signature" ``` ### 6. Test ABI compatibility ```typescript // test/abi.test.ts import { expect } from 'chai'; import { ethers } from 'hardhat'; import MyContractABI from '../abi.json'; describe('ABI Compatibility', () => { it('should match deployed contract', async () => { const contract = await ethers.getContractAt(MyContractABI, deployedAddress); // Verify functions exist expect(contract.getValue).to.exist; expect(contract.setValue).to.exist; // Call and verify const value = await contract.getValue(); expect(value).to.be.a('bigint'); }); }); ``` ### 7. Keep interfaces synchronized Use CI/CD to verify ABI is up to date: ```yaml # .github/workflows/check-abi.yml name: Check ABI on: [pull_request] jobs: check-abi: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Rust uses: actions-rs/toolchain@v1 - name: Generate ABI run: cargo stylus export-abi > /tmp/abi.sol - name: Check for changes run: diff /tmp/abi.sol interfaces/IMyContract.sol ``` ## Troubleshooting ### solc not found **Error**: `failed to run solc: No such file or directory` **Solution**: Install Solidity compiler: ```shell # macOS brew install solidity # Ubuntu/Debian sudo add-apt-repository ppa:ethereum/ethereum sudo apt-get update sudo apt-get install solc # Or use solc-select pip install solc-select solc-select install 0.8.23 solc-select use 0.8.23 ``` ### Feature not enabled **Error**: `no main function` **Solution**: Ensure `export-abi` feature is defined and main.rs exists: ```toml # Cargo.toml [features] export-abi = ["stylus-sdk/export-abi"] ``` ```rust // main.rs #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] #[cfg(feature = "export-abi")] fn main() { my_contract::print_from_args(); } ``` ### Type not supported **Error**: `the trait AbiType is not implemented for MyType` **Solution**: Use supported types, or derive `AbiType` for a custom struct: ```rust // ✅ Use supported types pub fn process(&self, amount: U256) -> U256 { } // ❌ Arbitrary Rust types have no ABI representation pub fn process(&self, amount: MyCustomType) -> MyCustomType { } ``` For custom struct types, derive `AbiType` inside a `sol!` block. The struct can then be used in public method parameters and return values: ```rust use stylus_sdk::alloy_sol_types::sol; use stylus_sdk::prelude::*; sol! { #[derive(AbiType)] struct Point { uint256 x; uint256 y; } } #[public] impl MyContract { pub fn echo_point(&self, p: Point) -> Point { p } } ``` ### Missing function in the ABI **Error**: Function doesn't appear in exported ABI **Solutions**: 1. Ensure function is in `#[public]` impl block: ```rust #[public] impl MyContract { pub fn my_function(&self) -> U256 { } // ✅ Exported } impl MyContract { pub fn helper(&self) -> U256 { } // ❌ Not exported } ``` 2. Check function visibility is `pub`: ```rust #[public] impl MyContract { pub fn exported(&self) -> U256 { } // ✅ Exported fn not_exported(&self) -> U256 { } // ❌ Not exported } ``` ## Advanced: Multiple contracts Export ABIs for all contracts in a workspace: The `--contract` value is a cargo package name. Repeat the flag to select several contracts; with no `--contract`, the workspace's default contracts are exported. ```shell # Export specific contract by package name cargo stylus export-abi --contract=my-token # Select several contracts in one invocation cargo stylus export-abi --contract=token --contract=staking # Export each to its own file for contract in token staking governance; do cargo stylus export-abi --contract=$contract > interfaces/I${contract^}.sol done ``` Or create a script: ```shell #!/bin/bash # export-all-abis.sh contracts=("token" "staking" "governance") for contract in "${contracts[@]}"; do echo "Exporting ABI for $contract..." cargo stylus export-abi --contract=$contract > "interfaces/I${contract^}.sol" cargo stylus export-abi --contract=$contract --json > "abis/${contract}.json" done echo "✅ All ABIs exported" ``` ## Resources * [Stylus SDK repository](https://github.com/OffchainLabs/stylus-sdk-rs) * [Cargo Stylus CLI](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus) * [ERC-20 example](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/examples/erc20) * [ABI specification](https://docs.soliditylang.org/en/latest/abi-spec.html) * [ethers.js documentation](https://docs.ethers.org/) * [viem documentation](https://viem.sh/) * [wagmi documentation](https://wagmi.sh/) --- > For a complete page index, fetch # Import and call external contract interfaces Interfaces enable your Stylus contract to interact with other contracts on the blockchain, regardless of whether they're written in Solidity, Rust, or another language. This guide shows you how to import and use external contract interfaces in your Stylus smart contracts. ## Why use interfaces Contract interfaces provide a type-safe way to communicate with other contracts on the blockchain. Common use cases include: * **Interacting with existing protocols**: Call methods on deployed Solidity contracts like **ERC-20** tokens, oracles, or DeFi protocols * **Composing functionality**: Build contracts that leverage other contracts' capabilities * **Cross-language interoperability**: Stylus contracts can call Solidity contracts and vice versa * **Upgradeability patterns**: Use interfaces to interact with proxy contracts Language agnostic Since interfaces operate at the ABI level, they work identically whether the target contract is written in Solidity, Rust, or any other language that compiles to EVM bytecode. ## Prerequisites Before implementing interfaces, ensure you have: Rust toolchain Follow the instructions on [Rust Lang's installation page](https://www.rust-lang.org/tools/install) to install a complete Rust toolchain (v1.91 or newer) on your system. After installation, ensure you can access the programs `rustup`, `rustc`, and `cargo` from your preferred terminal application. cargo stylus In your terminal, run: ```shell cargo install --force cargo-stylus ``` Add WASM ([WebAssembly](https://webassembly.org/)) as a build target for the specific Rust toolchain you are using. The example below sets your default Rust toolchain to 1.91, as well as adding the WASM build target: ```shell rustup default 1.91 rustup target add wasm32-unknown-unknown --toolchain 1.91 ``` You can verify that `cargo stylus` is installed by running `cargo stylus --help` in your terminal, which will return a list of helpful commands. ## Declaring interfaces with `sol_interface!` The [`sol_interface!`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/macro.sol_interface.html) macro allows you to declare interfaces using Solidity syntax. It generates Rust structs that represent external contracts and provides type-safe methods for calling them. ### Basic interface declaration ```rust use stylus_sdk::prelude::*; sol_interface! { interface IToken { function balanceOf(address account) external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function approve(address spender, uint256 amount) external returns (bool); } } ``` This macro generates an `IToken` struct that you can use to call methods on any deployed contract that implements this interface. ### Declaring multiple interfaces You can declare multiple interfaces in a single `sol_interface!` block: ```rust sol_interface! { interface IPaymentService { function makePayment(address user) payable returns (string); function getBalance(address user) view returns (uint256); } interface IOracle { function getPrice(bytes32 feedId) external view returns (uint256); function getLastUpdate() external view returns (uint256); } interface IVault { function deposit() external payable; function withdraw(uint256 amount) external; } } ``` Solidity syntax Interface declarations use standard Solidity syntax. The SDK computes the correct 4-byte function selectors based on the exact names and parameter types you provide. ## Calling external contract methods Once you've declared an interface, you can call methods on external contracts using instances of the generated struct. ### Creating interface instances Use the `::new(address)` constructor to create an interface instance pointing to a deployed contract: ```rust use alloy_primitives::Address; // Create an instance pointing to a deployed token contract let token_address = Address::from([0x12; 20]); // Replace with actual address let token = IToken::new(token_address); ``` ### CamelCase to snake\_case conversion The `sol_interface!` macro converts Solidity's CamelCase method names to Rust's snake\_case convention: | Solidity method | Rust method | | --------------- | --------------- | | `balanceOf` | `balance_of` | | `makePayment` | `make_payment` | | `getPrice` | `get_price` | | `transferFrom` | `transfer_from` | The macro preserves the original CamelCase name for computing the correct function selector, so your calls reach the right method on the target contract. ### Basic method calls Here's how to call methods on an external contract: ```rust use stylus_sdk::{call::Call, prelude::*}; use alloy_primitives::{Address, U256}; sol_interface! { interface IToken { function balanceOf(address account) external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); } } #[public] impl MyContract { pub fn check_balance(&self, token_address: Address, account: Address) -> U256 { let token = IToken::new(token_address); let config = Call::new(); token.balance_of(self.vm(), config, account).unwrap() } } ``` ## Configuring your calls The Stylus SDK provides three `Call` constructors for different types of external calls. Choosing the correct one is essential for your contract to work properly. Use this decision tree to choose the correct Call constructor: ![Call Decision Tree](/assets/images/stylus-call-decision-tree-966bcf145ef4d47dc970b9f7a6fd517e.png) *Figure 2: Decision tree for selecting the appropriate Call constructor based on state modification and payment requirements.* ### View calls with `Call::new()` Use `Call::new()` for read-only calls that don't modify state: ```rust use stylus_sdk::call::Call; #[public] impl MyContract { pub fn get_token_balance(&self, token: Address, account: Address) -> U256 { let token_contract = IToken::new(token); let config = Call::new(); token_contract.balance_of(self.vm(), config, account).unwrap() } pub fn get_oracle_price(&self, oracle: Address, feed_id: [u8; 32]) -> U256 { let oracle_contract = IOracle::new(oracle); let config = Call::new(); oracle_contract.get_price(self.vm(), config, feed_id.into()).unwrap() } } ``` ### State-changing calls with `Call::new_mutating(self)` Use `Call::new_mutating(self)` for calls that modify state on the target contract: ```rust #[public] impl MyContract { pub fn transfer_tokens( &mut self, token: Address, to: Address, amount: U256, ) -> bool { let token_contract = IToken::new(token); let config = Call::new_mutating(self); token_contract.transfer(self.vm(), config, to, amount).unwrap() } pub fn approve_spender( &mut self, token: Address, spender: Address, amount: U256, ) -> bool { let token_contract = IToken::new(token); let config = Call::new_mutating(self); token_contract.approve(self.vm(), config, spender, amount).unwrap() } } ``` Mutating calls require \&mut self When using `Call::new_mutating(self)`, your method must take `&mut self` as its first parameter. This ensures the Stylus runtime properly handles state changes and reentrancy protection. ### Payable calls with `Call::new_payable(self, value)` Use `Call::new_payable(self, value)` to send ETH along with your call: ```rust use alloy_primitives::U256; sol_interface! { interface IVault { function deposit() external payable; } } #[public] impl MyContract { #[payable] pub fn deposit_to_vault(&mut self, vault: Address) -> Result<(), Vec> { let vault_contract = IVault::new(vault); let value = self.vm().msg_value(); let config = Call::new_payable(self, value); vault_contract.deposit(self.vm(), config)?; Ok(()) } pub fn deposit_specific_amount( &mut self, vault: Address, amount: U256, ) -> Result<(), Vec> { let vault_contract = IVault::new(vault); let config = Call::new_payable(self, amount); vault_contract.deposit(self.vm(), config)?; Ok(()) } } ``` ### Configuring gas limits You can limit the gas forwarded to external calls using the `.gas()` method: ```rust #[public] impl MyContract { pub fn safe_transfer( &mut self, token: Address, to: Address, amount: U256, ) -> bool { let token_contract = IToken::new(token); // Use half of remaining gas let gas_limit = self.vm().evm_gas_left() / 2; let config = Call::new_mutating(self).gas(gas_limit); token_contract.transfer(self.vm(), config, to, amount).unwrap() } } ``` ### Call configuration summary | Constructor | Use case | State access | ETH transfer | | -------------------------------- | --------------- | ------------ | ------------ | | `Call::new()` | View/pure calls | Read-only | No | | `Call::new_mutating(self)` | Write calls | Read/write | No | | `Call::new_payable(self, value)` | Payable calls | Read/write | Yes | ## Complete example Here's a complete contract that demonstrates all aspects of interface usage: ```rust #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; use alloy_primitives::{Address, U256}; use alloy_sol_types::sol; use stylus_sdk::{call::Call, prelude::*}; // Declare interfaces for external contracts sol_interface! { interface IToken { function balanceOf(address account) external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function approve(address spender, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); } interface IOracle { function getPrice(bytes32 feedId) external view returns (uint256); } interface IVault { function deposit() external payable; function withdraw(uint256 amount) external; } } // Define events sol! { event TokensTransferred(address indexed token, address indexed to, uint256 amount); event DepositMade(address indexed vault, uint256 amount); } // Define errors sol! { error TransferFailed(address token, address to, uint256 amount); error InsufficientBalance(uint256 have, uint256 want); } #[derive(SolidityError)] pub enum InterfaceError { TransferFailed(TransferFailed), InsufficientBalance(InsufficientBalance), } // Contract storage sol_storage! { #[entrypoint] pub struct InterfaceExample { address owner; address default_token; address default_vault; } } #[public] impl InterfaceExample { #[constructor] pub fn constructor(&mut self, token: Address, vault: Address) { self.owner.set(self.vm().tx_origin()); self.default_token.set(token); self.default_vault.set(vault); } // View call example pub fn get_token_balance(&self, token: Address, account: Address) -> U256 { let token_contract = IToken::new(token); let config = Call::new(); token_contract.balance_of(self.vm(), config, account).unwrap() } // View call with oracle pub fn get_price(&self, oracle: Address, feed_id: [u8; 32]) -> U256 { let oracle_contract = IOracle::new(oracle); let config = Call::new(); oracle_contract.get_price(self.vm(), config, feed_id.into()).unwrap() } // Mutating call example pub fn transfer_tokens( &mut self, token: Address, to: Address, amount: U256, ) -> Result { let token_contract = IToken::new(token); let config = Call::new_mutating(self); let success = token_contract .transfer(self.vm(), config, to, amount) .map_err(|_| InterfaceError::TransferFailed(TransferFailed { token, to, amount, }))?; if success { self.vm().log(TokensTransferred { token, to, amount }); } Ok(success) } // Payable call example #[payable] pub fn deposit_to_vault(&mut self, vault: Address) -> Result<(), Vec> { let vault_contract = IVault::new(vault); let value = self.vm().msg_value(); let config = Call::new_payable(self, value); vault_contract.deposit(self.vm(), config)?; self.vm().log(DepositMade { vault, amount: value }); Ok(()) } // Using gas limits pub fn safe_withdraw(&mut self, vault: Address, amount: U256) -> Result<(), Vec> { let vault_contract = IVault::new(vault); // Limit gas to prevent reentrancy issues let gas_limit = self.vm().evm_gas_left() / 2; let config = Call::new_mutating(self).gas(gas_limit); vault_contract.withdraw(self.vm(), config, amount)?; Ok(()) } // Complex multi-call example pub fn swap_and_deposit( &mut self, token: Address, vault: Address, amount: U256, ) -> Result<(), InterfaceError> { let token_contract = IToken::new(token); // First, check balance let balance = token_contract .balance_of(self.vm(), Call::new(), self.vm().contract_address()) .unwrap(); if balance < amount { return Err(InterfaceError::InsufficientBalance(InsufficientBalance { have: balance, want: amount, })); } // Approve vault to spend tokens let config = Call::new_mutating(self); token_contract .approve(self.vm(), config, vault, amount) .map_err(|_| InterfaceError::TransferFailed(TransferFailed { token, to: vault, amount, }))?; Ok(()) } } ``` ## Best practices ### Validate addresses before calls Always verify that contract addresses are valid before making external calls: ```rust pub fn safe_transfer( &mut self, token: Address, to: Address, amount: U256, ) -> Result { // Validate addresses if token == Address::ZERO || to == Address::ZERO { return Err(InterfaceError::InvalidAddress); } let token_contract = IToken::new(token); let config = Call::new_mutating(self); Ok(token_contract.transfer(self.vm(), config, to, amount).unwrap()) } ``` ### Handle call failures gracefully External calls can fail for various reasons. Always handle errors appropriately: ```rust pub fn try_transfer( &mut self, token: Address, to: Address, amount: U256, ) -> Result { let token_contract = IToken::new(token); let config = Call::new_mutating(self); match token_contract.transfer(self.vm(), config, to, amount) { Ok(success) => Ok(success), Err(_) => Err(InterfaceError::TransferFailed(TransferFailed { token, to, amount, })), } } ``` ### Follow the checks-effects-interactions pattern When making external calls, update your contract's state before calling external contracts to prevent reentrancy attacks: ```rust pub fn withdraw_tokens( &mut self, token: Address, amount: U256, ) -> Result<(), InterfaceError> { let caller = self.vm().msg_sender(); // Checks let balance = self.balances.get(caller); if balance < amount { return Err(InterfaceError::InsufficientBalance(InsufficientBalance { have: balance, want: amount, })); } // Effects - update state BEFORE external call self.balances.setter(caller).set(balance - amount); // Interactions - external call last let token_contract = IToken::new(token); let config = Call::new_mutating(self); token_contract.transfer(self.vm(), config, caller, amount) .map_err(|_| InterfaceError::TransferFailed(TransferFailed { token, to: caller, amount, }))?; Ok(()) } ``` ### Use gas limits for untrusted contracts When calling untrusted contracts, limit the gas to prevent malicious behavior: ```rust pub fn call_untrusted( &mut self, target: Address, ) -> Result> { let contract = IToken::new(target); // Limit gas to prevent griefing attacks let config = Call::new().gas(100_000); Ok(contract.balance_of(self.vm(), config, self.vm().msg_sender()).unwrap()) } ``` ## Common pitfalls ### Using the wrong call constructor Using `Call::new()` for state-changing calls will cause the transaction to fail: ```rust //Correct - using Call::new_mutating(self) pub fn good_transfer(&mut self, token: Address, to: Address, amount: U256) -> bool { let token_contract = IToken::new(token); let config = Call::new_mutating(self); token_contract.transfer(self.vm(), config, to, amount).unwrap() } // Wrong - using Call::new() for a write operation pub fn bad_transfer(&mut self, token: Address, to: Address, amount: U256) -> bool { let token_contract = IToken::new(token); let config = Call::new(); // This will fail! token_contract.transfer(self.vm(), config, to, amount).unwrap() } ``` ### Forgetting to pass the VM context All interface method calls require `self.vm()` as the first argument: ```rust // Correct let balance = token_contract.balance_of(self.vm(), config, account).unwrap(); // Wrong - missing self.vm() let balance = token_contract.balance_of(config, account).unwrap(); ``` ### Incorrect method naming Remember that Solidity method names are converted to snake\_case in Rust: ```rust // Correct - using Rust snake_case let balance = token.balance_of(self.vm(), config, account); // Wrong - using Solidity naming let balance = token.balanceOf(self.vm(), config, account); ``` ## See also * [Stylus contracts reference](/stylus/fundamentals/contracts.md#external-contract-calls): Detailed reference for external contract calls * [Stylus by Example: Import interfaces](https://stylus-by-example.org/basic_examples/import_interfaces): Interactive examples * [Stylus SDK documentation](https://docs.rs/stylus-sdk/latest/stylus_sdk/): Complete API reference --- > For a complete page index, fetch # How to optimize Stylus WASM binaries To be deployed onchain, the size of your **uncompressed WebAssembly (WASM) file** must not exceed 128Kb, while the **compressed binary** must not exceed 24KB. Stylus conforms with the same contract size limit as the EVM to remain fully interoperable with all smart contracts on Arbitrum chains. [cargo-stylus](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus), the Stylus CLI tool, automatically compresses your WASM programs, but there are additional steps that you can take to further reduce the size of your binaries. Your options fall into two categories: Rust compiler flags, and third-party optimization tools. ## Rust compiler flags The Rust compiler supports various config options for shrinking binary sizes. ### `Cargo.toml` ```toml [profile.release] codegen-units = 1 # prefer efficiency to compile time panic = "abort" # use simple panics opt-level = "z" # optimize for size ("s" may also work) strip = true # remove debug info lto = true # link time optimization debug = false # no debug data rpath = false # no run-time search path debug-assertions = false # prune debug assertions incremental = false # no incremental builds ``` ### Nightly Rust flags (advanced) If standard release flags still leave you over budget, nightly Rust offers a flag that strips the panic-formatting infrastructure entirely: ```shell cargo +nightly -Z build-std=std,panic_abort -Zbuild-std-features=panic_immediate_abort ``` Combined with disabling storage caching, this can shave roughly 20% off binary size on small contracts. Use nightly flags at your own risk Nightly compiler flags are unstable and may change between toolchain releases. Always re-run `cargo stylus check` after enabling them to confirm the resulting binary is still valid. If your contract still exceeds the size limit after these optimizations, consider splitting functionality across multiple Stylus contracts and composing them via inter-contract calls. ## Third-party optimization tooling Additional WASM-specific tooling exists to shrink binaries. Due to being third party, users should use these at their own risk. ### `wasm-opt` [wasm-opt](https://docs.rs/wasm-opt/0.113.0/wasm_opt/) applies techniques to further reduce binary size, usually netting around 10%. ### `twiggy` [twiggy](https://github.com/rustwasm/twiggy) is a code size profiler for WASM, it can help you estimate the impact of each added component on your binaries' size. ### Inspecting WASM opcodes To examine the WebAssembly instructions in your compiled contract, convert the binary to WebAssembly Text format using the [WABT toolkit](https://github.com/WebAssembly/wabt): ```shell wasm2wat contract.wasm -o contract.wat ``` For one-off inspection without installing tools, use the online [wasm2wat demo](https://webassembly.github.io/wabt/demo/wasm2wat/). Our team has also curated a [list of recommended libraries](/stylus/advanced/recommended-libraries.md) that are helpful to Stylus development and optimally sized. ### Frequently asked questions #### Will future releases of Stylus introduce additional optimizations? Yes! We're actively working on improving WASM sizes generated by Rust code with the Stylus SDK. #### Why don't I have to worry about this type of optimization when I use `cargo` without using Stylus? On modern platforms, tools like `cargo` don’t have to worry about the size of the binaries they produce. This is because there’s many orders of magnitude more storage available than even the largest of binaries, and for most applications it’s media like images and videos that constitutes the majority of the footprint. Resource constraints when building on blockchains are extremely strict. Hence, while not the default option, tooling often provides mechanisms for reducing binary bloat, such as the options outlined in this document. --- > For a complete page index, fetch # Composition and trait-based routing model Inheritance allows you to build upon existing smart contract functionality without duplicating code. In Stylus, the Rust SDK provides tools to implement inheritance patterns similar to Solidity, but with some important differences. This guide walks you through implementing trait-based composition in your Stylus smart contracts. For Solidity developers There's no direct equivalent of inheritance in Rust, but the following will show you the Rust-way of achieving similar results. ## Overview The Stylus SDK offers trait-based composition using traits and the `#[implements]` annotation. This approach follows Rust's composition patterns and provides stronger type safety. ## Getting started Before implementing trait-based composition, ensure you have: Rust toolchain Follow the instructions on [Rust Lang's installation page](https://www.rust-lang.org/tools/install) to install a complete Rust toolchain (v1.91 or newer) on your system. After installation, ensure you can access the programs `rustup`, `rustc`, and `cargo` from your preferred terminal application. cargo stylus In your terminal, run: ```shell cargo install --force cargo-stylus ``` Add WASM ([WebAssembly](https://webassembly.org/)) as a build target for the specific Rust toolchain you are using. The below example sets your default Rust toolchain to 1.91 as well as adding the WASM build target: ```shell rustup default 1.91 rustup target add wasm32-unknown-unknown --toolchain 1.91 ``` You can verify that cargo stylus is installed by running `cargo stylus --help` in your terminal, which will return a list of helpful commands. ## Trait-based composition model The recommended approach to inheritance in Stylus uses traits and the `#[implements]` annotation, which follows Rust's standard composition patterns: ### Basic example of trait-based composition Trait-Based Inheritance Example: ERC-20 ```rust use stylus_sdk::{ alloy_primitives::{Address, U256}, prelude::*, storage::{StorageAddress, StorageMap, StorageU256}, }; // Define traits for different functionality #[public] trait IErc20 { fn name(&self) -> String; fn symbol(&self) -> String; fn decimals(&self) -> U256; fn total_supply(&self) -> U256; fn balance_of(&self, account: Address) -> U256; fn transfer(&mut self, to: Address, value: U256) -> bool; } #[public] trait IOwnable { fn owner(&self) -> Address; fn transfer_ownership(&mut self, new_owner: Address) -> bool; fn renounce_ownership(&mut self) -> bool; } // Define storage for each component #[storage] struct Erc20 { balances: StorageMap, total_supply: StorageU256, } #[storage] struct Ownable { owner: StorageAddress, } // Define the main contract that composes different functionality #[storage] #[entrypoint] struct Contract { erc20: Erc20, ownable: Ownable, } // The #[implements] attribute connects the contract to the traits it implements #[public] #[implements(IErc20, IOwnable)] impl Contract {} // Implement the ERC20 interface for the contract #[public] impl IErc20 for Contract { fn name(&self) -> String { "MyToken".to_string() } fn symbol(&self) -> String { "MTK".to_string() } fn decimals(&self) -> U256 { U256::from(18) } fn total_supply(&self) -> U256 { self.erc20.total_supply.get() } fn balance_of(&self, account: Address) -> U256 { self.erc20.balances.get(account) } fn transfer(&mut self, to: Address, value: U256) -> bool { // Implementation here true } } // Implement the Ownable interface for the contract #[public] impl IOwnable for Contract { fn owner(&self) -> Address { self.ownable.owner.get() } fn transfer_ownership(&mut self, new_owner: Address) -> bool { // Implementation here true } fn renounce_ownership(&mut self) -> bool { // Implementation here true } } ``` ### How trait-based composition works The trait-based composition model follows these principles: 1. Define traits that represent interfaces (similar to Solidity interfaces) 2. Implement these traits for your contract 3. Use the `#[implements(...)]` attribute to tell the Stylus SDK which traits your contract implements 4. The router will connect incoming calls to the appropriate implementation This approach is aligned with Rust's composition patterns and offers better type safety. ### Method overriding If both parent and child implement the same method, the one in the child will override the one in the parent. This allows for customizing inherited functionality. No explicit override keywords Stylus does not currently contain explicit override or virtual keywords for marking override functions. It is important to carefully ensure that contracts are only overriding the functions you intend to override. ### ABI export considerations with trait-based composition When using trait-based composition, you need to be careful about function selectors to ensure correct ABI generation. Due to how Rust handles traits, you may need to explicitly set selectors for methods to match Solidity's expected function signatures. Selector precision When implementing traits with methods that have matching names, you must manually use the `#[selector(name = "ActualName")]` attribute to avoid method selector collisions. This is particularly important when implementing standard interfaces like **ERC-20** or **ERC-721**. Selector issue example: ERC-721 ```rust // In Solidity, these two functions share the name `safeTransferFrom` but have // different selectors because their parameter lists differ: // function safeTransferFrom(address from, address to, uint256 tokenId) // function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) // Rust has no method overloading, so each variant needs a distinct Rust name. // Use the #[selector(name = "...")] attribute to export a Solidity-compatible // selector. Because the two variants take different parameters, both can keep // the `safeTransferFrom` name on the ABI side without colliding. #[public] impl Erc721 { #[selector(name = "safeTransferFrom")] pub fn safe_transfer_from(&mut self, from: Address, to: Address, token_id: U256) { // Implementation here } #[selector(name = "safeTransferFrom")] pub fn safe_transfer_from_with_data( &mut self, from: Address, to: Address, token_id: U256, data: Bytes, ) { // Implementation here } } ``` ABI generation and inheritance The Stylus SDK generates ABIs based on the methods that are available at the entrypoint contract. When using trait-based composition, make sure that all methods you want exposed in the ABI are properly included through the #\[implements] attribute. ## Methods search order When using trait-based composition, it's important to understand the order in which methods are searched: 1. The search starts in the type that uses the `#[entrypoint]` macro 2. If the method is not found, the search continues in the implemented traits, in the order specified in the `#[implements]` annotation 3. If the method is not found in any implemented trait, the call reverts In a typical composition chain: * Calling a method first searches in the contract itself * If not found there, it looks in the first trait specified in the inheritance list * If still not found, it searches in the next trait in the list * This continues until the method is found or all possibilities are exhausted --- > For a complete page index, fetch # Using constructors with Stylus Constructors allow you to initialize your Stylus smart contracts with specific parameters when deploying them. This guide will show you how to implement constructors in Rust, understand their behavior, and deploy contracts using them. ## What you'll accomplish By the end of this guide, you'll be able to: * Implement constructor functions in Stylus contracts * Understand the constructor rules and limitations * Deploy contracts with constructor parameters * Test the constructor functionality * Handle constructor errors and validation ## Prerequisites Before implementing constructors, ensure you have: Rust toolchain Follow the instructions on [Rust Lang's installation page](https://www.rust-lang.org/tools/install) to install a complete Rust toolchain (v1.91 or newer) on your system. After installation, ensure you can access the programs `rustup`, `rustc`, and `cargo` from your preferred terminal application. cargo stylus In your terminal, run: ```shell cargo install --force cargo-stylus ``` Add WASM ([WebAssembly](https://webassembly.org/)) as a build target for the specific Rust toolchain you are using. The below example sets your default Rust toolchain to 1.91 as well as adding the WASM build target: ```shell rustup default 1.91 rustup target add wasm32-unknown-unknown --toolchain 1.91 ``` You can verify that cargo stylus is installed by running `cargo stylus --help` in your terminal, which will return a list of helpful commands. A local Arbitrum test node Instructions on how to set up a local Arbitrum test node can be found [in the Nitro-devnode repository](https://github.com/OffchainLabs/nitro-devnode). ## Understanding Stylus constructors Stylus constructors provide an atomic way to deploy, activate, and initialize a contract in a single transaction. If your contract lacks a constructor, it may allow access to the contract's storage before the initialization logic runs, leading to unexpected behavior. Constructors and composition Stylus uses [trait-based composition](/stylus/how-tos/trait-based-composition.md) instead of traditional inheritance. When building contracts that compose multiple traits, constructors help initialize all components properly. See the [Constructor with trait-based composition](#constructor-with-trait-based-composition) section for examples. ### Constructor rules and guarantees Stylus constructors follow these important rules: | Rule | Why it exists | | ------------------------------------------- | --------------------------------------------------------------------------------------- | | **Exactly 0 or 1 constructor** per contract | Mimics Solidity behavior and avoids ambiguity | | **Must be annotated with `#[constructor]`** | Guarantees the deployer calls the correct initialization method | | **Must take `&mut self`** | Allows writing to contract storage during deployment | | **Returns `()` or `Result<(), Error>`** | Enables error handling; reverting aborts deployment | | **Use `tx_origin()` for deployer address** | Factory contracts are used in deployment, so `msg_sender()` returns the factory address | | **Constructor runs exactly once** | The SDK uses a sentinel system to prevent re-execution | Factory pattern in deployment Stylus uses a factory pattern for deployment, which means `msg_sender()` in a constructor returns the factory contract address, not the deployer. Always use `tx_origin()` to get the actual deployer address. ## Basic constructor implementation Here's a simple example of a constructor in a Stylus contract: ```rust #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; use alloy_primitives::{Address, U256}; use alloy_sol_types::sol; use stylus_sdk::prelude::*; sol! { #[derive(Debug)] error InvalidAmount(); } sol_storage! { #[entrypoint] pub struct SimpleToken { address owner; uint256 total_supply; string name; string symbol; mapping(address => uint256) balances; } } #[derive(SolidityError, Debug)] pub enum SimpleTokenError { InvalidAmount(InvalidAmount), } #[public] impl SimpleToken { /// Constructor initializes the token with a name, symbol, and initial supply #[constructor] #[payable] pub fn constructor( &mut self, name: String, symbol: String, initial_supply: U256, ) -> Result<(), SimpleTokenError> { // Validate input parameters if initial_supply == U256::ZERO { return Err(SimpleTokenError::InvalidAmount(InvalidAmount {})); } // Get the deployer address using tx_origin() let deployer = self.vm().tx_origin(); // Initialize contract state self.owner.set(deployer); self.name.set_str(&name); self.symbol.set_str(&symbol); self.total_supply.set(initial_supply); // Mint initial supply to deployer self.balances.setter(deployer).set(initial_supply); Ok(()) } // Additional contract methods... pub fn balance_of(&self, account: Address) -> U256 { self.balances.get(account) } pub fn total_supply(&self) -> U256 { self.total_supply.get() } } ``` ### Key implementation details 1. **Parameter validation**: Always validate constructor parameters before proceeding with initialization 2. **Error handling**: Use `Result<(), Error>` to handle initialization failures gracefully 3. **Payable constructors**: Add `#[payable]` to receive ETH during deployment 4. **State initialization**: Set all necessary contract state in the constructor ## Advanced constructor patterns ### Constructor with complex validation ```rust #[constructor] #[payable] pub fn constructor( &mut self, name: String, symbol: String, initial_supply: U256, max_supply: U256, ) -> Result<(), TokenContractError> { // Multiple validation checks if initial_supply == U256::ZERO { return Err(TokenContractError::InvalidAmount(InvalidAmount {})); } if initial_supply > max_supply { return Err(TokenContractError::TooManyTokens(TooManyTokens {})); } if name.is_empty() || symbol.is_empty() { return Err(TokenContractError::InvalidAmount(InvalidAmount {})); } let deployer = self.vm().tx_origin(); // Initialize with timestamp tracking self.owner.set(deployer); self.name.set_str(&name); self.symbol.set_str(&symbol); self.total_supply.set(initial_supply); self.max_supply.set(max_supply); self.created_at.set(U256::from(self.vm().block_timestamp())); // Mint tokens to deployer self.balances.setter(deployer).set(initial_supply); // Emit initialization event self.vm().log(TokenCreated { creator: deployer, name: name.clone(), symbol: symbol.clone(), initial_supply, }); Ok(()) } ``` ### Constructor with trait-based composition Stylus uses trait-based composition instead of traditional inheritance. When implementing constructors with traits, each component typically has its own initialization logic: ```rust use alloy_primitives::{Address, U256}; use alloy_sol_types::sol; use stylus_sdk::prelude::*; use stylus_sdk::storage::{StorageAddress, StorageMap, StorageString, StorageU256}; sol! { #[derive(Debug)] error InvalidSupply(); } #[derive(SolidityError, Debug)] pub enum TokenError { InvalidSupply(InvalidSupply), } // Define traits for different functionality. Traits exposed through // #[implements(...)] must themselves be annotated with #[public]. #[public] trait IErc20 { fn balance_of(&self, account: Address) -> U256; fn transfer(&mut self, to: Address, value: U256) -> bool; } #[public] trait IOwnable { fn owner(&self) -> Address; fn transfer_ownership(&mut self, new_owner: Address) -> bool; } // Define storage for each component #[storage] struct Erc20Component { balances: StorageMap, total_supply: StorageU256, } #[storage] struct OwnableComponent { owner: StorageAddress, } // Main contract that composes functionality #[storage] #[entrypoint] struct MyToken { erc20: Erc20Component, ownable: OwnableComponent, name: StorageString, symbol: StorageString, } #[public] #[implements(IErc20, IOwnable)] impl MyToken { #[constructor] pub fn constructor( &mut self, name: String, symbol: String, initial_supply: U256, ) -> Result<(), TokenError> { // Initialize each component self.initialize_ownable()?; self.initialize_erc20(initial_supply)?; // Initialize contract-specific state self.name.set_str(&name); self.symbol.set_str(&symbol); Ok(()) } fn initialize_ownable(&mut self) -> Result<(), TokenError> { let deployer = self.vm().tx_origin(); self.ownable.owner.set(deployer); Ok(()) } fn initialize_erc20(&mut self, initial_supply: U256) -> Result<(), TokenError> { if initial_supply == U256::ZERO { return Err(TokenError::InvalidSupply(InvalidSupply {})); } let deployer = self.vm().tx_origin(); self.erc20.total_supply.set(initial_supply); self.erc20.balances.setter(deployer).set(initial_supply); Ok(()) } } // Each trait listed in #[implements(...)] must have a matching implementation. #[public] impl IErc20 for MyToken { fn balance_of(&self, account: Address) -> U256 { self.erc20.balances.get(account) } fn transfer(&mut self, to: Address, value: U256) -> bool { let from = self.vm().msg_sender(); let from_balance = self.erc20.balances.get(from); if from_balance < value { return false; } self.erc20.balances.setter(from).set(from_balance - value); let to_balance = self.erc20.balances.get(to); self.erc20.balances.setter(to).set(to_balance + value); true } } #[public] impl IOwnable for MyToken { fn owner(&self) -> Address { self.ownable.owner.get() } fn transfer_ownership(&mut self, new_owner: Address) -> bool { if self.vm().msg_sender() != self.ownable.owner.get() { return false; } self.ownable.owner.set(new_owner); true } } ``` Trait-based composition in Stylus Unlike Solidity's inheritance, Stylus uses Rust's trait system for composition. Each component is initialized explicitly in the constructor. ## Testing constructors The Stylus SDK provides comprehensive testing tools for constructor functionality: ```rust #[cfg(test)] mod tests { use super::*; use stylus_sdk::testing::*; #[test] fn test_constructor_success() { let vm = TestVMBuilder::new() .sender(Address::from([0x01; 20])) .build(); let mut contract = SimpleToken::from(&vm); let result = contract.constructor( "Test Token".to_string(), "TEST".to_string(), U256::from(1000000), ); assert!(result.is_ok()); assert_eq!(contract.name.get_string(), "Test Token"); assert_eq!(contract.symbol.get_string(), "TEST"); assert_eq!(contract.total_supply.get(), U256::from(1000000)); assert_eq!( contract.balance_of(Address::from([0x01; 20])), U256::from(1000000) ); } #[test] fn test_constructor_validation() { let vm = TestVMBuilder::new() .sender(Address::from([0x01; 20])) .build(); let mut contract = SimpleToken::from(&vm); // Test zero supply rejection let result = contract.constructor( "Test Token".to_string(), "TEST".to_string(), U256::ZERO, ); assert!(result.is_err()); assert!(matches!( result.unwrap_err(), SimpleTokenError::InvalidAmount(_) )); } } ``` ## Deploying contracts with constructors ### Using cargo stylus Deploy your contract with constructor arguments using `cargo stylus deploy`: ```shell # Deploy with constructor parameters. # The constructor is annotated #[payable], so use --constructor-value to send ETH to it. cargo stylus deploy \ --private-key-path ~/.arbitrum/key \ --endpoint https://sepolia-rollup.arbitrum.io/rpc \ --constructor-args "MyToken" "MTK" 1000000 \ --constructor-value 0 ``` ### Constructor argument encoding `cargo stylus` automatically encodes the constructor arguments. The arguments should be provided in the same order as defined in your constructor function. For complex types: * **Strings**: Provide as quoted strings * **Numbers**: Provide as decimal or hex (0x prefix) * **Addresses**: Provide as hex strings with 0x prefix * **Arrays**: Use JSON array syntax ```shell # Example with multiple argument types cargo stylus deploy \ --constructor-args "TokenName" "TKN" 1000000 "0x742d35Cc6635C0532925a3b8D95B5C1b0ea3C28F" ``` ## Best practices ### Constructor parameter validation Always validate constructor parameters to prevent deployment of misconfigured contracts: ```rust #[constructor] pub fn constructor(&mut self, params: ConstructorParams) -> Result<(), Error> { // Validate all parameters before any state changes self.validate_parameters(¶ms)?; // Initialize state only after validation passes self.initialize_state(params)?; Ok(()) } fn validate_parameters(&self, params: &ConstructorParams) -> Result<(), Error> { if params.name.is_empty() { return Err(Error::InvalidName); } // Additional validation... Ok(()) } ``` ### Error handling patterns Use descriptive error types and provide meaningful error messages: ```rust sol! { #[derive(Debug)] error InvalidName(string reason); #[derive(Debug)] error InvalidSupply(uint256 provided, uint256 max_allowed); #[derive(Debug)] error Unauthorized(address caller); } #[derive(SolidityError, Debug)] pub enum ConstructorError { InvalidName(InvalidName), InvalidSupply(InvalidSupply), Unauthorized(Unauthorized), } ``` ### State initialization order Initialize contract state in a logical order to avoid dependency issues: ```rust #[constructor] pub fn constructor(&mut self, params: ConstructorParams) -> Result<(), Error> { // 1. Validate parameters first self.validate_parameters(¶ms)?; // 2. Set basic contract metadata self.name.set_str(¶ms.name); self.symbol.set_str(¶ms.symbol); // 3. Set ownership and permissions let deployer = self.vm().tx_origin(); self.owner.set(deployer); // 4. Initialize token economics self.total_supply.set(params.initial_supply); self.max_supply.set(params.max_supply); // 5. Set up initial balances self.balances.setter(deployer).set(params.initial_supply); // 6. Emit events last self.vm().log(ContractInitialized { /* ... */ }); Ok(()) } ``` ## Common pitfalls and solutions ### Using msg\_sender() instead of tx\_origin() **Problem**: Using `msg_sender()` in constructors returns the factory contract address, not the deployer. ```rust // ❌ Wrong - returns factory address let deployer = self.vm().msg_sender(); // ✅ Correct - returns actual deployer let deployer = self.vm().tx_origin(); ``` ### Missing parameter validation **Problem**: Not validating constructor parameters can lead to unusable contracts. ```rust // ❌ Wrong - no validation #[constructor] pub fn constructor(&mut self, supply: U256) { self.total_supply.set(supply); // Could be zero! } // ✅ Correct - validate first #[constructor] pub fn constructor(&mut self, supply: U256) -> Result<(), Error> { if supply == U256::ZERO { return Err(Error::InvalidSupply); } self.total_supply.set(supply); Ok(()) } ``` ### Forgetting the #\[constructor] annotation **Problem**: Functions named "constructor" without the annotation won't be recognized. ```rust // ❌ Wrong - missing annotation pub fn constructor(&mut self, value: U256) { // This won't be called during deployment } // ✅ Correct - properly annotated #[constructor] pub fn constructor(&mut self, value: U256) { // This will be called during deployment } ``` ## Summary Constructors in Stylus provide a powerful way to initialize your smart contracts during deployment. Key takeaways: * Use `#[constructor]` annotation and `&mut self` parameter * Always use `tx_origin()` to get the deployer address * Validate all parameters before initializing state * Handle errors gracefully with `Result<(), Error>` return type * Test the constructor behavior thoroughly * Deploy with `cargo stylus deploy --constructor-args` --- > For a complete page index, fetch # How to verify Stylus contracts on Arbiscan This how-to will show you how to verify deployed contracts using Arbiscan, Arbitrum's block explorer. Here's an example of a verified contract: the [English Auction Stylus contract](https://github.com/OffchainLabs/stylus-english-auction), which has been verified on Arbitrum Sepolia. You can view the verified contract [here](https://sepolia.arbiscan.io/address/0xe85a046fd3ea22ceeb3caef3a0d38123eecbe3ca). You can also see a list of all Stylus contracts verified on Arbiscan by visiting: * [Verified Stylus Contracts on Arbitrum One](https://arbiscan.io/contractsVerified?filter=stylus). * [Verified Stylus Contracts on Arbitrum Sepolia](https://sepolia.arbiscan.io/contractsVerified?filter=stylus). Here are the steps to take to verify a contract on Arbiscan: ## Step 1: Navigate to the verification page You have two options to access the contract verification page on Arbiscan: 1. **Direct link:** Visit [Arbiscan Verify Contract](https://arbiscan.io/verifyContract) to go directly to the verification form. This option is ideal if you already have the contract address and details ready. 2. **From the contract page:** If you're viewing the contract's page on Arbiscan: * Go to the **Contract** tab. * Click on **Verify and Publish**. ![Verify through the contract page](/img/stylus-arbiscan-verification-1.png) Both methods will take you to the contract verification form, where you can proceed to the next step. ## Step 2: Enter the contract's details You will need to fill in the following fields on the contract verification page: * **Contract address**: Enter the contract address you want to verify. * **Compiler type**: Select **Stylus** for Stylus contracts. * **Compiler version**: Choose the `cargo stylus` version that was used to deploy the contract. * **Open source license type**: Select the appropriate license for your contract. ![Enter contract details](/img/stylus-arbiscan-verification-2.png) ## Step 3: Submit source code After entering the contract details, you’ll need to provide the contract's source code: * **Manual submission**: Copy and paste the source code into the provided text box. * **Fetch from GitHub (Recommended)**: It's recommended to use the **Fetch from Git** option, as it's easier and helps automate the process. However, note that contracts located in subdirectories of the repository cannot be verified. Ensure that the contract's code is placed directly in the repository's root for verification to succeed. ![Fetch source code](/img/stylus-arbiscan-verification-3.png) ## Step 4: Set EVM version The **EVM Version to Target** can be left as default unless specific requirements dictate otherwise. ![Verify and publish](/img/stylus-arbiscan-verification-4.png) ## Step 5: Verify and publish Click **Verify and Publish**. The verification process will take a few seconds. Refresh the contract page, and if successful, the contract will be marked as verified. ![Verified](/img/stylus-arbiscan-verification-5.png) ## Behavior when deploying a verified contract When deploying another instance of a previously verified contract, if the bytecode matches, Arbiscan will automatically link the new instance to the verified source code, displaying a message like: > "This contract matches the deployed Bytecode of the Source Code for Contract \[verified contract address]." However, the new contract will still appear as "Not Verified" until you explicitly verify it. --- > For a complete page index, fetch # Quickstart: write a smart contract in Rust using Stylus This guide will get you started with Stylus' basics. We'll cover the following steps: 1. [Setting up your development environment](/stylus/quickstart.md#setting-up-your-development-environment) 2. [Creating a Stylus project with cargo stylus](/stylus/quickstart.md#creating-a-stylus-project-with-cargo-stylus) 3. [Checking the validity of your contract](/stylus/quickstart.md#checking-if-your-stylus-project-is-valid) 4. [Deploying your contract](/stylus/quickstart.md#deploying-your-contract) 5. [Exporting your contract's ABIs](#exporting-the-solidity-abi-interface) 6. [Calling your contract](/stylus/quickstart.md#calling-your-contract) 7. [Sending a transaction to your contract](/stylus/quickstart.md#sending-a-transaction-to-your-contract) ## Setting up your development environment ### Prerequisites Rust toolchain Follow the instructions on [Rust Lang's installation page](https://www.rust-lang.org/tools/install) to install a complete Rust toolchain (v1.91 or newer) on your system. After installation, ensure you can access the programs `rustup`, `rustc`, and `cargo` from your preferred terminal application. VS Code We recommend [VSCode](https://code.visualstudio.com/) as the IDE of choice for its excellent Rust support, but feel free to use another text editor or IDE if you're comfortable with those. Some helpful VS Code extensions for Rust development: * [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer): Provides advanced features like smart code completion and on-the-fly error checks * [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens): Immediately highlights errors and warnings in your code * [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml): Improves syntax highlighting and other features for TOML files, often used in Rust projects * [Dependi](https://marketplace.visualstudio.com/items?itemName=fill-labs.dependi): Helps manage Rust crate versions directly from the editor Docker The testnode we will use as well as some `cargo stylus` commands require Docker to operate. You can download Docker from [Docker's website](https://www.docker.com/products/docker-desktop). Foundry's Cast Foundry's Cast is a command-line tool that allows you to interact with your EVM contracts. You need to [install the Foundry CLI](https://getfoundry.sh) to use Cast. Nitro devnode Stylus is available on Arbitrum Sepolia, but we'll use nitro devnode which has a pre-funded wallet saving us the effort of wallet provisioning or running out of tokens to send transactions. Install your devnode ```shell git clone https://github.com/OffchainLabs/nitro-devnode.git cd nitro-devnode ``` Launch your devnode ```shell ./run-dev-node.sh ``` ## Creating a Stylus project with cargo stylus [cargo stylus](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus) is a CLI toolkit built to facilitate the development of Stylus contracts. For a full list of commands and options, see the [`cargo stylus` commands reference](/stylus/cli-tools/commands-reference.md). It is available as a plugin to the standard cargo tool used for developing Rust programs. ### Installing cargo stylus In your terminal, run: ```shell cargo install --force cargo-stylus ``` You can verify that cargo stylus is installed by running `cargo stylus --help` in your terminal, which will return a list of helpful commands; we will use some of them in this guide: cargo stylus --help returns: ```shell Cargo command for developing Stylus projects Usage: cargo stylus Commands: new Create a new Stylus project init Initializes a Stylus project in the current directory export-abi Export a Solidity ABI activate Activate an already deployed contract [aliases: a] cache Cache a contract using the Stylus CacheManager for Arbitrum chains check Check a contract [aliases: c] deploy Deploy a contract [aliases: d] verify Verify the deployment of a Stylus contract [aliases: v] cgen Generate c code bindings for a Stylus contract replay Replay a transaction in gdb [aliases: r] trace Trace a transaction [aliases: t] help Print this message or the help of the given command(s) Options: -h, --help Print help -V, --version Print version ``` ### Creating a project Let's create our first Stylus project by running: ```shell cargo stylus new cd ``` `cargo stylus new` generates a starter template that implements a Rust version of the Solidity `Counter` smart contract example: ```solidity // SPDX-License-Identifier: MIT pragma solidity >=0.4.22 <0.9.0; contract Counter { uint count; function setCount() public { count = count + 1; } function getCount() view public returns(uint) { return count; } } ``` At this point, you can move on to the next step of this guide or develop your first Rust smart contract. Feel free to use the [Stylus Rust SDK reference section](/stylus/reference/overview.md) as a starting point; it offers many examples to help you quickly familiarize yourself with Stylus. ## Checking if your Stylus project is valid By running `cargo stylus check` against your first contract, you can check if your program can be successfully **deployed and activated** onchain. Important Ensure your Docker service runs so this command works correctly. ```shell cargo stylus check ``` `cargo stylus check` executes a dry run on your project by compiling your contract to WASM and verifying if it can be deployed and activated onchain. If the command above fails, you'll see detailed information about why your contract would be rejected: ```shell Reading WASM file at bad-export.wat Compressed WASM size: 55 B Stylus checks failed: program pre-deployment check failed when checking against ARB_WASM_ADDRESS 0x0000…0071: (code: -32000, message: program activation failed: failed to parse program) Caused by: binary exports reserved symbol stylus_ink_left Location: prover/src/binary.rs:493:9, data: None ``` The contract can fail the check for various reasons (on compile, deployment, etc...). Reading the [Invalid Stylus WASM Contracts explainer](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/cargo-stylus/VALID_WASM.md) can help you understand what makes a WASM contract valid or not. For other deployment and activation errors (e.g., `program activation failed`), see [common issues](/stylus/troubleshooting/common-issues.md). If your contract succeeds, you'll see something like this: ```shell Finished release [optimized] target(s) in 1.88s Reading WASM file at hello-stylus/target/wasm32-unknown-unknown/release/hello-stylus.wasm Compressed WASM size: 3 KB Program succeeded Stylus onchain activation checks with Stylus version: 1 ``` Note that running `cargo stylus check` may take a few minutes, especially if you're verifying a contract for the first time. See `cargo stylus check --help` for more options. ## Deploying your contract Once you're ready to deploy your contract onchain, `cargo stylus deploy` will help you with the deployment and its gas estimation. ### Estimating gas Note: For every transaction, we'll use the testnode pre-funded wallet, you can use `0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659` as your private key. You can estimate the gas required to deploy your contract by running: ```shell cargo stylus deploy \ --endpoint='http://localhost:8547' \ --private-key="0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659" \ --estimate-gas ``` The command should return something like this: ```shell deployment tx gas: 7123737 gas price: "0.100000000" gwei deployment tx total cost: "0.000712373700000000" ETH ``` ### Deployment Let's move on to the contract's actual deployment. Two transactions will be sent onchain: the contract deployment and its activation. ```shell cargo stylus deploy \ --endpoint='http://localhost:8547' \ --private-key="0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659" ``` Once the deployment and activations are successful, you'll see an output similar to this: ```shell deployed code at address: 0x33f54de59419570a9442e788f5dd5cf635b3c7ac deployment tx hash: 0xa55efc05c45efc63647dff5cc37ad328a47ba5555009d92ad4e297bf4864de36 wasm already activated! ``` Make sure to save the contract's deployment address for future interactions! More options are available for sending and outputting your transaction data. See `cargo stylus deploy --help` for more details. ## Exporting the Solidity ABI interface The cargo stylus tool makes it easy to export your contract's ABI using `cargo stylus export-abi`. This command returns the Solidity ABI interface of your smart contract. If you have been running `cargo stylus new` without modifying the output, `cargo stylus export-abi` will return: ```shell /** * This file was automatically generated by Stylus and represents a Rust program. * For more information, please see [The Stylus SDK](https://github.com/OffchainLabs/stylus-sdk-rs). */ // SPDX-License-Identifier: MIT-OR-APACHE-2.0 pragma solidity ^0.8.23; interface ICounter { function number() external view returns (uint256); function setNumber(uint256 new_number) external; function mulNumber(uint256 new_number) external; function addNumber(uint256 new_number) external; function increment() external; function addFromMsgValue() external payable; } ``` Ensure you save the console output to a file that you'll be able to use with your decentralized app. ## Interacting with your Stylus contract Stylus contracts are EVM-compatible, you can interact with them with your tool of choice, such as [Hardhat](https://hardhat.org/), [Foundry's Cast](https://book.getfoundry.sh/cast/), or any other Ethereum-compatible tool. In this example, we'll use Foundry's Cast to send a call and then a transaction to our contract. ### Calling your contract Our contract is a counter; in its initial state, it should store a counter value of `0`. You can call your contract so it returns its current counter value by sending it the following command: Call to the function: number()(uint256) ```shell cast call --rpc-url 'http://localhost:8547' \ [deployed-contract-address] "number()(uint256)" ``` Let's break down the command: * `cast call` command sends a read-only call to your contract (no transaction is sent, so no private key is needed) * The `--rpc-url` option is the `RPC URL` endpoint of our testnode: * The \[deployed-contract-address] is the address we want to interact with, it's the address that was returned by `cargo stylus deploy` * `number()(uint256)` is the function we want to call in Solidity-style signature. The function returns the counter's current value Calling 'number()(uint256)' returns: ```shell 0 ``` The `number()(uint256)` function returns a value of `0`, the contract's initial state. ### Sending a transaction to your contract Let's increment the counter by sending a transaction to your contract's `increment()` function. We'll use Cast's `send` command to send our transaction. Sending a transaction to the function: increment() ```shell cast send --rpc-url 'http://localhost:8547' --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 \ [deployed-contract-address] "increment()" ``` Transaction returns: ```shell blockHash 0xfaa2cce3b9995f3f2e2a2f192dc50829784da9ca4b7a1ad21665a25b3b161f7c blockNumber 20 contractAddress cumulativeGasUsed 97334 effectiveGasPrice 100000000 from 0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E gasUsed 97334 logs [] logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 root status 1 (success) transactionHash 0x28c6ba8a0b9915ed3acc449cf6c645ecc406a4b19278ec1eb67f5a7091d18f6b transactionIndex 1 type 2 blobGasPrice blobGasUsed authorizationList to 0x11B57FE348584f042E436c6Bf7c3c3deF171de49 gasUsedForL1 "0x0" l1BlockNumber "0x1223" ``` Our transactions returned a status of `1`, indicating success, and the counter has been incremented (you can verify this by calling your contract's `number()(uint256)` function again). ## Testing your contract The Stylus testing framework includes `TestVM`, a simulation of the Stylus execution environment that enables you to test your contracts without deploying them. Here's a simple example of how to test the counter contract: ```rust #[cfg(test)] mod test { use super::*; use stylus_sdk::testing::*; #[test] fn test_counter_operations() { // Set up the test VM and instantiate the contract let vm = TestVM::default(); let mut contract = Counter::from(&vm); // Initial state: the counter starts at zero assert_eq!(contract.number(), U256::ZERO); // increment() updates storage contract.increment(); assert_eq!(contract.number(), U256::from(1)); // set_number() overwrites the stored value contract.set_number(U256::from(5)); assert_eq!(contract.number(), U256::from(5)); } } ``` To enable testing, you'll need to add the following to your `Cargo.toml`: ```toml [dev-dependencies] stylus-sdk = { version = "0.10.7", features = ["stylus-test"] } ``` ### Running your tests You can run your tests using the standard Rust test command: ```shell cargo test ``` The testing framework allows you to: * Simulate transaction context and block information * Test contract storage operations * Verify state transitions * Mock contract-to-contract interactions * Test various scenarios without deployment costs For more advanced testing techniques and best practices, see the [Testing contracts with Stylus guide](/stylus/fundamentals/testing-contracts.md). ## Conclusion Congratulations! You've successfully initialized, deployed, and interacted with your first contract using Stylus and Rust. Feel free to explore the [Stylus Rust SDK reference](/stylus/reference/overview.md) for more information on using Stylus in your Arbitrum projects. --- > For a complete page index, fetch # Gas and ink costs This reference provides the latest gas and ink costs for specific WASM opcodes and host I/Os when using Stylus. For a conceptual introduction to Stylus gas and ink, see [Gas and ink (Stylus)](/stylus/concepts/gas-metering.md). Info The per-opcode and per-host-I/O ink costs in the tables below are subject to change as Stylus matures. They are defined in the upstream Stylus/Arbitrator source rather than in the precompiles, so confirm the current values against that source before relying on exact figures. The ink-to-gas relationship is fixed: 10,000 ink = 1 gas. ## Opcode costs The Stylus VM charges for WASM opcodes according to the following table, which was determined via a conservative statistical analysis and is expected to change as Stylus matures. Prices may fluctuate across upgrades as our analysis evolves and optimizations are made. | Hex | Opcode | Ink | Gas | Notes | | ------ | ------------- | ------------ | -------------- | ----------------------------- | | 0x00 | Unreachable | 1 | 0.0001 | | | 0x01 | Nop | 1 | 0.0001 | | | 0x02 | Block | 1 | 0.0001 | | | 0x03 | Loop | 1 | 0.0001 | | | 0x04 | If | 765 | 0.0765 | | | 0x05 | Else | 1 | 0.0001 | | | 0x0b | End | 1 | 0.0001 | | | 0x0c | Br | 765 | 0.0765 | | | 0x0d | BrIf | 765 | 0.0765 | | | 0x0e | BrTable | 2400 + 325x | 0.24 + 0.0325x | Cost varies with table size | | 0x0f | Return | 1 | 0.0001 | | | 0x10 | Call | 3800 | 0.38 | | | 0x11 | CallIndirect | 13610 + 650x | 1.361 + 0.065x | Cost varies with no. of args | | 0x1a | Drop | 9 | 0.0009 | | | 0x1b | Select | 1250 | 0.125 | | | 0x20 | LocalGet | 75 | 0.0075 | | | 0x21 | LocalSet | 210 | 0.0210 | | | 0x22 | LocalTee | 75 | 0.0075 | | | 0x23 | GlobalGet | 225 | 0.0225 | | | 0x24 | GlobalSet | 575 | 0.0575 | | | 0x28 | I32Load | 670 | 0.067 | | | 0x29 | I64Load | 680 | 0.068 | | | 0x2c | I32Load8S | 670 | 0.067 | | | 0x2d | I32Load8U | 670 | 0.067 | | | 0x2e | I32Load16S | 670 | 0.067 | | | 0x2f | I32Load16U | 670 | 0.067 | | | 0x30 | I64Load8S | 680 | 0.068 | | | 0x31 | I64Load8U | 680 | 0.068 | | | 0x32 | I64Load16S | 680 | 0.068 | | | 0x33 | I64Load16U | 680 | 0.068 | | | 0x34 | I64Load32S | 680 | 0.068 | | | 0x35 | I64Load32U | 680 | 0.068 | | | 0x36 | I32Store | 825 | 0.0825 | | | 0x37 | I64Store | 950 | 0.095 | | | 0x3a | I32Store8 | 825 | 0.0825 | | | 0x3b | I32Store16 | 825 | 0.0825 | | | 0x3c | I64Store8 | 950 | 0.095 | | | 0x3d | I64Store16 | 950 | 0.095 | | | 0x3e | I64Store32 | 950 | 0.095 | | | 0x3f | MemorySize | 3000 | 0.3 | | | 0x40 | MemoryGrow | 8050 | 0.805 | | | 0x41 | I32Const | 1 | 0.0001 | | | 0x42 | I64Const | 1 | 0.0001 | | | 0x45 | I32Eqz | 170 | 0.017 | | | 0x46 | I32Eq | 170 | 0.017 | | | 0x47 | I32Ne | 170 | 0.017 | | | 0x48 | I32LtS | 170 | 0.017 | | | 0x49 | I32LtU | 170 | 0.017 | | | 0x4a | I32GtS | 170 | 0.017 | | | 0x4b | I32GtU | 170 | 0.017 | | | 0x4c | I32LeS | 170 | 0.017 | | | 0x4d | I32LeU | 170 | 0.017 | | | 0x4e | I32GeS | 170 | 0.017 | | | 0x4f | I32GeU | 170 | 0.017 | | | 0x50 | I64Eqz | 225 | 0.0225 | | | 0x51 | I64Eq | 225 | 0.0225 | | | 0x52 | I64Ne | 225 | 0.0225 | | | 0x53 | I64LtS | 225 | 0.0225 | | | 0x54 | I64LtU | 225 | 0.0225 | | | 0x55 | I64GtS | 225 | 0.0225 | | | 0x56 | I64GtU | 225 | 0.0225 | | | 0x57 | I64LeS | 225 | 0.0225 | | | 0x58 | I64LeU | 225 | 0.0225 | | | 0x59 | I64GeS | 225 | 0.0225 | | | 0x5a | I64GeU | 225 | 0.0225 | | | 0x67 | I32Clz | 210 | 0.021 | | | 0x68 | I32Ctz | 210 | 0.021 | | | 0x69 | I32Popcnt | 2650 | 0.265 | | | 0x6a | I32Add | 70 | 0.007 | | | 0x6b | I32Sub | 70 | 0.007 | | | 0x6c | I32Mul | 160 | 0.016 | | | 0x6d | I32DivS | 1120 | 0.112 | | | 0x6e | I32DivU | 1120 | 0.112 | | | 0x6f | I32RemS | 1120 | 0.112 | | | 0x70 | I32RemU | 1120 | 0.112 | | | 0x71 | I32And | 70 | 0.007 | | | 0x72 | I32Or | 70 | 0.007 | | | 0x73 | I32Xor | 70 | 0.007 | | | 0x74 | I32Shl | 70 | 0.007 | | | 0x75 | I32ShrS | 70 | 0.007 | | | 0x76 | I32ShrU | 70 | 0.007 | | | 0x77 | I32Rotl | 70 | 0.007 | | | 0x78 | I32Rotr | 70 | 0.007 | | | 0x79 | I64Clz | 210 | 0.021 | | | 0x7a | I64Ctz | 210 | 0.021 | | | 0x7b | I64Popcnt | 6000 | 0.6 | | | 0x7c | I64Add | 100 | 0.01 | | | 0x7d | I64Sub | 100 | 0.01 | | | 0x7e | I64Mul | 160 | 0.016 | | | 0x7f | I64DivS | 1270 | 0.127 | | | 0x80 | I64DivU | 1270 | 0.127 | | | 0x81 | I64RemS | 1270 | 0.127 | | | 0x82 | I64RemU | 1270 | 0.127 | | | 0x83 | I64And | 100 | 0.01 | | | 0x84 | I64Or | 100 | 0.01 | | | 0x85 | I64Xor | 100 | 0.01 | | | 0x86 | I64Shl | 100 | 0.01 | | | 0x87 | I64ShrS | 100 | 0.01 | | | 0x88 | I64ShrU | 100 | 0.01 | | | 0x89 | I64Rotl | 100 | 0.01 | | | 0x8a | I64Rotr | 100 | 0.01 | | | 0xa7 | I32WrapI64 | 100 | 0.01 | | | 0xac | I64ExtendI32S | 100 | 0.01 | | | 0xad | I64ExtendI32U | 100 | 0.01 | | | 0xc0 | I32Extend8S | 100 | 0.01 | | | 0xc1 | I32Extend16S | 100 | 0.01 | | | 0xc2 | I64Extend8S | 100 | 0.01 | | | 0xc3 | I64Extend16S | 100 | 0.01 | | | 0xc4 | I64Extend32S | 100 | 0.01 | | | 0xfc0a | MemoryCopy | 950 + 100x | 0.095 + 0.01x | Cost varies with no. of bytes | | 0xfc0b | MemoryFill | 950 + 100x | 0.095 + 0.01x | Cost varies with no. of bytes | ## Host I/O costs Certain operations require suspending WASM execution so that the Stylus VM can perform tasks natively in the host. This costs about `0.84 gas` to do. Though we’ll publish a full specification later, the following table details the costs of simple operations that run in the host. Note that the values in this table were determined via a conservative statistical analysis and are expected to change as Stylus matures. Prices may fluctuate across upgrades as our analysis evolves and optimizations are made. | Host I/O | Ink | Gas | Notes | | ------------------ | --------------- | -------------- | -------------------------- | | read\_args | 8400 + 5040b | 0.84 + 0.504b | `b` = bytes after first 32 | | write\_result | 8400 + 16381b | 0.84 + 1.6381b | `b` = bytes after first 32 | | keccak | 121800 + 21000w | 12.18 + 2.1w | `w` = EVM words | | block\_basefee | 13440 | 1.344 | | | block\_coinbase | 13440 | 1.344 | | | block\_gas\_limit | 8400 | 0.84 | | | block\_number | 8400 | 0.84 | | | block\_timestamp | 8400 | 0.84 | | | chain\_id | 8400 | 0.84 | | | contract\_address | 13440 | 1.344 | | | evm\_gas\_left | 8400 | 0.84 | | | evm\_ink\_left | 8400 | 0.84 | | | msg\_reentrant | 8400 | 0.84 | | | msg\_sender | 13440 | 1.344 | | | msg\_value | 13440 | 1.344 | | | return\_data\_size | 8400 | 0.84 | | | tx\_ink\_price | 8400 | 0.84 | | | tx\_gas\_price | 13440 | 1.344 | | | tx\_origin | 13440 | 1.344 | | | console\_log\_text | 0 | 0 | debug-only | | console\_log | 0 | 0 | debug-only | | console\_tee | 0 | 0 | debug-only | | null\_host | 0 | 0 | debug-only | ### See also * [Gas and ink (Stylus)](/stylus/concepts/gas-metering.md): A conceptual introduction to the "gas" and "ink" primitives --- > For a complete page index, fetch # Stylus Rust SDK overview The [Stylus Rust SDK](https://github.com/OffchainLabs/stylus-sdk-rs) (v0.10.7) lets you write Solidity ABI-equivalent smart contracts in Rust, compiled to WebAssembly and executed on Arbitrum chains. For a conceptual introduction, see [Stylus: A Gentle Introduction](/stylus/gentle-introduction.md). To deploy your first contract, see the [Quickstart](/stylus/quickstart.md). The SDK is built on [Alloy](https://docs.rs/alloy-primitives/latest/alloy_primitives/), the standard Rust Ethereum primitive library. Because both share the same types, Stylus contracts are compatible with the broader Rust Ethereum ecosystem. The SDK has been audited by OpenZeppelin: * [Initial Stylus Rust SDK audit](/assets/files/2024_09_05_open_zeppelin_security_audit_stylus_rust_sdk-a78b94ded01f4e5f96dfd55a47158680.pdf) (September 2024) * [Stylus SDK v0.10 audit](/assets/files/2025_12_10_open_zeppelin_stylus_sdk_v0_10_audit-6260b27df848854e82c63180a7f0841a.pdf) (December 2025) * [Stylus SDK PR #370 audit](/assets/files/2025_12_19_open_zeppelin_stylus_sdk_pull_request_370_audit-157f8eeec7c1dae1a00e9f1f02d5f818.pdf) (December 2025) All reports are listed on the [security audit reports](/audit-reports.md) page. ## SDK architecture The SDK is a Cargo workspace of six crates. Most developers only interact with `stylus-sdk` and `cargo-stylus` directly — the others are internal dependencies. | Crate | Purpose | | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | [`stylus-sdk`](https://docs.rs/stylus-sdk/latest/stylus_sdk/) | Main SDK — storage types, ABI encoding, call abstractions, crypto, deploy, and the `prelude` module | | `stylus-proc` | Procedural macros: `#[entrypoint]`, `#[public]`, `#[constructor]`, `sol_storage!`, `sol_interface!` | | `stylus-core` | Core trait definitions (`Host`, `HostAccess`) shared across the workspace | | `stylus-test` | Testing framework with `TestVM` for unit-testing contracts without a blockchain | | `stylus-tools` | Deployment, activation, verification, and caching operations (used by `cargo-stylus`) | | `cargo-stylus` | CLI tool for building, deploying, debugging, and managing Stylus contracts | **Requirements:** Rust 1.91.0 or later (pinned in `rust-toolchain.toml`). Contracts compile to the `wasm32-unknown-unknown` target. ## Feature flags The `stylus-sdk` crate exposes these Cargo features: | Feature | Default | Description | | ------------- | ------- | ------------------------------------------------------------------------------------------- | | `mini-alloc` | Yes | Uses an efficient WASM allocator. Disable to use a custom global allocator | | `export-abi` | No | Enables ABI export via `cargo stylus export-abi`. Adds `debug` and uses `tiny-keccak` | | `debug` | No | Enables the `console!` debugging macro for printing to the Blockchain Explorer's trace view | | `hostio` | No | Makes the low-level `hostio` module public for direct host I/O access | | `stylus-test` | No | Enables the `testing` module with `TestVM` for writing unit tests without deploying | | `reentrant` | No | Enables reentrancy support across `stylus-proc`, `stylus-core`, and `stylus-test` | ## Public modules The `stylus-sdk` crate re-exports these modules from its root: | Module | Description | | --------- | -------------------------------------------------------------------------------------------- | | `abi` | ABI encoding and decoding types | | `call` | Cross-contract call abstractions (static calls, delegate calls, value transfers) | | `crypto` | Cryptographic utilities (keccak256) | | `debug` | The `console!` macro for trace-level logging (requires the `debug` feature) | | `deploy` | Contract deployment via `RawDeploy` | | `host` | VM host interface (`self.vm()`) for accessing block context, message data, and storage | | `hostio` | Low-level host I/O FFI bindings (requires the `hostio` feature to be public) | | `methods` | Method routing utilities | | `prelude` | Common imports — re-exports the types most contracts need | | `storage` | Persistent storage types (`StorageU256`, `StorageAddress`, `StorageMap`, `StorageVec`, etc.) | ## Documentation index ### Fundamentals | Article | Description | | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | [Structure of a contract](/stylus/fundamentals/project-structure.md) | Project layout, the `#[entrypoint]` macro, and `#[public]` methods | | [Global variables and functions](/stylus/fundamentals/global-variables-and-functions.md) | Access blockchain context through `self.vm()` — message sender, block info, and crypto functions | | [Contracts](/stylus/fundamentals/contracts.md) | Storage definition, method declarations, and the contract lifecycle | | [Writing tests](/stylus/fundamentals/testing-contracts.md) | Unit test contracts with `TestVM` without deploying to a blockchain | ### Data types | Article | Description | | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | [Primitives](/stylus/fundamentals/data-types/primitives.md) | Rust primitives (bool, integers, addresses) with automatic ABI encoding and Solidity type mappings | | [Compound types](/stylus/fundamentals/data-types/compound-types.md) | Arrays, vectors, tuples, and custom structs | | [Storage](/stylus/fundamentals/data-types/storage.md) | Persistent contract state using storage types and the `sol_storage!` macro | | [Conversions between types](/stylus/fundamentals/data-types/conversions-between-types.md) | Converting between Rust types and Solidity-compatible types | ### Advanced topics | Article | Description | | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | [Rust to Solidity differences](/stylus/advanced/rust-to-solidity-differences.md) | Key differences between writing contracts in Solidity versus Stylus Rust | | [Recommended packages](/stylus/advanced/recommended-libraries.md) | Third-party Rust crates that work well with Stylus | | [Minimal entrypoint contracts](/stylus/advanced/minimal-entrypoint-contracts.md) | Lightweight contracts with custom entrypoints for maximum gas efficiency | | [Hostio exports](/stylus/advanced/hostio-exports.md) | Low-level host I/O functions for advanced contract behavior | ### Using the CLI | Article | Description | | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | [Commands reference](/stylus/cli-tools/commands-reference.md) | Complete reference for all `cargo stylus` commands, flags, and aliases | | [Verify contracts](/stylus/cli-tools/verify-contracts.md) | Verify deployed contracts using reproducible builds | | [Exporting ABI](/stylus/how-tos/exporting-abi.md) | Generate Solidity-compatible ABI files for frontend integration | | [Debugging with replay](/stylus/cli-tools/debugging-tx.md) | Debug transactions by replaying them locally with GDB, LLDB, or StylusDB | | [Optimizing WASM binary size](/stylus/how-tos/optimizing-binaries.md) | Reduce contract size for lower deployment costs | | [Deploying non-Rust WASM contracts](/stylus/how-tos/deploying-non-rust-wasm-contracts.md) | Deploy contracts written in C, C++, or other languages that compile to WebAssembly | ### WASM concepts | Article | Description | | -------------------------------------------------------- | ----------------------------------------------------------------------------- | | [WebAssembly](/stylus/concepts/webassembly.md) | How WASM compilation, deployment, and execution work in Arbitrum Nitro | | [VM differences](/stylus/concepts/vm-differences.md) | Behavioral differences between Stylus WASM execution and traditional EVM | | [Activation](/stylus/concepts/activation.md) | The contract activation process required before a Stylus contract can execute | | [Caching strategy](/stylus/how-tos/caching-contracts.md) | Leveraging the WASM caching system for contract performance | ### Troubleshooting | Article | Description | | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | [Configuration reference](/stylus/reference/stylus-toml-reference.md) | Complete reference for `Stylus.toml`, `Cargo.toml`, and `rust-toolchain.toml` options. | | [Troubleshooting](/stylus/troubleshooting-building-stylus.md) | Find solutions to common issues encountered when building and deploying Stylus contracts. | ### External references | Resource | Description | | -------------------------------------------------------------------- | ----------------------------------------------------------- | | [Stylus by Example](https://stylus-by-example.org/) | Practical, annotated code examples covering common patterns | | [Rust SDK on docs.rs](https://docs.rs/stylus-sdk/latest/stylus_sdk/) | API documentation generated from source | | [Source code](https://github.com/OffchainLabs/stylus-sdk-rs) | Complete source for the SDK workspace | --- > For a complete page index, fetch # Stylus Rust SDK advanced features This document provides information about advanced features included in the [Stylus Rust SDK](https://github.com/OffchainLabs/stylus-sdk-rs), that are not described in the previous pages. For information about deploying Rust smart contracts, see the `cargo stylus` [CLI Tool](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus). For a conceptual introduction to Stylus, see [Stylus: A Gentle Introduction](/stylus/gentle-introduction.md). To deploy your first Stylus smart contract using Rust, refer to the [Quickstart](/stylus/quickstart.md). Info Many of the affordances use macros. Though this section details what each does, it may be helpful to use [`cargo expand`](https://crates.io/crates/cargo-expand) to see what they expand into if you’re doing advanced work in Rust. ## Storage This section provides extra information about how the Stylus Rust SDK handles storage. You can find more information and basic examples in [Variables](https://stylus-by-example.org/basic_examples/variables). Rust smart contracts may use state that persists across transactions. There’s two primary ways to define storage, depending on if you want to use Rust or Solidity definitions. Both are equivalent, and are up to the developer depending on their needs. ### [`#[storage]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/attr.storage.html) The [`#[storage]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/attr.storage.html) macro allows a Rust struct to be used in persistent storage. ```rust #[storage] pub struct Contract { owner: StorageAddress, active: StorageBool, sub_struct: SubStruct, } #[storage] pub struct SubStruct { // types implementing the `StorageType` trait. } ``` Any type implementing the [`StorageType`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageType.html) trait may be used as a field, including other structs, which will implement the trait automatically when [`#[storage]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/attr.storage.html) is applied. You can even implement [`StorageType`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageType.html) yourself to define custom storage types. However, we’ve gone ahead and implemented the common ones. | Type | Info | | ---- | ---- | \| [`StorageBool`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageBool.html) | Stores a bool | | [`StorageAddress`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageAddress.html) | Stores an Alloy [`Address`](https://docs.rs/alloy-primitives/latest/alloy_primitives/struct.Address.html) | | [`StorageUint`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageUint.html) | Stores an Alloy [`Uint`](https://docs.rs/ruint/1.10.1/ruint/struct.Uint.html) | | [`StorageSigned`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageSigned.html) | Stores an Alloy [`Signed`](https://docs.rs/alloy-primitives/latest/alloy_primitives/struct.Signed.html) | | [`StorageFixedBytes`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageFixedBytes.html) | Stores an Alloy [`FixedBytes`](https://docs.rs/alloy-primitives/latest/alloy_primitives/struct.FixedBytes.html) | | [`StorageBytes`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageBytes.html) | Stores a Solidity bytes | | [`StorageString`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageString.html) | Stores a Solidity string | | [`StorageVec`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageVec.html) | Stores a vector of [`StorageType`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageType.html) | | [`StorageMap`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageMap.html) | Stores a mapping of [`StorageKey`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageKey.html) to [`StorageType`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageType.html) | | [`StorageArray`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageArray.html) | Stores a fixed-sized array of [`StorageType`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageType.html) | Every [Alloy primitive](https://docs.rs/alloy-primitives/latest/alloy_primitives/) has a corresponding [`StorageType`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageType.html) implementation with the word `Storage` before it. This includes aliases, like [`StorageU256`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/type.StorageU256.html) and [`StorageB64`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/type.StorageB64.html). ### [`sol_storage!`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/macro.sol_storage.html) The types in [`#[storage]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/attr.storage.html) are laid out in the EVM state trie exactly as they are in [Solidity](https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html). This means that the fields of a struct definition will map to the same storage slots as they would in EVM programming languages. Because of this, it is often nice to define your types using Solidity syntax, which makes that guarantee easier to see. For example, the earlier Rust struct can re-written to: ```rust sol_storage! { pub struct Contract { address owner; // becomes a StorageAddress bool active; // becomes a StorageBool SubStruct sub_struct; } pub struct SubStruct { // other solidity fields, such as mapping(address => uint) balances; // becomes a StorageMap Delegate delegates[]; // becomes a StorageVec } } ``` The above will expand to the equivalent definitions in Rust, each structure implementing the [`StorageType`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageType.html) trait. Many contracts, like [our example **ERC-20**](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/examples/erc20/src/lib.rs), do exactly this. Because the layout is identical to [Solidity’s](https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html), existing Solidity smart contracts can upgrade to Rust without fear of storage slots not lining up. You simply copy-paste your type definitions. Storage layout in contracts using inheritance One exception to this storage layout guarantee is contracts which utilize inheritance. The current solution in Stylus using `#[borrow]` and `#[inherits(...)]` packs nested (inherited) structs into their own slots. This is consistent with regular struct nesting in solidity, but not inherited structs. We plan to revisit this behavior in an upcoming release. Tip Existing Solidity smart contracts can upgrade to Rust if they use proxy patterns. Consequently, the order of fields will affect the JSON ABIs produced that explorers and tooling might use. Most developers won’t need to worry about this though and can freely order their types when working on a Rust contract from scratch. ### Reading and writing storage You can access storage types via getters and setters. For example, the `Contract` struct from earlier might access its `owner` address as follows. ```rust impl Contract { /// Gets the owner from storage. pub fn owner(&self) -> Address { self.owner.get() } /// Updates the owner in storage pub fn set_owner(&mut self, new_owner: Address) { if self.vm().msg_sender() == self.owner.get() { self.owner.set(new_owner); } } /// Unlike other storage type, stringStorage needs to /// use `.set_str()` and `.get_string()` to set and get. pub fn set_base_uri(&mut self, base_uri: String) { self.base_uri.set_str(base_uri); } pub fn get_base_uri(&self) -> String { self.base_uri.get_string() } } ``` In Solidity, one has to be very careful about storage access patterns. Getting or setting the same value twice doubles costs, leading developers to avoid storage access at all costs. By contrast, the Stylus SDK employs an optimal storage-caching policy that avoids the underlying [`SLOAD`](https://www.evm.codes/#54) or [`SSTORE`](https://www.evm.codes/#55) operations. Tip Stylus uses storage caching, so multiple accesses of the same variable is virtually free. However it must be said that storage is ultimately more expensive than memory. So if a value doesn’t need to be stored in state, you probably shouldn’t do it. ### Collections Collections like [`StorageVec`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageVec.html) and [`StorageMap`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageMap.html) are dynamic and have methods like [`push`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageVec.html#method.push), [`insert`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageMap.html#method.insert), [`replace`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageMap.html#method.replace), and similar. ```rust impl SubStruct { pub fn add_delegate(&mut self, delegate: Address) { self.delegates.push(delegate); } pub fn track_balance(&mut self, address: Address) { self.balances.insert(address, address.balance()); } } ``` You may notice that some methods return types like [`StorageGuard`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageGuard.html) and [`StorageGuardMut`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageGuardMut.html). This allows us to leverage the Rust borrow checker for storage mistakes, just like it does for memory. Here’s an example that will fail to compile. ```rust fn mistake(vec: &mut StorageVec) -> U64 { let value = vec.setter(0); let alias = vec.setter(0); value.set(32.into()); alias.set(48.into()); value.get() // uh, oh. what value should be returned? } ``` Under the hood, [`vec.setter()`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageVec.html#method.setter) returns a [`StorageGuardMut`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageGuardMut.html) instead of a [`&mut StorageU64`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/type.StorageU64.html). Because the guard is bound to a [`&mut StorageVec`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageVec.html) lifetime, `value` and `alias` cannot be alive simultaneously. This causes the Rust compiler to reject the above code, saving you from entire classes of storage aliasing errors. In this way the Stylus SDK safeguards storage access the same way Rust ensures memory safety. It should never be possible to alias Storage without `unsafe` Rust. ### [`SimpleStorageType`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.SimpleStorageType.html) You may run into scenarios where a collection’s methods like [`push`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageVec.html#method.push) and [`insert`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageMap.html#method.insert) aren’t available. This is because only primitives, which implement a special trait called [`SimpleStorageType`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.SimpleStorageType.html), can be added to a collection by value. For nested collections, one instead uses the equivalent [`grow`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageVec.html#method.grow) and [`setter`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageVec.html#method.setter). ```rust fn nested_vec(vec: &mut StorageVec>) { let mut inner = vec.grow(); // adds a new element accessible via `inner` inner.push(0.into()); // inner is a guard to a StorageVec } fn nested_map(map: &mut StorageMap>) { let mut slot = map.setter(0); slot.push(0); } ``` ### [`Erase`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.Erase.html) and [`#[derive(Erase)]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/derive.Erase.html) Some [`StorageType`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageType.html) values implement [`Erase`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.Erase.html), which provides an [`erase()`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.Erase.html#tymethod.erase) method for clearing state. We’ve implemented [`Erase`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.Erase.html) for all primitives, and for vectors of primitives, but not maps. This is because a solidity [`mapping`](https://docs.soliditylang.org/en/latest/types.html#mapping-types) does not provide iteration, and so it’s generally impossible to know which slots to set to zero. Structs may also be [`Erase`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.Erase.html) if all of the fields are. [`#[derive(Erase)]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/derive.Erase.html) lets you do this automatically. ```rust sol_storage! { #[derive(Erase)] pub struct Contract { address owner; // can erase primitive uint256[] hashes; // can erase vector of primitive } pub struct NotErase { mapping(address => uint) balances; // can't erase a map mapping(uint => uint)[] roots; // can't erase vector of maps } } ``` You can also implement [`Erase`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.Erase.html) manually if desired. Note that the reason we care about [`Erase`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.Erase.html) at all is that you get storage refunds when clearing state, lowering fees. There’s also minor implications for patterns using `unsafe` Rust. ### The storage cache The Stylus SDK employs an optimal storage-caching policy that avoids the underlying [`SLOAD`](https://www.evm.codes/#54) or [`SSTORE`](https://www.evm.codes/#55) operations needed to get and set state. For the vast majority of use cases, this happens in the background and requires no input from the user. However, developers working with `unsafe` Rust implementing their own custom [`StorageType`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageType.html) collections, the [`StorageCache`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageCache.html) type enables direct control over this data structure. Included are `unsafe` methods for manipulating the cache directly, as well as for bypassing it altogether. ### Immutables and [`PhantomData`](https://doc.rust-lang.org/core/marker/struct.PhantomData.html) So that generics are possible in [`sol_interface!`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/macro.sol_interface.html), [`core::marker::PhantomData`](https://doc.rust-lang.org/core/marker/struct.PhantomData.html) implements [`StorageType`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.StorageType.html) and takes up zero space, ensuring that it won’t cause storage slots to change. This can be useful when writing libraries. ```rust pub trait Erc20Params { const NAME: &'static str; const SYMBOL: &'static str; const DECIMALS: u8; } sol_storage! { pub struct Erc20 { mapping(address => uint256) balances; PhantomData phantom; } } ``` The above allows consumers of Erc20 to choose immutable constants via specialization. See our [**ERC-20** sample contract](https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/examples/erc20/src/lib.rs) for a full example of this feature. ## Functions This section provides extra information about how the Stylus Rust SDK handles functions. You can find more information and basic examples in [Functions](https://stylus-by-example.org/basic_examples/function), [Bytes in, bytes out programming](https://stylus-by-example.org/basic_examples/bytes_in_bytes_out), [Inheritance](https://stylus-by-example.org/basic_examples/inheritance) and [Sending ether](https://stylus-by-example.org/basic_examples/sending_ether). ### Pure, View, and Write functions For non-payable methods the [`#[public]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/attr.public.html) macro can figure state mutability out for you based on the types of the arguments. Functions with `&self` will be considered `view`, those with `&mut self` will be considered `write`, and those with neither will be considered `pure`. Please note that `pure` and `view` functions may change the state of other contracts by calling into them. ### [`#[entrypoint]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/attr.entrypoint.html) This macro allows you to define the entrypoint, which is where Stylus execution begins. Without it, the contract will fail to pass `cargo stylus check`. Most commonly, the macro is used to annotate the top level storage struct. ```rust sol_storage! { #[entrypoint] pub struct Contract { ... } // only one entrypoint is allowed pub struct SubStruct { ... } } ``` The above will make the public methods of `Contract` the first to consider during invocation. ### Reentrancy If a contract calls another that then calls the first, it is said to be reentrant. Stylus contracts are reentrancy-safe by default: the high-level call functions ([`call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/fn.call.html), [`static_call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/fn.static_call.html), and [`delegate_call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/fn.delegate_call.html)) automatically flush or clear the storage cache before invoking another contract, so cached values can't go stale across reentry. This happens through the type system and requires no configuration. Warning The `reentrant` Cargo feature flag and the `deny_reentrant` entrypoint guard are deprecated as of SDK 0.10.5 and should not be used. Reentrancy safety now comes from automatic storage-cache flushing, which makes the guard redundant. Automatic cache flushing protects your storage, but it is only part of defending against exploits. Continue to follow the checks-effects-interactions pattern—update your own state before calling untrusted contracts—and review reentrant code carefully, ideally with third-party auditors. You can detect whether the current call is reentrant via `self.vm().msg_reentrant()` and condition your business logic accordingly. ### [`TopLevelStorage`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.TopLevelStorage.html) The [`#[entrypoint]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/attr.entrypoint.html) macro will automatically implement the [`TopLevelStorage`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.TopLevelStorage.html) trait for the annotated `struct`. The single type implementing [`TopLevelStorage`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.TopLevelStorage.html) is special in that mutable access to it represents mutable access to the entire program’s state. This idea will become important when discussing calls to other programs in later sections. ### Inheritance, `#[inherit]`, and `#[borrow]`. Info Stylus doesn't support contract multi-inheritance yet. Composition in Rust follows that of Solidity. Types that implement [`Router`](https://docs.rs/stylus-sdk/latest/stylus_sdk/abi/trait.Router.html), the trait that [`#[public]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/attr.public.html) provides, can be connected via inheritance. ```rust #[public] #[inherit(Erc20)] impl Token { pub fn mint(&mut self, amount: U256) -> Result<(), Vec> { ... } } #[public] impl Erc20 { pub fn balance_of() -> Result { ... } } ``` Because `Token` inherits `Erc20` in the above, if `Token` has the [`#[entrypoint]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/attr.entrypoint.html), calls to the contract will first check if the requested method exists within `Token`. If a matching function is not found, it will then try the `Erc20`. Only after trying everything `Token` inherits will the call revert. Note that because methods are checked in that order, if both implement the same method, the one in `Token` will override the one in `Erc20`, which won’t be callable. This allows for patterns where the developer imports a crate implementing a standard, like the **ERC-20**, and then adds or overrides just the methods they want to without modifying the imported `Erc20` type. Warning Stylus does not currently contain explicit `override` or `virtual` keywords for explicitly marking override functions. It is important, therefore, to carefully ensure that contracts are only overriding the functions. Inheritance can also be chained. `#[inherit(Erc20, Erc721)]` will inherit both `Erc20` and `Erc721`, checking for methods in that order. `Erc20` and `Erc721` may also inherit other types themselves. Method resolution finds the first matching method by [Depth First Search](https://en.wikipedia.org/wiki/Depth-first_search). For the above to work, `Token` must implement [`Borrow`](https://doc.rust-lang.org/core/borrow/trait.Borrow.html). You can implement this yourself, but for simplicity, [`#[storage]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/attr.storage.html) and [`sol_storage!`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/macro.sol_storage.html) provide a `#[borrow]` annotation. ```rust sol_storage! { #[entrypoint] pub struct Token { #[borrow] Erc20 erc20; ... } pub struct Erc20 { ... } } ``` ### Fallback and receive functions Starting with SDK version 0.7.0, the [`Router`](https://docs.rs/stylus-sdk/latest/stylus_sdk/abi/trait.Router.html) trait supports the `fallback` and `receive` methods, which work similar to their Solidity counterparts: * [`fallback`](https://docs.rs/stylus-sdk/latest/stylus_sdk/abi/trait.Router.html#tymethod.fallback): This method is called when a transaction is sent to the contract with calldata that doesn't match any function signature. It serves as a catch-all function for contract interactions that don't match any defined interface. * [`receive`](https://docs.rs/stylus-sdk/latest/stylus_sdk/abi/trait.Router.html#tymethod.receive): This method is called when a transaction is sent to the contract with no calldata (empty calldata). It allows the contract to receive **ETH**. Here's an example implementation: ```rust #[public] impl Contract { // Automatically called when transaction has calldata that doesn't match any function #[fallback] pub fn fallback(&mut self, calldata: Vec) -> Result, Vec> { // Handle arbitrary calldata Ok(Vec::new()) // Return empty response or custom response data } // Automatically called when transaction has empty calldata #[receive] pub fn receive(&mut self) -> Result<(), Vec> { // Handle ETH receiving logic Ok(()) } } ``` Both methods can be annotated with `#[payable]` to accept **ETH** along with the transaction. Without this annotation, transactions that send **ETH** will be rejected. ## Calls Just as with storage and functions, Stylus SDK calls are Solidity ABI equivalent. This means you never have to know the implementation details of other contracts to invoke them. You simply import the Solidity interface of the target contract, which can be auto-generated via the `cargo stylus` [CLI tool](https://github.com/OffchainLabs/stylus-sdk-rs/tree/main/cargo-stylus#exporting-solidity-abis). Tip You can call contracts in any programming language with the Stylus SDK. ### [`sol_interface!`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/macro.sol_interface.html) This macro defines a `struct` for each of the Solidity interfaces provided. ```rust sol_interface! { interface IService { function makePayment(address user) payable returns (string); function getConstant() pure returns (bytes32) } interface ITree { // other interface methods } } ``` The above will define `IService` and `ITree` for calling the methods of the two contracts. Info Currently only functions are supported, and any other items in the interface will cause an error. For example, `IService` will have a `make_payment` method that accepts an [`Address`](https://docs.rs/alloy-primitives/latest/alloy_primitives/struct.Address.html) and returns a [`B256`](https://docs.rs/alloy-primitives/latest/alloy_primitives/aliases/type.B256.html). ```rust pub fn do_call(&mut self, account: IService, user: Address) -> Result { account.make_payment(self, user) // note the snake case } ``` Observe the casing change. [`sol_interface!`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/macro.sol_interface.html) computes the selector based on the exact name passed in, which should almost always be `CamelCase`. For aesthetics, the rust functions will instead use `snake_case`. ### Configuring gas and value with [`Call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.Call.html) [`Call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.Call.html) lets you configure a call via optional configuration methods. This is similar to how one would configure opening a [`File`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#examples) in Rust. ```rust #[payable] pub fn do_call(&mut self, account: IService, user: Address) -> Result> { let config = Call::new_payable(self, self.vm().msg_value()) // set the callvalue .gas(self.vm().evm_gas_left() / 2); // limit to half the gas left Ok(account.make_payment(self.vm(), config, user)?) } ``` Use [`Call::new_mutating(self)`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.Call.html#method.new_mutating) for a non-payable call and [`Call::new_payable(self, value)`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.Call.html#method.new_payable) when forwarding value. By default [`Call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.Call.html) supplies all gas remaining and zero value, which often means [`Call::new()`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.Call.html#method.new) may be passed to the method directly. ### Reentrant calls Cross-contract calls flush or clear the [`StorageCache`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageCache.html) to safeguard state across reentry. This happens automatically via the type system, so you don't need to manage the cache yourself. ```rust sol_interface! { interface IMethods { function pureFoo() external pure; function viewFoo() external view; function writeFoo() external; function payableFoo() external payable; } } #[public] impl Contract { pub fn call_pure(&self, methods: IMethods) -> Result<(), Vec> { Ok(methods.pure_foo(self.vm(), Call::new())?) // `pure` methods might lie about not being `view` } pub fn call_view(&self, methods: IMethods) -> Result<(), Vec> { Ok(methods.view_foo(self.vm(), Call::new())?) } pub fn call_write(&mut self, methods: IMethods) -> Result<(), Vec> { methods.view_foo(self.vm(), Call::new())?; // allows `pure` and `view` methods too let config = Call::new_mutating(self); Ok(methods.write_foo(self.vm(), config)?) } #[payable] pub fn call_payable(&mut self, methods: IMethods) -> Result<(), Vec> { let config = Call::new_mutating(self); methods.write_foo(self.vm(), config)?; let config = Call::new_payable(self, U256::ZERO); Ok(methods.payable_foo(self.vm(), config)?) } } ``` In the above, we’re able to pass `self.vm()` and `&mut self` because `Contract` implements [`TopLevelStorage`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.TopLevelStorage.html), which means that a reference to it entails access to the entirety of the contract’s state. This is the reason it is sound to make a call, since it ensures all cached values are invalidated and/or persisted to state at the right time. Note that the call context is built as a separate `let` binding before the call so the immutable borrow from `self.vm()` and the mutable borrow from `Call::new_mutating(self)` don't overlap. When writing Stylus libraries, a type might not be [`TopLevelStorage`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/trait.TopLevelStorage.html) and therefore `&self` or `&mut self` won’t work directly. Building a [`Call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.Call.html) from a generic parameter is the usual solution. ```rust pub fn do_call>( storage: &mut T, // can be generic, but often just &mut self account: IService, // serializes as an Address user: Address, ) -> Result> { let vm = storage.borrow().vm(); let value = vm.msg_value(); // set the callvalue let gas = vm.evm_gas_left() / 2; // limit to half the gas left let config = Call::new_payable(storage, value).gas(gas); // exclusive access to all contract storage Ok(account.make_payment(storage.borrow().vm(), config, user)?) // note the snake case } ``` In the context of a [`#[public]`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/attr.public.html) call, the `&mut impl` argument will correctly distinguish the method as being `write` or [`payable`](https://docs.alchemy.com/docs/solidity-payable-functions). ### [`call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/fn.call.html), [`static_call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/fn.static_call.html), and [`delegate_call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/fn.delegate_call.html) Though [`sol_interface!`](https://docs.rs/stylus-sdk/latest/stylus_sdk/prelude/macro.sol_interface.html) and [`Call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.Call.html) form the most common idiom to invoke other contracts, their underlying [`call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/fn.call.html) and [`static_call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/fn.static_call.html) are exposed for direct access. ```rust let config = Call::new_mutating(self); let return_data = call(self.vm(), config, contract, &call_data)?; ``` The host (`self.vm()`) is the first argument, followed by the call context, the target address, and the calldata. In each case the calldata is supplied as a [`Vec`](https://doc.rust-lang.org/alloc/vec/struct.Vec.html). The return result is either the raw return data on success, or a call [`Error`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/enum.Error.html) on failure. [`delegate_call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/fn.delegate_call.html) is also available, though it's `unsafe` and doesn't have a richly-typed equivalent. This is because a delegate call must trust the other contract to uphold safety requirements. Though this function clears any cached values, the other contract may arbitrarily change storage, spend ether, and do other things one should never blindly allow other contracts to do. ### [`transfer_eth`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/fn.transfer_eth.html) This method provides a convenient shorthand for transferring ether. Note This method invokes the other contract, which may in turn call others. All gas is supplied, which the recipient may burn. If this is not desired, the [`call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/fn.call.html) function may be used instead. ```rust // these two are equivalent transfer_eth(self.vm(), recipient, value)?; let config = Call::new_payable(self, value); call(self.vm(), config, recipient, &[])?; ``` ### [`RawCall`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.RawCall.html) and `unsafe` calls Occasionally, an untyped call to another contract is necessary. [`RawCall`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.RawCall.html) lets you configure an `unsafe` call by calling optional configuration methods. This is similar to how one would configure opening a [`File`](https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#examples) in Rust. ```rust let data = unsafe { RawCall::new_delegate(self.vm()) // configure a delegate call .gas(2100) // supply 2100 gas .limit_return_data(0, 32) // only read the first 32 bytes back .flush_storage_cache() // flush the storage cache before the call .call(contract, &calldata)? // do the call }; ``` Pass the host (`self.vm()`) to the constructor: use [`RawCall::new(self.vm())`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.RawCall.html#method.new) for a regular call or [`RawCall::new_delegate(self.vm())`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.RawCall.html#method.new_delegate) for a delegate call. Note The [`call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.RawCall.html#method.call) method is always `unsafe`. Unlike the high-level [`call`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/fn.call.html) functions, `RawCall` does not flush the storage cache for you, so use [`flush_storage_cache`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.RawCall.html#method.flush_storage_cache) and [`clear_storage_cache`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.RawCall.html#method.clear_storage_cache) when reentry could observe stale state. ## [`RawDeploy`](https://docs.rs/stylus-sdk/latest/stylus_sdk/deploy/struct.RawDeploy.html) and `unsafe` deployments Right now the only way to deploy a contract from inside Rust is to use [`RawDeploy`](https://docs.rs/stylus-sdk/latest/stylus_sdk/deploy/struct.RawDeploy.html), similar to [`RawCall`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.RawCall.html). As with [`RawCall`](https://docs.rs/stylus-sdk/latest/stylus_sdk/call/struct.RawCall.html), this mechanism is inherently unsafe due to reentrancy concerns, and requires manual management of the [`StorageCache`](https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageCache.html). Note That the EVM allows init code to make calls to other contracts, which provides a vector for reentrancy. This means that this technique may enable storage aliasing if used in the middle of a storage reference's lifetime and if reentrancy is allowed. When configured with a `salt`, [`RawDeploy`](https://docs.rs/stylus-sdk/latest/stylus_sdk/deploy/struct.RawDeploy.html) will use [`CREATE2`](https://www.evm.codes/#f5) instead of the default [`CREATE`](https://www.evm.codes/#f0), facilitating address determinism. --- > For a complete page index, fetch # Configuration reference A Stylus Rust project uses three configuration files: | File | Purpose | | --------------------- | ----------------------------------------------------------------------- | | `Stylus.toml` | Marks a package as a Stylus contract and stores deployment metadata | | `Cargo.toml` | Defines dependencies, feature flags, library targets, and build profile | | `rust-toolchain.toml` | Pins the Rust compiler version and WebAssembly compilation target | Running `cargo stylus new ` generates all three files with sensible defaults. The sections below document every Stylus-specific option you can configure. ## `Stylus.toml` `Stylus.toml` serves two purposes: 1. **Contract marker** — `cargo stylus` identifies a package as a Stylus contract by the presence of this file alongside `Cargo.toml`. Without it, the package is ignored during workspace builds and deployments. 2. **Deployment metadata** — Stores per-contract deployment records and, at the workspace level, network endpoint definitions. ### Contract-level `Stylus.toml` Place this file in the root of each contract package (next to `Cargo.toml`). ```toml [contract] [contract.deployments.my-deployment] network = "arbitrum-sepolia" no_activate = false deployer_address = "0x1234567890abcdef1234567890abcdef12345678" ``` #### `[contract]` section The `[contract]` table is required. It can be empty — its presence marks the package as a Stylus contract. #### `[contract.deployments.]` section Each named deployment stores a record of where and how the contract was deployed. | Key | Type | Description | | ------------------ | --------- | --------------------------------------------------------------------------------------- | | `network` | `string` | Network identifier matching a `[workspace.networks]` entry or a well-known network name | | `no_activate` | `bool` | If `true`, the contract was deployed without activation. Default: `false` | | `deployer_address` | `address` | The Ethereum address (checksummed hex) of the account that deployed the contract | ### Workspace-level `Stylus.toml` Place this file at the root of a Cargo workspace to define shared network configuration. ```toml [workspace] [workspace.networks.arbitrum-sepolia] endpoint = "https://sepolia-rollup.arbitrum.io/rpc" [workspace.networks.arbitrum-one] endpoint = "https://arb1.arbitrum.io/rpc" ``` #### `[workspace]` section The `[workspace]` table is required. It can be empty. #### `[workspace.networks.]` section Define named networks that contracts in the workspace can reference. | Key | Type | Description | | ---------- | -------- | -------------------------------------- | | `endpoint` | `string` | RPC endpoint URL for the named network | ## `Cargo.toml` Standard Rust manifest with Stylus-specific sections. The `cargo stylus new` command generates these defaults automatically. ### `[lib]` Stylus contracts must compile as both a Rust library and a C-compatible dynamic library (for WASM export): ```toml [lib] crate-type = ["lib", "cdylib"] ``` * `lib` enables standard Rust imports and testing. * `cdylib` produces the WebAssembly binary that gets deployed onchain. Warning Removing `cdylib` from `crate-type` prevents the contract from compiling to WASM. Removing `lib` prevents running tests and exporting ABI. ### `[dependencies]` At minimum, a Stylus contract requires `stylus-sdk`: ```toml [dependencies] stylus-sdk = "0.10.7" alloy-primitives = "1.5.7" alloy-sol-types = "1.5.7" ``` The `alloy-primitives` and `alloy-sol-types` crates are transitive dependencies of `stylus-sdk` but are typically listed explicitly when your contract uses Ethereum types (`Address`, `U256`, `sol!` macro) directly. ### `[features]` Feature flags control compilation behavior. The defaults generated by `cargo stylus new`: ```toml [features] default = ["mini-alloc"] export-abi = ["stylus-sdk/export-abi"] debug = ["stylus-sdk/debug"] mini-alloc = ["stylus-sdk/mini-alloc"] contract-client-gen = [] ``` | Feature | Purpose | | --------------------- | ------------------------------------------------------------------------------------------------- | | `mini-alloc` | Uses a minimal memory allocator optimized for contract binary size. Enabled by default. | | `export-abi` | Generates a Solidity-compatible ABI via `cargo stylus export-abi`. Enables `debug` automatically. | | `debug` | Enables `console!` logging for local debugging. Has no effect in onchain execution. | | `contract-client-gen` | Reserved for tooling that generates Solidity interface contracts from Rust source. | #### SDK features These features are defined in the `stylus-sdk` crate and can be enabled via `stylus-sdk/feature-name` syntax in your `[features]` section: | Feature | Purpose | | ------------- | ----------------------------------------------------------------------------------------- | | `stylus-test` | Enables the `TestVM` testing framework for writing unit tests without a blockchain. | | `reentrant` | Enables reentrancy support for contracts that need to make external calls that call back. | | `hostio` | Exposes low-level host I/O function signatures for advanced use cases. | **Example** enabling `stylus-test` for development: ```toml [dev-dependencies] stylus-sdk = { version = "0.10.7", features = ["stylus-test"] } ``` ### `[profile.release]` The release profile controls how the WASM binary is compiled. These defaults optimize for small binary size: ```toml [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = 3 ``` | Key | Default | Description | | --------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `codegen-units` | `1` | Compile the entire crate as a single unit for maximum optimization. | | `strip` | `true` | Remove debug symbols and metadata from the binary. | | `lto` | `true` | Enable link-time optimization across all crates. | | `panic` | `abort` | Use immediate abort on panic instead of unwinding (smaller binary). | | `opt-level` | `3` | Optimize for speed. Use `"s"` or `"z"` to optimize for binary size instead. See [Optimizing binaries](/stylus/how-tos/optimizing-binaries.md) for guidance. | Tip If your contract exceeds the 24KB compressed size limit, change `opt-level` to `"z"` for maximum size reduction. This typically produces smaller binaries at the cost of some runtime performance. ## `rust-toolchain.toml` Pins the Rust compiler version and ensures the WASM compilation target is installed: ```toml [toolchain] channel = "1.91.0" targets = ["wasm32-unknown-unknown"] ``` | Key | Description | | --------- | --------------------------------------------------------------------------------------------------------------- | | `channel` | Rust compiler version. Used by block explorers for deterministic source verification. | | `targets` | Must include `wasm32-unknown-unknown` for Stylus contracts. Additional targets can be listed for local testing. | Warning Changing the Rust version may affect source code verification on block explorers like Arbiscan. Pin to a specific stable version rather than using `stable` or `nightly`. ## Complete examples ### Single contract A minimal Stylus contract project: ```text my-contract/ ├── src/ │ ├── lib.rs │ └── main.rs ├── Cargo.toml ├── Stylus.toml └── rust-toolchain.toml ``` **`Stylus.toml`** ```toml [contract] ``` **`Cargo.toml`** ```toml [package] name = "my-contract" version = "0.1.0" edition = "2021" [dependencies] stylus-sdk = "0.10.7" alloy-primitives = "1.5.7" alloy-sol-types = "1.5.7" [dev-dependencies] stylus-sdk = { version = "0.10.7", features = ["stylus-test"] } [features] default = ["mini-alloc"] export-abi = ["stylus-sdk/export-abi"] debug = ["stylus-sdk/debug"] mini-alloc = ["stylus-sdk/mini-alloc"] [lib] crate-type = ["lib", "cdylib"] [profile.release] codegen-units = 1 strip = true lto = true panic = "abort" opt-level = "z" ``` **`rust-toolchain.toml`** ```toml [toolchain] channel = "1.91.0" targets = ["wasm32-unknown-unknown"] ``` ### Workspace with multiple contracts A workspace containing two contracts with shared network configuration: ```text my-workspace/ ├── contracts/ │ ├── token/ │ │ ├── src/ │ │ ├── Cargo.toml │ │ └── Stylus.toml │ └── governance/ │ ├── src/ │ ├── Cargo.toml │ └── Stylus.toml ├── crates/ │ └── shared-lib/ │ ├── src/ │ └── Cargo.toml ├── Cargo.toml ├── Stylus.toml └── rust-toolchain.toml ``` **Workspace `Stylus.toml`** ```toml [workspace] [workspace.networks.arbitrum-sepolia] endpoint = "https://sepolia-rollup.arbitrum.io/rpc" [workspace.networks.arbitrum-one] endpoint = "https://arb1.arbitrum.io/rpc" ``` **Workspace `Cargo.toml`** ```toml [workspace] resolver = "2" members = ["contracts/*", "crates/*"] ``` **Contract `Stylus.toml`** (in `contracts/token/`) ```toml [contract] [contract.deployments.sepolia] network = "arbitrum-sepolia" no_activate = false deployer_address = "0x1234567890abcdef1234567890abcdef12345678" ``` Note that `crates/shared-lib/` does not have a `Stylus.toml` because it is a shared Rust library, not a deployable contract. Only packages with a `Stylus.toml` are treated as contracts by `cargo stylus`. --- > For a complete page index, fetch # Troubleshooting Stylus ### How does Stylus manage security issues in smart contracts when interacting with so many different languages? All languages are compiled to WASM for them to be able to work with Stylus. So it just needs to verify that the produced WASM programs behave as they should inside the new virtual machine. ### Is there any analogue of the fallback function from Solidity in the Rust Stylus SDK? Yes, starting with SDK version 0.7.0, the Router trait supports both `fallback` and `receive` methods, similar to their Solidity counterparts. The `fallback` method is called when a transaction has calldata that doesn't match any defined function, while the `receive` method is called when a transaction has empty calldata. You can find more information in [Fallback and receive functions](https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#fallback-and-receive-functions). For older SDK versions (pre-0.7.0), you can use a minimal entrypoint and perform raw delegate calls, forwarding your calldata. You can find more information in [Bytes-in, bytes-out programming](https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#bytes-in-bytes-out-programming) and [call, static\_call and delegate\_call](https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#call-static_call-and-delegate_call). ### Is it possible to verify Stylus contracts on the block explorer? Currently it is not possible to verify contracts compiled to WASM on the block explorer, but we are actively working with providers to have the verification process ready for when Stylus reaches mainnet-ready status. ### Do Stylus contracts compile down to EVM bytecode like prior other attempts? No. Stylus contracts are compiled down to WASM. The user writes a program in Rust / C / C++ which is then compiled down to WebAssembly. ### How is a Stylus contract deployed? Stylus contracts are deployed onchain as a blob of bytes, just like EVM ones. The only difference is that when the contract executes, instead of invoking the EVM, we invoke a separate WASM runtime. Note that a special EOF-inspired prefix distinguishes Stylus contracts from traditional EVM contracts: when a contract's bytecode starts with the magic `0xEFF00000` prefix, it's a Stylus WASM contract. ### Is there a new transaction type to deploy Stylus contracts? You deploy a Stylus contract the same way that Solidity contracts are deployed. There are no special transaction types. As a UX note: a WASM will revert until a special instrumentation operation is performed by a call to the new  `ArbWasm` precompile, which readies the program for calls onchain. You can find instructions for deploying a Stylus contract in our [Quickstart](https://docs.arbitrum.io/stylus/stylus-quickstart#checking-your-stylus-project-is-valid). ### Do Stylus contracts use a different type of ABI? Stylus contracts use solidity ABIs. Methods, signatures, logs, calls, etc. work exactly as in the EVM. From a user's / explorer's perspective, it all just looks and behaves like Solidity. ### Does the Stylus SDK for Rust support custom data structures? For in-memory usage, you should be able to use any implementation of custom data structures without problems. For storage usage, it may be more complicated. Stylus uses the EVM storage system, so you'll need to define the data structure on top of it. However, in the SDK, there's a storage trait that custom types can implement to back their collections with the EVM state trie. The SDK macros are also compatible with them, although it's still fundamentally a global key-value system. You can read more about it in the [Stylus Rust SDK page](https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#storage). As an alternative solution, you can use [entrypoint-style contracts](https://docs.arbitrum.io/stylus/reference/rust-sdk-guide#bytes-in-bytes-out-programming) for your custom data structures. ### Why do I get an error "no library targets found in package" when trying to compile and old example? Some of the first Stylus examples were built and deployed using a previous version of [cargo-stylus](https://github.com/OffchainLabs/cargo-stylus) (`0.1.x`). In that version, Stylus projects were structured as regular Rust binaries. Since [cargo-stylus v0.2.1](https://github.com/OffchainLabs/cargo-stylus/releases/tag/v0.2.1), Stylus projects are structured as libraries, so when trying to compile old projects you might get an error `no library targets found in package`. To solve this, it's usually enough to rename the `main.rs` file to a `lib.rs` file. ### How can I generate the ABI of my Stylus contract? The has a command that allows you to export the ABI of your Stylus contract: `cargo stylus export-abi`. If you're using the Stylus Rust SDK, you'll need to enable the `export-abi` feature in your `Cargo.toml` file like so: ```rust [features] export-abi = ["stylus-sdk/export-abi"] ``` You'll also need to have a `main.rs` file that selects that feature. This is an example of a `main.rs` file that allows you to export the ABI of the [stylus-hello-world](https://github.com/OffchainLabs/stylus-hello-world) example project: ```rust #![cfg_attr(not(feature = "export-abi"), no_main)] #[cfg(feature = "export-abi")] fn main() { stylus_hello_world::main(); } ``` ### How can I find out if the smart contract bytecode is from a Stylus contract or Solidity contract? You can check the first three bytes of the code at the contract address. If they read `0xEFF000`, it's a Stylus program. Otherwise, it will be from a Solidity contract. ### I'm trying to work with cargo stylus on Windows and got error: failed to resolve: could not find unix in os Cargo Stylus is just compatible with Unix operating systems and not Windows. You should install `WSL` (Windows Subsystem for Linux) before using that and then use `WSL terminal` to install and use cargo stylus. ### How can I return a struct from a function? Currently, the SDK doesn't support external structs directly, but support is in progress. For now, you can use tuples as output instead of structs. Keep in mind that structs are mapped to tuples by the Solidity ABI. You can read more about this mapping here: [Solidity ABI Specification](https://docs.soliditylang.org/en/v0.8.19/abi-spec.html#mapping-solidity-to-abi-types). Since the current SDK macro doesn't automatically handle struct-to-tuple conversions, you'll need to manually convert your struct into a tuple in the return type. ### How can I get the WASM opcodes of the compiled Stylus contracts? To view the WASM opcodes, you can convert the WebAssembly binary (WASM) to WebAssembly Text (WAT). This conversion is possible using the WebAssembly Binary Toolkit (wabt). An easy way to do this is to use the online tool [Wasm-to-Wat converter](https://webassembly.github.io/wabt/demo/wasm2wat/), which is part of wabt. For more information or if you'd like to use the toolkit locally, you can find wabt on [GitHub](https://github.com/WebAssembly/wabt). ### What is the difference between .set and .setter? *** * **`.set`\*\***:\*\* This method is used directly to set a value in storage. It's a straightforward way to assign a value if you only need to perform a one-time set operation. * **`.setter`\*\***:\*\* This method provides a handle to a storage slot. It allows you to both `set` and `get` the value of the storage slot, making it more versatile if you need to access the current value and also update it. If you plan to both retrieve and modify a value frequently, it's more efficient to use `.setter`, assign it to a variable, and then use the `.get` and `.set` methods on that variable. However, if you only need to set a value, you can use `.set` directly. --- > For a complete page index, fetch # Common issues and solutions This guide covers frequently encountered issues in Stylus development with step-by-step solutions. ## Installation and setup ### cargo stylus not found **Problem**: Running `cargo stylus` returns "command not found." **Solution**: ```shell # Install cargo-stylus cargo install --force cargo-stylus # Verify installation cargo stylus --version # If still not found, check your PATH includes ~/.cargo/bin echo $PATH | grep cargo ``` **Alternative**: Add cargo bin to your PATH: ```shell # Add to ~/.bashrc or ~/.zshrc export PATH="$HOME/.cargo/bin:$PATH" # Reload shell configuration source ~/.bashrc # or source ~/.zshrc ``` ### WASM target not installed **Problem**: Build fails with "can't find crate for `std`" or "target 'wasm32-unknown-unknown' not found." **Solution**: ```shell # Add WASM target for your Rust version rustup target add wasm32-unknown-unknown # If using specific toolchain rustup target add wasm32-unknown-unknown --toolchain 1.91 ``` **Verify**: ```shell rustup target list | grep wasm32 # Should show: wasm32-unknown-unknown (installed) ``` ### Docker not running **Problem**: `cargo stylus check` fails with "Cannot connect to Docker daemon." **Solution**: 1. Start Docker Desktop 2. Verify Docker is running: ```shell docker ps ``` 3. If still failing, restart Docker: ```shell # macOS/Linux sudo systemctl restart docker # Or restart Docker Desktop application ``` ## Build errors ### Contract exceeds size limit **Problem**: "Program activation failed: program too large." **Solution**: 1. **Optimize build settings** in `Cargo.toml`: ```toml [profile.release] opt-level = "z" # Optimize for size lto = true # Link-time optimization codegen-units = 1 # Better optimization strip = true # Remove debug symbols panic = "abort" # Smaller panic handling ``` 2. **Run `wasm-opt` on the compiled binary**: ```shell wasm-opt -Oz target/wasm32-unknown-unknown/release/your_contract.wasm \ -o optimized.wasm ``` 3. **Remove unused dependencies**: ```toml # Remove unnecessary features stylus-sdk = { version = "0.10.7", default-features = false } ``` 4. **Check binary size**: ```shell ls -lh target/wasm32-unknown-unknown/release/*.wasm ``` See [Optimizing binaries guide](/stylus/how-tos/optimizing-binaries.md) for more strategies. ### Linker errors **Problem**: Build fails with linker errors or "undefined reference" messages. **Solution**: ```shell # Clean build artifacts cargo clean # Rebuild cargo build --target wasm32-unknown-unknown --release # If still failing, update Rust rustup update ``` ### Dependency compilation failures **Problem**: Dependencies fail to compile for WASM target. **Solution**: 1. **Check dependency compatibility**: Not all crates support WASM. Look for: * `no_std` support * WASM-compatible versions * Alternatives designed for blockchain 2. **Disable incompatible features**: ```toml [dependencies] # Disable std-only features serde = { version = "1.0", default-features = false, features = ["derive"] } ``` 3. **Use Stylus-compatible alternatives**: See [recommended libraries](/stylus/advanced/recommended-libraries.md). ## Deployment errors ### Insufficient funds **Problem**: "Insufficient funds for gas \* price + value." **Solution**: ```shell # Check your balance cast balance --rpc-url # Fund your account (testnet) # Visit faucet: https://faucet.quicknode.com/arbitrum/sepolia ``` ### Activation failed **Problem**: "Program activation failed" after deployment. **Solution**: 1. **Check contract size**: ```shell cargo stylus check ``` 2. **Verify WASM validity**: ```shell # Ensure binary is valid WASM wasm-validate target/wasm32-unknown-unknown/release/*.wasm ``` 3. **Check gas limits**: ```shell # Increase gas limit for activation cargo stylus deploy \ --endpoint= \ --private-key= \ --gas-limit=10000000 ``` See [Activation concepts](/stylus/concepts/activation.md) for more details. ### Transaction reverted **Problem**: Deployment transaction reverts without clear error. **Solution**: 1. **Enable verbose logging**: ```shell RUST_LOG=debug cargo stylus deploy \ --endpoint= \ --private-key= ``` 2. **Check network status**: ```shell # Verify RPC is responding cast block-number --rpc-url ``` 3. **Use replay for debugging**: ```shell cargo stylus replay \ --endpoint= \ ``` ## Runtime errors ### Panic in contract code **Problem**: Contract panics during execution. **Solution**: 1. **Use `Result` instead of `panic!`**: ```rust // ❌ Bad: Panics pub fn divide(&self, a: U256, b: U256) -> U256 { if b.is_zero() { panic!("Division by zero"); } a / b } // ✅ Good: Returns error pub fn divide(&self, a: U256, b: U256) -> Result> { if b.is_zero() { return Err(b"Division by zero".to_vec()); } Ok(a / b) } ``` 2. **Enable backtrace for debugging**: ```shell RUST_BACKTRACE=1 cargo test ``` Panics produce a dataless revert Panicking and other hard errors in Stylus programs cause a **dataless revert** — the transaction reverts but no error data is returned to the caller. If you need rich error messages onchain, return `Result>` from your function and encode the error explicitly rather than relying on `panic!`. ### Storage slot conflicts **Problem**: Storage values overwriting each other unexpectedly. **Solution**: 1. **Use explicit storage layout**: ```rust sol_storage! { pub struct MyContract { #[borrow] // Use borrow for nested structs StorageMap users; StorageU256 total_supply; } } ``` 2. **Avoid manual slot manipulation** unless necessary. 3. **Check for storage collisions** in upgradeable contracts. ### Out of gas **Problem**: Transactions fail with "out of gas" error. **Solution**: 1. **Optimize loops**: ```rust // ❌ Bad: Unbounded loop pub fn process_all(&self, items: Vec
) -> Result<(), Vec> { for item in items { self.process(item)?; // Could exceed gas limit } Ok(()) } // ✅ Good: Paginated pub fn process_batch(&self, items: Vec
, max: usize) -> Result<(), Vec> { for item in items.iter().take(max) { self.process(*item)?; } Ok(()) } ``` 2. **Increase gas limit** when calling: ```shell cast send "function()" \ --gas-limit 1000000 \ --rpc-url \ --private-key ``` 3. **Profile gas usage**: ```shell cargo stylus trace --endpoint ``` See [Gas optimization guide](/stylus/best-practices/gas-optimization.md) for more strategies. ## Testing issues ### Tests failing with storage errors **Problem**: Tests fail with "storage not initialized" or similar errors. **Solution**: ```rust #[cfg(test)] mod tests { use super::*; use stylus_sdk::testing::*; #[test] fn test_contract() { // ✅ Always initialize TestVM let vm = TestVM::default(); let mut contract = MyContract::from(&vm); // Configure test environment vm.set_sender(address!("0x0000000000000000000000000000000000000001")); // Run tests assert_eq!(contract.get_value().unwrap(), U256::ZERO); } } ``` ### Tests pass locally but fail on CI **Problem**: Tests work on your machine but fail in continuous integration. **Solution**: 1. **Pin Rust version** in `rust-toolchain.toml`: ```toml [toolchain] channel = "1.91.0" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] ``` 2. **Ensure WASM target in CI**: ```yaml # .github/workflows/test.yml - name: Add WASM target run: rustup target add wasm32-unknown-unknown ``` 3. **Check for environment-specific dependencies**. ## ABI and integration ### ABI export fails **Problem**: `cargo stylus export-abi` returns empty or incorrect ABI. **Solution**: 1. **Ensure methods use `#[public]` macro**: ```rust #[public] impl MyContract { // ✅ This will be exported pub fn public_method(&self) -> U256 { U256::from(42) } // ❌ This won't be exported (no #[public]) fn internal_method(&self) -> U256 { U256::from(42) } } ``` 2. **Check for compilation errors**: ```shell cargo build --target wasm32-unknown-unknown --release cargo stylus export-abi ``` ### Type conversion errors **Problem**: "Type mismatch" when calling from Solidity or TypeScript. **Solution**: 1. **Use explicit type conversions**: ```rust use stylus_sdk::abi::AbiType; // ✅ Ensure types match Solidity ABI pub fn get_balance(&self, account: Address) -> U256 { self.balances.get(account) } ``` 2. **Check ABI matches expectations**: ```shell cargo stylus export-abi > abi.json # Verify types in abi.json match your frontend ``` See [Type conversions guide](/stylus/fundamentals/data-types/conversions-between-types.md). ## Network and RPC issues ### RPC connection timeout **Problem**: "Connection timeout" or "Connection refused" errors. **Solution**: 1. **Verify RPC URL**: ```shell # Test RPC connection curl -X POST \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' ``` 2. **Check rate limits**: Public RPCs may have rate limits. Consider: * Using a dedicated RPC provider * Adding retry logic * Implementing backoff strategies 3. **Network-specific endpoints**: * Arbitrum Sepolia: * Arbitrum One: ### Nonce errors **Problem**: "Nonce too low" or "nonce too high" errors. **Solution**: 1. **Let tooling manage nonces** (default behavior): ```shell # cargo stylus automatically manages nonces cargo stylus deploy --endpoint --private-key ``` 2. **Reset nonce manually if needed**: ```shell # Check current nonce cast nonce --rpc-url # Send transaction with specific nonce cast send "function()" \ --nonce \ --rpc-url \ --private-key ``` ## Debugging strategies ### Enable verbose logging ```shell # Set log level export RUST_LOG=debug # Run with logging cargo stylus check cargo stylus deploy --endpoint --private-key ``` ### Use replay debugging ```shell # Replay failed transaction cargo stylus replay \ --endpoint \ # With GDB for advanced debugging cargo stylus replay \ --endpoint \ --use-gdb \ ``` See [Debugging guide](/stylus/cli-tools/debugging-tx.md) for detailed walkthrough. ### Trace execution ```shell # Trace transaction execution cargo stylus trace --endpoint ``` ### Check contract state ```shell # Read storage values cast storage --rpc-url # Call view functions cast call "getValue()(uint256)" --rpc-url ``` ## Getting help If you're still stuck: 1. **Check documentation**: Search [Stylus docs](https://docs.arbitrum.io/stylus) 2. **Search GitHub issues**: [Stylus SDK Issues](https://github.com/OffchainLabs/stylus-sdk-rs/issues) 3. **Ask in Discord**: [Arbitrum Discord](https://discord.gg/arbitrum) #stylus channel 4. **Post in forums**: [Arbitrum Developer Forum](https://forum.arbitrum.foundation/) ### Creating a good bug report Include: * Rust version: `rustc --version` * cargo-stylus version: `cargo stylus --version` * Minimal reproducible example * Full error message and stack trace * Steps to reproduce * Expected vs. actual behavior ## Common error messages quick reference | Error Message | Common Cause | Quick Fix | | ------------------------------------------ | ------------------------- | ------------------------------------------ | | "target not found: wasm32-unknown-unknown" | WASM target not installed | `rustup target add wasm32-unknown-unknown` | | "Cannot connect to Docker daemon" | Docker not running | Start Docker Desktop | | "Program too large" | Binary exceeds size limit | Add optimization flags, run `wasm-opt` | | "Insufficient funds" | Not enough ETH for gas | Fund account from faucet | | "Nonce too low" | Nonce mismatch | Let tooling manage nonces | | "Out of gas" | Gas limit exceeded | Increase gas limit or optimize code | | "Storage not initialized" | Missing TestVM setup | Initialize `TestVM::default()` in tests | | "Division by zero" | Unchecked arithmetic | Return an error or use `checked_div()` | ## Next steps * Review [security best practices](/stylus/best-practices/security.md) * Study [gas optimization](/stylus/best-practices/gas-optimization.md) * Learn [debugging techniques](/stylus/cli-tools/debugging-tx.md) * Explore [testing strategies](/stylus/fundamentals/testing-contracts.md) ---