Commit 1f8044e5 authored by protolambda's avatar protolambda Committed by GitHub

op-supervisor: fix executing message decoding (#12782)

parent cc22e243
package contracts
import (
"bytes"
"errors"
"fmt"
"io"
"math/big"
"github.com/ethereum-optimism/optimism/op-service/predeploys"
"github.com/ethereum-optimism/optimism/op-service/solabi"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots"
"github.com/ethereum/go-ethereum/common"
ethTypes "github.com/ethereum/go-ethereum/core/types"
)
const (
eventExecutingMessage = "ExecutingMessage"
)
var (
ErrEventNotFound = errors.New("event not found")
)
type contractIdentifier struct {
// Origin represents the address that initiated the message
// it is used in combination with the MsgHash to uniquely identify a message
// and is hashed into the log hash, not stored directly.
Origin common.Address
LogIndex *big.Int
BlockNumber *big.Int
ChainId *big.Int
Timestamp *big.Int
}
type CrossL2Inbox struct {
contract *batching.BoundContract
}
func NewCrossL2Inbox() *CrossL2Inbox {
abi := snapshots.LoadCrossL2InboxABI()
return &CrossL2Inbox{
contract: batching.NewBoundContract(abi, predeploys.CrossL2InboxAddr),
}
}
func (i *CrossL2Inbox) DecodeExecutingMessageLog(l *ethTypes.Log) (types.ExecutingMessage, error) {
if l.Address != i.contract.Addr() {
return types.ExecutingMessage{}, fmt.Errorf("%w: log not from CrossL2Inbox", ErrEventNotFound)
}
// use DecodeEvent to check the name of the event
// but the actual decoding is done manually to extract the contract identifier
name, _, err := i.contract.DecodeEvent(l)
if errors.Is(err, batching.ErrUnknownEvent) {
return types.ExecutingMessage{}, fmt.Errorf("%w: %v", ErrEventNotFound, err.Error())
} else if err != nil {
return types.ExecutingMessage{}, fmt.Errorf("failed to decode event: %w", err)
}
if name != eventExecutingMessage {
return types.ExecutingMessage{}, fmt.Errorf("%w: event %v not an ExecutingMessage event", ErrEventNotFound, name)
}
// the second topic is the hash of the payload (the first is the event ID)
msgHash := l.Topics[1]
// the first 32 bytes of the data are the msgHash, so we skip them
identifierBytes := bytes.NewReader(l.Data[32:])
identifier, err := identifierFromBytes(identifierBytes)
if err != nil {
return types.ExecutingMessage{}, fmt.Errorf("failed to read contract identifier: %w", err)
}
chainID, err := types.ChainIDFromBig(identifier.ChainId).ToUInt32()
if err != nil {
return types.ExecutingMessage{}, fmt.Errorf("failed to convert chain ID %v to uint32: %w", identifier.ChainId, err)
}
hash := types.PayloadHashToLogHash(msgHash, identifier.Origin)
return types.ExecutingMessage{
Chain: types.ChainIndex(chainID), // TODO(#11105): translate chain ID to chain index
Hash: hash,
BlockNum: identifier.BlockNumber.Uint64(),
LogIdx: uint32(identifier.LogIndex.Uint64()),
Timestamp: identifier.Timestamp.Uint64(),
}, nil
}
// identifierFromBytes reads a contract identifier from a byte stream.
// it follows the spec and matches the CrossL2Inbox.json definition,
// rather than relying on reflection, as that can be error-prone regarding struct ordering
func identifierFromBytes(identifierBytes io.Reader) (contractIdentifier, error) {
origin, err := solabi.ReadAddress(identifierBytes)
if err != nil {
return contractIdentifier{}, fmt.Errorf("failed to read origin address: %w", err)
}
originAddr := common.BytesToAddress(origin[:])
blockNumber, err := solabi.ReadUint256(identifierBytes)
if err != nil {
return contractIdentifier{}, fmt.Errorf("failed to read block number: %w", err)
}
logIndex, err := solabi.ReadUint256(identifierBytes)
if err != nil {
return contractIdentifier{}, fmt.Errorf("failed to read log index: %w", err)
}
timestamp, err := solabi.ReadUint256(identifierBytes)
if err != nil {
return contractIdentifier{}, fmt.Errorf("failed to read timestamp: %w", err)
}
chainID, err := solabi.ReadUint256(identifierBytes)
if err != nil {
return contractIdentifier{}, fmt.Errorf("failed to read chain ID: %w", err)
}
return contractIdentifier{
Origin: originAddr,
BlockNumber: blockNumber,
LogIndex: logIndex,
Timestamp: timestamp,
ChainId: chainID,
}, nil
}
package contracts
import (
"bytes"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-service/predeploys"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots"
"github.com/ethereum/go-ethereum/common"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
)
func TestDecodeExecutingMessageEvent(t *testing.T) {
inbox := NewCrossL2Inbox()
payload := bytes.Repeat([]byte{0xaa, 0xbb}, 50)
payloadHash := crypto.Keccak256Hash(payload)
expected := types.ExecutingMessage{
Chain: 42424, // TODO(#11105): translate chain ID to chain index
BlockNum: 12345,
LogIdx: 98,
Timestamp: 9578295,
}
contractIdent := contractIdentifier{
Origin: common.Address{0xbb, 0xcc},
ChainId: new(big.Int).SetUint64(uint64(expected.Chain)), // TODO(#11105): translate chain ID to chain index
BlockNumber: new(big.Int).SetUint64(expected.BlockNum),
Timestamp: new(big.Int).SetUint64(expected.Timestamp),
LogIndex: new(big.Int).SetUint64(uint64(expected.LogIdx)),
}
expected.Hash = types.PayloadHashToLogHash(payloadHash, contractIdent.Origin)
abi := snapshots.LoadCrossL2InboxABI()
validData, err := abi.Events[eventExecutingMessage].Inputs.Pack(payloadHash, contractIdent)
require.NoError(t, err)
createValidLog := func() *ethTypes.Log {
//protoHack := bytes.Repeat([]byte{0x00}, 32*5)
return &ethTypes.Log{
Address: predeploys.CrossL2InboxAddr,
Topics: []common.Hash{abi.Events[eventExecutingMessage].ID, payloadHash},
Data: validData,
}
}
t.Run("ParseValid", func(t *testing.T) {
l := createValidLog()
result, err := inbox.DecodeExecutingMessageLog(l)
require.NoError(t, err)
require.Equal(t, expected, result)
})
t.Run("IgnoreIncorrectContract", func(t *testing.T) {
l := createValidLog()
l.Address = common.Address{0xff}
_, err := inbox.DecodeExecutingMessageLog(l)
require.ErrorIs(t, err, ErrEventNotFound)
})
t.Run("IgnoreWrongEvent", func(t *testing.T) {
l := createValidLog()
l.Topics[0] = common.Hash{0xbb}
_, err := inbox.DecodeExecutingMessageLog(l)
require.ErrorIs(t, err, ErrEventNotFound)
})
t.Run("ErrorOnInvalidEvent", func(t *testing.T) {
l := createValidLog()
l.Data = []byte{0xbb, 0xcc}
_, err := inbox.DecodeExecutingMessageLog(l)
require.ErrorIs(t, err, batching.ErrInvalidEvent)
})
}
package processors
import (
"fmt"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/interoptypes"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
)
type EventDecoderFn func(*ethTypes.Log) (*types.ExecutingMessage, error)
func DecodeExecutingMessageLog(l *ethTypes.Log) (*types.ExecutingMessage, error) {
if l.Address != params.InteropCrossL2InboxAddress {
return nil, nil
}
if len(l.Topics) != 2 { // topics: event-id and payload-hash
return nil, nil
}
if l.Topics[0] != interoptypes.ExecutingMessageEventTopic {
return nil, nil
}
var msg interoptypes.Message
if err := msg.DecodeEvent(l.Topics, l.Data); err != nil {
return nil, fmt.Errorf("invalid executing message: %w", err)
}
logHash := types.PayloadHashToLogHash(msg.PayloadHash, msg.Identifier.Origin)
return &types.ExecutingMessage{
// TODO(#11105): translate chain index to chain ID
Chain: types.ChainIndex(msg.Identifier.ChainID.Uint64()),
BlockNum: msg.Identifier.BlockNumber,
LogIdx: msg.Identifier.LogIndex,
Timestamp: msg.Identifier.Timestamp,
Hash: logHash,
}, nil
}
package processors
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
)
func TestDecodeExecutingMessageLog(t *testing.T) {
data := `
{
"address": "0x4200000000000000000000000000000000000022",
"topics": [
"0x5c37832d2e8d10e346e55ad62071a6a2f9fa5130614ef2ec6617555c6f467ba7",
"0xc3f57e1f0dd62a4f77787d834029bfeaab8894022c47edbe13b044fb658c9190"
],
"data": "0x0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa3000000000000000000000000000000000000000000000000000000000000119d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006724d56300000000000000000000000000000000000000000000000000000000000dbc68",
"blockHash": "0x355b82e9db9105fe3e7b7ed1897878ecbba8be7f30f94aca9dc55b6296a624cf",
"blockNumber": "0x13a8",
"transactionHash": "0x6eb22bb67562ac6a8fdbf60d6227c0b1f3f9d1d15ead1b0de055358f4fb9fa69",
"transactionIndex": "0x2",
"logIndex": "0x0",
"removed": false
}
`
var logEvent ethTypes.Log
require.NoError(t, json.Unmarshal([]byte(data), &logEvent))
msg, err := DecodeExecutingMessageLog(&logEvent)
require.NoError(t, err)
require.NotNil(t, msg)
// struct Identifier {
// address origin;
// uint256 blockNumber;
// uint256 logIndex;
// uint256 timestamp;
// uint256 chainId;
// }
// function executeMessage(Identifier calldata _id,
// address _target, bytes calldata _message) external payable;
originAddr := common.HexToAddress("0x5fbdb2315678afecb367f032d93f642f64180aa3")
payloadHash := common.HexToHash("0xc3f57e1f0dd62a4f77787d834029bfeaab8894022c47edbe13b044fb658c9190")
logHash := types.PayloadHashToLogHash(payloadHash, originAddr)
require.Equal(t, logHash, msg.Hash)
require.Equal(t, uint64(4509), msg.BlockNum)
require.Equal(t, uint32(0), msg.LogIdx)
require.Equal(t, uint64(1730467171), msg.Timestamp)
require.Equal(t, types.ChainIndex(900200), msg.Chain)
}
...@@ -2,7 +2,6 @@ package processors ...@@ -2,7 +2,6 @@ package processors
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -10,7 +9,6 @@ import ( ...@@ -10,7 +9,6 @@ import (
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/backend/processors/contracts"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
) )
...@@ -24,21 +22,17 @@ type ChainsDBClientForLogProcessor interface { ...@@ -24,21 +22,17 @@ type ChainsDBClientForLogProcessor interface {
AddLog(chain types.ChainID, logHash common.Hash, parentBlock eth.BlockID, logIdx uint32, execMsg *types.ExecutingMessage) error AddLog(chain types.ChainID, logHash common.Hash, parentBlock eth.BlockID, logIdx uint32, execMsg *types.ExecutingMessage) error
} }
type EventDecoder interface {
DecodeExecutingMessageLog(log *ethTypes.Log) (types.ExecutingMessage, error)
}
type logProcessor struct { type logProcessor struct {
chain types.ChainID chain types.ChainID
logStore LogStorage logStore LogStorage
eventDecoder EventDecoder eventDecoder EventDecoderFn
} }
func NewLogProcessor(chain types.ChainID, logStore LogStorage) LogProcessor { func NewLogProcessor(chain types.ChainID, logStore LogStorage) LogProcessor {
return &logProcessor{ return &logProcessor{
chain: chain, chain: chain,
logStore: logStore, logStore: logStore,
eventDecoder: contracts.NewCrossL2Inbox(), eventDecoder: DecodeExecutingMessageLog,
} }
} }
...@@ -49,19 +43,15 @@ func (p *logProcessor) ProcessLogs(_ context.Context, block eth.BlockRef, rcpts ...@@ -49,19 +43,15 @@ func (p *logProcessor) ProcessLogs(_ context.Context, block eth.BlockRef, rcpts
for _, l := range rcpt.Logs { for _, l := range rcpt.Logs {
// log hash represents the hash of *this* log as a potentially initiating message // log hash represents the hash of *this* log as a potentially initiating message
logHash := logToLogHash(l) logHash := logToLogHash(l)
var execMsg *types.ExecutingMessage // The log may be an executing message emitted by the CrossL2Inbox
msg, err := p.eventDecoder.DecodeExecutingMessageLog(l) execMsg, err := p.eventDecoder(l)
if err != nil && !errors.Is(err, contracts.ErrEventNotFound) { if err != nil {
return fmt.Errorf("failed to decode executing message log: %w", err) return fmt.Errorf("invalid log %d from block %s: %w", l.Index, block.ID(), err)
} else if err == nil {
// if the log is an executing message, store the message
execMsg = &msg
} }
// executing messages have multiple entries in the database // executing messages have multiple entries in the database
// they should start with the initiating message and then include the execution // they should start with the initiating message and then include the execution
err = p.logStore.AddLog(p.chain, logHash, block.ParentID(), uint32(l.Index), execMsg) if err := p.logStore.AddLog(p.chain, logHash, block.ParentID(), uint32(l.Index), execMsg); err != nil {
if err != nil { return fmt.Errorf("failed to add log %d from block %s: %w", l.Index, block.ID(), err)
return fmt.Errorf("failed to add log %d from block %v: %w", l.Index, block.ID(), err)
} }
} }
} }
...@@ -77,6 +67,6 @@ func (p *logProcessor) ProcessLogs(_ context.Context, block eth.BlockRef, rcpts ...@@ -77,6 +67,6 @@ func (p *logProcessor) ProcessLogs(_ context.Context, block eth.BlockRef, rcpts
// The address is hashed into the payload hash to save space in the log storage, // The address is hashed into the payload hash to save space in the log storage,
// and because they represent paired data. // and because they represent paired data.
func logToLogHash(l *ethTypes.Log) common.Hash { func logToLogHash(l *ethTypes.Log) common.Hash {
payloadHash := crypto.Keccak256(types.LogToMessagePayload(l)) payloadHash := crypto.Keccak256Hash(types.LogToMessagePayload(l))
return types.PayloadHashToLogHash(common.Hash(payloadHash), l.Address) return types.PayloadHashToLogHash(payloadHash, l.Address)
} }
...@@ -107,7 +107,7 @@ func TestLogProcessor(t *testing.T) { ...@@ -107,7 +107,7 @@ func TestLogProcessor(t *testing.T) {
}, },
}, },
} }
execMsg := types.ExecutingMessage{ execMsg := &types.ExecutingMessage{
Chain: 4, // TODO(#11105): translate chain ID to chain index Chain: 4, // TODO(#11105): translate chain ID to chain index
BlockNum: 6, BlockNum: 6,
LogIdx: 8, LogIdx: 8,
...@@ -116,10 +116,10 @@ func TestLogProcessor(t *testing.T) { ...@@ -116,10 +116,10 @@ func TestLogProcessor(t *testing.T) {
} }
store := &stubLogStorage{} store := &stubLogStorage{}
processor := NewLogProcessor(types.ChainID{4}, store).(*logProcessor) processor := NewLogProcessor(types.ChainID{4}, store).(*logProcessor)
processor.eventDecoder = EventDecoderFn(func(l *ethTypes.Log) (types.ExecutingMessage, error) { processor.eventDecoder = func(l *ethTypes.Log) (*types.ExecutingMessage, error) {
require.Equal(t, rcpts[0].Logs[0], l) require.Equal(t, rcpts[0].Logs[0], l)
return execMsg, nil return execMsg, nil
}) }
err := processor.ProcessLogs(ctx, block1, rcpts) err := processor.ProcessLogs(ctx, block1, rcpts)
require.NoError(t, err) require.NoError(t, err)
...@@ -128,7 +128,7 @@ func TestLogProcessor(t *testing.T) { ...@@ -128,7 +128,7 @@ func TestLogProcessor(t *testing.T) {
parent: block1.ParentID(), parent: block1.ParentID(),
logIdx: 0, logIdx: 0,
logHash: logToLogHash(rcpts[0].Logs[0]), logHash: logToLogHash(rcpts[0].Logs[0]),
execMsg: &execMsg, execMsg: execMsg,
}, },
} }
require.Equal(t, expected, store.logs) require.Equal(t, expected, store.logs)
...@@ -242,9 +242,3 @@ type storedLog struct { ...@@ -242,9 +242,3 @@ type storedLog struct {
logHash common.Hash logHash common.Hash
execMsg *types.ExecutingMessage execMsg *types.ExecutingMessage
} }
type EventDecoderFn func(*ethTypes.Log) (types.ExecutingMessage, error)
func (f EventDecoderFn) DecodeExecutingMessageLog(log *ethTypes.Log) (types.ExecutingMessage, error) {
return f(log)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment