Skip to content

Improve L1 Pricing Estimation Using MEL in ArbOS#4572

Closed
rauljordan wants to merge 2 commits intomel-validator-createvalidationentryfrom
raul/mel-injects-l1-data-into-arbos
Closed

Improve L1 Pricing Estimation Using MEL in ArbOS#4572
rauljordan wants to merge 2 commits intomel-validator-createvalidationentryfrom
raul/mel-injects-l1-data-into-arbos

Conversation

@rauljordan
Copy link
Copy Markdown
Contributor

Background

The Problem

ArbOS charges L2 users an "L1 fee" on every transaction to cover the cost of posting their data to Ethereum. This fee is pricePerUnit * calldataUnits, where pricePerUnit is ArbOS's estimate of what it costs per unit of calldata on L1. The challenge: ArbOS lives on L2 and needs to track L1 gas prices with limited information.

Today, the pricePerUnit of data only updates inside UpdateForBatchPosterSpending in ArbOS which runs when a BatchPostingReport delayed message arrives, which is only after the batch poster actually posts a batch on L1.

The update logic is an indirect feedback controller:

  1. Collect fees from users → goes into l1FeesAvailable pool
  2. Pay the batch poster what they actually spent on L1 → recorded as fundsDue
  3. Compute surplus = l1FeesAvailable - totalFundsDue - fundsDueForRewards
  4. Adjust price based on surplus dynamics

If we overcharged users, we lower the price
If we undercharged users, we raise the price

The issue is this pricer is inherently quite inaccurate and has many reasons it can accumulate error over time. For instance, it only fires when batches are posted. If L1 gas spikes between batches, pricePerUnit stays stale and users underpay. If gas drops, they overpay. The feedback is also indirect as it infers instead of observing directly.

The Solution

Thanks to the Message Extraction Layer, we now have a piece of Arbitrum consensus that reads the parent chain to extract batches and produce messages for L2 execution. This consensus system could also inject parent chain information about pricing into ArbOS as delayed messages. This PR attempts to do that and updates the pricer in ArbOS to use live information each parent chain epoch (32 blocks on Ethereum).

A system test is added that proves the new pricer accumulates 10x fewer absolute error than the old pricer and is way better because it does not depend on batches to be posted to have access to L1 pricing information. Moreover, we can inject useful info such as parent chain block hash which can be exposed via a precompile. The new pricer uses an exponential moving average:

new = (old * (N-1) + sample) / N

Where N is the smoothing window. Recent samples matter more, old samples decay exponentially.

newPrice = (currentPrice * (inertia - 1) + effectiveBaseFee) / inertia

With inertia = 10, each update moves pricePerUnit 10% of the way toward the actual L1 base fee. After 32 updates (one epoch), it's covered ~96% of the gap. This runs every time an epoch pricing message is processed.

The effectiveBaseFee is min(l1BaseFee, blobBaseFee/16) because batch posters choose whichever posting method is cheaper. Post-EIP-4844, blob gas is typically much cheaper. The /16 converts blob gas cost to "per calldata unit" to match the existing unit system.

@github-actions
Copy link
Copy Markdown
Contributor

❌ 350 Tests Failed:

Tests completed Failed Passed Skipped
1824 350 1474 0
View the top 3 failed tests by shortest run time
TestMain
Stack Traces | 0.000s run time
panic: method parentChainPricingReport does not exist

goroutine 1 [running]:
github.com/offchainlabs/nitro/arbos/util.NewCallParser({0x2e1d8e6, 0x58f}, {0x2dad8c7, 0x18})
	/home/runner/work/nitro/nitro/arbos/util/util.go:65 +0x499
github.com/offchainlabs/nitro/arbos/util.init.0()
	/home/runner/work/nitro/nitro/arbos/util/util.go:53 +0x3f1
FAIL	github.com/offchainlabs/nitro/arbnode	0.032s
TestMain
Stack Traces | 0.000s run time
panic: method parentChainPricingReport does not exist

goroutine 1 [running]:
github.com/offchainlabs/nitro/arbos/util.NewCallParser({0x1677f1d, 0x58f}, {0x1631b55, 0x18})
	/home/runner/work/nitro/nitro/arbos/util/util.go:65 +0x499
github.com/offchainlabs/nitro/arbos/util.init.0()
	/home/runner/work/nitro/nitro/arbos/util/util.go:53 +0x3f1
FAIL	github.com/offchainlabs/nitro/arbnode/dataposter	0.022s
TestMain
Stack Traces | 0.000s run time
panic: method parentChainPricingReport does not exist

goroutine 1 [running]:
github.com/offchainlabs/nitro/arbos/util.NewCallParser({0xfb48b8, 0x58f}, {0xf80fed, 0x18})
	/home/runner/work/nitro/nitro/arbos/util/util.go:65 +0x499
github.com/offchainlabs/nitro/arbos/util.init.0()
	/home/runner/work/nitro/nitro/arbos/util/util.go:53 +0x3f1
FAIL	github.com/offchainlabs/nitro/arbnode/mel	0.019s

📣 Thoughts on this report? Let Codecov know! | Powered by Codecov

@rauljordan
Copy link
Copy Markdown
Contributor Author

Closing as we will not get to this for a while, but attached it to NIT-4315 so we can keep track of this branch in the future

@rauljordan rauljordan closed this Apr 1, 2026
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