Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions arbos/arbosState/arbosstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,13 @@ func (state *ArbosState) UpgradeArbosVersion(
ensure(p.UpgradeToArbosVersion(nextArbosVersion))
ensure(p.Save())
ensure(state.l2PricingState.SetMaxPerTxGasLimit(l2pricing.InitialPerTxGasLimitV50))

case 51, 52, 53, 54, 55, 56, 57, 58, 59:
// these versions are left to Orbit chains for custom upgrades.

case params.ArbosVersion_60:
// no change state needed

default:
return fmt.Errorf(
"the chain is upgrading to unsupported ArbOS version %v, %w",
Expand Down
12 changes: 6 additions & 6 deletions arbos/constraints/multi_dimensional.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const (
// gas resource types, each with a corresponding weight (0 = unused).
type MultiGasConstraint struct {
target storage.StorageBackedUint64
adjustmentWindow storage.StorageBackedUint64
adjustmentWindow storage.StorageBackedUint32
backlog storage.StorageBackedUint64
sumWeights storage.StorageBackedUint64
weightedResources [multigas.NumResourceKind]storage.StorageBackedUint64
Expand All @@ -40,7 +40,7 @@ type MultiGasConstraint struct {
func OpenMultiGasConstraint(sto *storage.Storage) *MultiGasConstraint {
c := &MultiGasConstraint{
target: sto.OpenStorageBackedUint64(targetOffset),
adjustmentWindow: sto.OpenStorageBackedUint64(adjustmentWindowOffset),
adjustmentWindow: sto.OpenStorageBackedUint32(adjustmentWindowOffset),
backlog: sto.OpenStorageBackedUint64(backlogOffset),
sumWeights: sto.OpenStorageBackedUint64(sumWeightsOffset),
}
Expand All @@ -57,7 +57,7 @@ func (c *MultiGasConstraint) Clear() error {
if err := c.target.Clear(); err != nil {
return err
}
if err := c.adjustmentWindow.Clear(); err != nil {
if err := c.adjustmentWindow.Set(0); err != nil {
return err
}
if err := c.backlog.Clear(); err != nil {
Expand Down Expand Up @@ -130,7 +130,7 @@ func (c *MultiGasConstraint) ComputeExponent(kind uint8) (arbmath.Bips, error) {
dividend := arbmath.NaturalToBips(
arbmath.SaturatingCast[int64](arbmath.SaturatingUMul(backlog, weight)))
divisor := arbmath.SaturatingCastToBips(
arbmath.SaturatingUMul(adjustmentWindow,
arbmath.SaturatingUMul(uint64(adjustmentWindow),
arbmath.SaturatingUMul(target, sumWeights)))
exponent := dividend / divisor

Expand Down Expand Up @@ -169,11 +169,11 @@ func (c *MultiGasConstraint) SetTarget(v uint64) error {
return c.target.Set(v)
}

func (c *MultiGasConstraint) AdjustmentWindow() (uint64, error) {
func (c *MultiGasConstraint) AdjustmentWindow() (uint32, error) {
return c.adjustmentWindow.Get()
}

func (c *MultiGasConstraint) SetAdjustmentWindow(v uint64) error {
func (c *MultiGasConstraint) SetAdjustmentWindow(v uint32) error {
return c.adjustmentWindow.Set(v)
}

Expand Down
2 changes: 1 addition & 1 deletion arbos/constraints/multi_dimensional_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestMultiGasConstraint(t *testing.T) {
window, _ := c.AdjustmentWindow()
backlog, _ := c.Backlog()
require.Equal(t, uint64(123), target)
require.Equal(t, uint64(456), window)
require.Equal(t, uint32(456), window)
require.Equal(t, uint64(789), backlog)

weights := map[uint8]uint64{
Expand Down
15 changes: 13 additions & 2 deletions arbos/l2pricing/l2pricing.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ var gasConstraintsKey []byte = []byte{0}
var multigasConstraintsKey []byte = []byte{1}

const GethBlockGasLimit = 1 << 50
const MaxExponentBips = arbmath.Bips(85_000)

func InitializeL2PricingState(sto *storage.Storage) error {
_ = sto.SetUint64ByUint64(speedLimitPerSecondOffset, InitialSpeedLimitPerSecondV0)
Expand Down Expand Up @@ -217,7 +218,7 @@ func (ps *L2PricingState) OpenMultiGasConstraintAt(i uint64) *constraints.MultiG

func (ps *L2PricingState) AddMultiGasConstraint(
target uint64,
adjustmentWindow uint64,
adjustmentWindow uint32,
backlog uint64,
resourceWeights map[uint8]uint64,
) error {
Expand All @@ -236,10 +237,20 @@ func (ps *L2PricingState) AddMultiGasConstraint(
if err := constraint.SetBacklog(backlog); err != nil {
return fmt.Errorf("failed to set backlog: %w", err)
}

if err := constraint.SetResourceWeights(resourceWeights); err != nil {
return fmt.Errorf("failed to set resource weights: %w", err)
}

for kind := range resourceWeights {
exp, err := constraint.ComputeExponent(kind)
if err != nil {
return fmt.Errorf("failed to compute exponent for resource kind %v: %w", kind, err)
}
if exp > MaxExponentBips {

@MishkaRogachev MishkaRogachev Nov 14, 2025 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@gligneul I'm not 100% sure it's a good place for this check. Probably in the following PR with implemented model it's worth to extract math into a pure function and use it to sample new base fee for all constraints at once

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sure, but this code is good also.

return fmt.Errorf("resource kind %v has exponent %v bips exceeding max of %v bips", kind, exp, MaxExponentBips)
}
}

return nil
}

Expand Down
36 changes: 34 additions & 2 deletions arbos/l2pricing/l2pricing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ func TestMultiGasConstraints(t *testing.T) {
uint8(multigas.ResourceKindStorageAccess): 20 + i,
}
Require(t,
pricing.AddMultiGasConstraint(100*i+1, 100*i+2, 100*i+3, resourceWeights),
// #nosec G115
pricing.AddMultiGasConstraint(100*i+1, uint32(100*i+2), 100*i+3, resourceWeights),
)
}

Expand All @@ -191,7 +192,8 @@ func TestMultiGasConstraints(t *testing.T) {

window, err := c.AdjustmentWindow()
Require(t, err)
if want := 100*i + 2; window != want {
// #nosec G115
if want := uint32(100*i + 2); window != want {
t.Errorf("wrong window: got %v, want %v", window, want)
}

Expand Down Expand Up @@ -219,6 +221,36 @@ func TestMultiGasConstraints(t *testing.T) {
}
}

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

weights := map[uint8]uint64{
uint8(multigas.ResourceKindComputation): 1,
}
// backlog=100, target=100, A=10 -> exponent = (100*1)/(10*100*1) = 0.01 -> safe
err := pricing.AddMultiGasConstraint(
100,
10,
100,
weights,
)
if err != nil {
t.Fatalf("expected valid constraint, got error: %v", err)
}

// backlog very large -> exponent blows past MaxExponentBips
err = pricing.AddMultiGasConstraint(
1,
1,
1_000_000_000_000,
weights,
)

if err == nil {
t.Fatalf("expected AddMultiGasConstraint to reject unsafe exponent, but got no error")
}
}

func Require(t *testing.T, err error, printables ...interface{}) {
t.Helper()
testhelpers.RequireImpl(t, err, printables...)
Expand Down
5 changes: 3 additions & 2 deletions arbos/l2pricing/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import (
"github.com/offchainlabs/nitro/util/arbmath"
)

const ArbosMultiConstraintsVersion = params.ArbosVersion_50
const ArbosSingleGasConstraintsVersion = params.ArbosVersion_50
const ArbosMultiGasConstraintsVersion = params.ArbosVersion_60

const InitialSpeedLimitPerSecondV0 = 1000000
const InitialPerBlockGasLimitV0 uint64 = 20 * 1000000
Expand All @@ -25,7 +26,7 @@ const InitialBacklogTolerance = 10
const InitialPerTxGasLimitV50 uint64 = 32 * 1000000

func (ps *L2PricingState) ShouldUseGasConstraints(arbosVersion uint64) (bool, error) {
if arbosVersion >= ArbosMultiConstraintsVersion {
if arbosVersion >= ArbosSingleGasConstraintsVersion {
constraintsLength, err := ps.GasConstraintsLength()
if err != nil {
return false, err
Expand Down
8 changes: 4 additions & 4 deletions cmd/l2pricing-simulator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ func runLegacyModel(args []string) error {
for i := range config.Iterations() {
baseFee, _ := pricing.BaseFeeWei()
gas := gasSimulator.compute(i, baseFee)
_ = pricing.AddToGasPool(-arbmath.SaturatingCast[int64](gas), l2pricing.ArbosMultiConstraintsVersion)
pricing.UpdatePricingModel(1, l2pricing.ArbosMultiConstraintsVersion)
_ = pricing.AddToGasPool(-arbmath.SaturatingCast[int64](gas), l2pricing.ArbosSingleGasConstraintsVersion)
pricing.UpdatePricingModel(1, l2pricing.ArbosSingleGasConstraintsVersion)
baseFee, _ = pricing.BaseFeeWei()
results = append(results, Result{
baseFee: baseFee,
Expand Down Expand Up @@ -102,8 +102,8 @@ func runConstraintsModel(args []string) error {
for i := range config.Iterations() {
baseFee, _ := pricing.BaseFeeWei()
gas := gasSimulator.compute(i, baseFee)
_ = pricing.AddToGasPool(-arbmath.SaturatingCast[int64](gas), l2pricing.ArbosMultiConstraintsVersion)
pricing.UpdatePricingModel(1, l2pricing.ArbosMultiConstraintsVersion)
_ = pricing.AddToGasPool(-arbmath.SaturatingCast[int64](gas), l2pricing.ArbosSingleGasConstraintsVersion)
pricing.UpdatePricingModel(1, l2pricing.ArbosSingleGasConstraintsVersion)
baseFee, _ = pricing.BaseFeeWei()
results = append(results, Result{
baseFee: baseFee,
Expand Down
2 changes: 1 addition & 1 deletion contracts-local/src/precompiles
2 changes: 1 addition & 1 deletion go-ethereum
Submodule go-ethereum updated 0 files
2 changes: 1 addition & 1 deletion nitro-testnode
53 changes: 53 additions & 0 deletions precompiles/ArbGasInfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package precompiles

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -321,3 +322,55 @@ func (con ArbGasInfo) GetGasPricingConstraints(c ctx, evm mech) ([][3]uint64, er
}
return constraints, nil
}

// GetMultiGasPricingConstraints returns the current configuration of multi-gas pricing constraints
func (con ArbGasInfo) GetMultiGasPricingConstraints(
c ctx,
evm mech,
) ([]MultiGasConstraint, error) {
length, err := c.State.L2PricingState().MultiGasConstraintsLength()
if err != nil {
return nil, fmt.Errorf("failed to get multi-gas constraint count: %w", err)
}

result := make([]MultiGasConstraint, 0, length)

for i := range length {
constraint := c.State.L2PricingState().OpenMultiGasConstraintAt(i)

target, err := constraint.Target()
if err != nil {
return nil, fmt.Errorf("failed to read target for constraint %d: %w", i, err)
}
window, err := constraint.AdjustmentWindow()
if err != nil {
return nil, fmt.Errorf("failed to read adjustment window for constraint %d: %w", i, err)
}
backlog, err := constraint.Backlog()
if err != nil {
return nil, fmt.Errorf("failed to read backlog for constraint %d: %w", i, err)
}

resourceMap, err := constraint.ResourcesWithWeights()
if err != nil {
return nil, fmt.Errorf("failed to read resource weights for constraint %d: %w", i, err)
}

resources := make([]WeightedResource, 0, len(resourceMap))
for kind, weight := range resourceMap {
resources = append(resources, WeightedResource{
Resource: uint8(kind),
Weight: weight,
})
}

result = append(result, MultiGasConstraint{
Resources: resources,
AdjustmentWindowSecs: window,
TargetPerSec: target,
Backlog: backlog,
})
}

return result, nil
}
18 changes: 18 additions & 0 deletions precompiles/ArbMultiGasConstraintsTypes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2025, Offchain Labs, Inc.
// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md

package precompiles

// Defines the relative contribution of a specific resource type within a constraint.
type WeightedResource = struct {
Resource uint8
Weight uint64
}

// Describes a single pricing constraint that applies to one or more weighted resources.
type MultiGasConstraint = struct {
Resources []WeightedResource
AdjustmentWindowSecs uint32
TargetPerSec uint64
Backlog uint64
}
36 changes: 36 additions & 0 deletions precompiles/ArbOwner.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,3 +482,39 @@ func (con ArbOwner) SetGasPricingConstraints(c ctx, evm mech, constraints [][3]u
}
return nil
}

// SetMultiGasPricingConstraints configures the multi-dimensional gas pricing model
func (con ArbOwner) SetMultiGasPricingConstraints(
Comment thread
gligneul marked this conversation as resolved.
c ctx,
evm mech,
constraints []MultiGasConstraint,
) error {
if err := c.State.L2PricingState().ClearMultiGasConstraints(); err != nil {
return fmt.Errorf("failed to clear existing multi-gas constraints: %w", err)
}

for _, constraint := range constraints {
if constraint.TargetPerSec == 0 || constraint.AdjustmentWindowSecs == 0 {
return fmt.Errorf(
"invalid constraint: target=%d adjustmentWindow=%d",
constraint.TargetPerSec, constraint.AdjustmentWindowSecs,
)
}

// Build map of resource weights
resourceWeights := make(map[uint8]uint64, len(constraint.Resources))
for _, r := range constraint.Resources {
resourceWeights[r.Resource] = r.Weight
}

if err := c.State.L2PricingState().AddMultiGasConstraint(
constraint.TargetPerSec,
constraint.AdjustmentWindowSecs,
constraint.Backlog,
resourceWeights,
); err != nil {
return fmt.Errorf("failed to add multi-gas constraint: %w", err)
}
}
return nil
}
2 changes: 1 addition & 1 deletion precompiles/ArbRetryableTx.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func (con ArbRetryableTx) Redeem(c ctx, evm mech, ticketId bytes32) (bytes32, er

// Multi-Constraint pricer requires an extra storage read, since ArbOS must load the constraints from state.
// This overhead applies even when no constraints are configured.
if c.State.ArbOSVersion() >= l2pricing.ArbosMultiConstraintsVersion {
if c.State.ArbOSVersion() >= l2pricing.ArbosSingleGasConstraintsVersion {
gasPoolUpdateCost += storage.StorageReadCost
}

Expand Down
Loading
Loading