diff --git a/arbos/arbosState/arbosstate.go b/arbos/arbosState/arbosstate.go index 8203dbb9a6..162548f1b8 100644 --- a/arbos/arbosState/arbosstate.go +++ b/arbos/arbosState/arbosstate.go @@ -25,6 +25,7 @@ import ( "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbos/blockhash" "github.com/offchainlabs/nitro/arbos/burn" + "github.com/offchainlabs/nitro/arbos/constraints" "github.com/offchainlabs/nitro/arbos/features" "github.com/offchainlabs/nitro/arbos/l1pricing" "github.com/offchainlabs/nitro/arbos/l2pricing" @@ -57,6 +58,7 @@ type ArbosState struct { programs *programs.Programs features *features.Features blockhashes *blockhash.Blockhashes + resourceConstraints *constraints.ResourceConstraintsStorage chainId storage.StorageBackedBigInt chainConfig storage.StorageBackedBytes genesisBlockNum storage.StorageBackedUint64 @@ -79,6 +81,7 @@ func OpenArbosState(stateDB vm.StateDB, burner burn.Burner) (*ArbosState, error) if arbosVersion == 0 { return nil, ErrUninitializedArbOS } + constraintsBytes := backingStorage.OpenStorageBackedBytes(constraintsSubspace) return &ArbosState{ arbosVersion, backingStorage.OpenStorageBackedUint64(uint64(upgradeVersionOffset)), @@ -94,6 +97,7 @@ func OpenArbosState(stateDB vm.StateDB, burner burn.Burner) (*ArbosState, error) programs.Open(arbosVersion, backingStorage.OpenSubStorage(programsSubspace)), features.Open(backingStorage.OpenSubStorage(featuresSubspace)), blockhash.OpenBlockhashes(backingStorage.OpenCachedSubStorage(blockhashesSubspace)), + constraints.Open(&constraintsBytes), backingStorage.OpenStorageBackedBigInt(uint64(chainIdOffset)), backingStorage.OpenStorageBackedBytes(chainConfigSubspace), backingStorage.OpenStorageBackedUint64(uint64(genesisBlockNumOffset)), @@ -180,6 +184,7 @@ var ( programsSubspace SubspaceID = []byte{8} featuresSubspace SubspaceID = []byte{9} nativeTokenOwnerSubspace SubspaceID = []byte{10} + constraintsSubspace SubspaceID = []byte{11} ) var PrecompileMinArbOSVersions = make(map[common.Address]uint64) @@ -507,6 +512,10 @@ func (state *ArbosState) Blockhashes() *blockhash.Blockhashes { return state.blockhashes } +func (state *ArbosState) ResourceConstraints() *constraints.ResourceConstraintsStorage { + return state.resourceConstraints +} + func (state *ArbosState) NetworkFeeAccount() (common.Address, error) { return state.networkFeeAccount.Get() } diff --git a/arbos/constraints/constraints.go b/arbos/constraints/constraints.go index f5cf4660b3..4242e5ffd8 100644 --- a/arbos/constraints/constraints.go +++ b/arbos/constraints/constraints.go @@ -15,8 +15,8 @@ type PeriodSecs uint32 // resourceConstraint defines the max gas target per second for the given period for a single resource. type resourceConstraint struct { - period time.Duration - target uint64 + Period time.Duration `json:"period"` + Target uint64 `json:"target"` } // ResourceConstraints is a set of constraints for all resources. @@ -46,8 +46,8 @@ func (rc ResourceConstraints) SetConstraint( resource multigas.ResourceKind, periodSecs PeriodSecs, targetPerPeriod uint64, ) { rc[resource][periodSecs] = resourceConstraint{ - period: time.Duration(periodSecs) * time.Second, - target: targetPerPeriod / uint64(periodSecs), + Period: time.Duration(periodSecs) * time.Second, + Target: targetPerPeriod / uint64(periodSecs), } } @@ -55,3 +55,23 @@ func (rc ResourceConstraints) SetConstraint( func (rc ResourceConstraints) ClearConstraint(resource multigas.ResourceKind, periodSecs PeriodSecs) { delete(rc[resource], periodSecs) } + +type resourceConstraintDescription struct { + resource multigas.ResourceKind + periodSecs PeriodSecs + targetPerPeriod uint64 +} + +func (rc ResourceConstraints) getConstraints() []resourceConstraintDescription { + constraints := []resourceConstraintDescription{} + for resource := multigas.ResourceKindUnknown + 1; resource < multigas.NumResourceKind; resource++ { + for period, constraint := range rc[resource] { + constraints = append(constraints, resourceConstraintDescription{ + resource: resource, + periodSecs: period, + targetPerPeriod: constraint.Target * uint64(period), + }) + } + } + return constraints +} diff --git a/arbos/constraints/constraints_test.go b/arbos/constraints/constraints_test.go index 27e03ccb8e..c7ab8b76f4 100644 --- a/arbos/constraints/constraints_test.go +++ b/arbos/constraints/constraints_test.go @@ -27,25 +27,25 @@ func TestResourceConstraints(t *testing.T) { if got, want := len(rc[multigas.ResourceKindComputation]), 2; got != want { t.Fatalf("unexpected number of computation constraints: got %v, want %v", got, want) } - if got, want := rc[multigas.ResourceKindComputation][minuteSecs].period, time.Duration(minuteSecs)*time.Second; got != want { + if got, want := rc[multigas.ResourceKindComputation][minuteSecs].Period, time.Duration(minuteSecs)*time.Second; got != want { t.Errorf("unexpected constraint period: got %v, want %v", got, want) } - if got, want := rc[multigas.ResourceKindComputation][minuteSecs].target, uint64(5_000_000); got != want { + if got, want := rc[multigas.ResourceKindComputation][minuteSecs].Target, uint64(5_000_000); got != want { t.Errorf("unexpected constraint target: got %v, want %v", got, want) } - if got, want := rc[multigas.ResourceKindComputation][weekSecs].period, time.Duration(weekSecs)*time.Second; got != want { + if got, want := rc[multigas.ResourceKindComputation][weekSecs].Period, time.Duration(weekSecs)*time.Second; got != want { t.Errorf("unexpected constraint period: got %v, want %v", got, want) } - if got, want := rc[multigas.ResourceKindComputation][weekSecs].target, uint64(3_000_000); got != want { + if got, want := rc[multigas.ResourceKindComputation][weekSecs].Target, uint64(3_000_000); got != want { t.Errorf("unexpected constraint target: got %v, want %v", got, want) } if got, want := len(rc[multigas.ResourceKindHistoryGrowth]), 1; got != want { t.Fatalf("unexpected number of history growth constraints: got %v, want %v", got, want) } - if got, want := rc[multigas.ResourceKindHistoryGrowth][monthSecs].period, time.Duration(monthSecs)*time.Second; got != want { + if got, want := rc[multigas.ResourceKindHistoryGrowth][monthSecs].Period, time.Duration(monthSecs)*time.Second; got != want { t.Errorf("unexpected constraint period: got %v, want %v", got, want) } - if got, want := rc[multigas.ResourceKindHistoryGrowth][monthSecs].target, uint64(1_000_000); got != want { + if got, want := rc[multigas.ResourceKindHistoryGrowth][monthSecs].Target, uint64(1_000_000); got != want { t.Errorf("unexpected constraint target: got %v, want %v", got, want) } if got, want := len(rc[multigas.ResourceKindStorageAccess]), 0; got != want { @@ -60,7 +60,7 @@ func TestResourceConstraints(t *testing.T) { if got, want := len(rc[multigas.ResourceKindHistoryGrowth]), 1; got != want { t.Fatalf("unexpected number of history growth constraints: got %v, want %v", got, want) } - if got, want := rc[multigas.ResourceKindHistoryGrowth][monthSecs].target, uint64(500_000); got != want { + if got, want := rc[multigas.ResourceKindHistoryGrowth][monthSecs].Target, uint64(500_000); got != want { t.Errorf("unexpected constraint target: got %v, want %v", got, want) } diff --git a/arbos/constraints/storage.go b/arbos/constraints/storage.go new file mode 100644 index 0000000000..881faf22f9 --- /dev/null +++ b/arbos/constraints/storage.go @@ -0,0 +1,87 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package constraints + +import ( + "encoding/json" + "fmt" + + "github.com/ethereum/go-ethereum/arbitrum/multigas" +) + +type storageBytes interface { + Get() ([]byte, error) + Set(val []byte) error +} + +// ResourceConstraintsStorage stores the resources constraints in the ArbOS storage as bytes. +// When updating the storage, the code will read the bytes, deserialize them, make the changes, +// serialize the struct, and write it back to storage. +type ResourceConstraintsStorage struct { + bytes storageBytes +} + +// Open returns a struct that manages the storage for the resource constraints. +// This function receives a storageBytes to facilitate unit testing. +func Open(bytes storageBytes) *ResourceConstraintsStorage { + return &ResourceConstraintsStorage{ + bytes: bytes, + } +} + +// SetConstraint adds or updates the given resource constraint. +func (sto *ResourceConstraintsStorage) SetConstraint(resourceId uint8, periodSecs uint32, targetPerPeriod uint64) error { + resource, err := multigas.CheckResourceKind(resourceId) + if err != nil { + return err + } + constraints, err := sto.load() + if err != nil { + return err + } + constraints.SetConstraint(resource, PeriodSecs(periodSecs), targetPerPeriod) + return sto.store(constraints) +} + +// ClearConstraint removes the given resource constraint. +func (sto *ResourceConstraintsStorage) ClearConstraint(resourceId uint8, periodSecs uint32) error { + resource, err := multigas.CheckResourceKind(resourceId) + if err != nil { + return err + } + constraints, err := sto.load() + if err != nil { + return err + } + constraints.ClearConstraint(resource, PeriodSecs(periodSecs)) + return sto.store(constraints) +} + +func (sto *ResourceConstraintsStorage) store(constraints ResourceConstraints) error { + bytes, err := json.Marshal(constraints) + if err != nil { + return fmt.Errorf("failed to marshal resource constraints: %w", err) + } + err = sto.bytes.Set(bytes) + if err != nil { + return fmt.Errorf("failed to set resource constraints: %w", err) + } + return nil +} + +func (sto *ResourceConstraintsStorage) load() (ResourceConstraints, error) { + bytes, err := sto.bytes.Get() + if err != nil { + return nil, fmt.Errorf("failed to get resources constraints: %w", err) + } + constraints := NewResourceConstraints() + if len(bytes) == 0 { + return constraints, nil + } + err = json.Unmarshal(bytes, &constraints) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal resources constraints: %w", err) + } + return constraints, nil +} diff --git a/arbos/constraints/storage_test.go b/arbos/constraints/storage_test.go new file mode 100644 index 0000000000..073c515e75 --- /dev/null +++ b/arbos/constraints/storage_test.go @@ -0,0 +1,169 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package constraints + +import ( + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/arbitrum/multigas" +) + +type storageBytesMock struct { + bytes []byte +} + +func (sto *storageBytesMock) Get() ([]byte, error) { + return sto.bytes, nil +} + +func (sto *storageBytesMock) Set(val []byte) error { + sto.bytes = val + return nil +} + +func TestStorageSetConstraint(t *testing.T) { + mock := &storageBytesMock{bytes: nil} + storage := Open(mock) + if err := storage.SetConstraint(1, 10, 500); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := storage.SetConstraint(1, 20, 800); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := storage.SetConstraint(2, 60, 30); err != nil { + t.Fatalf("unexpected error: %v", err) + } + wantStorage := `{"1":{` + + `"10":{"period":10000000000,"target":50},` + + `"20":{"period":20000000000,"target":40}},"2":{` + + `"60":{"period":60000000000,"target":0}},"3":{},"4":{}}` + if string(mock.bytes) != wantStorage { + t.Errorf("wrong resource constraint storage: got %v, want %v", string(mock.bytes), wantStorage) + } +} + +func TestStorageClearConstraint(t *testing.T) { + initialStorage := `{"1":{` + + `"10":{"period":10000000000,"target":50},` + + `"20":{"period":20000000000,"target":40}},"2":{` + + `"60":{"period":60000000000,"target":0}},"3":{},"4":{}}` + mock := &storageBytesMock{bytes: []byte(initialStorage)} + storage := Open(mock) + if err := storage.ClearConstraint(1, 10); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := storage.ClearConstraint(1, 20); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := storage.ClearConstraint(2, 60); err != nil { + t.Fatalf("unexpected error: %v", err) + } + wantStorage := `{"1":{},"2":{},"3":{},"4":{}}` + if string(mock.bytes) != wantStorage { + t.Errorf("wrong resource constraint storage: got %v, want %v", string(mock.bytes), wantStorage) + } +} + +func TestStorageStore(t *testing.T) { + for _, tc := range []struct { + name string + constraints []resourceConstraintDescription + wantStorage string + }{ + { + name: "EmptyConstraints", + constraints: []resourceConstraintDescription{}, + wantStorage: `{"1":{},"2":{},"3":{},"4":{}}`, + }, + { + name: "OneConstraint", + constraints: []resourceConstraintDescription{ + {multigas.ResourceKindComputation, 100, 33000}, + }, + wantStorage: `{"1":{"100":{"period":100000000000,"target":330}},"2":{},"3":{},"4":{}}`, + }, + { + name: "MultipleConstraints", + constraints: []resourceConstraintDescription{ + {multigas.ResourceKindComputation, 100, 33000}, + {multigas.ResourceKindComputation, 200, 44000}, + {multigas.ResourceKindHistoryGrowth, 300, 55000}, + }, + wantStorage: `{"1":{` + + `"100":{"period":100000000000,"target":330},` + + `"200":{"period":200000000000,"target":220}},"2":{` + + `"300":{"period":300000000000,"target":183}},"3":{},"4":{}}`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + mock := &storageBytesMock{bytes: nil} + constraints := NewResourceConstraints() + for _, constraint := range tc.constraints { + constraints.SetConstraint(constraint.resource, constraint.periodSecs, constraint.targetPerPeriod) + } + err := Open(mock).store(constraints) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if string(mock.bytes) != tc.wantStorage { + t.Errorf("wrong storage: got %v, want %v", string(mock.bytes), tc.wantStorage) + } + }) + } +} + +func TestStorageLoad(t *testing.T) { + for _, tc := range []struct { + name string + storage string + wantConstraints []resourceConstraintDescription + }{ + { + name: "ZeroBytes", + storage: "", + wantConstraints: []resourceConstraintDescription{}, + }, + { + name: "NoResources", + storage: "{}", + wantConstraints: []resourceConstraintDescription{}, + }, + { + name: "EmptyConstraints", + storage: `{"1":{},"2":{},"3":{},"4":{}}`, + wantConstraints: []resourceConstraintDescription{}, + }, + { + name: "OneConstraint", + storage: `{"1":{"100":{"period":100000000000,"target":330}},"2":{},"3":{},"4":{}}`, + wantConstraints: []resourceConstraintDescription{ + {multigas.ResourceKindComputation, 100, 33000}, + }, + }, + { + name: "MultipleConstraints", + storage: `{"1":{` + + `"100":{"period":10000000000,"target":220},` + + `"200":{"period":20000000000,"target":330}},"2":{` + + `"300":{"period":30000000000,"target":440}},"3":{},"4":{}}`, + wantConstraints: []resourceConstraintDescription{ + {multigas.ResourceKindComputation, 100, 22000}, + {multigas.ResourceKindComputation, 200, 66000}, + {multigas.ResourceKindHistoryGrowth, 300, 132000}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + mock := &storageBytesMock{bytes: []byte(tc.storage)} + constraints, err := Open(mock).load() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := constraints.getConstraints(); !reflect.DeepEqual(got, tc.wantConstraints) { + t.Errorf("wrong resource constraints: got %v, want %v", got, tc.wantConstraints) + } + }) + } +} diff --git a/contracts-local/src/precompiles b/contracts-local/src/precompiles index fe4121240c..07114dc9f8 160000 --- a/contracts-local/src/precompiles +++ b/contracts-local/src/precompiles @@ -1 +1 @@ -Subproject commit fe4121240ca1ee2cbf07d67d0e6c38015d94e704 +Subproject commit 07114dc9f8d8eed2734dc18905daa1b9b48197e0 diff --git a/go-ethereum b/go-ethereum index 060ec4165f..c40afa9fef 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit 060ec4165f5fef525ddff1d3facff142532413a2 +Subproject commit c40afa9fefc817182ee193398fcb96091ab84283 diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index 175733d7e6..91e07a9f2d 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -440,3 +440,13 @@ func (con ArbOwner) SetChainConfig(c ctx, evm mech, serializedChainConfig []byte func (con ArbOwner) SetCalldataPriceIncrease(c ctx, _ mech, enable bool) error { return c.State.Features().SetCalldataPriceIncrease(enable) } + +// SetResourceConstraint adds or updates a resource constraint +func (con ArbOwner) SetResourceConstraint(c ctx, evm mech, resource uint8, periodSecs uint32, targetPerPeriod uint64) error { + return c.State.ResourceConstraints().SetConstraint(resource, periodSecs, targetPerPeriod) +} + +// ClearConstraint removes a resource constraint +func (con ArbOwner) ClearConstraint(c ctx, evm mech, resource uint8, periodSecs uint32) error { + return c.State.ResourceConstraints().ClearConstraint(resource, periodSecs) +}