feat(arbos60): implement multi-gas constraints pricing model for comparison mode#723
Merged
AnkushinDaniil merged 39 commits intodaniil/arbos60-multigas-constraintsfrom Mar 22, 2026
Conversation
16 tasks
82c4953 to
a524679
Compare
- Register ArbitrumReceiptStorageDecoder in ArbitrumPlugin to ensure receipts are decoded with MultiGasUsed field when retrieved from storage - Enable ExposeMultiGas flag in comparison runner for proper testing - Add ILogManager to NitroExecutionRpcModule for debug logging - Update passing-tests.txt with TestMultigasDataCanBeDisabled This fixes the comparison test failure where Nethermind returned MultiGas=0 while Nitro returned proper values. The root cause was that receipts stored as ArbitrumTxReceipt were being decoded as plain TxReceipt, losing the MultiGasUsed data.
Fix comparison test mismatches between Nethermind and Nitro for MultiGas tracking and EffectiveGasPrice calculation in receipts. MultiGas fixes: - Track MultiGas in SystemBurner for ArbOS storage operations - Use ZeroGasBurner for FreeArbosState (matches Nitro's separate burner) - Aggregate precompile context MultiGas into systemBurner - Skip MultiGas charging for owner precompiles (Nitro returns ZeroGas) - Preserve accumulated MultiGas in ConsumeAllGas/ReturnSomeGas - Add ResourceKind-aware Burn() methods to IBurner EffectiveGasPrice fixes: - Store EffectiveGasPrice in ArbitrumTxReceipt during block processing - Add encode/decode for EffectiveGasPrice in ArbitrumReceiptStorageDecoder - Use baseFee directly for ArbitrumTransaction types (matches Nitro) - Apply ShouldDropTip logic for regular EVM transactions - Use stored receipt value in ArbitrumReceiptForRpc instead of recalculating Verified all receipt comparisons pass (MultiGas and EffectiveGasPrice match).
…rTesting Update ArbitrumBlockTree.ResetForTesting() to call the new base class ResetInternalState() method, ensuring Head, BestSuggestedHeader, and BestKnownNumber are cleared when reinitializing between comparison tests. Fixes genesis block processing timeout in multi-worker test scenarios.
…h Nitro - EventsEncoder: charge precompile log gas as HistoryGrowth (matches Nitro precompile.go:370) - ArbitrumGasPolicy: add ConsumeCodeDepositGas for StorageGrowth tracking during CREATE/CREATE2 - ArbitrumExecutionEngine: add Genesis null checks for test state reinitialization stability - Remove TestMultigasDataCanBeDisabled from passing-tests.txt (config sync issue)
Nitro charges precompile return data gas as Computation (precompile.go:856). Previously PayForOutput() used context.Burn(outputGasCost) which didn't track the gas category, causing multigas comparison mismatches. This fixes TestOutboxProofs and aligns with Nitro's multigas accounting.
Pass -test_loglevel=-4 (DEBUG level) to Go tests via the -- delimiter for custom test flags. This enables visibility into comparison pass/fail results without needing to wait for a mismatch error.
077fc94 to
a60e311
Compare
Avoids naming conflict with Nethermind.Blockchain.IResettableBlockTree from the upstream submodule (PR #10765).
- Add Burn(ResourceKind, ulong) and BurnedMultiGas to TestBurner - Update ArbitrumDebugRpcModuleTests to use IArbitrumResettableBlockTree
IDE2001: Embedded statements must be on their own line
When a precompile runs out of gas, the exception type should be OutOfGas, not Revert. For ArbOS >= 11, precompile failures normally revert, but OutOfGas is a hard failure that should take precedence. Changes: - Swap check order in HandlePrecompileException so ranOutOfGas is checked before shouldRevert - Add early return in DefaultExceptionHandling for OutOfGas case that consumes all gas (no refund) instead of reverting This fixes the test CallingOwnerPrecompile_OutOfGasDuringIsChainOwnerCheckAsOwner_FailsAndConsumesAllGas
For ArbOS >= 11, precompile OOG exceptions should revert (not fail as OutOfGas) because shouldRevert=true takes precedence in HandlePrecompileException. The previous commit incorrectly prioritized OutOfGas over Revert, breaking tests that expect Revert for ArbOS >= 11 precompile failures. Also fixes the owner precompile OOG test's gas limit calculation: - FreeArbosState uses ZeroGasBurner, so IsMember doesn't burn system gas - To trigger OOG during owner check, gas limit must be < ArbosStorage.StorageReadCost - Changed from +100 to -1 to ensure OOG happens during owner check
- Remove dead ConsumeCodeDepositGas method (duplicate of ConsumeCodeDeposit) - Refactor OwnerPrecompileCall to use try/finally for BurnedMultiGas restoration - Consolidate duplicate Genesis null checks into IsGenesisInitialized helper - Update Nethermind submodule with beacon state reset fixes
…on mode - Fix EffectiveGasPrice in eth_getBlockReceipts to always return header.BaseFeePerGas matching Nitro's MarshalReceipt behavior (api.go:1815) - Fix MultiGas cold access cost split in ArbitrumGasPolicy: - Cold SLOAD: (ColdSLoad - WarmStateRead) → StorageAccess, WarmStateRead → Computation - Cold Account: (ColdAccountAccess - WarmStateRead) → StorageAccess, WarmStateRead → Computation - Clean up unused EffectiveGasPrice field from ArbitrumTxReceipt (calculated at RPC layer) - Simplify receipt handling in NitroExecutionRpcModule
Three fixes to align MultiGas tracking with Nitro implementation: 1. ArbitrumSubmitRetryable gas → L2Calldata (not Computation) Matches Nitro's tx_processor.go L2CalldataGas usage for retryables. 2. SSTORE cold access → full cost to StorageAccess (no split) Matches Nitro's gasSStoreEIP2929 which doesn't split cold access, unlike SLOAD which splits 2000+100. 3. SELFDESTRUCT new account → StorageGrowth tracking Use ConsumeNewAccountCreation to properly track CreateBySelfdestructGas as StorageGrowth resource kind.
Match Nitro's precompile.go:773 behavior where argument data gas (CopyGas * words) is charged as L2Calldata, not default Computation. This fixes small L2Calldata differences (3, 6, 9 gas) seen in comparison tests where precompile calls were involved.
Previously, the intrinsic gas calculation incorrectly charged L2Calldata as (zeroCount + nonZeroCount * 16) * 4 gas, which was using the token count formula instead of actual gas costs. This fix matches Nitro's state_transition.go:127,132 by charging: - Non-zero bytes: count * 16 gas (post-EIP2028) as L2Calldata - Zero bytes: count * 4 gas as L2Calldata The previous formula resulted in 3-gas (1 word) discrepancies in comparison tests. With this fix, all MultiGas dimensions now match Nitro exactly. Fixes TestArbAddressTableDoesntRevert comparison test.
Fix MultiGas resource kind categorization for various gas burns: - ArbosStorage.ComputeKeccakHash: burn as Computation (matches Nitro storage.go:354) - RetryableState.KeepAlive (reap price): burn as Computation (matches retryable.go:250) - ArbRetryableTx.Keepalive (update cost): burn as StorageAccess (matches ArbRetryableTx.go:194) - ArbitrumGasPolicy.ConsumeSelfDestructBeneficiaryAccessGas: charge cold access as full StorageAccess (matches Nitro makeSelfdestructGasFn in operations_acl.go) Also updates Nethermind submodule with IGasPolicy interface changes.
Poster gas was added to multiGas as L1Calldata in GasChargingHook but not removed before GrowBacklog, causing state divergence with Nitro. Nitro decrements L1Calldata by posterGas before updating backlog (tx_processor.go:785). This fix adds the equivalent subtraction in Nethermind's UpdateGasPool to match Nitro's behavior. Root cause: L1 costs shouldn't affect L2 pricing backlog calculation.
- Create ArbitrumBlockValidationTransactionsExecutor to commit multi-gas fees at block start during validation (fixes state scope issue where PrepareBlock's temporary scope was discarded) - Add HandleMultiGasRefund in ArbitrumTransactionProcessor for refunding the difference between flat-rate and per-resource costs - Add BalanceChangeMultiGasRefund reason for balance tracking - Register new executor in ArbitrumBlockValidationModule with proper interface binding This enables multi-gas refunds for ArbOS 60+ with MultiGasConstraints gas model, where users are charged the flat-rate cost upfront and refunded the difference when per-resource cost is lower.
ReturnSomeGas and ConsumeAllGas were resetting _allocatedByParent to gasToReturn instead of preserving the original allocation. This caused Refund() to add the wrong value to parent's _retained, making GetTotalAccumulated() higher than expected. Added FromLongPreservingAllocated() to preserve both _accumulated and _allocatedByParent when adjusting gas after precompile execution.
…port - Add Free flag to ArbitrumPrecompileExecutionContext for unmetered gas - Add BurnAllowingOutOfGas for ArbTest.BurnArbGas matching Nitro behavior - Fix ResourceKind in Burn calls: ArbInfo, ArbWasm, ArbNativeTokenManager, ArbRetryableTx, ArbTest - Add SetMultiGasPricingConstraints precompile (ArbOS v60+) - Fix BacklogOperation enum order (Shrink=0, Grow=1) - Update ArbRetryableTx.Redeem to use Free flag for MultiGasConstraints
ArbitrumBlockProductionTransactionsExecutor.ProcessTransactions() was missing the CommitMultiGasFees() call that rotates next-block per-resource fees into current-block fees. The validation executor already had this call, but the production executor did not. PrepareBlock calls CommitMultiGasFees inside a temporary BeginScope(parent) that gets disposed before actual block processing begins, so those changes were lost. Without the rotation, all currentBlockFee values remained at 0, causing GetMultiGasBaseFeePerResource to fall back to baseFeeWei for every resource kind. This inflated multiDimensionalCost (e.g. using 122M instead of 100M for Computation), producing a smaller refund than Nitro and diverging state roots on TestMultiGasRefundForNormalTx.
- AbiMetadata: support nested tuple/struct types in ABI parsing with canonical signature computation for correct method ID generation - ArbOwner: update ABI string to include ChainOwner/NativeTokenOwner events - MultiGas: add ToString() override for debug-friendly output - Update passing-tests.txt and debug RPC module tests
- Add multi-gas refund handling for retry transactions, replacing AddToGasPool with GrowBacklog to match Nitro tx_processor.go:706 - Add MaxPricingExponentBips constant (85,000 = exp(8.5) cap) - Use stackalloc for per-resource fee buffer in MultiDimensionalPriceForRefund
…he clearing CodeDb is content-addressed (keyed by Keccak-256 of bytecode) so entries can never be stale. Clearing it breaks StateProvider._persistedCodeInsertFilter which survives reinitialize — the filter causes InsertCode() to skip re-writing code on subsequent genesis inits, leading to "Code X is missing from the database" errors when LRU cache evicts entries. Also adds TrieStoreClearableCache (IClearableCache wrapper) and retryable TX multi-gas refund with correct Nitro ordering (refund before lifecycle).
Remove BlockDebugLogger, GenesisDebugLogger, and all File.AppendAllText debug writes that were used during multi-gas pricing development. Clean up dead variables, simplify try-catch blocks that existed only for logging, and merge UpgradeArbosVersionWithLog back into UpgradeArbosVersion.
Nitro's Storage.ClearFree writes the zero value without burning gas, but the C# DeleteFree was calling Clear which routes through Set and charges StorageWriteZeroCost. Add a matching ClearFree that skips the burner, and wire FilteredTransactionsState.DeleteFree to use it. Constraint: Must match Nitro storage.go:159-163 ClearFree semantics Rejected: Reuse Clear with a skipGas flag | pollutes the common path Confidence: high Scope-risk: narrow
Convert single-statement if/else/for/foreach bodies to braceless form and single-statement methods to expression-bodied members.
Nethermind unconditionally zeroed multigas for all callers to owner precompiles via RestoreBurnedMultiGas in the finally block. Nitro's OwnerPrecompile.Call returns burner.gasUsed (non-zero) for non-owner paths but multigas.ZeroGas() only for actual owners. Adds AddOwnerCheckMultiGasDelta helper that computes the system burner's multigas delta and injects it into the gas policy accumulator for non-owner and OOG paths before RestoreBurnedMultiGas cleans up. Constraint: _systemBurner.BurnedMultiGas is never consumed by final accounting Constraint: context.Burn(gasUsed) single-arg overload does not track multigas Rejected: Conditional restore only for owners | leaves stale delta in shared _systemBurner Confidence: high Scope-risk: narrow
…erns Replace field-by-field BlockTree reset with ResettableArbitrumBlockTree decorator that creates fresh instances. ArbOSVersionOverride now uses thread-safe consume-once pattern via Interlocked.Exchange. - Delete TrieStoreClearableCache (TrieStore implements IClearableCache directly) - Register HeaderStore, BlockStore, ChainLevelInfoRepository as IClearableCache - ArbitrumBlockTree simplified to pure constructor delegation Constraint: BlockTree constructor must be fast on empty DBs for reset performance Rejected: Field-level reset | fragile, requires tracking every new field Rejected: Keep TrieStoreClearableCache wrapper | unnecessary indirection Confidence: medium Scope-risk: moderate Not-tested: BlockTree decorator reset performance under load
Failed tests are retried up to N times (default 3, --retries flag). Duration accumulates across attempts. Summary distinguishes flaky tests (passed on retry) from hard failures. Parallel runner checks worker health between retry attempts and restarts if needed. Constraint: Retry wraps _run_single_test — no changes to test execution itself Rejected: Retry only non-deterministic failures | impossible to classify reliably Confidence: high Scope-risk: narrow
…eset config Multi-gas resource split: cold access costs now correctly split between StorageAccess (cold premium) and Computation (warm base), matching Nitro's operations_acl.go pattern: ColdCost - WarmRead → StorageAccess, WarmRead → Computation. BurnArbGas: changed from expecting throw to asserting GasLeft==0, matching Nitro's ArbosTest.go which intentionally ignores the Burn error (//nolint:errcheck) and silently consumes all remaining gas. Redeem backlog: removed Math.Min(long.MaxValue, gasToDonate) cap that caused SaturateSub to zero the backlog, triggering StorageWriteZeroCost (5000) instead of StorageWriteCost (20000) — a 15000 gas discrepancy. CodeDb: assert NOT cleared during reinitialize since code is content-addressed and immutable across reinitializations. EnableTestReset: new config flag (default: false) gates test-only behavior — ResettableArbitrumBlockTree, debug_reinitialize RPC, and skipVersionCheck for per-test ArbOS version overrides. Constraint: Comparison tests send varying ArbOS versions per test Rejected: effectiveArbOSVersion override | failed when test version != chainspec version Directive: EnableTestReset must remain false in production — it gates mutable BlockTree and debug RPC Confidence: high Scope-risk: moderate Not-tested: EnableTestReset in non-comparison production scenarios (always false)
The smart rebuild feature (git-based change detection, build state tracking, auto Nethermind build) added unnecessary complexity. Users should build manually before running comparison tests. Removed: _get_git_hash, _needs_rebuild, _record_build, build_nethermind, BUILD_STATE_FILE, and git-based skip logic from compile_test_binary.
Revert comment edits, style changes, and formatting-only modifications that were inadvertently included in the multigas feature branch. Preserves all functional multigas/ResourceKind changes. Files affected: 13 C# files across Arbos, Evm, Execution, Genesis, Modules, and Rpc layers.
The unconditional MixHash recomputation in InitializeAndBuildGenesisBlock could overwrite the chainspec's canonical MixHash value in production, causing genesis block hash mismatch. Now only recomputes when EnableTestReset is true, where the init message may carry a different ArbOS version than the chainspec. Constraint: Production genesis must use chainspec MixHash verbatim Rejected: Always recompute MixHash | breaks production genesis hash Confidence: high Scope-risk: narrow
Replace misleading comments with actionable TODO referencing Nitro arbosstate.go:240-256. NativeTokenSupplyManagementEnabled and TransactionFilteringEnabled are hardcoded to disabled — correct for standard chains but needs implementation for custom Orbit chains. Confidence: high Scope-risk: narrow
Apply ruff format to 3 Python files and add missing flaky/retries_enabled args to SummaryStats constructors in test_formatters.py. Confidence: high Scope-risk: narrow
d51ec10
into
daniil/arbos60-multigas-constraints
10 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes MultiGas tracking and improves comparison mode test isolation.
Key Fixes
MultiGas Alignment
Comparison Mode
Dependencies
IResettableBlockTreeinterface