Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions arbos/l2pricing/l2pricing.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,48 @@
package l2pricing

import (
"fmt"
"math/big"

"github.com/offchainlabs/nitro/arbos/storage"
"github.com/offchainlabs/nitro/util/arbmath"
)

const (
gasConstraintTargetOffset uint64 = iota
gasConstraintDivisorOffset
gasConstraintBacklogOffset
)

// GasConstraint tries to keep the gas backlog under the target (per second) for the given period.
// The divisor is based on the target and period, and can be computed by computeConstraintDivisor.
type GasConstraint struct {
target storage.StorageBackedUint64
divisor storage.StorageBackedUint64
backlog storage.StorageBackedUint64
}

func OpenGasConstraint(storage *storage.Storage) *GasConstraint {
return &GasConstraint{
target: storage.OpenStorageBackedUint64(gasConstraintTargetOffset),
divisor: storage.OpenStorageBackedUint64(gasConstraintDivisorOffset),
backlog: storage.OpenStorageBackedUint64(gasConstraintBacklogOffset),
}
}

func (c *GasConstraint) Clear() error {
if err := c.target.Clear(); err != nil {
return err
}
if err := c.divisor.Clear(); err != nil {
return err
}
if err := c.backlog.Clear(); err != nil {
return err
}
return nil
}

type L2PricingState struct {
storage *storage.Storage
speedLimitPerSecond storage.StorageBackedUint64
Expand All @@ -19,6 +56,7 @@ type L2PricingState struct {
pricingInertia storage.StorageBackedUint64
backlogTolerance storage.StorageBackedUint64
perTxGasLimit storage.StorageBackedUint64
constraints *storage.SubStorageVector
}

const (
Expand All @@ -32,6 +70,8 @@ const (
perTxGasLimitOffset
)

var constraintsKey []byte = []byte{0}

const GethBlockGasLimit = 1 << 50

func InitializeL2PricingState(sto *storage.Storage) error {
Expand All @@ -55,6 +95,7 @@ func OpenL2PricingState(sto *storage.Storage) *L2PricingState {
pricingInertia: sto.OpenStorageBackedUint64(pricingInertiaOffset),
backlogTolerance: sto.OpenStorageBackedUint64(backlogToleranceOffset),
perTxGasLimit: sto.OpenStorageBackedUint64(perTxGasLimitOffset),
constraints: storage.OpenSubStorageVector(sto.OpenSubStorage(constraintsKey)),
}
}

Expand Down Expand Up @@ -128,3 +169,66 @@ func (ps *L2PricingState) SetBacklogTolerance(val uint64) error {
func (ps *L2PricingState) Restrict(err error) {
ps.storage.Burner().Restrict(err)
}

func (ps *L2PricingState) SetConstraintsFromLegacy() error {
if err := ps.ClearConstraints(); err != nil {
return err
}
target, err := ps.SpeedLimitPerSecond()
if err != nil {
return err
}
inertia, err := ps.PricingInertia()
if err != nil {
return err
}
// Make an approximation of the period based on the inertia
periodSqrt := inertia / ConstraintDivisorMultiplier
period := arbmath.SaturatingUMul(periodSqrt, periodSqrt)
if period == 0 {
// Ensure the period is at least 1
period = 1
}
return ps.AddConstraint(target, period)
Copy link
Contributor

@MishkaRogachev MishkaRogachev Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably this is the right place to transfer legacy backlog. I can do it in my PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, my idea is to call SetConstraintsFromLegacy when upgrading to Arbos50 here.

}

func (ps *L2PricingState) AddConstraint(target uint64, period uint64) error {
subStorage, err := ps.constraints.Push()
if err != nil {
return fmt.Errorf("failed to push constraint: %w", err)
}
constraint := OpenGasConstraint(subStorage)
if err := constraint.target.Set(target); err != nil {
return fmt.Errorf("failed to set target: %w", err)
}
if err := constraint.divisor.Set(computeConstraintDivisor(target, period)); err != nil {
return fmt.Errorf("failed to set period: %w", err)
}
return nil
}

func (ps *L2PricingState) ConstraintsLength() (uint64, error) {
return ps.constraints.Length()
}

func (ps *L2PricingState) OpenConstraintAt(i uint64) *GasConstraint {
return OpenGasConstraint(ps.constraints.At(i))
}

func (ps *L2PricingState) ClearConstraints() error {
length, err := ps.ConstraintsLength()
if err != nil {
return err
}
for range length {
subStorage, err := ps.constraints.Pop()
if err != nil {
return err
}
constraint := OpenGasConstraint(subStorage)
if err := constraint.Clear(); err != nil {
return err
}
}
return nil
}
42 changes: 42 additions & 0 deletions arbos/l2pricing/l2pricing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,48 @@ func getSpeedLimit(t *testing.T, pricing *L2PricingState) uint64 {
return value
}

func getConstraintsLength(t *testing.T, pricing *L2PricingState) uint64 {
length, err := pricing.ConstraintsLength()
Require(t, err)
return length
}

func TestGasConstraints(t *testing.T) {
pricing := PricingForTest(t)
if got := getConstraintsLength(t, pricing); got != 0 {
t.Fatalf("wrong number of constraints: got %v want 0", got)
}
const n uint64 = 10
for i := range n {
Require(t, pricing.AddConstraint(100*i+1, 100*i+2))
}
if got := getConstraintsLength(t, pricing); got != n {
t.Fatalf("wrong number of constraints: got %v want %v", got, n)
}
for i := range n {
constraint := pricing.OpenConstraintAt(i)
target, err := constraint.target.Get()
Require(t, err)
if want := 100*i + 1; target != want {
t.Errorf("wrong target: got %v, want %v", target, want)
}
period, err := constraint.divisor.Get()
Require(t, err)
if want := computeConstraintDivisor(100*i+1, 100*i+2); period != want {
t.Errorf("wrong period: got %v, want %v", period, want)
}
backlog, err := constraint.backlog.Get()
Require(t, err)
if want := uint64(0); backlog != want {
t.Errorf("wrong backlog: got %v, want %v", backlog, want)
}
}
Require(t, pricing.ClearConstraints())
if got := getConstraintsLength(t, pricing); got != 0 {
t.Fatalf("wrong number of constraints: got %v want 0", got)
}
}

func Require(t *testing.T, err error, printables ...interface{}) {
t.Helper()
testhelpers.RequireImpl(t, err, printables...)
Expand Down
72 changes: 68 additions & 4 deletions arbos/l2pricing/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package l2pricing

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/params"
Expand All @@ -21,18 +22,43 @@ const InitialPricingInertia = 102
const InitialBacklogTolerance = 10
const InitialPerTxGasLimitV50 uint64 = 32 * 1000000

const ConstraintDivisorMultiplier = 30

func (ps *L2PricingState) AddToGasPool(gas int64) error {
backlog, err := ps.GasBacklog()
if err != nil {
return err
}
// pay off some of the backlog with the added gas, stopping at 0
backlog = addToBacklog(backlog, gas)
return ps.SetGasBacklog(backlog)
}

func (ps *L2PricingState) AddToGasPoolMultiConstraints(gas int64) error {
constraintsLength, err := ps.constraints.Length()
if err != nil {
return fmt.Errorf("failed to get number of constraints: %w", err)
}
for i := range constraintsLength {
constraint := ps.OpenConstraintAt(i)
backlog, err := constraint.backlog.Get()
if err != nil {
return fmt.Errorf("failed to get backlog of constraint %v: %w", i, err)
}
err = constraint.backlog.Set(addToBacklog(backlog, gas))
if err != nil {
return fmt.Errorf("failed to set backlog of constraint %v: %w", i, err)
}
}
return nil
}

// addToBacklog grows the backlog if the gas is negative and pays off if the gas is positive.
func addToBacklog(backlog uint64, gas int64) uint64 {
if gas > 0 {
backlog = arbmath.SaturatingUSub(backlog, uint64(gas))
return arbmath.SaturatingUSub(backlog, uint64(gas))
} else {
backlog = arbmath.SaturatingUAdd(backlog, uint64(-gas))
return arbmath.SaturatingUAdd(backlog, uint64(-gas))
}
return ps.SetGasBacklog(backlog)
}

// UpdatePricingModel updates the pricing model with info from the last block
Expand All @@ -51,3 +77,41 @@ func (ps *L2PricingState) UpdatePricingModel(l2BaseFee *big.Int, timePassed uint
}
_ = ps.SetBaseFeeWei(baseFee)
}

func (ps *L2PricingState) UpdatePricingModelMultiConstraints(timePassed uint64) {
// Compute exponent used in the basefee formula
totalExponent := arbmath.Bips(0)
constraintsLength, _ := ps.constraints.Length()
for i := range constraintsLength {
constraint := ps.OpenConstraintAt(i)
target, _ := constraint.target.Get()

// Pay off backlog
backlog, _ := constraint.backlog.Get()
gas := arbmath.SaturatingCast[int64](arbmath.SaturatingUMul(timePassed, target))
backlog = addToBacklog(backlog, gas)
_ = constraint.backlog.Set(backlog)

// Calculate exponent with the formula backlog/divisor
if backlog > 0 {
divisor, _ := constraint.divisor.Get()
exponent := arbmath.NaturalToBips(arbmath.SaturatingCast[int64](backlog)) / arbmath.SaturatingCastToBips(divisor)
totalExponent = arbmath.SaturatingBipsAdd(totalExponent, exponent)
}
}

// Compute base fee
minBaseFee, _ := ps.MinBaseFeeWei()
var baseFee *big.Int
if totalExponent > 0 {
baseFee = arbmath.BigMulByBips(minBaseFee, arbmath.ApproxExpBasisPoints(totalExponent, 4))
} else {
baseFee = minBaseFee
}
_ = ps.SetBaseFeeWei(baseFee)
}

func computeConstraintDivisor(target uint64, period uint64) uint64 {
inertia := arbmath.SaturatingUMul(ConstraintDivisorMultiplier, arbmath.ApproxSquareRoot(period))
return arbmath.SaturatingUMul(target, inertia)
}
71 changes: 71 additions & 0 deletions arbos/l2pricing/model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2025, Offchain Labs, Inc.
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md

package l2pricing

import (
"testing"
)

func TestComputeConstraintDivisor(t *testing.T) {
cases := []struct {
target uint64
period uint64
want uint64
}{
{
target: 7_000_000,
period: 12,
want: 630_000_000,
},
{
target: 15_000_000,
period: 86_400, // one day
want: 131_850_000_000,
},
}
for _, test := range cases {
got := computeConstraintDivisor(test.target, test.period)
if got != test.want {
t.Errorf("wrong result for target=%v period=%v: got %v, want %v",
test.target, test.period, got, test.want)
}
}
}

func TestCompareLecayPricingModelWithMultiConstraints(t *testing.T) {
pricing := PricingForTest(t)

// In this test, we don't check for storage set errors because they won't happen and they
// are not the focus of the test.

// Set the innertia to a value that is divisible by 30 to negate the rounding error
_ = pricing.SetPricingInertia(120)

// Set the tolerance to zero because this doesn't exist in the new model
_ = pricing.SetBacklogTolerance(0)

// Initialize with a single constraint based on the legacy model
_ = pricing.SetConstraintsFromLegacy()

// Compare the basefee for both models with different backlogs
for backlogShift := range uint64(16) {
for timePassed := range uint64(5) {
backlog := uint64(1 << backlogShift)

_ = pricing.gasBacklog.Set(backlog)
pricing.UpdatePricingModel(nil, timePassed, false)
legacyPrice, _ := pricing.baseFeeWei.Get()

constraint := pricing.OpenConstraintAt(0)
_ = constraint.backlog.Set(backlog)
pricing.UpdatePricingModelMultiConstraints(timePassed)
multiPrice, _ := pricing.baseFeeWei.Get()

if multiPrice.Cmp(legacyPrice) != 0 {
t.Errorf("wrong result: backlog=%v, timePassed=%v, multiPrice=%v, legacyPrice=%v",
backlog, timePassed, multiPrice, legacyPrice)
}
}
}
}
Loading
Loading