Commit 76980a01 authored by OptimismBot's avatar OptimismBot Committed by GitHub

Merge pull request #5911 from ethereum-optimism/refcell/parseoutputs

feat(op-challenger): Parse Logs
parents 35c035fa 78a88771
...@@ -5,6 +5,9 @@ import ( ...@@ -5,6 +5,9 @@ import (
"errors" "errors"
"math/big" "math/big"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
) )
...@@ -15,38 +18,60 @@ var ( ...@@ -15,38 +18,60 @@ var (
ErrInvalidBlockNumber = errors.New("invalid block number") ErrInvalidBlockNumber = errors.New("invalid block number")
// ErrUnsupportedL2OOVersion is returned when the output version is not supported. // ErrUnsupportedL2OOVersion is returned when the output version is not supported.
ErrUnsupportedL2OOVersion = errors.New("unsupported l2oo version") ErrUnsupportedL2OOVersion = errors.New("unsupported l2oo version")
// ErrInvalidOutputLogTopic is returned when the output log topic is invalid.
ErrInvalidOutputLogTopic = errors.New("invalid output log topic")
// ErrInvalidOutputTopicLength is returned when the output log topic length is invalid.
ErrInvalidOutputTopicLength = errors.New("invalid output log topic length")
) )
// ParseOutputLog parses a log from the L2OutputOracle contract.
func (c *Challenger) ParseOutputLog(log *types.Log) (*bindings.TypesOutputProposal, error) {
// Check the length of log topics
if len(log.Topics) != 4 {
return nil, ErrInvalidOutputTopicLength
}
// Validate the first topic is the output log topic
if log.Topics[0] != c.l2ooABI.Events["OutputProposed"].ID {
return nil, ErrInvalidOutputLogTopic
}
l2BlockNumber := new(big.Int).SetBytes(log.Topics[3][:])
expected := log.Topics[1]
return &bindings.TypesOutputProposal{
L2BlockNumber: l2BlockNumber,
OutputRoot: eth.Bytes32(expected),
}, nil
}
// ValidateOutput checks that a given output is expected via a trusted rollup node rpc. // ValidateOutput checks that a given output is expected via a trusted rollup node rpc.
// It returns: if the output is correct, the fetched output, error // It returns: if the output is correct, the fetched output, error
func (c *Challenger) ValidateOutput(ctx context.Context, l2BlockNumber *big.Int, expected eth.Bytes32) (bool, *eth.Bytes32, error) { func (c *Challenger) ValidateOutput(ctx context.Context, proposal bindings.TypesOutputProposal) (bool, eth.Bytes32, error) {
// Fetch the output from the rollup node // Fetch the output from the rollup node
ctx, cancel := context.WithTimeout(ctx, c.networkTimeout) ctx, cancel := context.WithTimeout(ctx, c.networkTimeout)
defer cancel() defer cancel()
output, err := c.rollupClient.OutputAtBlock(ctx, l2BlockNumber.Uint64()) output, err := c.rollupClient.OutputAtBlock(ctx, proposal.L2BlockNumber.Uint64())
if err != nil { if err != nil {
c.log.Error("Failed to fetch output", "blockNum", l2BlockNumber, "err", err) c.log.Error("Failed to fetch output", "blockNum", proposal.L2BlockNumber, "err", err)
return false, nil, err return false, eth.Bytes32{}, err
} }
// Compare the output root to the expected output root // Compare the output root to the expected output root
equalRoots, err := c.compareOutputRoots(output, expected, l2BlockNumber) equalRoots, err := c.compareOutputRoots(output, proposal)
if err != nil { if err != nil {
return false, nil, err return false, eth.Bytes32{}, err
} }
return equalRoots, &output.OutputRoot, nil return equalRoots, output.OutputRoot, nil
} }
// compareOutputRoots compares the output root of the given block number to the expected output root. // compareOutputRoots compares the output root of the given block number to the expected output root.
func (c *Challenger) compareOutputRoots(received *eth.OutputResponse, expected eth.Bytes32, blockNumber *big.Int) (bool, error) { func (c *Challenger) compareOutputRoots(received *eth.OutputResponse, expected bindings.TypesOutputProposal) (bool, error) {
if received.Version != supportedL2OutputVersion { if received.Version != supportedL2OutputVersion {
c.log.Error("Unsupported l2 output version", "version", received.Version) c.log.Error("Unsupported l2 output version", "version", received.Version)
return false, ErrUnsupportedL2OOVersion return false, ErrUnsupportedL2OOVersion
} }
if received.BlockRef.Number != blockNumber.Uint64() { if received.BlockRef.Number != expected.L2BlockNumber.Uint64() {
c.log.Error("Invalid blockNumber", "expected", blockNumber, "actual", received.BlockRef.Number) c.log.Error("Invalid blockNumber", "expected", expected.L2BlockNumber, "actual", received.BlockRef.Number)
return false, ErrInvalidBlockNumber return false, ErrInvalidBlockNumber
} }
return received.OutputRoot == expected, nil return received.OutputRoot == expected.OutputRoot, nil
} }
...@@ -10,13 +10,55 @@ import ( ...@@ -10,13 +10,55 @@ import (
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
) )
func TestChallenger_OutputProposed_Signature(t *testing.T) {
computed := crypto.Keccak256Hash([]byte("OutputProposed(bytes32,uint256,uint256,uint256)"))
challenger := newTestChallenger(t, eth.OutputResponse{}, true)
expected := challenger.l2ooABI.Events["OutputProposed"].ID
require.Equal(t, expected, computed)
}
func TestParseOutputLog_Succeeds(t *testing.T) {
challenger := newTestChallenger(t, eth.OutputResponse{}, true)
expectedBlockNumber := big.NewInt(0x04)
expectedOutputRoot := [32]byte{0x02}
logTopic := challenger.l2ooABI.Events["OutputProposed"].ID
log := types.Log{
Topics: []common.Hash{logTopic, common.Hash(expectedOutputRoot), {0x03}, common.BigToHash(expectedBlockNumber)},
}
outputProposal, err := challenger.ParseOutputLog(&log)
require.NoError(t, err)
require.Equal(t, expectedBlockNumber, outputProposal.L2BlockNumber)
require.Equal(t, expectedOutputRoot, outputProposal.OutputRoot)
}
func TestParseOutputLog_WrongLogTopic_Errors(t *testing.T) {
challenger := newTestChallenger(t, eth.OutputResponse{}, true)
_, err := challenger.ParseOutputLog(&types.Log{
Topics: []common.Hash{{0x01}, {0x02}, {0x03}, {0x04}},
})
require.ErrorIs(t, err, ErrInvalidOutputLogTopic)
}
func TestParseOutputLog_WrongTopicLength_Errors(t *testing.T) {
challenger := newTestChallenger(t, eth.OutputResponse{}, true)
logTopic := challenger.l2ooABI.Events["OutputProposed"].ID
_, err := challenger.ParseOutputLog(&types.Log{
Topics: []common.Hash{logTopic, {0x02}, {0x03}},
})
require.ErrorIs(t, err, ErrInvalidOutputTopicLength)
}
func TestChallenger_ValidateOutput_RollupClientErrors(t *testing.T) { func TestChallenger_ValidateOutput_RollupClientErrors(t *testing.T) {
output := eth.OutputResponse{ output := eth.OutputResponse{
Version: supportedL2OutputVersion, Version: supportedL2OutputVersion,
...@@ -26,9 +68,13 @@ func TestChallenger_ValidateOutput_RollupClientErrors(t *testing.T) { ...@@ -26,9 +68,13 @@ func TestChallenger_ValidateOutput_RollupClientErrors(t *testing.T) {
challenger := newTestChallenger(t, output, true) challenger := newTestChallenger(t, output, true)
valid, received, err := challenger.ValidateOutput(context.Background(), big.NewInt(0), output.OutputRoot) checked := bindings.TypesOutputProposal{
L2BlockNumber: big.NewInt(0),
OutputRoot: output.OutputRoot,
}
valid, received, err := challenger.ValidateOutput(context.Background(), checked)
require.False(t, valid) require.False(t, valid)
require.Nil(t, received) require.Equal(t, eth.Bytes32{}, received)
require.ErrorIs(t, err, mockOutputApiError) require.ErrorIs(t, err, mockOutputApiError)
} }
...@@ -41,9 +87,13 @@ func TestChallenger_ValidateOutput_ErrorsWithWrongVersion(t *testing.T) { ...@@ -41,9 +87,13 @@ func TestChallenger_ValidateOutput_ErrorsWithWrongVersion(t *testing.T) {
challenger := newTestChallenger(t, output, false) challenger := newTestChallenger(t, output, false)
valid, received, err := challenger.ValidateOutput(context.Background(), big.NewInt(0), eth.Bytes32{}) checked := bindings.TypesOutputProposal{
L2BlockNumber: big.NewInt(0),
OutputRoot: output.OutputRoot,
}
valid, received, err := challenger.ValidateOutput(context.Background(), checked)
require.False(t, valid) require.False(t, valid)
require.Nil(t, received) require.Equal(t, eth.Bytes32{}, received)
require.ErrorIs(t, err, ErrUnsupportedL2OOVersion) require.ErrorIs(t, err, ErrUnsupportedL2OOVersion)
} }
...@@ -56,9 +106,13 @@ func TestChallenger_ValidateOutput_ErrorsInvalidBlockNumber(t *testing.T) { ...@@ -56,9 +106,13 @@ func TestChallenger_ValidateOutput_ErrorsInvalidBlockNumber(t *testing.T) {
challenger := newTestChallenger(t, output, false) challenger := newTestChallenger(t, output, false)
valid, received, err := challenger.ValidateOutput(context.Background(), big.NewInt(1), output.OutputRoot) checked := bindings.TypesOutputProposal{
L2BlockNumber: big.NewInt(1),
OutputRoot: output.OutputRoot,
}
valid, received, err := challenger.ValidateOutput(context.Background(), checked)
require.False(t, valid) require.False(t, valid)
require.Nil(t, received) require.Equal(t, eth.Bytes32{}, received)
require.ErrorIs(t, err, ErrInvalidBlockNumber) require.ErrorIs(t, err, ErrInvalidBlockNumber)
} }
...@@ -71,8 +125,12 @@ func TestOutput_ValidateOutput(t *testing.T) { ...@@ -71,8 +125,12 @@ func TestOutput_ValidateOutput(t *testing.T) {
challenger := newTestChallenger(t, output, false) challenger := newTestChallenger(t, output, false)
valid, expected, err := challenger.ValidateOutput(context.Background(), big.NewInt(0), output.OutputRoot) checked := bindings.TypesOutputProposal{
require.Equal(t, *expected, output.OutputRoot) L2BlockNumber: big.NewInt(0),
OutputRoot: output.OutputRoot,
}
valid, expected, err := challenger.ValidateOutput(context.Background(), checked)
require.Equal(t, expected, output.OutputRoot)
require.True(t, valid) require.True(t, valid)
require.NoError(t, err) require.NoError(t, err)
} }
...@@ -86,7 +144,11 @@ func TestChallenger_CompareOutputRoots_ErrorsWithDifferentRoots(t *testing.T) { ...@@ -86,7 +144,11 @@ func TestChallenger_CompareOutputRoots_ErrorsWithDifferentRoots(t *testing.T) {
challenger := newTestChallenger(t, output, false) challenger := newTestChallenger(t, output, false)
valid, err := challenger.compareOutputRoots(&output, output.OutputRoot, big.NewInt(0)) checked := bindings.TypesOutputProposal{
L2BlockNumber: big.NewInt(0),
OutputRoot: output.OutputRoot,
}
valid, err := challenger.compareOutputRoots(&output, checked)
require.False(t, valid) require.False(t, valid)
require.ErrorIs(t, err, ErrUnsupportedL2OOVersion) require.ErrorIs(t, err, ErrUnsupportedL2OOVersion)
} }
...@@ -100,7 +162,11 @@ func TestChallenger_CompareOutputRoots_ErrInvalidBlockNumber(t *testing.T) { ...@@ -100,7 +162,11 @@ func TestChallenger_CompareOutputRoots_ErrInvalidBlockNumber(t *testing.T) {
challenger := newTestChallenger(t, output, false) challenger := newTestChallenger(t, output, false)
valid, err := challenger.compareOutputRoots(&output, output.OutputRoot, big.NewInt(1)) checked := bindings.TypesOutputProposal{
L2BlockNumber: big.NewInt(1),
OutputRoot: output.OutputRoot,
}
valid, err := challenger.compareOutputRoots(&output, checked)
require.False(t, valid) require.False(t, valid)
require.ErrorIs(t, err, ErrInvalidBlockNumber) require.ErrorIs(t, err, ErrInvalidBlockNumber)
} }
...@@ -114,11 +180,19 @@ func TestChallenger_CompareOutputRoots_Succeeds(t *testing.T) { ...@@ -114,11 +180,19 @@ func TestChallenger_CompareOutputRoots_Succeeds(t *testing.T) {
challenger := newTestChallenger(t, output, false) challenger := newTestChallenger(t, output, false)
valid, err := challenger.compareOutputRoots(&output, output.OutputRoot, big.NewInt(0)) checked := bindings.TypesOutputProposal{
L2BlockNumber: big.NewInt(0),
OutputRoot: output.OutputRoot,
}
valid, err := challenger.compareOutputRoots(&output, checked)
require.True(t, valid) require.True(t, valid)
require.NoError(t, err) require.NoError(t, err)
valid, err = challenger.compareOutputRoots(&output, eth.Bytes32{0x01}, big.NewInt(0)) checked = bindings.TypesOutputProposal{
L2BlockNumber: big.NewInt(0),
OutputRoot: eth.Bytes32{0x01},
}
valid, err = challenger.compareOutputRoots(&output, checked)
require.False(t, valid) require.False(t, valid)
require.NoError(t, err) require.NoError(t, err)
} }
...@@ -127,11 +201,14 @@ func newTestChallenger(t *testing.T, output eth.OutputResponse, errors bool) *Ch ...@@ -127,11 +201,14 @@ func newTestChallenger(t *testing.T, output eth.OutputResponse, errors bool) *Ch
outputApi := newMockOutputApi(output, errors) outputApi := newMockOutputApi(output, errors)
log := testlog.Logger(t, log.LvlError) log := testlog.Logger(t, log.LvlError)
metr := metrics.NewMetrics("test") metr := metrics.NewMetrics("test")
parsedL2oo, err := bindings.L2OutputOracleMetaData.GetAbi()
require.NoError(t, err)
challenger := Challenger{ challenger := Challenger{
rollupClient: outputApi, rollupClient: outputApi,
log: log, log: log,
metr: metr, metr: metr,
networkTimeout: time.Duration(5) * time.Second, networkTimeout: time.Duration(5) * time.Second,
l2ooABI: parsedL2oo,
} }
return &challenger return &challenger
} }
......
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