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
5 changes: 5 additions & 0 deletions op-e2e/actions/interop/dsl/dsl.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/rollup/event"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/backend/depset"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -97,6 +98,10 @@ func NewInteropDSL(t helpers.Testing) *InteropDSL {
}
}

func (d *InteropDSL) DepSet() *depset.StaticConfigDependencySet {
return d.setup.DepSet
}

func (d *InteropDSL) defaultChainOpts() ChainOpts {
return ChainOpts{
// Defensive copy to make sure the original slice isn't modified
Expand Down
2 changes: 1 addition & 1 deletion op-e2e/actions/interop/dsl/interop.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func worldToDepSet(t helpers.Testing, worldOutput *interopgen.WorldOutput) *deps
HistoryMinTime: 0,
}
}
depSet, err := depset.NewStaticConfigDependencySet(depSetCfg)
depSet, err := depset.NewStaticConfigDependencySetWithMessageExpiryOverride(depSetCfg, messageExpiryTime)
require.NoError(t, err)
return depSet
}
Expand Down
29 changes: 5 additions & 24 deletions op-e2e/actions/interop/proofs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/backend/depset"
supervisortypes "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -533,9 +532,6 @@ func TestInteropFaultProofs_CascadeInvalidBlock(gt *testing.T) {

func TestInteropFaultProofs_MessageExpiry(gt *testing.T) {
t := helpers.NewDefaultTesting(gt)
// TODO(#14234): Check message expiry in op-supervisor
t.Skip("Message expiry not yet implemented")

system := dsl.NewInteropDSL(t)

actors := system.Actors
Expand Down Expand Up @@ -772,7 +768,7 @@ func runFppAndChallengerTests(gt *testing.T, system *dsl.InteropDSL, tests []*tr
for _, test := range tests {
test := test
gt.Run(fmt.Sprintf("%s-fpp", test.name), func(gt *testing.T) {
runFppTest(gt, test, system.Actors)
runFppTest(gt, test, system.Actors, system.DepSet())
})

gt.Run(fmt.Sprintf("%s-challenger", test.name), func(gt *testing.T) {
Expand All @@ -781,7 +777,7 @@ func runFppAndChallengerTests(gt *testing.T, system *dsl.InteropDSL, tests []*tr
}
}

func runFppTest(gt *testing.T, test *transitionTest, actors *dsl.InteropActors) {
func runFppTest(gt *testing.T, test *transitionTest, actors *dsl.InteropActors, depSet *depset.StaticConfigDependencySet) {
t := helpers.NewDefaultTesting(gt)
if test.skipProgram {
t.Skip("Not yet implemented")
Expand All @@ -805,7 +801,7 @@ func runFppTest(gt *testing.T, test *transitionTest, actors *dsl.InteropActors)
logger,
actors.L1Miner,
checkResult,
WithInteropEnabled(t, actors, test.agreedClaim, crypto.Keccak256Hash(test.disputedClaim), proposalTimestamp),
WithInteropEnabled(t, actors, depSet, test.agreedClaim, crypto.Keccak256Hash(test.disputedClaim), proposalTimestamp),
fpHelpers.WithL1Head(l1Head),
)
}
Expand Down Expand Up @@ -853,29 +849,14 @@ func runChallengerTest(gt *testing.T, test *transitionTest, actors *dsl.InteropA
}
}

func WithInteropEnabled(t helpers.StatefulTesting, actors *dsl.InteropActors, agreedPrestate []byte, disputedClaim common.Hash, claimTimestamp uint64) fpHelpers.FixtureInputParam {
func WithInteropEnabled(t helpers.StatefulTesting, actors *dsl.InteropActors, depSet *depset.StaticConfigDependencySet, agreedPrestate []byte, disputedClaim common.Hash, claimTimestamp uint64) fpHelpers.FixtureInputParam {
return func(f *fpHelpers.FixtureInputs) {
f.InteropEnabled = true
f.AgreedPrestate = agreedPrestate
f.L2OutputRoot = crypto.Keccak256Hash(agreedPrestate)
f.L2Claim = disputedClaim
f.L2BlockNumber = claimTimestamp

deps := map[eth.ChainID]*depset.StaticConfigDependency{
actors.ChainA.ChainID: {
ChainIndex: supervisortypes.ChainIndex(0),
ActivationTime: 0,
HistoryMinTime: 0,
},
actors.ChainB.ChainID: {
ChainIndex: supervisortypes.ChainIndex(1),
ActivationTime: 0,
HistoryMinTime: 0,
},
}
var err error
f.DependencySet, err = depset.NewStaticConfigDependencySet(deps)
require.NoError(t, err)
f.DependencySet = depSet

for _, chain := range []*dsl.Chain{actors.ChainA, actors.ChainB} {
f.L2Sources = append(f.L2Sources, &fpHelpers.FaultProofProgramL2Source{
Expand Down
6 changes: 3 additions & 3 deletions op-e2e/interop/interop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,15 +215,15 @@ func TestInterop_EmitLogs(t *testing.T) {
// all logs should be cross-safe
for _, log := range logsA {
identifier, expectedHash := logToIdentifier(chainA, log)
safety, err := supervisor.CheckMessage(context.Background(), identifier, expectedHash)
safety, err := supervisor.CheckMessage(context.Background(), identifier, expectedHash, types.ExecutingDescriptor{Timestamp: identifier.Timestamp})
require.NoError(t, err)
// the supervisor could progress the safety level more quickly than we expect,
// which is why we check for a minimum safety level
require.True(t, safety.AtLeastAsSafe(types.CrossSafe), "log: %v should be at least Cross-Safe, but is %s", log, safety.String())
}
for _, log := range logsB {
identifier, expectedHash := logToIdentifier(chainB, log)
safety, err := supervisor.CheckMessage(context.Background(), identifier, expectedHash)
safety, err := supervisor.CheckMessage(context.Background(), identifier, expectedHash, types.ExecutingDescriptor{Timestamp: identifier.Timestamp})
require.NoError(t, err)
// the supervisor could progress the safety level more quickly than we expect,
// which is why we check for a minimum safety level
Expand All @@ -234,7 +234,7 @@ func TestInterop_EmitLogs(t *testing.T) {
identifier, expectedHash := logToIdentifier(chainA, logsA[0])
// make the timestamp incorrect
identifier.Timestamp = 333
safety, err := supervisor.CheckMessage(context.Background(), identifier, expectedHash)
safety, err := supervisor.CheckMessage(context.Background(), identifier, expectedHash, types.ExecutingDescriptor{Timestamp: 333})
require.NoError(t, err)
require.Equal(t, types.Invalid, safety)

Expand Down
21 changes: 2 additions & 19 deletions op-program/client/interop/consolidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"errors"
"fmt"

"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-program/client/boot"
"github.com/ethereum-optimism/optimism/op-program/client/interop/types"
"github.com/ethereum-optimism/optimism/op-program/client/l1"
Expand Down Expand Up @@ -102,11 +101,7 @@ func RunConsolidation(
Number: optimisticBlock.NumberU64(),
Timestamp: optimisticBlock.Time(),
}
rollupCfg, err := bootInfo.Configs.RollupConfig(chain.ChainID)
if err != nil {
return eth.Bytes32{}, fmt.Errorf("no rollup config available for chain ID %v: %w", chain.ChainID, err)
}
if err := checkHazards(rollupCfg, deps, candidate, chain.ChainID, execMsgs); err != nil {
if err := checkHazards(deps, candidate, chain.ChainID, execMsgs); err != nil {
if !isInvalidMessageError(err) {
return eth.Bytes32{}, err
}
Expand Down Expand Up @@ -159,27 +154,15 @@ func isInvalidMessageError(err error) bool {
type ConsolidateCheckDeps interface {
cross.UnsafeFrontierCheckDeps
cross.CycleCheckDeps
Contains(chain eth.ChainID, query supervisortypes.ContainsQuery) (includedIn supervisortypes.BlockSeal, err error)
cross.UnsafeStartDeps
}

func checkHazards(
rollupCfg *rollup.Config,
deps ConsolidateCheckDeps,
candidate supervisortypes.BlockSeal,
chainID eth.ChainID,
execMsgs []*supervisortypes.ExecutingMessage,
) error {
// TODO(#14234): remove this check once the supervisor is updated handle msg expiry
messageExpiryTimeSeconds := rollupCfg.GetMessageExpiryTimeInterop()
for _, msg := range execMsgs {
if msg.Timestamp+messageExpiryTimeSeconds < candidate.Timestamp {
return fmt.Errorf(
"message timestamp is too old: %d < %d: %w",
msg.Timestamp+messageExpiryTimeSeconds, candidate.Timestamp, supervisortypes.ErrConflict,
)
}
}

hazards, err := cross.CrossUnsafeHazards(deps, chainID, candidate, execMsgs)
if err != nil {
return err
Expand Down
8 changes: 5 additions & 3 deletions op-service/sources/supervisor_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,22 @@ func (cl *SupervisorClient) AddL2RPC(ctx context.Context, rpc string, auth eth.B
return result
}

func (cl *SupervisorClient) CheckMessage(ctx context.Context, identifier types.Identifier, logHash common.Hash) (types.SafetyLevel, error) {
func (cl *SupervisorClient) CheckMessage(ctx context.Context, identifier types.Identifier, logHash common.Hash, executingDescriptor types.ExecutingDescriptor) (types.SafetyLevel, error) {
var result types.SafetyLevel
err := cl.client.CallContext(
ctx,
&result,
"supervisor_checkMessage",
identifier,
logHash)
logHash,
executingDescriptor)
if err != nil {
return types.Invalid, fmt.Errorf("failed to check message (chain %s), (block %v), (index %v), (logHash %s): %w",
return types.Invalid, fmt.Errorf("failed to check message (chain %s), (block %v), (index %v), (logHash %s), (executingTimestamp %v): %w",
identifier.ChainID,
identifier.BlockNumber,
identifier.LogIndex,
logHash,
executingDescriptor.Timestamp,
err)
}
return result, nil
Expand Down
42 changes: 40 additions & 2 deletions op-supervisor/supervisor/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ func (su *SupervisorBackend) DependencySet() depset.DependencySet {
// Query methods
// ----------------------------

func (su *SupervisorBackend) CheckMessage(identifier types.Identifier, payloadHash common.Hash) (types.SafetyLevel, error) {
func (su *SupervisorBackend) CheckMessage(identifier types.Identifier, payloadHash common.Hash, executingDescriptor types.ExecutingDescriptor) (types.SafetyLevel, error) {
logHash := types.PayloadHashToLogHash(payloadHash, identifier.Origin)
chainID := identifier.ChainID
blockNum := identifier.BlockNumber
Expand All @@ -446,9 +446,45 @@ func (su *SupervisorBackend) CheckMessage(identifier types.Identifier, payloadHa
if err != nil {
return types.Invalid, fmt.Errorf("failed to check log: %w", err)
}
if identifier.Timestamp+su.depSet.MessageExpiryWindow() < executingDescriptor.Timestamp {
su.logger.Debug("Message expired", "identifier", identifier, "payloadHash", payloadHash, "executingTimestamp", executingDescriptor.Timestamp)
return types.Invalid, nil
}
if identifier.Timestamp > executingDescriptor.Timestamp {
su.logger.Debug("Message timestamp is in the future", "identifier", identifier, "payloadHash", payloadHash, "executingTimestamp", executingDescriptor.Timestamp)
return types.Invalid, nil
}
return su.chainDBs.Safest(chainID, blockNum, logIdx)
}

func (su *SupervisorBackend) CheckMessagesV2(
messages []types.Message,
minSafety types.SafetyLevel,
executingDescriptor types.ExecutingDescriptor) error {
su.logger.Debug("Checking messages", "count", len(messages), "minSafety", minSafety, "executingTimestamp", executingDescriptor.Timestamp)

for _, msg := range messages {
su.logger.Debug("Checking message",
"identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String(), "executingTimestamp", executingDescriptor.Timestamp)
safety, err := su.CheckMessage(msg.Identifier, msg.PayloadHash, executingDescriptor)
if err != nil {
su.logger.Error("Check message failed", "err", err,
"identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String(), "executingTimestamp", executingDescriptor.Timestamp)
return fmt.Errorf("failed to check message: %w", err)
}
if !safety.AtLeastAsSafe(minSafety) {
su.logger.Error("Message is not sufficiently safe",
"safety", safety, "minSafety", minSafety,
"identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String(), "executingTimestamp", executingDescriptor.Timestamp)
return fmt.Errorf("message %v (safety level: %v) does not meet the minimum safety %v",
msg.Identifier,
safety,
minSafety)
}
}
return nil
}

func (su *SupervisorBackend) CheckMessages(
messages []types.Message,
minSafety types.SafetyLevel) error {
Expand All @@ -457,7 +493,9 @@ func (su *SupervisorBackend) CheckMessages(
for _, msg := range messages {
su.logger.Debug("Checking message",
"identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String())
safety, err := su.CheckMessage(msg.Identifier, msg.PayloadHash)
// Guarantee message expiry checks do not fail by setting the executing timestamp to the message timestamp
// This is intentionally done to avoid breaking checkMessagesV1 which doesn't handle message expiry checks
safety, err := su.CheckMessage(msg.Identifier, msg.PayloadHash, types.ExecutingDescriptor{Timestamp: msg.Identifier.Timestamp})
if err != nil {
su.logger.Error("Check message failed", "err", err,
"identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String())
Expand Down
14 changes: 11 additions & 3 deletions op-supervisor/supervisor/backend/cross/safe_frontier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,10 @@ func (m *mockSafeFrontierCheckDeps) DependencySet() depset.DependencySet {
}

type mockDependencySet struct {
chainIDFromIndexfn func() (eth.ChainID, error)
canExecuteAtfn func() (bool, error)
canInitiateAtfn func() (bool, error)
chainIDFromIndexfn func() (eth.ChainID, error)
canExecuteAtfn func() (bool, error)
canInitiateAtfn func() (bool, error)
messageExpiryWindow uint64
}

func (m mockDependencySet) CanExecuteAt(chain eth.ChainID, timestamp uint64) (bool, error) {
Expand Down Expand Up @@ -209,3 +210,10 @@ func (m mockDependencySet) Chains() []eth.ChainID {
func (m mockDependencySet) HasChain(chain eth.ChainID) bool {
return true
}

func (m mockDependencySet) MessageExpiryWindow() uint64 {
if m.messageExpiryWindow == 0 {
return 100
}
return m.messageExpiryWindow
}
4 changes: 4 additions & 0 deletions op-supervisor/supervisor/backend/cross/safe_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ func CrossSafeHazards(d SafeStartDeps, chainID eth.ChainID, inL1Source eth.Block
return nil, fmt.Errorf("msg %s was included in block %s derived from %s which is not in cross-safe scope %s: %w",
msg, includedIn, initSource, inL1Source, types.ErrOutOfScope)
}
// Run expiry window invariant check *after* verifying that the message is non-conflicting.
if msg.Timestamp+depSet.MessageExpiryWindow() < candidate.Timestamp {
return nil, fmt.Errorf("timestamp of message %s (chain %s) has expired: %d < %d: %w", msg, chainID, msg.Timestamp+depSet.MessageExpiryWindow(), candidate.Timestamp, types.ErrConflict)
}
} else if msg.Timestamp == candidate.Timestamp {
// If timestamp is equal: we have to inspect ordering of individual
// log events to ensure non-cyclic cross-chain message ordering.
Expand Down
31 changes: 31 additions & 0 deletions op-supervisor/supervisor/backend/cross/safe_start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,37 @@ func TestCrossSafeHazards(t *testing.T) {
require.NoError(t, err)
require.Empty(t, hazards)
})
t.Run("message expiry", func(t *testing.T) {
ssd := &mockSafeStartDeps{}
ssd.deps.messageExpiryWindow = 10
chainID := eth.ChainIDFromUInt64(0)
inL1Source := eth.BlockID{Number: 1}
candidate := types.BlockSeal{Timestamp: 12}
em1 := &types.ExecutingMessage{Chain: types.ChainIndex(0), Timestamp: 1}
execMsgs := []*types.ExecutingMessage{em1}
// when there is one execMsg, and the timestamp is less than the candidate,
// and DerivedToSource returns a BlockSeal with a equal to the Number of inL1Source,
// no error is returned
hazards, err := CrossSafeHazards(ssd, chainID, inL1Source, candidate, execMsgs)
require.ErrorIs(t, err, types.ErrConflict)
require.ErrorContains(t, err, "has expired")
require.Empty(t, hazards)
})
t.Run("message close to expiry", func(t *testing.T) {
ssd := &mockSafeStartDeps{}
ssd.deps.messageExpiryWindow = 10
chainID := eth.ChainIDFromUInt64(0)
inL1Source := eth.BlockID{Number: 1}
candidate := types.BlockSeal{Timestamp: 11}
em1 := &types.ExecutingMessage{Chain: types.ChainIndex(0), Timestamp: 1}
execMsgs := []*types.ExecutingMessage{em1}
// when there is one execMsg, and the timestamp is less than the candidate,
// and DerivedToSource returns a BlockSeal with a equal to the Number of inL1Source,
// no error is returned
hazards, err := CrossSafeHazards(ssd, chainID, inL1Source, candidate, execMsgs)
require.NoError(t, err)
require.Empty(t, hazards)
})
}

type mockSafeStartDeps struct {
Expand Down
Loading