Skip to content

feat(arbos60): implement multi-gas constraints pricing model for comparison mode#723

Merged
AnkushinDaniil merged 39 commits intodaniil/arbos60-multigas-constraintsfrom
daniil/fix-multigas-comparison
Mar 22, 2026
Merged

feat(arbos60): implement multi-gas constraints pricing model for comparison mode#723
AnkushinDaniil merged 39 commits intodaniil/arbos60-multigas-constraintsfrom
daniil/fix-multigas-comparison

Conversation

@AnkushinDaniil
Copy link
Copy Markdown
Collaborator

@AnkushinDaniil AnkushinDaniil commented Mar 9, 2026

Summary

Fixes MultiGas tracking and improves comparison mode test isolation.

Key Fixes

MultiGas Alignment

  • Register receipt decoder so MultiGas fields persist correctly in storage
  • Track precompile output data as Computation gas
  • Track code deposit gas as StorageGrowth
  • Owner precompiles don't charge multigas (return zero gas)

Comparison Mode

  • Reset BlockTree internal state (Head, BestKnownNumber) between tests
  • Add Genesis null checks to handle reinitialized state gracefully
  • Enable debug logging for troubleshooting

Dependencies

PR Repository Description
#10765 nethermind IResettableBlockTree interface

@AnkushinDaniil AnkushinDaniil changed the base branch from main to daniil/arbos60-multigas-constraints March 9, 2026 13:07
@AnkushinDaniil AnkushinDaniil force-pushed the daniil/arbos60-multigas-constraints branch from 82c4953 to a524679 Compare March 10, 2026 09:53
- 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.
@AnkushinDaniil AnkushinDaniil force-pushed the daniil/fix-multigas-comparison branch from 077fc94 to a60e311 Compare March 10, 2026 10:01
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
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 10, 2026

Codecov Report

❌ Patch coverage is 46.98355% with 290 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.67%. Comparing base (8743e0d) to head (90f425c).
⚠️ Report is 54 commits behind head on daniil/arbos60-multigas-constraints.

Files with missing lines Patch % Lines
...rmind.Arbitrum/Core/ResettableArbitrumBlockTree.cs 0.00% 85 Missing ⚠️
src/Nethermind.Arbitrum/Precompiles/ArbOwner.cs 2.12% 46 Missing ⚠️
...Arbitrum/Execution/ArbitrumTransactionProcessor.cs 41.30% 25 Missing and 2 partials ⚠️
src/Nethermind.Arbitrum/ArbitrumPlugin.cs 31.03% 19 Missing and 1 partial ⚠️
....Arbitrum/Rpc/ReceiptForRpcPolymorphicConverter.cs 0.00% 14 Missing ⚠️
...rbitrum/Genesis/ArbitrumGenesisStateInitializer.cs 45.45% 9 Missing and 3 partials ⚠️
src/Nethermind.Arbitrum/Evm/ArbitrumGasPolicy.cs 70.27% 10 Missing and 1 partial ⚠️
...Nethermind.Arbitrum/Precompiles/Abi/AbiMetadata.cs 78.00% 6 Missing and 5 partials ⚠️
src/Nethermind.Arbitrum/Arbos/ArbosState.cs 23.07% 9 Missing and 1 partial ⚠️
...rbitrum/Arbos/Storage/FilteredTransactionsState.cs 0.00% 10 Missing ⚠️
... and 13 more
Additional details and impacted files
@@                           Coverage Diff                           @@
##           daniil/arbos60-multigas-constraints     #723      +/-   ##
=======================================================================
- Coverage                                77.11%   75.67%   -1.44%     
=======================================================================
  Files                                      179      185       +6     
  Lines                                    11773    12195     +422     
  Branches                                  1629     1691      +62     
=======================================================================
+ Hits                                      9079     9229     +150     
- Misses                                    2116     2366     +250     
- Partials                                   578      600      +22     
Files with missing lines Coverage Δ
src/Nethermind.Arbitrum/Arbos/ArbosAddresses.cs 100.00% <100.00%> (ø)
src/Nethermind.Arbitrum/Arbos/ArbosSubspaceIDs.cs 100.00% <100.00%> (ø)
src/Nethermind.Arbitrum/Arbos/Precompiles.cs 100.00% <100.00%> (ø)
...ethermind.Arbitrum/Arbos/Storage/L2PricingState.cs 98.03% <100.00%> (+<0.01%) ⬆️
...rmind.Arbitrum/Arbos/Storage/MultiGasConstraint.cs 100.00% <ø> (ø)
...ethermind.Arbitrum/Arbos/Storage/RetryableState.cs 98.33% <100.00%> (ø)
src/Nethermind.Arbitrum/Config/ArbitrumConfig.cs 70.00% <100.00%> (+3.33%) ⬆️
src/Nethermind.Arbitrum/Core/ArbitrumBlockTree.cs 100.00% <100.00%> (+25.00%) ⬆️
src/Nethermind.Arbitrum/Data/ParsedInitMessage.cs 76.92% <100.00%> (+0.36%) ⬆️
...rmind.Arbitrum/Execution/ArbitrumBlockProcessor.cs 89.18% <100.00%> (+0.90%) ⬆️
... and 31 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- 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
@AnkushinDaniil AnkushinDaniil marked this pull request as ready for review March 22, 2026 13:34
@AnkushinDaniil AnkushinDaniil merged commit d51ec10 into daniil/arbos60-multigas-constraints Mar 22, 2026
10 checks passed
@AnkushinDaniil AnkushinDaniil deleted the daniil/fix-multigas-comparison branch March 22, 2026 13:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant