diff --git a/arbos/l2pricing/l2pricing.go b/arbos/l2pricing/l2pricing.go index 8de1735047..83bfdd14e1 100644 --- a/arbos/l2pricing/l2pricing.go +++ b/arbos/l2pricing/l2pricing.go @@ -4,11 +4,46 @@ package l2pricing import ( + "fmt" "math/big" "github.com/offchainlabs/nitro/arbos/storage" ) +const ( + gasConstraintTargetOffset uint64 = iota + gasConstraintPeriodOffset + gasConstraintBacklogOffset +) + +// GasConstraint tries to keep the gas backlog under the target (per second) for the given period. +type GasConstraint struct { + target storage.StorageBackedUint64 + period storage.StorageBackedUint64 + backlog storage.StorageBackedUint64 +} + +func OpenGasConstraint(storage *storage.Storage) *GasConstraint { + return &GasConstraint{ + target: storage.OpenStorageBackedUint64(gasConstraintTargetOffset), + period: storage.OpenStorageBackedUint64(gasConstraintPeriodOffset), + backlog: storage.OpenStorageBackedUint64(gasConstraintBacklogOffset), + } +} + +func (c *GasConstraint) Clear() error { + if err := c.target.Clear(); err != nil { + return err + } + if err := c.period.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 @@ -19,6 +54,7 @@ type L2PricingState struct { pricingInertia storage.StorageBackedUint64 backlogTolerance storage.StorageBackedUint64 perTxGasLimit storage.StorageBackedUint64 + constraints *storage.SubStorageVector } const ( @@ -32,6 +68,8 @@ const ( perTxGasLimitOffset ) +var constraintsKey []byte = []byte{0} + const GethBlockGasLimit = 1 << 50 func InitializeL2PricingState(sto *storage.Storage) error { @@ -55,6 +93,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)), } } @@ -128,3 +167,47 @@ func (ps *L2PricingState) SetBacklogTolerance(val uint64) error { func (ps *L2PricingState) Restrict(err error) { ps.storage.Burner().Restrict(err) } + +func (ps *L2PricingState) AddConstraint(target uint64, period uint64, backlog 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.period.Set(period); err != nil { + return fmt.Errorf("failed to set period: %w", err) + } + if err := constraint.backlog.Set(backlog); err != nil { + return fmt.Errorf("failed to set backlog: %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 +} diff --git a/arbos/l2pricing/l2pricing_test.go b/arbos/l2pricing/l2pricing_test.go index 0f32243851..acd40bf30f 100644 --- a/arbos/l2pricing/l2pricing_test.go +++ b/arbos/l2pricing/l2pricing_test.go @@ -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, 100*i+3)) + } + 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.period.Get() + Require(t, err) + if want := 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 := 100*i + 3; 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...) diff --git a/arbos/storage/vector.go b/arbos/storage/vector.go new file mode 100644 index 0000000000..01f7ec1877 --- /dev/null +++ b/arbos/storage/vector.go @@ -0,0 +1,74 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package storage + +import ( + "encoding/binary" + "errors" +) + +const subStorageVectorLengthOffset uint64 = 0 + +// SubStorageVector is a storage space that contains a vector of sub-storages. +// It keeps track of the number of sub-storages and It is possible to push and pop them. +type SubStorageVector struct { + storage *Storage + length StorageBackedUint64 +} + +// OpenSubStorageVector creates a SubStorageVector in given the root storage. +func OpenSubStorageVector(sto *Storage) *SubStorageVector { + return &SubStorageVector{ + sto.WithoutCache(), + sto.OpenStorageBackedUint64(subStorageVectorLengthOffset), + } +} + +// Length returns the number of sub-storages. +func (v *SubStorageVector) Length() (uint64, error) { + length, err := v.length.Get() + if err != nil { + return 0, err + } + return length, err +} + +// Push adds a new sub-storage at the end of the vector and return it. +func (v *SubStorageVector) Push() (*Storage, error) { + length, err := v.length.Get() + if err != nil { + return nil, err + } + id := binary.BigEndian.AppendUint64(nil, length) + subStorage := v.storage.OpenSubStorage(id) + if err := v.length.Set(length + 1); err != nil { + return nil, err + } + return subStorage, nil +} + +// Pop removes the last sub-storage from the end of the vector and return it. +func (v *SubStorageVector) Pop() (*Storage, error) { + length, err := v.length.Get() + if err != nil { + return nil, err + } + if length == 0 { + return nil, errors.New("sub-storage vector: can't pop empty") + } + id := binary.BigEndian.AppendUint64(nil, length-1) + subStorage := v.storage.OpenSubStorage(id) + if err := v.length.Set(length - 1); err != nil { + return nil, err + } + return subStorage, nil +} + +// At returns the substorage at the given index. +// NOTE: This function does not verify out-of-bounds. +func (v *SubStorageVector) At(i uint64) *Storage { + id := binary.BigEndian.AppendUint64(nil, i) + subStorage := v.storage.OpenSubStorage(id) + return subStorage +} diff --git a/arbos/vector_test.go b/arbos/vector_test.go new file mode 100644 index 0000000000..0613411777 --- /dev/null +++ b/arbos/vector_test.go @@ -0,0 +1,81 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package arbos + +import ( + "testing" + + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/storage" + "github.com/offchainlabs/nitro/arbos/util" +) + +// This file contains tests for the storage submodule, but it needs to be in the top-level module to +// avoid cross dependencies. + +func TestSubStorageVector(t *testing.T) { + state, statedb := arbosState.NewArbosMemoryBackedArbOSState() + sto := state.BackingStorage().OpenCachedSubStorage([]byte{}) + vec := storage.OpenSubStorageVector(sto) + + stateBefore := statedb.IntermediateRoot(false) + + getLength := func() uint64 { + length, err := vec.Length() + Require(t, err) + return length + } + + // Check it starts empty + if got, want := getLength(), uint64(0); got != want { + t.Fatalf("wrong vector length: got %v, want %v", got, want) + } + + // Adds n elements + const n = uint64(100) + for i := range n { + subStorage, err := vec.Push() + Require(t, err) + err = subStorage.SetByUint64(0, util.UintToHash(i)) + Require(t, err) + } + + // Check the length with n elements + if got, want := getLength(), uint64(100); got != want { + t.Fatalf("wrong vector length: got %v, want %v", got, want) + } + + // Check each element + for i := range n { + subStorage := vec.At(i) + got, err := subStorage.GetByUint64(0) + Require(t, err) + want := util.UintToHash(i) + if got != want { + t.Errorf("wrong sub-storage: got %v, want %v", got, want) + } + } + + // Pop each element and clear storage + for i := range n { + subStorage, err := vec.Pop() + Require(t, err) + got, err := subStorage.GetByUint64(0) + Require(t, err) + want := util.UintToHash(n - i - 1) + if got != want { + t.Errorf("wrong sub-storage: got %v, want %v", got, want) + } + err = subStorage.ClearByUint64(0) + Require(t, err) + } + + // Check it ends empty + if got, want := getLength(), uint64(0); got != want { + t.Fatalf("wrong vector length: got %v, want %v", got, want) + } + if stateBefore != statedb.IntermediateRoot(false) { + Fail(t, "state is not clear") + } +}