diff --git a/docs/arbitrum-essentials/how-to-get-l2block-on-l1.mdx b/docs/arbitrum-essentials/how-to-get-l2block-on-l1.mdx deleted file mode 100644 index 7ae77a75e8..0000000000 --- a/docs/arbitrum-essentials/how-to-get-l2block-on-l1.mdx +++ /dev/null @@ -1,189 +0,0 @@ ---- -title: 'How to verify child chain state on the parent chain' -description: Learn how to verify child chain state on its parent chain -user_story: As a developer, I want to understand how to verify child chain state on the parent chain. -content_type: how-to -displayed_sidebar: buildAppsSidebar ---- - -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/01-inside-arbitrum-nitro.mdx#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. diff --git a/docs/launch-arbitrum-chain/02-configure-your-chain/common/gas/priority-fees.mdx b/docs/launch-arbitrum-chain/02-configure-your-chain/common/gas/priority-fees.mdx new file mode 100644 index 0000000000..2d823f48c1 --- /dev/null +++ b/docs/launch-arbitrum-chain/02-configure-your-chain/common/gas/priority-fees.mdx @@ -0,0 +1,61 @@ +--- +title: 'Priority fees collection' +sidebar_label: 'Priority fees' +description: "How to enable priority-fee (tip) collection on an Arbitrum chain in ArbOS61 'Elara', and the additional requirements for tip-based transaction ordering." +content_type: concept +author: 'gaelblanchemain' +sme: 'vbridge-hash' +--- + +ArbOS61 ("Elara") introduces the capability for chain owners to collect priority fees (tips) on their Arbitrum chain. The feature ships disabled, and a chain owner can enable it through a deliberate, two-step action. + + + +For Arbitrum One and Nova, enabling this feature requires a **Constitutional DAO vote**—it isn't activated as part of the ArbOS61 upgrade itself. The upgrade only makes tip collection _possible_ for chains that opt in. + + + +## How to enable tip collection + +The chain owner calls the [`ArbOwner` precompile](/arbitrum-essentials/precompiles/reference.mdx#arbowner) at `0x0000000000000000000000000000000000000070`: + +```solidity +// Enable tip collection +IArbOwner(address(0x70)).setCollectTips(true); + +// Disable it again +IArbOwner(address(0x70)).setCollectTips(false); + +// Read the current state +bool collecting = IArbOwner(address(0x70)).getCollectTips(); +``` + +`ArbOwner` is access-controlled—only the chain owner (typically a designated operator address or DAO) can call these methods. The implementation is in [`precompiles/ArbOwner.go`](https://github.com/OffchainLabs/nitro/blob/6dce8d13902649a1acdfd3f2504129f1f5612358/precompiles/ArbOwner.go#L666). + +## How tips are collected + +Once enabled, any `priorityFee` (EIP-1559 `maxPriorityFeePerGas`) included in a transaction is collected the same way all other gas fees are, and routed into an onchain account. No separate infrastructure or modifications are needed. + +## Activation and receipt timing + +If tip-paying transactions land in the same block but _before_ the transaction that enables fee collection, tips aren't collected from those earlier transactions—but their receipts will report tips as if they were. This is a known issue. + +To avoid it, the chain owner can either: + +- enable tip collection inside a delayed transaction, or +- ensure the enabling transaction is the first (or only) transaction in its block. + +## Tip-based transaction ordering + +Enabling collection alone does **not** change how transactions are ordered. For priority fees to actually influence sequencing, the chain needs **both**: + +1. **`setCollectTips(true)`**: activates fee collection via `ArbOwner`. +2. **Updated sequencer logic**: the sequencer must be configured to read the `PriorityFee` field when sorting transactions. + +Without Step 2, the sequencer continues to use its existing ordering rules (such as [Timeboost](/how-arbitrum-works/timeboost/gentle-introduction.mdx), if enabled) regardless of the transaction's priority fee. + +## References + +- [Constitutional AIP: ArbOS61 Elara — Priority fees](https://forum.arbitrum.foundation/t/constitutional-aip-arbos-60-elara/30601) +- [`ArbOwner` precompile reference](/arbitrum-essentials/precompiles/reference.mdx#arbowner) +- [`setCollectTips` source](https://github.com/OffchainLabs/nitro/blob/6dce8d13902649a1acdfd3f2504129f1f5612358/precompiles/ArbOwner.go#L666) diff --git a/redirects.config.js b/redirects.config.js index 8e9f1f3836..9d09d812cd 100644 --- a/redirects.config.js +++ b/redirects.config.js @@ -415,9 +415,5 @@ export const redirects = [ from: '/launch-arbitrum-chain/configure-your-chain/advanced/compliance-filtering', to: '/launch-arbitrum-chain/chain-config/validation/compliance-filtering', }, - { - from: '/build-decentralized-apps/how-to-get-l2block-on-l1', - to: '/arbitrum-essentials/how-to-get-l2block-on-l1', - }, // AUTO-GENERATED REDIRECTS END ]; diff --git a/sidebars.js b/sidebars.js index 80b2b3d8dd..6346cf623d 100644 --- a/sidebars.js +++ b/sidebars.js @@ -300,6 +300,43 @@ const sidebars = { id: 'launch-arbitrum-chain/chain-config/data-availability/dac-configuration-defaults', label: `DAC defaults`, }, + { + type: 'category', + label: 'Gas', + collapsed: true, + items: [ + { + type: 'doc', + id: 'launch-arbitrum-chain/chain-config/costs/custom-gas-token-anytrust', + label: `AnyTrust custom gas token`, + }, + { + type: 'doc', + id: 'launch-arbitrum-chain/chain-config/costs/custom-gas-token-rollup', + label: `Rollup custom gas token`, + }, + { + type: 'doc', + id: 'launch-arbitrum-chain/chain-config/costs/configure-native-mint-burn', + label: 'Native mint/burn gas token', + }, + { + type: 'doc', + id: 'launch-arbitrum-chain/chain-config/costs/gas-optimization', + label: `Gas optimization tools`, + }, + { + type: 'doc', + id: 'launch-arbitrum-chain/chain-config/costs/dynamic-pricing', + label: 'Dynamic Pricing for Arbitrum Chains', + }, + { + type: 'doc', + id: 'launch-arbitrum-chain/configure-your-chain/common/gas/priority-fees', + label: 'Priority fees', + }, + ], + }, { type: 'doc', id: 'launch-arbitrum-chain/chain-config/data-availability/configure-dac', @@ -1214,11 +1251,6 @@ const sidebars = { label: 'Cross-chain messaging', id: 'arbitrum-essentials/bridging/cross-chain-messaging', }, - { - type: 'doc', - label: 'Verify child chain state', - id: 'arbitrum-essentials/how-to-get-l2block-on-l1', - }, { type: 'doc', label: 'L1-to-L3 teleportation',