Skip to content
Closed
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
9 changes: 9 additions & 0 deletions arbos/arbosState/arbosstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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)),
Expand All @@ -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)),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
}
Expand Down
28 changes: 24 additions & 4 deletions arbos/constraints/constraints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -46,12 +46,32 @@ 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),
}
}

// ClearConstraint removes the given resource constraint.
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
}
14 changes: 7 additions & 7 deletions arbos/constraints/constraints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}

Expand Down
87 changes: 87 additions & 0 deletions arbos/constraints/storage.go
Original file line number Diff line number Diff line change
@@ -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
}
169 changes: 169 additions & 0 deletions arbos/constraints/storage_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
Loading
Loading