Skip to content
Merged
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
184 changes: 134 additions & 50 deletions op-supervisor/supervisor/backend/depset/depset_test.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,68 @@
package depset

import (
"bytes"
"context"
"encoding/json"
"os"
"path"
"testing"

"github.com/BurntSushi/toml"
"github.com/ethereum-optimism/optimism/op-node/params"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/stretchr/testify/require"
)

func TestDependencySet(t *testing.T) {
d := path.Join(t.TempDir(), "tmp_dep_set.json")
t.Run("JSON serialization", func(t *testing.T) {
testDependencySetSerialization(t, "json",
func(depSet *StaticConfigDependencySet) ([]byte, error) { return json.Marshal(depSet) },
func(data []byte, depSet *StaticConfigDependencySet) error { return json.Unmarshal(data, depSet) },
)
})

t.Run("TOML serialization", func(t *testing.T) {
testDependencySetSerialization(t, "toml",
func(depSet *StaticConfigDependencySet) ([]byte, error) {
var buf bytes.Buffer
encoder := toml.NewEncoder(&buf)
if err := encoder.Encode(depSet); err != nil {
return nil, err
}
return buf.Bytes(), nil
},
func(data []byte, depSet *StaticConfigDependencySet) error {
_, err := toml.Decode(string(data), depSet)
return err
},
)
})

t.Run("invalid TOML", func(t *testing.T) {
bad := []byte(`dependencies = { bad = 1 }`)
var ds StaticConfigDependencySet
_, err := toml.Decode(string(bad), &ds)
require.Error(t, err)
})

t.Run("duplicate index", func(t *testing.T) {
_, err := NewStaticConfigDependencySet(map[eth.ChainID]*StaticConfigDependency{
eth.ChainIDFromUInt64(900): {ChainIndex: 1},
eth.ChainIDFromUInt64(901): {ChainIndex: 1}, // duplicate
})
require.ErrorIs(t, err, errDuplicateChainIndex)
})

}

func testDependencySetSerialization(
t *testing.T,
fileExt string,
marshal func(*StaticConfigDependencySet) ([]byte, error),
unmarshal func([]byte, *StaticConfigDependencySet) error,
) {
d := path.Join(t.TempDir(), "tmp_dep_set."+fileExt)

depSet, err := NewStaticConfigDependencySet(
map[eth.ChainID]*StaticConfigDependency{
Expand All @@ -29,88 +78,123 @@ func TestDependencySet(t *testing.T) {
},
})
require.NoError(t, err)
data, err := json.Marshal(depSet)
require.NoError(t, err)

require.NoError(t, os.WriteFile(d, data, 0644))
t.Run("DefaultExpiryWindow", func(t *testing.T) {
data, err := marshal(depSet)
require.NoError(t, err)

require.NoError(t, os.WriteFile(d, data, 0644))

// For JSON, use the loader. For TOML, unmarshal directly
var result DependencySet
if fileExt == "json" {
loader := &JsonDependencySetLoader{Path: d}
result, err = loader.LoadDependencySet(context.Background())
require.NoError(t, err)
} else {
fileData, err := os.ReadFile(d)
require.NoError(t, err)

var newDepSet StaticConfigDependencySet
err = unmarshal(fileData, &newDepSet)
require.NoError(t, err)
result = &newDepSet
}

chainIDs := result.Chains()
require.ElementsMatch(t, []eth.ChainID{
eth.ChainIDFromUInt64(900),
eth.ChainIDFromUInt64(901),
}, chainIDs)

require.Equal(t, uint64(params.MessageExpiryTimeSecondsInterop), result.MessageExpiryWindow())
testChainCapabilities(t, result)
})

t.Run("CustomExpiryWindow", func(t *testing.T) {
depSet.overrideMessageExpiryWindow = 15

data, err := marshal(depSet)
require.NoError(t, err)
require.NoError(t, os.WriteFile(d, data, 0644))

var result DependencySet
if fileExt == "json" {
loader := &JsonDependencySetLoader{Path: d}
result, err = loader.LoadDependencySet(context.Background())
require.NoError(t, err)
} else {
fileData, err := os.ReadFile(d)
require.NoError(t, err)

var newDepSet StaticConfigDependencySet
err = unmarshal(fileData, &newDepSet)
require.NoError(t, err)
result = &newDepSet
}

require.Equal(t, uint64(15), result.MessageExpiryWindow())
testChainCapabilities(t, result)
})

t.Run("chain index round trip", func(t *testing.T) {
id900 := eth.ChainIDFromUInt64(900)
idx, _ := depSet.ChainIndexFromID(id900)
idBack, _ := depSet.ChainIDFromIndex(idx)
require.Equal(t, id900, idBack)

_, err := depSet.ChainIndexFromID(eth.ChainIDFromUInt64(999))
require.ErrorContains(t, err, "unknown chain")
})

t.Run("HasChain", func(t *testing.T) {
require.True(t, depSet.HasChain(eth.ChainIDFromUInt64(900)))
require.False(t, depSet.HasChain(eth.ChainIDFromUInt64(902)))
})

loader := &JsonDependencySetLoader{Path: d}
result, err := loader.LoadDependencySet(context.Background())
require.NoError(t, err)

chainIDs := result.Chains()
require.Equal(t, []eth.ChainID{
eth.ChainIDFromUInt64(900),
eth.ChainIDFromUInt64(901),
}, chainIDs)
}

func testChainCapabilities(t *testing.T, result DependencySet) {
// Test chain 900
v, err := result.CanExecuteAt(eth.ChainIDFromUInt64(900), 42)
require.NoError(t, err)
require.True(t, v)

v, err = result.CanExecuteAt(eth.ChainIDFromUInt64(900), 41)
require.NoError(t, err)
require.False(t, v)

v, err = result.CanInitiateAt(eth.ChainIDFromUInt64(900), 100)
require.NoError(t, err)
require.True(t, v)

v, err = result.CanInitiateAt(eth.ChainIDFromUInt64(900), 99)
require.NoError(t, err)
require.False(t, v)

// Test chain 901
v, err = result.CanExecuteAt(eth.ChainIDFromUInt64(901), 30)
require.NoError(t, err)
require.True(t, v)

v, err = result.CanExecuteAt(eth.ChainIDFromUInt64(901), 29)
require.NoError(t, err)
require.False(t, v)

v, err = result.CanInitiateAt(eth.ChainIDFromUInt64(901), 20)
require.NoError(t, err)
require.True(t, v)

v, err = result.CanInitiateAt(eth.ChainIDFromUInt64(901), 19)
require.NoError(t, err)
require.False(t, v)

// Test non-existent chain
v, err = result.CanExecuteAt(eth.ChainIDFromUInt64(902), 100000)
require.NoError(t, err)
require.False(t, v, "902 not a dependency")

v, err = result.CanInitiateAt(eth.ChainIDFromUInt64(902), 100000)
require.NoError(t, err)
require.False(t, v, "902 not a dependency")

require.Equal(t, uint64(params.MessageExpiryTimeSecondsInterop), result.MessageExpiryWindow())
}

func TestDependencySetWithMessageExpiryOverride(t *testing.T) {
d := path.Join(t.TempDir(), "tmp_dep_set.json")

depSet, err := NewStaticConfigDependencySet(
map[eth.ChainID]*StaticConfigDependency{
eth.ChainIDFromUInt64(900): {
ChainIndex: 900,
ActivationTime: 42,
HistoryMinTime: 100,
},
eth.ChainIDFromUInt64(901): {
ChainIndex: 901,
ActivationTime: 30,
HistoryMinTime: 20,
},
})
require.NoError(t, err)
depSet.overrideMessageExpiryWindow = 10
data, err := json.Marshal(depSet)
require.NoError(t, err)

require.NoError(t, os.WriteFile(d, data, 0644))

loader := &JsonDependencySetLoader{Path: d}
result, err := loader.LoadDependencySet(context.Background())
require.NoError(t, err)

chainIDs := result.Chains()
require.Equal(t, []eth.ChainID{
eth.ChainIDFromUInt64(900),
eth.ChainIDFromUInt64(901),
}, chainIDs)
require.Equal(t, uint64(10), result.MessageExpiryWindow())
}
87 changes: 75 additions & 12 deletions op-supervisor/supervisor/backend/depset/static.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package depset

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"slices"
"sort"

"github.com/BurntSushi/toml"
"github.com/ethereum-optimism/optimism/op-node/params"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
Expand All @@ -17,16 +19,16 @@ var errDuplicateChainIndex = errors.New("duplicate chain index")

type StaticConfigDependency struct {
// ChainIndex is the unique short identifier of this chain.
ChainIndex types.ChainIndex `json:"chainIndex"`
ChainIndex types.ChainIndex `json:"chainIndex" toml:"chain_index"`

// ActivationTime is when the chain becomes part of the dependency set.
// This is the minimum timestamp of the inclusion of an executing message.
ActivationTime uint64 `json:"activationTime"`
ActivationTime uint64 `json:"activationTime" toml:"activation_time"`

// HistoryMinTime is what the lower bound of data is to store.
// This is the minimum timestamp of an initiating message to be accessible to others.
// This is set to 0 when all data since genesis is executable.
HistoryMinTime uint64 `json:"historyMinTime"`
HistoryMinTime uint64 `json:"historyMinTime" toml:"history_min_time"`
}

// StaticConfigDependencySet statically declares a DependencySet.
Expand Down Expand Up @@ -60,32 +62,93 @@ func NewStaticConfigDependencySetWithMessageExpiryOverride(dependencies map[eth.
return out, nil
}

// jsonStaticConfigDependencySet is a util for JSON encoding/decoding,
// to encode/decode just the attributes that matter,
// minStaticConfigDependencySet is a util for JSON/TOML encoding/decoding,
// for just the minimal set of attributes that matter,
// while wrapping the decoding functionality with additional hydration step.
type jsonStaticConfigDependencySet struct {
Dependencies map[eth.ChainID]*StaticConfigDependency `json:"dependencies"`
OverrideMessageExpiryWindow uint64 `json:"overrideMessageExpiryWindow,omitempty"`
type minStaticConfigDependencySet struct {
Dependencies map[string]*StaticConfigDependency `json:"dependencies" toml:"dependencies"`
OverrideMessageExpiryWindow uint64 `json:"overrideMessageExpiryWindow,omitempty" toml:"override_message_expiry_window,omitempty"`
}

func (ds *StaticConfigDependencySet) MarshalJSON() ([]byte, error) {
out := &jsonStaticConfigDependencySet{
Dependencies: ds.dependencies,
// Convert map keys to strings
stringMap := make(map[string]*StaticConfigDependency)
for id, dep := range ds.dependencies {
stringMap[id.String()] = dep
}

out := &minStaticConfigDependencySet{
Dependencies: stringMap,
OverrideMessageExpiryWindow: ds.overrideMessageExpiryWindow,
}
return json.Marshal(out)
}

func (ds *StaticConfigDependencySet) UnmarshalJSON(data []byte) error {
var v jsonStaticConfigDependencySet
var v minStaticConfigDependencySet
if err := json.Unmarshal(data, &v); err != nil {
return err
}
ds.dependencies = v.Dependencies

// Convert string keys back to ChainID
ds.dependencies = make(map[eth.ChainID]*StaticConfigDependency)
for idStr, dep := range v.Dependencies {
id, err := eth.ParseDecimalChainID(idStr)
if err != nil {
return fmt.Errorf("invalid chain ID in JSON: %w", err)
}
ds.dependencies[id] = dep
}

ds.overrideMessageExpiryWindow = v.OverrideMessageExpiryWindow
return ds.hydrate()
}

func (ds *StaticConfigDependencySet) MarshalTOML() ([]byte, error) {
// Convert map keys (ChainID) to strings so TOML can encode the map.
stringMap := make(map[string]*StaticConfigDependency, len(ds.dependencies))
for id, dep := range ds.dependencies {
stringMap[id.String()] = dep
}

payload := &minStaticConfigDependencySet{
Dependencies: stringMap,
OverrideMessageExpiryWindow: ds.overrideMessageExpiryWindow,
}

var buf bytes.Buffer
if err := toml.NewEncoder(&buf).Encode(payload); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

func (ds *StaticConfigDependencySet) UnmarshalTOML(v interface{}) error {
var buf bytes.Buffer
if err := toml.NewEncoder(&buf).Encode(v); err != nil {
return fmt.Errorf("re-encoding TOML: %w", err)
}

// Decode into the minimal helper struct that has the right tags.
var tmp minStaticConfigDependencySet
if _, err := toml.Decode(buf.String(), &tmp); err != nil {
return fmt.Errorf("decoding into helper struct: %w", err)
}

// Convert string keys back to ChainID and copy the data.
ds.dependencies = make(map[eth.ChainID]*StaticConfigDependency, len(tmp.Dependencies))
for idStr, dep := range tmp.Dependencies {
id, err := eth.ParseDecimalChainID(idStr)
if err != nil {
return fmt.Errorf("invalid chain ID %q: %w", idStr, err)
}
ds.dependencies[id] = dep
}

ds.overrideMessageExpiryWindow = tmp.OverrideMessageExpiryWindow
return ds.hydrate()
}

// hydrate sets all the cached values, based on the dependencies attribute
func (ds *StaticConfigDependencySet) hydrate() error {
ds.indexToID = make(map[types.ChainIndex]eth.ChainID)
Expand Down