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
1 change: 0 additions & 1 deletion devnet-sdk/system/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ type InteropSet interface {

// Supervisor provides access to the query interface of the supervisor
type Supervisor interface {
CheckMessage(context.Context, supervisorTypes.Identifier, common.Hash, supervisorTypes.ExecutingDescriptor) (supervisorTypes.SafetyLevel, error)
LocalUnsafe(context.Context, eth.ChainID) (eth.BlockID, error)
CrossSafe(context.Context, eth.ChainID) (supervisorTypes.DerivedIDPair, error)
Finalized(context.Context, eth.ChainID) (eth.BlockID, error)
Expand Down
4 changes: 0 additions & 4 deletions devnet-sdk/testing/systest/testing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,6 @@ func (m *mockInteropSet) L2s() []system.Chain { return []system.Chain{&mockChain
// mockSupervisor implements the system.Supervisor interface for testing
type mockSupervisor struct{}

func (m *mockSupervisor) CheckMessage(ctx context.Context, id supervisorTypes.Identifier, hash common.Hash, desc supervisorTypes.ExecutingDescriptor) (supervisorTypes.SafetyLevel, error) {
return supervisorTypes.Invalid, nil
}

func (m *mockSupervisor) LocalUnsafe(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error) {
return eth.BlockID{}, nil
}
Expand Down
57 changes: 27 additions & 30 deletions op-e2e/interop/interop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ func TestInterop_EmitLogs(t *testing.T) {

supervisor := s2.SupervisorClient()

// helper function to turn a log into an identifier and the expected hash of the payload
logToIdentifier := func(chainID string, log gethTypes.Log) (types.Identifier, common.Hash) {
// helper function to turn a log into an access-list object
logToAccess := func(chainID string, log gethTypes.Log) types.Access {
client := s2.L2GethClient(chainID, "sequencer")
// construct the expected hash of the log's payload
// (topics concatenated with data)
Expand All @@ -207,51 +207,44 @@ func TestInterop_EmitLogs(t *testing.T) {
msgPayload = append(msgPayload, topic.Bytes()...)
}
msgPayload = append(msgPayload, log.Data...)
expectedHash := common.BytesToHash(crypto.Keccak256(msgPayload))
msgHash := crypto.Keccak256Hash(msgPayload)

// get block for the log (for timestamp)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
block, err := client.BlockByHash(ctx, log.BlockHash)
require.NoError(t, err)

// make an identifier out of the sample log
identifier := types.Identifier{
Origin: log.Address,
args := types.ChecksumArgs{
BlockNumber: log.BlockNumber,
LogIndex: uint32(log.Index),
Timestamp: block.Time(),
LogIndex: uint32(log.Index),
ChainID: eth.ChainIDFromBig(s2.ChainID(chainID)),
LogHash: types.PayloadHashToLogHash(msgHash, log.Address),
}
return identifier, expectedHash
return args.Access()
}

// all logs should be cross-safe
for _, log := range logsA {
identifier, expectedHash := logToIdentifier(chainA, log)
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())
var accessEntries []types.Access
for _, evLog := range logsA {
accessEntries = append(accessEntries, logToAccess(chainA, evLog))
}
for _, log := range logsB {
identifier, expectedHash := logToIdentifier(chainB, log)
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 _, evLog := range logsB {
accessEntries = append(accessEntries, logToAccess(chainB, evLog))
}
accessList := types.EncodeAccessList(accessEntries)

// a log should be invalid if the timestamp is incorrect
identifier, expectedHash := logToIdentifier(chainA, logsA[0])
// make the timestamp incorrect
identifier.Timestamp = 333
safety, err := supervisor.CheckMessage(context.Background(), identifier, expectedHash, types.ExecutingDescriptor{Timestamp: 333})
require.NoError(t, err)
require.Equal(t, types.Invalid, safety)
timestamp := uint64(time.Now().Unix())
ed := types.ExecutingDescriptor{Timestamp: timestamp}
ctx = context.Background()
err = supervisor.CheckAccessList(ctx, accessList, types.CrossSafe, ed)
require.NoError(t, err, "logsA must all be cross-safe")

// a log should be invalid if the timestamp is incorrect
accessEntries[0].Timestamp = 333
accessList = types.EncodeAccessList(accessEntries)
err = supervisor.CheckAccessList(ctx, accessList, types.CrossSafe, ed)
require.ErrorContains(t, err, "conflict")
}
config := SuperSystemConfig{
mempoolFiltering: false,
Expand All @@ -265,6 +258,10 @@ func TestInteropBlockBuilding(t *testing.T) {
logger := testlog.Logger(t, log.LevelInfo)
oplog.SetGlobalLogHandler(logger.Handler())

// TODO(14697): re-enable once op-geth block-building uses access-lists.
// When re-enabling, the txs that execute other messages will need access-lists.
t.Skip("blocked by issue #14697")

test := func(t *testing.T, s2 SuperSystem) {
ids := s2.L2IDs()
chainA := ids[0]
Expand Down
14 changes: 10 additions & 4 deletions op-program/client/interop/consolidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,15 @@ func (d *consolidateCheckDeps) Contains(chain eth.ChainID, query supervisortypes
for _, receipt := range receipts {
for i, log := range receipt.Logs {
if current+uint32(i) == query.LogIdx {
msgHash := logToMessageHash(log)
if msgHash != query.LogHash {
return supervisortypes.BlockSeal{}, fmt.Errorf("payload hash mismatch: %s != %s: %w", msgHash, query.LogHash, supervisortypes.ErrConflict)
checksum := supervisortypes.ChecksumArgs{
BlockNumber: query.BlockNum,
LogIndex: query.LogIdx,
Timestamp: query.Timestamp,
ChainID: chain,
LogHash: logToLogHash(log),
}.Checksum()
if checksum != query.Checksum {
return supervisortypes.BlockSeal{}, fmt.Errorf("checksum mismatch: %s != %s: %w", checksum, query.Checksum, supervisortypes.ErrConflict)
} else if block.Time() != query.Timestamp {
return supervisortypes.BlockSeal{}, fmt.Errorf("block timestamp mismatch: %d != %d: %w", block.Time(), query.Timestamp, supervisortypes.ErrConflict)
} else {
Expand All @@ -294,7 +300,7 @@ func (d *consolidateCheckDeps) Contains(chain eth.ChainID, query supervisortypes
return supervisortypes.BlockSeal{}, fmt.Errorf("log not found")
}

func logToMessageHash(l *ethtypes.Log) common.Hash {
func logToLogHash(l *ethtypes.Log) common.Hash {
payloadHash := crypto.Keccak256Hash(supervisortypes.LogToMessagePayload(l))
return supervisortypes.PayloadHashToLogHash(payloadHash, l.Address)
}
Expand Down
31 changes: 5 additions & 26 deletions op-service/sources/supervisor_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ type SupervisorAdminAPI interface {
}

type SupervisorQueryAPI interface {
CheckMessage(ctx context.Context, identifier types.Identifier, payloadHash common.Hash, executingDescriptor types.ExecutingDescriptor) (types.SafetyLevel, error)
CheckMessages(ctx context.Context, messages []types.Message, minSafety types.SafetyLevel) error
CheckMessagesV2(ctx context.Context, messages []types.Message, minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error
CheckAccessList(ctx context.Context, inboxEntries []common.Hash,
minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error
CrossDerivedToSource(ctx context.Context, chainID eth.ChainID, derived eth.BlockID) (derivedFrom eth.BlockRef, err error)
LocalUnsafe(ctx context.Context, chainID eth.ChainID) (eth.BlockID, error)
CrossSafe(ctx context.Context, chainID eth.ChainID) (types.DerivedIDPair, error)
Expand Down Expand Up @@ -74,29 +73,9 @@ 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,
executingDescriptor types.ExecutingDescriptor) (types.SafetyLevel, error) {

var result types.SafetyLevel
err := cl.client.CallContext(ctx, &result, "supervisor_checkMessage", identifier, logHash, executingDescriptor)
if err != nil {
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
}

func (cl *SupervisorClient) CheckMessages(ctx context.Context, messages []types.Message, minSafety types.SafetyLevel) error {
return cl.client.CallContext(ctx, nil, "supervisor_checkMessages", messages, minSafety)
}

func (cl *SupervisorClient) CheckMessagesV2(ctx context.Context, messages []types.Message, minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error {
return cl.client.CallContext(ctx, nil, "supervisor_checkMessagesV2", messages, minSafety, executingDescriptor)
func (cl *SupervisorClient) CheckAccessList(ctx context.Context, inboxEntries []common.Hash,
minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error {
return cl.client.CallContext(ctx, nil, "supervisor_checkAccessList", inboxEntries, minSafety, executingDescriptor)
}

func (cl *SupervisorClient) CrossDerivedToSource(ctx context.Context, chainID eth.ChainID, derived eth.BlockID) (derivedFrom eth.BlockRef, err error) {
Expand Down
140 changes: 64 additions & 76 deletions op-supervisor/supervisor/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,94 +423,82 @@ func (su *SupervisorBackend) DependencySet() depset.DependencySet {
// Query methods
// ----------------------------

func (su *SupervisorBackend) CheckMessage(ctx context.Context, identifier types.Identifier, payloadHash common.Hash, executingDescriptor types.ExecutingDescriptor) (types.SafetyLevel, error) {
logHash := types.PayloadHashToLogHash(payloadHash, identifier.Origin)
chainID := identifier.ChainID
blockNum := identifier.BlockNumber
logIdx := identifier.LogIndex
_, err := su.chainDBs.Contains(chainID,
types.ContainsQuery{
BlockNum: blockNum,
Timestamp: identifier.Timestamp,
LogIdx: logIdx,
LogHash: logHash,
})
if errors.Is(err, types.ErrFuture) {
su.logger.Debug("Future message", "identifier", identifier, "payloadHash", payloadHash, "err", err)
return types.LocalUnsafe, nil
}
if errors.Is(err, types.ErrConflict) {
su.logger.Debug("Conflicting message", "identifier", identifier, "payloadHash", payloadHash, "err", err)
return types.Invalid, nil
// checkAccess checks message timestamp invariants and inclusion in the chain.
// If the initiating message exists, the block it is included in is returned.
func (su *SupervisorBackend) checkAccess(acc types.Access, execAt types.ExecutingDescriptor) (eth.BlockID, error) {
// Check if message passes time checks
if err := execAt.AccessCheck(su.depSet.MessageExpiryWindow(), acc.Timestamp); err != nil {
return eth.BlockID{}, err
}

// Check if message exists
bl, err := su.chainDBs.Contains(acc.ChainID, types.ContainsQuery{
Timestamp: acc.Timestamp,
BlockNum: acc.BlockNumber,
LogIdx: acc.LogIndex,
Checksum: acc.Checksum,
})
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
return eth.BlockID{}, err
}
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 bl.ID(), nil
}

// checkSafety is a helper method to check if a block has the given safety level.
// It is already assumed to exist in the canonical unsafe chain.
func (su *SupervisorBackend) checkSafety(chainID eth.ChainID, blockID eth.BlockID, safetyLevel types.SafetyLevel) error {
switch safetyLevel {
case types.LocalUnsafe:
return nil // msg exists, nothing more to check
case types.CrossUnsafe:
return su.chainDBs.IsCrossUnsafe(chainID, blockID)
case types.LocalSafe:
return su.chainDBs.IsLocalSafe(chainID, blockID)
case types.CrossSafe:
return su.chainDBs.IsCrossSafe(chainID, blockID)
case types.Finalized:
return su.chainDBs.IsFinalized(chainID, blockID)
default:
return types.ErrConflict
}
return su.chainDBs.Safest(chainID, blockNum, logIdx)
}

func (su *SupervisorBackend) CheckMessagesV2(
ctx context.Context,
messages []types.Message,
minSafety types.SafetyLevel,
executingDescriptor types.ExecutingDescriptor) error {
su.logger.Debug("Checking messages", "count", len(messages), "minSafety", minSafety, "executingTimestamp", executingDescriptor.Timestamp)
func (su *SupervisorBackend) CheckAccessList(ctx context.Context, inboxEntries []common.Hash,
minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error {
switch minSafety {
case types.LocalUnsafe, types.CrossUnsafe, types.LocalSafe, types.CrossSafe, types.Finalized:
// valid safety level
default:
return errors.New("unexpected min-safety level")
}

for _, msg := range messages {
su.logger.Debug("Checking message",
"identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String(), "executingTimestamp", executingDescriptor.Timestamp)
safety, err := su.CheckMessage(ctx, 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)
su.logger.Debug("Checking access-list",
"minSafety", minSafety, "length", len(inboxEntries))

// TODO(#14800): acquire a rewind-read-lock, so we can ensure the safety of all entries is consistent

entries := inboxEntries
for len(entries) > 0 {
if err := ctx.Err(); err != nil {
return fmt.Errorf("stopped acces-list check early: %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)
remaining, acc, err := types.ParseAccess(entries)
if err != nil {
return fmt.Errorf("failed to read data: %w", err)
}
}
return nil
}
entries = remaining

func (su *SupervisorBackend) CheckMessages(
ctx context.Context,
messages []types.Message,
minSafety types.SafetyLevel) error {
su.logger.Debug("Checking messages", "count", len(messages), "minSafety", minSafety)

for _, msg := range messages {
su.logger.Debug("Checking message",
"identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String())
// 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(ctx, msg.Identifier, msg.PayloadHash, types.ExecutingDescriptor{Timestamp: msg.Identifier.Timestamp})
msgBlock, err := su.checkAccess(acc, executingDescriptor)
if err != nil {
su.logger.Error("Check message failed", "err", err,
"identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String())
return fmt.Errorf("failed to check message: %w", err)
su.logger.Debug("Access-list inclusion check failed", "err", err)
return types.ErrConflict
}
if !safety.AtLeastAsSafe(minSafety) {
su.logger.Error("Message is not sufficiently safe",
"safety", safety, "minSafety", minSafety,
"identifier", msg.Identifier, "payloadHash", msg.PayloadHash.String())
return fmt.Errorf("message %v (safety level: %v) does not meet the minimum safety %v",
msg.Identifier,
safety,
minSafety)
// TODO(#14800) add msgBlock to rewind lock

// TODO(#14800): this can be deferred to only check the latest block of all access entries
if err := su.checkSafety(acc.ChainID, msgBlock, minSafety); err != nil {
su.logger.Debug("Access-list safety check failed", "err", err)
return types.ErrConflict
}
}
return nil
Expand Down
15 changes: 8 additions & 7 deletions op-supervisor/supervisor/backend/cross/hazard_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,14 @@ func (h *HazardSet) build(deps HazardDeps, logger log.Logger, chainID eth.ChainI
if err := h.checkChainCanInitiate(depSet, srcChainID, candidate, msg); err != nil {
return err
}
includedIn, err := deps.Contains(srcChainID,
types.ContainsQuery{
Timestamp: msg.Timestamp,
BlockNum: msg.BlockNum,
LogIdx: msg.LogIdx,
LogHash: msg.Hash,
})
q := types.ChecksumArgs{
BlockNumber: msg.BlockNum,
LogIndex: msg.LogIdx,
Timestamp: msg.Timestamp,
ChainID: srcChainID,
LogHash: msg.Hash,
}.Query()
includedIn, err := deps.Contains(srcChainID, q)
if err != nil {
return fmt.Errorf("executing msg %s failed inclusion check: %w", msg, err)
}
Expand Down
Loading