diff --git a/arbos/internal_tx.go b/arbos/internal_tx.go index 8b2e2d1ddbb..02fd6650209 100644 --- a/arbos/internal_tx.go +++ b/arbos/internal_tx.go @@ -94,9 +94,6 @@ func ApplyInternalTxUpdate(tx *types.ArbitrumInternalTx, state *arbosState.Arbos oldL1BlockNumber, err := state.Blockhashes().L1BlockNumber() state.Restrict(err) - l2BaseFee, err := state.L2PricingState().BaseFeeWei() - state.Restrict(err) - if l1BlockNumber > oldL1BlockNumber { state.Restrict(state.Blockhashes().RecordNewL1Block(l1BlockNumber-1, prevHash, state.ArbOSVersion())) } @@ -107,7 +104,7 @@ func ApplyInternalTxUpdate(tx *types.ArbitrumInternalTx, state *arbosState.Arbos _ = state.RetryableState().TryToReapOneRetryable(currentTime, evm, util.TracingDuringEVM) _ = state.RetryableState().TryToReapOneRetryable(currentTime, evm, util.TracingDuringEVM) - state.L2PricingState().UpdatePricingModel(l2BaseFee, timePassed, false) + state.L2PricingState().UpdatePricingModel(timePassed, state.ArbOSVersion()) return state.UpgradeArbosVersionIfNecessary(currentTime, evm.StateDB, evm.ChainConfig()) case InternalTxBatchPostingReportMethodID: diff --git a/arbos/l2pricing/l2pricing.go b/arbos/l2pricing/l2pricing.go index 17698a678d2..86fe69b965e 100644 --- a/arbos/l2pricing/l2pricing.go +++ b/arbos/l2pricing/l2pricing.go @@ -13,24 +13,24 @@ import ( const ( gasConstraintTargetOffset uint64 = iota - gasConstraintInertiaOffset + gasConstraintAdjustmentWindowOffset gasConstraintBacklogOffset ) -// GasConstraint tries to keep the gas backlog under the target (per second) for the given inertia factor. -// The inertia can be computed as 30*sqrt(period), where period is the number of seconds which the -// constraint is acting over. For instance, the default inertia of 102 is an approximation of 30*sqrt(12). +// GasConstraint tries to keep the gas backlog under the target (per second) for the given adjustment window. +// Target stands for gas usage per second +// Adjustment window is the time frame over which the price will rise by a factor of e if demand is 2x the target type GasConstraint struct { - target storage.StorageBackedUint64 - inertia storage.StorageBackedUint64 - backlog storage.StorageBackedUint64 + target storage.StorageBackedUint64 + adjustmentWindow storage.StorageBackedUint64 + backlog storage.StorageBackedUint64 } func OpenGasConstraint(storage *storage.Storage) *GasConstraint { return &GasConstraint{ - target: storage.OpenStorageBackedUint64(gasConstraintTargetOffset), - inertia: storage.OpenStorageBackedUint64(gasConstraintInertiaOffset), - backlog: storage.OpenStorageBackedUint64(gasConstraintBacklogOffset), + target: storage.OpenStorageBackedUint64(gasConstraintTargetOffset), + adjustmentWindow: storage.OpenStorageBackedUint64(gasConstraintAdjustmentWindowOffset), + backlog: storage.OpenStorageBackedUint64(gasConstraintBacklogOffset), } } @@ -38,7 +38,7 @@ func (c *GasConstraint) Clear() error { if err := c.target.Clear(); err != nil { return err } - if err := c.inertia.Clear(); err != nil { + if err := c.adjustmentWindow.Clear(); err != nil { return err } if err := c.backlog.Clear(); err != nil { @@ -47,6 +47,22 @@ func (c *GasConstraint) Clear() error { return nil } +func (c *GasConstraint) Target() (uint64, error) { + return c.target.Get() +} + +func (c *GasConstraint) AdjustmentWindow() (uint64, error) { + return c.adjustmentWindow.Get() +} + +func (c *GasConstraint) Backlog() (uint64, error) { + return c.backlog.Get() +} + +func (c *GasConstraint) SetBacklog(val uint64) error { + return c.backlog.Set(val) +} + type L2PricingState struct { storage *storage.Storage speedLimitPerSecond storage.StorageBackedUint64 @@ -171,7 +187,7 @@ func (ps *L2PricingState) Restrict(err error) { ps.storage.Burner().Restrict(err) } -func (ps *L2PricingState) SetConstraintsFromLegacy() error { +func (ps *L2PricingState) setConstraintsFromLegacy() error { if err := ps.ClearConstraints(); err != nil { return err } @@ -179,7 +195,7 @@ func (ps *L2PricingState) SetConstraintsFromLegacy() error { if err != nil { return err } - inertia, err := ps.PricingInertia() + adjustmentWindow, err := ps.PricingInertia() if err != nil { return err } @@ -192,10 +208,10 @@ func (ps *L2PricingState) SetConstraintsFromLegacy() error { return err } backlog := arbmath.SaturatingUSub(oldBacklog, arbmath.SaturatingUMul(backlogTolerance, target)) - return ps.AddConstraint(target, inertia, backlog) + return ps.AddConstraint(target, adjustmentWindow, backlog) } -func (ps *L2PricingState) AddConstraint(target uint64, inertia uint64, backlog uint64) error { +func (ps *L2PricingState) AddConstraint(target uint64, adjustmentWindow uint64, backlog uint64) error { subStorage, err := ps.constraints.Push() if err != nil { return fmt.Errorf("failed to push constraint: %w", err) @@ -204,8 +220,8 @@ func (ps *L2PricingState) AddConstraint(target uint64, inertia uint64, backlog u if err := constraint.target.Set(target); err != nil { return fmt.Errorf("failed to set target: %w", err) } - if err := constraint.inertia.Set(inertia); err != nil { - return fmt.Errorf("failed to set inertia: %w", err) + if err := constraint.adjustmentWindow.Set(adjustmentWindow); err != nil { + return fmt.Errorf("failed to set adjustment window: %w", err) } if err := constraint.backlog.Set(backlog); err != nil { return fmt.Errorf("failed to set backlog: %w", err) diff --git a/arbos/l2pricing/l2pricing_test.go b/arbos/l2pricing/l2pricing_test.go index 95db3e29af6..731e42d725f 100644 --- a/arbos/l2pricing/l2pricing_test.go +++ b/arbos/l2pricing/l2pricing_test.go @@ -22,9 +22,10 @@ func PricingForTest(t *testing.T) *L2PricingState { } func fakeBlockUpdate(t *testing.T, pricing *L2PricingState, gasUsed int64, timePassed uint64) { - basefee := getPrice(t, pricing) - pricing.storage.Burner().Restrict(pricing.AddToGasPool(-gasUsed)) - pricing.UpdatePricingModel(arbmath.UintToBig(basefee), timePassed, true) + t.Helper() + + pricing.storage.Burner().Restrict(pricing.addToGasPoolLegacy(-gasUsed)) + pricing.updatePricingModelLegacy(timePassed) } func TestPricingModelExp(t *testing.T) { @@ -133,7 +134,7 @@ func TestGasConstraints(t *testing.T) { if want := 100*i + 1; target != want { t.Errorf("wrong target: got %v, want %v", target, want) } - inertia, err := constraint.inertia.Get() + inertia, err := constraint.adjustmentWindow.Get() Require(t, err) if want := 100*i + 2; inertia != want { t.Errorf("wrong inertia: got %v, want %v", inertia, want) diff --git a/arbos/l2pricing/model.go b/arbos/l2pricing/model.go index 001cea1a9f3..e616b326785 100644 --- a/arbos/l2pricing/model.go +++ b/arbos/l2pricing/model.go @@ -12,6 +12,8 @@ import ( "github.com/offchainlabs/nitro/util/arbmath" ) +const ArbosMultiConstraintsVersion = params.ArbosVersion_50 + const InitialSpeedLimitPerSecondV0 = 1000000 const InitialPerBlockGasLimitV0 uint64 = 20 * 1000000 const InitialSpeedLimitPerSecondV6 = 7000000 @@ -22,7 +24,29 @@ const InitialPricingInertia = 102 const InitialBacklogTolerance = 10 const InitialPerTxGasLimitV50 uint64 = 32 * 1000000 -func (ps *L2PricingState) AddToGasPool(gas int64) error { +func (ps *L2PricingState) ShouldUseMultiConstraints(arbosVersion uint64) (bool, error) { + if arbosVersion >= ArbosMultiConstraintsVersion { + constraintsLength, err := ps.ConstraintsLength() + if err != nil { + return false, err + } + return constraintsLength > 0, nil + } + return false, nil +} + +func (ps *L2PricingState) AddToGasPool(gas int64, arbosVersion uint64) error { + shouldUseMultiConstraints, err := ps.ShouldUseMultiConstraints(arbosVersion) + if err != nil { + return err + } + if shouldUseMultiConstraints { + return ps.addToGasPoolMultiConstraints(gas) + } + return ps.addToGasPoolLegacy(gas) +} + +func (ps *L2PricingState) addToGasPoolLegacy(gas int64) error { backlog, err := ps.GasBacklog() if err != nil { return err @@ -31,7 +55,7 @@ func (ps *L2PricingState) AddToGasPool(gas int64) error { return ps.SetGasBacklog(backlog) } -func (ps *L2PricingState) AddToGasPoolMultiConstraints(gas int64) error { +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) @@ -50,6 +74,16 @@ func (ps *L2PricingState) AddToGasPoolMultiConstraints(gas int64) error { return nil } +// UpdatePricingModel updates the pricing model with info from the last block +func (ps *L2PricingState) UpdatePricingModel(timePassed uint64, arbosVersion uint64) { + shouldUseMultiConstraints, _ := ps.ShouldUseMultiConstraints(arbosVersion) + if shouldUseMultiConstraints { + ps.updatePricingModelMultiConstraints(timePassed) + } else { + ps.updatePricingModelLegacy(timePassed) + } +} + // applyGasDelta grows the backlog if the gas is negative and pays off if the gas is positive. func applyGasDelta(backlog uint64, gas int64) uint64 { if gas > 0 { @@ -59,10 +93,9 @@ func applyGasDelta(backlog uint64, gas int64) uint64 { } } -// UpdatePricingModel updates the pricing model with info from the last block -func (ps *L2PricingState) UpdatePricingModel(l2BaseFee *big.Int, timePassed uint64, debug bool) { +func (ps *L2PricingState) updatePricingModelLegacy(timePassed uint64) { speedLimit, _ := ps.SpeedLimitPerSecond() - _ = ps.AddToGasPool(arbmath.SaturatingCast[int64](arbmath.SaturatingUMul(timePassed, speedLimit))) + _ = ps.addToGasPoolLegacy(arbmath.SaturatingCast[int64](arbmath.SaturatingUMul(timePassed, speedLimit))) inertia, _ := ps.PricingInertia() tolerance, _ := ps.BacklogTolerance() backlog, _ := ps.GasBacklog() @@ -76,23 +109,23 @@ func (ps *L2PricingState) UpdatePricingModel(l2BaseFee *big.Int, timePassed uint _ = ps.SetBaseFeeWei(baseFee) } -func (ps *L2PricingState) UpdatePricingModelMultiConstraints(timePassed uint64) { +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() + target, _ := constraint.Target() // Pay off backlog - backlog, _ := constraint.backlog.Get() + backlog, _ := constraint.Backlog() gas := arbmath.SaturatingCast[int64](arbmath.SaturatingUMul(timePassed, target)) backlog = applyGasDelta(backlog, gas) _ = constraint.backlog.Set(backlog) // Calculate exponent with the formula backlog/divisor if backlog > 0 { - inertia, _ := constraint.inertia.Get() + inertia, _ := constraint.AdjustmentWindow() divisor := arbmath.SaturatingCastToBips(arbmath.SaturatingUMul(inertia, target)) exponent := arbmath.NaturalToBips(arbmath.SaturatingCast[int64](backlog)) / divisor totalExponent = arbmath.SaturatingBipsAdd(totalExponent, exponent) diff --git a/arbos/l2pricing/model_test.go b/arbos/l2pricing/model_test.go index 0dbb8171af1..be9164ab8c4 100644 --- a/arbos/l2pricing/model_test.go +++ b/arbos/l2pricing/model_test.go @@ -45,12 +45,12 @@ func TestCompareLegacyPricingModelWithMultiConstraints(t *testing.T) { _ = pricing.gasBacklog.Set(backlog) // Initialize with a single constraint based on the legacy model - _ = pricing.SetConstraintsFromLegacy() + _ = pricing.setConstraintsFromLegacy() - pricing.UpdatePricingModel(nil, timePassed, false) + pricing.updatePricingModelLegacy(timePassed) legacyPrice, _ := pricing.baseFeeWei.Get() - pricing.UpdatePricingModelMultiConstraints(timePassed) + pricing.updatePricingModelMultiConstraints(timePassed) multiPrice, _ := pricing.baseFeeWei.Get() if timePassed == 0 { diff --git a/arbos/tx_processor.go b/arbos/tx_processor.go index 74074ca9c51..47fbb3f4437 100644 --- a/arbos/tx_processor.go +++ b/arbos/tx_processor.go @@ -632,7 +632,7 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, success bool) { } } // we've already credited the network fee account, but we didn't charge the gas pool yet - p.state.Restrict(p.state.L2PricingState().AddToGasPool(-arbmath.SaturatingCast[int64](gasUsed))) + p.state.Restrict(p.state.L2PricingState().AddToGasPool(-arbmath.SaturatingCast[int64](gasUsed), p.state.ArbOSVersion())) return } @@ -695,7 +695,7 @@ func (p *TxProcessor) EndTxHook(gasLeft uint64, success bool) { log.Error("total gas used < poster gas component", "gasUsed", gasUsed, "posterGas", p.posterGas) computeGas = gasUsed } - p.state.Restrict(p.state.L2PricingState().AddToGasPool(-arbmath.SaturatingCast[int64](computeGas))) + p.state.Restrict(p.state.L2PricingState().AddToGasPool(-arbmath.SaturatingCast[int64](computeGas), p.state.ArbOSVersion())) } } diff --git a/contracts-local/src/precompiles b/contracts-local/src/precompiles index c395f7b6860..f49a4889b48 160000 --- a/contracts-local/src/precompiles +++ b/contracts-local/src/precompiles @@ -1 +1 @@ -Subproject commit c395f7b6860bdccce940b27d79f2e08b04de6dd5 +Subproject commit f49a4889b486fd804a7901203f5f663cfd1581c8 diff --git a/nitro-testnode b/nitro-testnode index 947cb198289..88aa1f4ee31 160000 --- a/nitro-testnode +++ b/nitro-testnode @@ -1 +1 @@ -Subproject commit 947cb19828993fa95cc4f734ffe8c44afff8f7fd +Subproject commit 88aa1f4ee31182011195ec99992b5ff4d2e2c24d diff --git a/precompiles/ArbGasInfo.go b/precompiles/ArbGasInfo.go index eeffc02196f..323f1b48dcb 100644 --- a/precompiles/ArbGasInfo.go +++ b/precompiles/ArbGasInfo.go @@ -4,7 +4,6 @@ package precompiles import ( - "fmt" "math/big" "github.com/ethereum/go-ethereum/common" @@ -288,12 +287,37 @@ func (con ArbGasInfo) GetLastL1PricingSurplus(c ctx, evm mech) (*big.Int, error) // GetMaxBlockGasLimit gets the maximum block gas limit func (con ArbGasInfo) GetMaxBlockGasLimit(c ctx, evm mech) (uint64, error) { - // todo: update with real code once https://github.com/OffchainLabs/nitro/pull/3860 is merged - return 0, fmt.Errorf("GetMaxBlockGasLimit is a stub implementation, since definitions were upstreamed, but implementations not done") + return c.State.L2PricingState().PerBlockGasLimit() } // GetGasPricingConstraints gets the current gas pricing constraints used by the Multi-Constraint Pricer. func (con ArbGasInfo) GetGasPricingConstraints(c ctx, evm mech) ([][3]uint64, error) { - // todo: update with real code once https://github.com/OffchainLabs/nitro/pull/3860 is merged - return nil, fmt.Errorf("GetGasPricingConstraints is a stub implementation, since definitions were upstreamed, but implementations not done") + len, err := c.State.L2PricingState().ConstraintsLength() + if err != nil { + return nil, err + } + + constraints := make([][3]uint64, 0, len) + for i := range len { + constraint := c.State.L2PricingState().OpenConstraintAt(i) + gasTargetPerSecond, err := constraint.Target() + if err != nil { + return nil, err + } + adjustmentWindowSeconds, err := constraint.AdjustmentWindow() + if err != nil { + return nil, err + } + backlog, err := constraint.Backlog() + if err != nil { + return nil, err + } + + constraints = append(constraints, [3]uint64{ + gasTargetPerSecond, + adjustmentWindowSeconds, + backlog, + }) + } + return constraints, nil } diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index 7ececacde3b..b76f8555605 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -454,8 +454,31 @@ func (con ArbOwner) SetCalldataPriceIncrease(c ctx, _ mech, enable bool) error { return c.State.Features().SetCalldataPriceIncrease(enable) } -// SetGasPricingConstraints sets the gas pricing constraints used by the Multi-Constraint Pricer. +// SetGasBacklog sets the L2 gas backlog directly (used by single-constraint pricing model only) +func (con ArbOwner) SetGasBacklog(c ctx, evm mech, backlog uint64) error { + return c.State.L2PricingState().SetGasBacklog(backlog) +} + +// SetGasPricingConstraints sets the gas pricing constraints used by the multi-constraint pricing model func (con ArbOwner) SetGasPricingConstraints(c ctx, evm mech, constraints [][3]uint64) error { - // todo: update with real code once https://github.com/OffchainLabs/nitro/pull/3860 is merged - return fmt.Errorf("SetGasPricingConstraints is a stub implementation, since definitions were upstreamed, but implementations not done") + err := c.State.L2PricingState().ClearConstraints() + if err != nil { + return fmt.Errorf("failed to clear existing constraints: %w", err) + } + + for _, constraint := range constraints { + gasTargetPerSecond := constraint[0] + adjustmentWindowSeconds := constraint[1] + startingBacklogValue := constraint[2] + + if gasTargetPerSecond == 0 || adjustmentWindowSeconds == 0 { + return fmt.Errorf("invalid constraint with target %d and adjustment window %d", gasTargetPerSecond, adjustmentWindowSeconds) + } + + err := c.State.L2PricingState().AddConstraint(gasTargetPerSecond, adjustmentWindowSeconds, startingBacklogValue) + if err != nil { + return fmt.Errorf("failed to add constraint (target: %d, adjustment window: %d): %w", gasTargetPerSecond, adjustmentWindowSeconds, err) + } + } + return nil } diff --git a/precompiles/ArbRetryableTx.go b/precompiles/ArbRetryableTx.go index 6e4207aef0e..658f227f627 100644 --- a/precompiles/ArbRetryableTx.go +++ b/precompiles/ArbRetryableTx.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/arbos/l2pricing" "github.com/offchainlabs/nitro/arbos/retryables" "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" @@ -99,6 +100,9 @@ func (con ArbRetryableTx) Redeem(c ctx, evm mech, ticketId bytes32) (bytes32, er // Result is 32 bytes long which is 1 word gasCostToReturnResult := params.CopyGas gasPoolUpdateCost := storage.StorageReadCost + storage.StorageWriteCost + if c.State.ArbOSVersion() >= l2pricing.ArbosMultiConstraintsVersion { + gasPoolUpdateCost += storage.StorageReadCost + } futureGasCosts := eventCost + gasCostToReturnResult + gasPoolUpdateCost if c.GasLeft() < futureGasCosts { return hash{}, c.Burn(multigas.ResourceKindComputation, futureGasCosts) // this will error @@ -128,7 +132,7 @@ func (con ArbRetryableTx) Redeem(c ctx, evm mech, ticketId bytes32) (bytes32, er // Add the gasToDonate back to the gas pool: the retryable attempt will then consume it. // This ensures that the gas pool has enough gas to run the retryable attempt. - return retryTxHash, c.State.L2PricingState().AddToGasPool(arbmath.SaturatingCast[int64](gasToDonate)) + return retryTxHash, c.State.L2PricingState().AddToGasPool(arbmath.SaturatingCast[int64](gasToDonate), c.State.ArbOSVersion()) } // GetLifetime gets the default lifetime period a retryable has at creation diff --git a/precompiles/constraints_test.go b/precompiles/constraints_test.go new file mode 100644 index 00000000000..6d0a0dcf76f --- /dev/null +++ b/precompiles/constraints_test.go @@ -0,0 +1,185 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package precompiles + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/burn" + "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/util/testhelpers" +) + +func setupResourceConstraintHandles( + t *testing.T, +) ( + *vm.EVM, + *arbosState.ArbosState, + *Context, + *ArbGasInfo, + *ArbOwner, +) { + t.Helper() + + evm := newMockEVMForTesting() + caller := common.BytesToAddress(crypto.Keccak256([]byte{})[:20]) + tracer := util.NewTracingInfo(evm, testhelpers.RandomAddress(), types.ArbosAddress, util.TracingDuringEVM) + state, err := arbosState.OpenArbosState(evm.StateDB, burn.NewSystemBurner(tracer, false)) + require.NoError(t, err) + + arbGasInfo := &ArbGasInfo{} + arbOwner := &ArbOwner{} + + callCtx := testContext(caller, evm) + + return evm, state, callCtx, arbGasInfo, arbOwner +} + +func TestFailToSetInvalidConstraints(t *testing.T) { + t.Parallel() + + evm, _, callCtx, _, arbOwner := setupResourceConstraintHandles(t) + + // Zero target + err := arbOwner.SetGasPricingConstraints(callCtx, evm, [][3]uint64{{0, 17, 1000}}) + require.Error(t, err) + + // Zero adjustment window + err = arbOwner.SetGasPricingConstraints(callCtx, evm, [][3]uint64{{10_000_000, 0, 0}}) + require.Error(t, err) +} + +func TestSetLegacyBacklog(t *testing.T) { + t.Parallel() + + evm, _, callCtx, arbGasInfo, arbOwner := setupResourceConstraintHandles(t) + + backlog, err := arbGasInfo.GetGasBacklog(callCtx, evm) + require.NoError(t, err) + require.Equal(t, uint64(0), backlog) + + newBacklog := uint64(80_000) + err = arbOwner.SetGasBacklog(callCtx, evm, newBacklog) + require.NoError(t, err) + + backlog, err = arbGasInfo.GetGasBacklog(callCtx, evm) + require.NoError(t, err) + require.Equal(t, newBacklog, backlog) +} + +func TestConstraintsStorage(t *testing.T) { + t.Parallel() + + evm, state, callCtx, arbGasInfo, arbOwner := setupResourceConstraintHandles(t) + + // Set constraints + constraints := [][3]uint64{ + {30_000_000, 1, 800_000}, // short-term + {15_000_000, 102, 1_600_000}, // long-term + } + err := arbOwner.SetGasPricingConstraints(callCtx, evm, constraints) + require.NoError(t, err) + + // Verify constraints are stored correctly + length, err := state.L2PricingState().ConstraintsLength() + require.NoError(t, err) + require.Equal(t, uint64(2), length) + + first := state.L2PricingState().OpenConstraintAt(0) + second := state.L2PricingState().OpenConstraintAt(1) + + firstTarget, err := first.Target() + require.NoError(t, err) + require.Equal(t, uint64(30_000_000), firstTarget) + + firstWindow, err := first.AdjustmentWindow() + require.NoError(t, err) + require.Equal(t, uint64(1), firstWindow) + + firstBacklog, err := first.Backlog() + require.NoError(t, err) + require.Equal(t, uint64(800_000), firstBacklog) + + secondTarget, err := second.Target() + require.NoError(t, err) + require.Equal(t, uint64(15_000_000), secondTarget) + + secondWindow, err := second.AdjustmentWindow() + require.NoError(t, err) + require.Equal(t, uint64(102), secondWindow) + + secondBacklog, err := second.Backlog() + require.NoError(t, err) + require.Equal(t, uint64(1_600_000), secondBacklog) + + // Get constraints and verify + result, err := arbGasInfo.GetGasPricingConstraints(callCtx, evm) + require.NoError(t, err) + require.Equal(t, 2, len(result)) + require.Equal(t, uint64(30_000_000), result[0][0]) + require.Equal(t, uint64(1), result[0][1]) + require.Equal(t, uint64(15_000_000), result[1][0]) + require.Equal(t, uint64(102), result[1][1]) + + // Set new constraints + constraints = [][3]uint64{ + {7_000_000, 12, 50_000_000}, + } + err = arbOwner.SetGasPricingConstraints(callCtx, evm, constraints) + require.NoError(t, err) + + // Verify old constraints are cleared and new constraint is stored correctly + length, err = state.L2PricingState().ConstraintsLength() + require.NoError(t, err) + require.Equal(t, uint64(1), length) + + first = state.L2PricingState().OpenConstraintAt(0) + target, err := first.Target() + require.NoError(t, err) + window, err := first.AdjustmentWindow() + require.NoError(t, err) + require.Equal(t, uint64(7_000_000), target) + require.Equal(t, uint64(12), window) + + result, err = arbGasInfo.GetGasPricingConstraints(callCtx, evm) + require.NoError(t, err) + require.Equal(t, len(result), 1) + require.Equal(t, result[0][0], uint64(7_000_000)) + require.Equal(t, result[0][1], uint64(12)) + require.Equal(t, result[0][2], uint64(50_000_000)) +} + +func TestConstraintsBacklogUpdate(t *testing.T) { + t.Parallel() + + evm, state, callCtx, arbGasInfo, arbOwner := setupResourceConstraintHandles(t) + + // Set constraints + constraints := [][3]uint64{ + {30_000_000, 1, 0}, // short-term + {15_000_000, 86400, 8000}, // long-term + } + err := arbOwner.SetGasPricingConstraints(callCtx, evm, constraints) + require.NoError(t, err) + + err = state.L2PricingState().OpenConstraintAt(0).SetBacklog(5_000_000) + require.NoError(t, err) + err = state.L2PricingState().OpenConstraintAt(1).SetBacklog(10_000_000) + require.NoError(t, err) + + // Verify backlogs are updated correctly + result, err := arbGasInfo.GetGasPricingConstraints(callCtx, evm) + require.NoError(t, err) + require.Equal(t, 2, len(result)) + require.Equal(t, uint64(5_000_000), result[0][2]) + require.Equal(t, uint64(10_000_000), result[1][2]) +} diff --git a/precompiles/precompile.go b/precompiles/precompile.go index 1ad1e486fff..73315a4f150 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -535,6 +535,8 @@ func Precompiles() map[addr]ArbosPrecompile { ArbGasInfo.methodsByName["GetL1PricingUnitsSinceUpdate"].arbosVersion = params.ArbosVersion_20 ArbGasInfo.methodsByName["GetLastL1PricingSurplus"].arbosVersion = params.ArbosVersion_20 ArbGasInfo.methodsByName["GetMaxTxGasLimit"].arbosVersion = params.ArbosVersion_50 + ArbGasInfo.methodsByName["GetMaxBlockGasLimit"].arbosVersion = params.ArbosVersion_50 + ArbGasInfo.methodsByName["GetGasPricingConstraints"].arbosVersion = params.ArbosVersion_50 insert(MakePrecompile(precompilesgen.ArbAggregatorMetaData, &ArbAggregator{Address: types.ArbAggregatorAddress})) insert(MakePrecompile(precompilesgen.ArbStatisticsMetaData, &ArbStatistics{Address: types.ArbStatisticsAddress})) @@ -615,6 +617,8 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwner.methodsByName["ReleaseL1PricerSurplusFunds"].arbosVersion = params.ArbosVersion_10 ArbOwner.methodsByName["SetChainConfig"].arbosVersion = params.ArbosVersion_11 ArbOwner.methodsByName["SetBrotliCompressionLevel"].arbosVersion = params.ArbosVersion_20 + ArbOwner.methodsByName["SetGasPricingConstraints"].arbosVersion = params.ArbosVersion_50 + ArbOwner.methodsByName["SetGasBacklog"].arbosVersion = params.ArbosVersion_50 stylusMethods := []string{ "SetInkPrice", "SetWasmMaxStackDepth", "SetWasmFreePages", "SetWasmPageGas", "SetWasmPageLimit", "SetWasmMinInitGas", "SetWasmInitCostScalar", diff --git a/precompiles/precompile_test.go b/precompiles/precompile_test.go index 400df63e7b9..61398d4ff11 100644 --- a/precompiles/precompile_test.go +++ b/precompiles/precompile_test.go @@ -182,7 +182,7 @@ func TestPrecompilesPerArbosVersion(t *testing.T) { // Each new precompile contract and each method on new or existing precompile // contracts should be counted. expectedNewEntriesPerArbosVersion := map[uint64]int{ - 0: 102, + 0: 99, params.ArbosVersion_5: 3, params.ArbosVersion_10: 2, params.ArbosVersion_11: 4, @@ -191,7 +191,7 @@ func TestPrecompilesPerArbosVersion(t *testing.T) { params.ArbosVersion_31: 1, params.ArbosVersion_40: 3, params.ArbosVersion_41: 10, - params.ArbosVersion_50: 5, + params.ArbosVersion_50: 9, } precompiles := Precompiles() diff --git a/system_tests/multi_constraint_pricer_test.go b/system_tests/multi_constraint_pricer_test.go new file mode 100644 index 00000000000..510ebd673e7 --- /dev/null +++ b/system_tests/multi_constraint_pricer_test.go @@ -0,0 +1,53 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package arbtest + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" + + "github.com/offchainlabs/nitro/solgen/go/precompilesgen" +) + +func TestSetAndGetGasPricingConstraints(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx).DefaultConfig(t, false) + cleanup := builder.Build(t) + defer cleanup() + + auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + callOpts := &bind.CallOpts{Context: ctx} + + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + require.NoError(t, err) + arbGasInfo, err := precompilesgen.NewArbGasInfo(types.ArbGasInfoAddress, builder.L2.Client) + require.NoError(t, err) + + // Set constraints + constraints := [][3]uint64{ + {30_000_000, 102, 800_000}, // short-term + {15_000_000, 600, 1_600_000}, // long-term + } + tx, err := arbOwner.SetGasPricingConstraints(&auth, constraints) + require.NoError(t, err) + require.NotNil(t, tx) + + // Get and check values + constraints, err = arbGasInfo.GetGasPricingConstraints(callOpts) + require.NoError(t, err) + require.Equal(t, 2, len(constraints)) + require.Equal(t, uint64(30_000_000), constraints[0][0]) + require.Equal(t, uint64(102), constraints[0][1]) + require.GreaterOrEqual(t, constraints[0][2], uint64(800_000)) + require.Equal(t, uint64(15_000_000), constraints[1][0]) + require.Equal(t, uint64(600), constraints[1][1]) + require.GreaterOrEqual(t, constraints[1][2], uint64(1_600_000)) +}