From fa076c60dfb3a5a84e8891943da9df3d8a6ef508 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Wed, 12 Nov 2025 16:32:54 +0100 Subject: [PATCH 1/3] Add Arbos 60 version --- arbos/arbosState/arbosstate.go | 7 +++++++ contracts-local/src/precompiles | 2 +- system_tests/precompile_inclusion_test.go | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/arbos/arbosState/arbosstate.go b/arbos/arbosState/arbosstate.go index cbaba842297..af13041e792 100644 --- a/arbos/arbosState/arbosstate.go +++ b/arbos/arbosState/arbosstate.go @@ -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", diff --git a/contracts-local/src/precompiles b/contracts-local/src/precompiles index f49a4889b48..2ed42b12495 160000 --- a/contracts-local/src/precompiles +++ b/contracts-local/src/precompiles @@ -1 +1 @@ -Subproject commit f49a4889b486fd804a7901203f5f663cfd1581c8 +Subproject commit 2ed42b12495d64e02715450baf2acc1a26e5ccc4 diff --git a/system_tests/precompile_inclusion_test.go b/system_tests/precompile_inclusion_test.go index fdb32e0ef0e..53f81952417 100644 --- a/system_tests/precompile_inclusion_test.go +++ b/system_tests/precompile_inclusion_test.go @@ -75,6 +75,10 @@ func TestArbOSVersion50(t *testing.T) { testPrecompiles(t, params.ArbosVersion_50, kzgPointEvaluation.Included(), bls12381G1Add.Included(), bls12381G1MultiExp.Included()) } +func TestArbOSVersion60(t *testing.T) { + testPrecompiles(t, params.ArbosVersion_60, kzgPointEvaluation.Included(), bls12381G1Add.Included(), bls12381G1MultiExp.Included()) +} + func testPrecompiles(t *testing.T, arbosVersion uint64, cases ...precompileCase) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() From 0b6beab665dbb26923bad2e33dc6577945053a0a Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Wed, 12 Nov 2025 16:46:06 +0100 Subject: [PATCH 2/3] Implement multi-gas constraints precompiles --- arbos/constraints/multi_dimensional.go | 12 +- arbos/constraints/multi_dimensional_test.go | 2 +- arbos/l2pricing/l2pricing.go | 2 +- arbos/l2pricing/l2pricing_test.go | 6 +- arbos/l2pricing/model.go | 1 + contracts-local/src/precompiles | 2 +- go-ethereum | 2 +- nitro-testnode | 2 +- precompiles/ArbGasInfo.go | 83 ++++++++++ precompiles/ArbOwner.go | 44 ++++++ precompiles/constraints_test.go | 159 ++++++++++++++++++++ precompiles/precompile.go | 2 + precompiles/precompile_test.go | 1 + 13 files changed, 305 insertions(+), 13 deletions(-) diff --git a/arbos/constraints/multi_dimensional.go b/arbos/constraints/multi_dimensional.go index cb8314712cf..1990c619d04 100644 --- a/arbos/constraints/multi_dimensional.go +++ b/arbos/constraints/multi_dimensional.go @@ -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 @@ -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), } @@ -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 { @@ -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 @@ -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) } diff --git a/arbos/constraints/multi_dimensional_test.go b/arbos/constraints/multi_dimensional_test.go index d6c37e411c0..7a4c4cadb1f 100644 --- a/arbos/constraints/multi_dimensional_test.go +++ b/arbos/constraints/multi_dimensional_test.go @@ -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{ diff --git a/arbos/l2pricing/l2pricing.go b/arbos/l2pricing/l2pricing.go index 21c9204f67c..578de822bfc 100644 --- a/arbos/l2pricing/l2pricing.go +++ b/arbos/l2pricing/l2pricing.go @@ -217,7 +217,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 { diff --git a/arbos/l2pricing/l2pricing_test.go b/arbos/l2pricing/l2pricing_test.go index 1cd4ec96ceb..dd627ed42e2 100644 --- a/arbos/l2pricing/l2pricing_test.go +++ b/arbos/l2pricing/l2pricing_test.go @@ -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), ) } @@ -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) } diff --git a/arbos/l2pricing/model.go b/arbos/l2pricing/model.go index eb7b3d67052..65bf32ea46f 100644 --- a/arbos/l2pricing/model.go +++ b/arbos/l2pricing/model.go @@ -13,6 +13,7 @@ import ( ) const ArbosMultiConstraintsVersion = params.ArbosVersion_50 +const ArbosMultiGasMultiConstraintsVersion = params.ArbosVersion_60 const InitialSpeedLimitPerSecondV0 = 1000000 const InitialPerBlockGasLimitV0 uint64 = 20 * 1000000 diff --git a/contracts-local/src/precompiles b/contracts-local/src/precompiles index 2ed42b12495..a70694e8a08 160000 --- a/contracts-local/src/precompiles +++ b/contracts-local/src/precompiles @@ -1 +1 @@ -Subproject commit 2ed42b12495d64e02715450baf2acc1a26e5ccc4 +Subproject commit a70694e8a08ecdf95d055439ab1f87ff8a31a78e diff --git a/go-ethereum b/go-ethereum index 339b719be12..21a80cbfa4d 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit 339b719be1202e2cc45c3c7f451a44825c8857aa +Subproject commit 21a80cbfa4d0587f5aa17abcc4458823316ecdbf diff --git a/nitro-testnode b/nitro-testnode index 88aa1f4ee31..cc522989690 160000 --- a/nitro-testnode +++ b/nitro-testnode @@ -1 +1 @@ -Subproject commit 88aa1f4ee31182011195ec99992b5ff4d2e2c24d +Subproject commit cc5229896901f90f260934bfce085b1f3f519b0e diff --git a/precompiles/ArbGasInfo.go b/precompiles/ArbGasInfo.go index b03d4663e91..97f95a63e70 100644 --- a/precompiles/ArbGasInfo.go +++ b/precompiles/ArbGasInfo.go @@ -4,6 +4,7 @@ package precompiles import ( + "fmt" "math/big" "github.com/ethereum/go-ethereum/common" @@ -321,3 +322,85 @@ 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, +) ([]struct { + Resources []struct { + Resource uint8 + Weight uint64 + } + AdjustmentWindowSecs uint32 + TargetPerSec uint64 + Backlog uint64 +}, 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([]struct { + Resources []struct { + Resource uint8 + Weight uint64 + } + AdjustmentWindowSecs uint32 + TargetPerSec uint64 + Backlog uint64 + }, 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([]struct { + Resource uint8 + Weight uint64 + }, 0, len(resourceMap)) + for kind, weight := range resourceMap { + resources = append(resources, struct { + Resource uint8 + Weight uint64 + }{ + Resource: uint8(kind), + Weight: weight, + }) + } + + result = append(result, struct { + Resources []struct { + Resource uint8 + Weight uint64 + } + AdjustmentWindowSecs uint32 + TargetPerSec uint64 + Backlog uint64 + }{ + Resources: resources, + AdjustmentWindowSecs: window, + TargetPerSec: target, + Backlog: backlog, + }) + } + + return result, nil +} diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index f640f5ca487..7d912b8a5d0 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -482,3 +482,47 @@ 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( + c ctx, + evm mech, + constraints []struct { + Resources []struct { + Resource uint8 + Weight uint64 + } + AdjustmentWindowSecs uint32 + TargetPerSec uint64 + Backlog uint64 + }, +) 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 +} diff --git a/precompiles/constraints_test.go b/precompiles/constraints_test.go index b4ffa83dd3b..4b428b5bd79 100644 --- a/precompiles/constraints_test.go +++ b/precompiles/constraints_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/arbitrum/multigas" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -214,3 +215,161 @@ func TestEnableAndDisableMultiConstraints(t *testing.T) { require.NoError(t, err) require.False(t, shouldUseMultiConstraints) } + +func TestMultiGasConstraintsStorage(t *testing.T) { + t.Parallel() + + evm, state, callCtx, arbGasInfo, arbOwner := setupResourceConstraintHandles(t) + + // Set two multi-gas constraints + constraints := []struct { + Resources []struct { + Resource uint8 + Weight uint64 + } + AdjustmentWindowSecs uint32 + TargetPerSec uint64 + Backlog uint64 + }{ + { + Resources: []struct { + Resource uint8 + Weight uint64 + }{ + {uint8(multigas.ResourceKindComputation), 1}, + {uint8(multigas.ResourceKindStorageAccess), 2}, + }, + AdjustmentWindowSecs: 1, + TargetPerSec: 30_000_000, + Backlog: 800_000, + }, + { + Resources: []struct { + Resource uint8 + Weight uint64 + }{ + {uint8(multigas.ResourceKindComputation), 2}, + {uint8(multigas.ResourceKindStorageAccess), 3}, + }, + AdjustmentWindowSecs: 102, + TargetPerSec: 15_000_000, + Backlog: 1_600_000, + }, + } + + err := arbOwner.SetMultiGasPricingConstraints(callCtx, evm, constraints) + require.NoError(t, err) + + length, err := state.L2PricingState().MultiGasConstraintsLength() + require.NoError(t, err) + require.Equal(t, uint64(2), length) + + first := state.L2PricingState().OpenMultiGasConstraintAt(0) + second := state.L2PricingState().OpenMultiGasConstraintAt(1) + + // First constraint + target, err := first.Target() + require.NoError(t, err) + require.Equal(t, uint64(30_000_000), target) + window, err := first.AdjustmentWindow() + require.NoError(t, err) + require.Equal(t, uint32(1), window) + backlog, err := first.Backlog() + require.NoError(t, err) + require.Equal(t, uint64(800_000), backlog) + + resMap, err := first.ResourcesWithWeights() + require.NoError(t, err) + require.Equal(t, uint64(1), resMap[multigas.ResourceKindComputation]) + require.Equal(t, uint64(2), resMap[multigas.ResourceKindStorageAccess]) + + // Second constraint + target, err = second.Target() + require.NoError(t, err) + require.Equal(t, uint64(15_000_000), target) + window, err = second.AdjustmentWindow() + require.NoError(t, err) + require.Equal(t, uint32(102), window) + backlog, err = second.Backlog() + require.NoError(t, err) + require.Equal(t, uint64(1_600_000), backlog) + + resMap, err = second.ResourcesWithWeights() + require.NoError(t, err) + require.Equal(t, uint64(2), resMap[multigas.ResourceKindComputation]) + require.Equal(t, uint64(3), resMap[multigas.ResourceKindStorageAccess]) + + // Verify via getter precompile + results, err := arbGasInfo.GetMultiGasPricingConstraints(callCtx, evm) + require.NoError(t, err) + require.Equal(t, 2, len(results)) + + require.Equal(t, uint32(1), results[0].AdjustmentWindowSecs) + require.Equal(t, uint64(30_000_000), results[0].TargetPerSec) + require.Equal(t, uint64(800_000), results[0].Backlog) + require.Equal(t, 2, len(results[0].Resources)) + + require.Equal(t, uint32(102), results[1].AdjustmentWindowSecs) + require.Equal(t, uint64(15_000_000), results[1].TargetPerSec) + require.Equal(t, uint64(1_600_000), results[1].Backlog) + require.Equal(t, 2, len(results[1].Resources)) + + // Replace with a single new constraint + newConstraints := []struct { + Resources []struct { + Resource uint8 + Weight uint64 + } + AdjustmentWindowSecs uint32 + TargetPerSec uint64 + Backlog uint64 + }{ + { + Resources: []struct { + Resource uint8 + Weight uint64 + }{ + {uint8(multigas.ResourceKindComputation), 5}, + {uint8(multigas.ResourceKindStorageAccess), 7}, + }, + AdjustmentWindowSecs: 12, + TargetPerSec: 7_000_000, + Backlog: 50_000_000, + }, + } + + err = arbOwner.SetMultiGasPricingConstraints(callCtx, evm, newConstraints) + require.NoError(t, err) + + length, err = state.L2PricingState().MultiGasConstraintsLength() + require.NoError(t, err) + require.Equal(t, uint64(1), length) + + first = state.L2PricingState().OpenMultiGasConstraintAt(0) + target, err = first.Target() + require.NoError(t, err) + require.Equal(t, uint64(7_000_000), target) + window, err = first.AdjustmentWindow() + require.NoError(t, err) + require.Equal(t, uint32(12), window) + backlog, err = first.Backlog() + require.NoError(t, err) + require.Equal(t, uint64(50_000_000), backlog) + + resMap, err = first.ResourcesWithWeights() + require.NoError(t, err) + require.Equal(t, uint64(5), resMap[multigas.ResourceKindComputation]) + require.Equal(t, uint64(7), resMap[multigas.ResourceKindStorageAccess]) + + results, err = arbGasInfo.GetMultiGasPricingConstraints(callCtx, evm) + require.NoError(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, uint32(12), results[0].AdjustmentWindowSecs) + require.Equal(t, uint64(7_000_000), results[0].TargetPerSec) + require.Equal(t, uint64(50_000_000), results[0].Backlog) + require.Equal(t, 2, len(results[0].Resources)) + require.Equal(t, uint8(multigas.ResourceKindComputation), results[0].Resources[0].Resource) + require.Equal(t, uint64(5), results[0].Resources[0].Weight) + require.Equal(t, uint8(multigas.ResourceKindStorageAccess), results[0].Resources[1].Resource) + require.Equal(t, uint64(7), results[0].Resources[1].Weight) +} diff --git a/precompiles/precompile.go b/precompiles/precompile.go index 73315a4f150..8084a3f1415 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -537,6 +537,7 @@ func Precompiles() map[addr]ArbosPrecompile { ArbGasInfo.methodsByName["GetMaxTxGasLimit"].arbosVersion = params.ArbosVersion_50 ArbGasInfo.methodsByName["GetMaxBlockGasLimit"].arbosVersion = params.ArbosVersion_50 ArbGasInfo.methodsByName["GetGasPricingConstraints"].arbosVersion = params.ArbosVersion_50 + ArbGasInfo.methodsByName["GetMultiGasPricingConstraints"].arbosVersion = params.ArbosVersion_60 insert(MakePrecompile(precompilesgen.ArbAggregatorMetaData, &ArbAggregator{Address: types.ArbAggregatorAddress})) insert(MakePrecompile(precompilesgen.ArbStatisticsMetaData, &ArbStatistics{Address: types.ArbStatisticsAddress})) @@ -619,6 +620,7 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwner.methodsByName["SetBrotliCompressionLevel"].arbosVersion = params.ArbosVersion_20 ArbOwner.methodsByName["SetGasPricingConstraints"].arbosVersion = params.ArbosVersion_50 ArbOwner.methodsByName["SetGasBacklog"].arbosVersion = params.ArbosVersion_50 + ArbOwner.methodsByName["SetMultiGasPricingConstraints"].arbosVersion = params.ArbosVersion_60 stylusMethods := []string{ "SetInkPrice", "SetWasmMaxStackDepth", "SetWasmFreePages", "SetWasmPageGas", "SetWasmPageLimit", "SetWasmMinInitGas", "SetWasmInitCostScalar", diff --git a/precompiles/precompile_test.go b/precompiles/precompile_test.go index 61398d4ff11..d0eeca4ebcf 100644 --- a/precompiles/precompile_test.go +++ b/precompiles/precompile_test.go @@ -192,6 +192,7 @@ func TestPrecompilesPerArbosVersion(t *testing.T) { params.ArbosVersion_40: 3, params.ArbosVersion_41: 10, params.ArbosVersion_50: 9, + params.ArbosVersion_60: 2, } precompiles := Precompiles() From d7eb2d68fda1c8d42446dc82be620a21322129cf Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Fri, 14 Nov 2025 16:51:14 +0100 Subject: [PATCH 3/3] Add precompile constraint types and check max bips --- arbos/l2pricing/l2pricing.go | 13 ++++++- arbos/l2pricing/l2pricing_test.go | 30 ++++++++++++++++ arbos/l2pricing/model.go | 6 ++-- cmd/l2pricing-simulator/main.go | 8 ++--- precompiles/ArbGasInfo.go | 40 +++------------------- precompiles/ArbMultiGasConstraintsTypes.go | 18 ++++++++++ precompiles/ArbOwner.go | 10 +----- precompiles/ArbRetryableTx.go | 2 +- precompiles/constraints_test.go | 35 +++---------------- 9 files changed, 79 insertions(+), 83 deletions(-) create mode 100644 precompiles/ArbMultiGasConstraintsTypes.go diff --git a/arbos/l2pricing/l2pricing.go b/arbos/l2pricing/l2pricing.go index 578de822bfc..c1260d34a55 100644 --- a/arbos/l2pricing/l2pricing.go +++ b/arbos/l2pricing/l2pricing.go @@ -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) @@ -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 { + return fmt.Errorf("resource kind %v has exponent %v bips exceeding max of %v bips", kind, exp, MaxExponentBips) + } + } + return nil } diff --git a/arbos/l2pricing/l2pricing_test.go b/arbos/l2pricing/l2pricing_test.go index dd627ed42e2..fa9dcdb5380 100644 --- a/arbos/l2pricing/l2pricing_test.go +++ b/arbos/l2pricing/l2pricing_test.go @@ -221,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...) diff --git a/arbos/l2pricing/model.go b/arbos/l2pricing/model.go index 65bf32ea46f..00cd4eb8685 100644 --- a/arbos/l2pricing/model.go +++ b/arbos/l2pricing/model.go @@ -12,8 +12,8 @@ import ( "github.com/offchainlabs/nitro/util/arbmath" ) -const ArbosMultiConstraintsVersion = params.ArbosVersion_50 -const ArbosMultiGasMultiConstraintsVersion = params.ArbosVersion_60 +const ArbosSingleGasConstraintsVersion = params.ArbosVersion_50 +const ArbosMultiGasConstraintsVersion = params.ArbosVersion_60 const InitialSpeedLimitPerSecondV0 = 1000000 const InitialPerBlockGasLimitV0 uint64 = 20 * 1000000 @@ -26,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 diff --git a/cmd/l2pricing-simulator/main.go b/cmd/l2pricing-simulator/main.go index e2c8c6b38c5..a8d9148ac05 100644 --- a/cmd/l2pricing-simulator/main.go +++ b/cmd/l2pricing-simulator/main.go @@ -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, @@ -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, diff --git a/precompiles/ArbGasInfo.go b/precompiles/ArbGasInfo.go index 97f95a63e70..1e12b3cb419 100644 --- a/precompiles/ArbGasInfo.go +++ b/precompiles/ArbGasInfo.go @@ -327,29 +327,13 @@ func (con ArbGasInfo) GetGasPricingConstraints(c ctx, evm mech) ([][3]uint64, er func (con ArbGasInfo) GetMultiGasPricingConstraints( c ctx, evm mech, -) ([]struct { - Resources []struct { - Resource uint8 - Weight uint64 - } - AdjustmentWindowSecs uint32 - TargetPerSec uint64 - Backlog uint64 -}, error) { +) ([]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([]struct { - Resources []struct { - Resource uint8 - Weight uint64 - } - AdjustmentWindowSecs uint32 - TargetPerSec uint64 - Backlog uint64 - }, 0, length) + result := make([]MultiGasConstraint, 0, length) for i := range length { constraint := c.State.L2PricingState().OpenMultiGasConstraintAt(i) @@ -372,29 +356,15 @@ func (con ArbGasInfo) GetMultiGasPricingConstraints( return nil, fmt.Errorf("failed to read resource weights for constraint %d: %w", i, err) } - resources := make([]struct { - Resource uint8 - Weight uint64 - }, 0, len(resourceMap)) + resources := make([]WeightedResource, 0, len(resourceMap)) for kind, weight := range resourceMap { - resources = append(resources, struct { - Resource uint8 - Weight uint64 - }{ + resources = append(resources, WeightedResource{ Resource: uint8(kind), Weight: weight, }) } - result = append(result, struct { - Resources []struct { - Resource uint8 - Weight uint64 - } - AdjustmentWindowSecs uint32 - TargetPerSec uint64 - Backlog uint64 - }{ + result = append(result, MultiGasConstraint{ Resources: resources, AdjustmentWindowSecs: window, TargetPerSec: target, diff --git a/precompiles/ArbMultiGasConstraintsTypes.go b/precompiles/ArbMultiGasConstraintsTypes.go new file mode 100644 index 00000000000..7a5e4180aab --- /dev/null +++ b/precompiles/ArbMultiGasConstraintsTypes.go @@ -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 +} diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index 7d912b8a5d0..7e13a1d557d 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -487,15 +487,7 @@ func (con ArbOwner) SetGasPricingConstraints(c ctx, evm mech, constraints [][3]u func (con ArbOwner) SetMultiGasPricingConstraints( c ctx, evm mech, - constraints []struct { - Resources []struct { - Resource uint8 - Weight uint64 - } - AdjustmentWindowSecs uint32 - TargetPerSec uint64 - Backlog uint64 - }, + constraints []MultiGasConstraint, ) error { if err := c.State.L2PricingState().ClearMultiGasConstraints(); err != nil { return fmt.Errorf("failed to clear existing multi-gas constraints: %w", err) diff --git a/precompiles/ArbRetryableTx.go b/precompiles/ArbRetryableTx.go index 10eeac63e33..237bf499f37 100644 --- a/precompiles/ArbRetryableTx.go +++ b/precompiles/ArbRetryableTx.go @@ -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 } diff --git a/precompiles/constraints_test.go b/precompiles/constraints_test.go index 4b428b5bd79..5595f606a85 100644 --- a/precompiles/constraints_test.go +++ b/precompiles/constraints_test.go @@ -222,20 +222,9 @@ func TestMultiGasConstraintsStorage(t *testing.T) { evm, state, callCtx, arbGasInfo, arbOwner := setupResourceConstraintHandles(t) // Set two multi-gas constraints - constraints := []struct { - Resources []struct { - Resource uint8 - Weight uint64 - } - AdjustmentWindowSecs uint32 - TargetPerSec uint64 - Backlog uint64 - }{ + constraints := []MultiGasConstraint{ { - Resources: []struct { - Resource uint8 - Weight uint64 - }{ + Resources: []WeightedResource{ {uint8(multigas.ResourceKindComputation), 1}, {uint8(multigas.ResourceKindStorageAccess), 2}, }, @@ -244,10 +233,7 @@ func TestMultiGasConstraintsStorage(t *testing.T) { Backlog: 800_000, }, { - Resources: []struct { - Resource uint8 - Weight uint64 - }{ + Resources: []WeightedResource{ {uint8(multigas.ResourceKindComputation), 2}, {uint8(multigas.ResourceKindStorageAccess), 3}, }, @@ -315,20 +301,9 @@ func TestMultiGasConstraintsStorage(t *testing.T) { require.Equal(t, 2, len(results[1].Resources)) // Replace with a single new constraint - newConstraints := []struct { - Resources []struct { - Resource uint8 - Weight uint64 - } - AdjustmentWindowSecs uint32 - TargetPerSec uint64 - Backlog uint64 - }{ + newConstraints := []MultiGasConstraint{ { - Resources: []struct { - Resource uint8 - Weight uint64 - }{ + Resources: []WeightedResource{ {uint8(multigas.ResourceKindComputation), 5}, {uint8(multigas.ResourceKindStorageAccess), 7}, },