Commit 6e524df2 authored by OptimismBot's avatar OptimismBot Committed by GitHub

Merge pull request #5422 from ethereum-optimism/aj/fpp-engine-unknown-block

op-program: Fix l2 engine API to avoid fetching L2 blocks after the agreed head
parents 52779a2b 1029095c
...@@ -191,7 +191,7 @@ func TestL2EngineAPIFail(gt *testing.T) { ...@@ -191,7 +191,7 @@ func TestL2EngineAPIFail(gt *testing.T) {
} }
func TestEngineAPITests(t *testing.T) { func TestEngineAPITests(t *testing.T) {
test.RunEngineAPITests(t, func() engineapi.EngineBackend { test.RunEngineAPITests(t, func(t *testing.T) engineapi.EngineBackend {
jwtPath := e2eutils.WriteDefaultJWT(t) jwtPath := e2eutils.WriteDefaultJWT(t)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams) dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
sd := e2eutils.Setup(t, dp, defaultAlloc) sd := e2eutils.Setup(t, dp, defaultAlloc)
......
...@@ -18,14 +18,15 @@ import ( ...@@ -18,14 +18,15 @@ import (
) )
type OracleBackedL2Chain struct { type OracleBackedL2Chain struct {
log log.Logger log log.Logger
oracle Oracle oracle Oracle
chainCfg *params.ChainConfig chainCfg *params.ChainConfig
engine consensus.Engine engine consensus.Engine
head *types.Header oracleHead *types.Header
safe *types.Header head *types.Header
finalized *types.Header safe *types.Header
vmCfg vm.Config finalized *types.Header
vmCfg vm.Config
// Inserted blocks // Inserted blocks
blocks map[common.Hash]*types.Block blocks map[common.Hash]*types.Block
...@@ -44,11 +45,12 @@ func NewOracleBackedL2Chain(logger log.Logger, oracle Oracle, chainCfg *params.C ...@@ -44,11 +45,12 @@ func NewOracleBackedL2Chain(logger log.Logger, oracle Oracle, chainCfg *params.C
engine: beacon.New(nil), engine: beacon.New(nil),
// Treat the agreed starting head as finalized - nothing before it can be disputed // Treat the agreed starting head as finalized - nothing before it can be disputed
head: head.Header(), head: head.Header(),
safe: head.Header(), safe: head.Header(),
finalized: head.Header(), finalized: head.Header(),
blocks: make(map[common.Hash]*types.Block), oracleHead: head.Header(),
db: NewOracleBackedDB(oracle), blocks: make(map[common.Hash]*types.Block),
db: NewOracleBackedDB(oracle),
}, nil }, nil
} }
...@@ -82,11 +84,7 @@ func (o *OracleBackedL2Chain) CurrentFinalBlock() *types.Header { ...@@ -82,11 +84,7 @@ func (o *OracleBackedL2Chain) CurrentFinalBlock() *types.Header {
} }
func (o *OracleBackedL2Chain) GetHeaderByHash(hash common.Hash) *types.Header { func (o *OracleBackedL2Chain) GetHeaderByHash(hash common.Hash) *types.Header {
block := o.GetBlockByHash(hash) return o.GetBlockByHash(hash).Header()
if block == nil {
return nil
}
return block.Header()
} }
func (o *OracleBackedL2Chain) GetBlockByHash(hash common.Hash) *types.Block { func (o *OracleBackedL2Chain) GetBlockByHash(hash common.Hash) *types.Block {
...@@ -96,15 +94,18 @@ func (o *OracleBackedL2Chain) GetBlockByHash(hash common.Hash) *types.Block { ...@@ -96,15 +94,18 @@ func (o *OracleBackedL2Chain) GetBlockByHash(hash common.Hash) *types.Block {
return block return block
} }
// Retrieve from the oracle // Retrieve from the oracle
block = o.oracle.BlockByHash(hash) return o.oracle.BlockByHash(hash)
if block == nil {
return nil
}
return block
} }
func (o *OracleBackedL2Chain) GetBlock(hash common.Hash, number uint64) *types.Block { func (o *OracleBackedL2Chain) GetBlock(hash common.Hash, number uint64) *types.Block {
block := o.GetBlockByHash(hash) var block *types.Block
if o.oracleHead.Number.Uint64() < number {
// For blocks above the chain head, only consider newly built blocks
// Avoids requesting an unknown block from the oracle which would panic.
block = o.blocks[hash]
} else {
block = o.GetBlockByHash(hash)
}
if block == nil { if block == nil {
return nil return nil
} }
...@@ -116,9 +117,6 @@ func (o *OracleBackedL2Chain) GetBlock(hash common.Hash, number uint64) *types.B ...@@ -116,9 +117,6 @@ func (o *OracleBackedL2Chain) GetBlock(hash common.Hash, number uint64) *types.B
func (o *OracleBackedL2Chain) GetHeader(hash common.Hash, u uint64) *types.Header { func (o *OracleBackedL2Chain) GetHeader(hash common.Hash, u uint64) *types.Header {
block := o.GetBlock(hash, u) block := o.GetBlock(hash, u)
if block == nil {
return nil
}
return block.Header() return block.Header()
} }
......
...@@ -42,17 +42,6 @@ func TestGetBlocks(t *testing.T) { ...@@ -42,17 +42,6 @@ func TestGetBlocks(t *testing.T) {
} }
} }
func TestUnknownBlock(t *testing.T) {
_, chain := setupOracleBackedChain(t, 1)
hash := common.HexToHash("0x556677881122")
blockNumber := uint64(1)
require.Nil(t, chain.GetBlockByHash(hash))
require.Nil(t, chain.GetHeaderByHash(hash))
require.Nil(t, chain.GetBlock(hash, blockNumber))
require.Nil(t, chain.GetHeader(hash, blockNumber))
require.False(t, chain.HasBlockAndState(hash, blockNumber))
}
func TestCanonicalHashNotFoundPastChainHead(t *testing.T) { func TestCanonicalHashNotFoundPastChainHead(t *testing.T) {
blocks, chain := setupOracleBackedChainWithLowerHead(t, 5, 3) blocks, chain := setupOracleBackedChainWithLowerHead(t, 5, 3)
...@@ -69,7 +58,7 @@ func TestCanonicalHashNotFoundPastChainHead(t *testing.T) { ...@@ -69,7 +58,7 @@ func TestCanonicalHashNotFoundPastChainHead(t *testing.T) {
func TestAppendToChain(t *testing.T) { func TestAppendToChain(t *testing.T) {
blocks, chain := setupOracleBackedChainWithLowerHead(t, 4, 3) blocks, chain := setupOracleBackedChainWithLowerHead(t, 4, 3)
newBlock := blocks[4] newBlock := blocks[4]
require.Nil(t, chain.GetBlockByHash(newBlock.Hash()), "block unknown before being added") require.Nil(t, chain.GetBlock(newBlock.Hash(), newBlock.NumberU64()), "block unknown before being added")
require.NoError(t, chain.InsertBlockWithoutSetHead(newBlock)) require.NoError(t, chain.InsertBlockWithoutSetHead(newBlock))
require.Equal(t, blocks[3].Header(), chain.CurrentHeader(), "should not update chain head yet") require.Equal(t, blocks[3].Header(), chain.CurrentHeader(), "should not update chain head yet")
...@@ -113,9 +102,7 @@ func TestUpdateStateDatabaseWhenImportingBlock(t *testing.T) { ...@@ -113,9 +102,7 @@ func TestUpdateStateDatabaseWhenImportingBlock(t *testing.T) {
require.NotEqual(t, blocks[1].Root(), newBlock.Root(), "block should have modified world state") require.NotEqual(t, blocks[1].Root(), newBlock.Root(), "block should have modified world state")
require.Panics(t, func() { require.False(t, chain.HasBlockAndState(newBlock.Root(), newBlock.NumberU64()), "state from non-imported block should not be available")
_, _ = chain.StateAt(newBlock.Root())
}, "state from non-imported block should not be available")
err = chain.InsertBlockWithoutSetHead(newBlock) err = chain.InsertBlockWithoutSetHead(newBlock)
require.NoError(t, err) require.NoError(t, err)
...@@ -181,7 +168,7 @@ func setupOracle(t *testing.T, blockCount int, headBlockNumber int) (*params.Cha ...@@ -181,7 +168,7 @@ func setupOracle(t *testing.T, blockCount int, headBlockNumber int) (*params.Cha
genesisBlock := l2Genesis.MustCommit(db) genesisBlock := l2Genesis.MustCommit(db)
blocks, _ := core.GenerateChain(chainCfg, genesisBlock, consensus, db, blockCount, func(i int, gen *core.BlockGen) {}) blocks, _ := core.GenerateChain(chainCfg, genesisBlock, consensus, db, blockCount, func(i int, gen *core.BlockGen) {})
blocks = append([]*types.Block{genesisBlock}, blocks...) blocks = append([]*types.Block{genesisBlock}, blocks...)
oracle := newStubBlockOracle(blocks[:headBlockNumber+1], db) oracle := newStubBlockOracle(t, blocks[:headBlockNumber+1], db)
return chainCfg, blocks, oracle return chainCfg, blocks, oracle
} }
...@@ -213,23 +200,27 @@ type stubBlockOracle struct { ...@@ -213,23 +200,27 @@ type stubBlockOracle struct {
kvStateOracle kvStateOracle
} }
func newStubBlockOracle(chain []*types.Block, db ethdb.Database) *stubBlockOracle { func newStubBlockOracle(t *testing.T, chain []*types.Block, db ethdb.Database) *stubBlockOracle {
blocks := make(map[common.Hash]*types.Block, len(chain)) blocks := make(map[common.Hash]*types.Block, len(chain))
for _, block := range chain { for _, block := range chain {
blocks[block.Hash()] = block blocks[block.Hash()] = block
} }
return &stubBlockOracle{ return &stubBlockOracle{
blocks: blocks, blocks: blocks,
kvStateOracle: kvStateOracle{source: db}, kvStateOracle: kvStateOracle{t: t, source: db},
} }
} }
func (o stubBlockOracle) BlockByHash(blockHash common.Hash) *types.Block { func (o stubBlockOracle) BlockByHash(blockHash common.Hash) *types.Block {
return o.blocks[blockHash] block, ok := o.blocks[blockHash]
if !ok {
o.t.Fatalf("requested unknown block %s", blockHash)
}
return block
} }
func TestEngineAPITests(t *testing.T) { func TestEngineAPITests(t *testing.T) {
test.RunEngineAPITests(t, func() engineapi.EngineBackend { test.RunEngineAPITests(t, func(t *testing.T) engineapi.EngineBackend {
_, chain := setupOracleBackedChain(t, 0) _, chain := setupOracleBackedChain(t, 0)
return chain return chain
}) })
......
...@@ -301,7 +301,7 @@ func (ea *L2EngineAPI) NewPayloadV1(ctx context.Context, payload *eth.ExecutionP ...@@ -301,7 +301,7 @@ func (ea *L2EngineAPI) NewPayloadV1(ctx context.Context, payload *eth.ExecutionP
} }
// If we already have the block locally, ignore the entire execution and just // If we already have the block locally, ignore the entire execution and just
// return a fake success. // return a fake success.
if block := ea.backend.GetBlockByHash(payload.BlockHash); block != nil { if block := ea.backend.GetBlock(payload.BlockHash, uint64(payload.BlockNumber)); block != nil {
ea.log.Warn("Ignoring already known beacon payload", "number", payload.BlockNumber, "hash", payload.BlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0))) ea.log.Warn("Ignoring already known beacon payload", "number", payload.BlockNumber, "hash", payload.BlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)))
hash := block.Hash() hash := block.Hash()
return &eth.PayloadStatusV1{Status: eth.ExecutionValid, LatestValidHash: &hash}, nil return &eth.PayloadStatusV1{Status: eth.ExecutionValid, LatestValidHash: &hash}, nil
......
...@@ -18,7 +18,7 @@ import ( ...@@ -18,7 +18,7 @@ import (
var gasLimit = eth.Uint64Quantity(30_000_000) var gasLimit = eth.Uint64Quantity(30_000_000)
var feeRecipient = common.Address{} var feeRecipient = common.Address{}
func RunEngineAPITests(t *testing.T, createBackend func() engineapi.EngineBackend) { func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.EngineBackend) {
t.Run("CreateBlock", func(t *testing.T) { t.Run("CreateBlock", func(t *testing.T) {
api := newTestHelper(t, createBackend) api := newTestHelper(t, createBackend)
...@@ -292,10 +292,10 @@ type testHelper struct { ...@@ -292,10 +292,10 @@ type testHelper struct {
assert *require.Assertions assert *require.Assertions
} }
func newTestHelper(t *testing.T, createBackend func() engineapi.EngineBackend) *testHelper { func newTestHelper(t *testing.T, createBackend func(t *testing.T) engineapi.EngineBackend) *testHelper {
logger := testlog.Logger(t, log.LvlDebug) logger := testlog.Logger(t, log.LvlDebug)
ctx := context.Background() ctx := context.Background()
backend := createBackend() backend := createBackend(t)
api := engineapi.NewL2EngineAPI(logger, backend) api := engineapi.NewL2EngineAPI(logger, backend)
test := &testHelper{ test := &testHelper{
t: t, t: t,
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
...@@ -24,19 +25,25 @@ type CallContext interface { ...@@ -24,19 +25,25 @@ type CallContext interface {
type FetchingL2Oracle struct { type FetchingL2Oracle struct {
ctx context.Context ctx context.Context
logger log.Logger logger log.Logger
head eth.BlockInfo
blockSource BlockSource blockSource BlockSource
callContext CallContext callContext CallContext
} }
func NewFetchingL2Oracle(ctx context.Context, logger log.Logger, l2Url string) (*FetchingL2Oracle, error) { func NewFetchingL2Oracle(ctx context.Context, logger log.Logger, l2Url string, l2Head common.Hash) (*FetchingL2Oracle, error) {
rpcClient, err := rpc.Dial(l2Url) rpcClient, err := rpc.Dial(l2Url)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ethClient := ethclient.NewClient(rpcClient) ethClient := ethclient.NewClient(rpcClient)
head, err := ethClient.HeaderByHash(ctx, l2Head)
if err != nil {
return nil, fmt.Errorf("retrieve l2 head %v: %w", l2Head, err)
}
return &FetchingL2Oracle{ return &FetchingL2Oracle{
ctx: ctx, ctx: ctx,
logger: logger, logger: logger,
head: eth.HeaderBlockInfo(head),
blockSource: ethClient, blockSource: ethClient,
callContext: rpcClient, callContext: rpcClient,
}, nil }, nil
...@@ -78,5 +85,8 @@ func (o *FetchingL2Oracle) BlockByHash(blockHash common.Hash) *types.Block { ...@@ -78,5 +85,8 @@ func (o *FetchingL2Oracle) BlockByHash(blockHash common.Hash) *types.Block {
if err != nil { if err != nil {
panic(fmt.Errorf("fetch block %s: %w", blockHash.Hex(), err)) panic(fmt.Errorf("fetch block %s: %w", blockHash.Hex(), err))
} }
if block.NumberU64() > o.head.NumberU64() {
panic(fmt.Errorf("fetched block %v number %d above head block number %d", blockHash, block.NumberU64(), o.head.NumberU64()))
}
return block return block
} }
...@@ -5,12 +5,14 @@ import ( ...@@ -5,12 +5,14 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"math/big"
"math/rand" "math/rand"
"reflect" "reflect"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-node/testutils" "github.com/ethereum-optimism/optimism/op-node/testutils"
cll2 "github.com/ethereum-optimism/optimism/op-program/client/l2" cll2 "github.com/ethereum-optimism/optimism/op-program/client/l2"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
...@@ -23,35 +25,7 @@ import ( ...@@ -23,35 +25,7 @@ import (
// Require the fetching oracle to implement StateOracle // Require the fetching oracle to implement StateOracle
var _ cll2.StateOracle = (*FetchingL2Oracle)(nil) var _ cll2.StateOracle = (*FetchingL2Oracle)(nil)
type callContextRequest struct { const headBlockNumber = 1000
ctx context.Context
method string
args []interface{}
}
type stubCallContext struct {
nextResult any
nextErr error
requests []callContextRequest
}
func (c *stubCallContext) CallContext(ctx context.Context, result any, method string, args ...interface{}) error {
if result != nil && reflect.TypeOf(result).Kind() != reflect.Ptr {
return fmt.Errorf("call result parameter must be pointer or nil interface: %v", result)
}
c.requests = append(c.requests, callContextRequest{ctx: ctx, method: method, args: args})
if c.nextErr != nil {
return c.nextErr
}
res, err := json.Marshal(c.nextResult)
if err != nil {
return fmt.Errorf("json marshal: %w", err)
}
err = json.Unmarshal(res, result)
if err != nil {
return fmt.Errorf("json unmarshal: %w", err)
}
return nil
}
func TestNodeByHash(t *testing.T) { func TestNodeByHash(t *testing.T) {
rng := rand.New(rand.NewSource(1234)) rng := rand.New(rand.NewSource(1234))
...@@ -152,31 +126,12 @@ func TestCodeByHash(t *testing.T) { ...@@ -152,31 +126,12 @@ func TestCodeByHash(t *testing.T) {
}) })
} }
type blockRequest struct {
ctx context.Context
blockHash common.Hash
}
type stubBlockSource struct {
requests []blockRequest
nextErr error
nextResult *types.Block
}
func (s *stubBlockSource) BlockByHash(ctx context.Context, blockHash common.Hash) (*types.Block, error) {
s.requests = append(s.requests, blockRequest{
ctx: ctx,
blockHash: blockHash,
})
return s.nextResult, s.nextErr
}
func TestBlockByHash(t *testing.T) { func TestBlockByHash(t *testing.T) {
rng := rand.New(rand.NewSource(1234)) rng := rand.New(rand.NewSource(1234))
hash := testutils.RandomHash(rng) hash := testutils.RandomHash(rng)
t.Run("Success", func(t *testing.T) { t.Run("Success", func(t *testing.T) {
block, _ := testutils.RandomBlock(rng, 1) block := blockWithNumber(rng, headBlockNumber-1)
stub := &stubBlockSource{nextResult: block} stub := &stubBlockSource{nextResult: block}
fetcher := newFetcher(stub, nil) fetcher := newFetcher(stub, nil)
...@@ -194,7 +149,7 @@ func TestBlockByHash(t *testing.T) { ...@@ -194,7 +149,7 @@ func TestBlockByHash(t *testing.T) {
}) })
t.Run("RequestArgs", func(t *testing.T) { t.Run("RequestArgs", func(t *testing.T) {
stub := &stubBlockSource{} stub := &stubBlockSource{nextResult: blockWithNumber(rng, 1)}
fetcher := newFetcher(stub, nil) fetcher := newFetcher(stub, nil)
fetcher.BlockByHash(hash) fetcher.BlockByHash(hash)
...@@ -203,11 +158,86 @@ func TestBlockByHash(t *testing.T) { ...@@ -203,11 +158,86 @@ func TestBlockByHash(t *testing.T) {
req := stub.requests[0] req := stub.requests[0]
require.Equal(t, hash, req.blockHash) require.Equal(t, hash, req.blockHash)
}) })
t.Run("PanicWhenBlockAboveHeadRequested", func(t *testing.T) {
// Block that the source can provide but is above the head block number
block := blockWithNumber(rng, headBlockNumber+1)
stub := &stubBlockSource{nextResult: block}
fetcher := newFetcher(stub, nil)
require.Panics(t, func() {
fetcher.BlockByHash(block.Hash())
})
})
}
func blockWithNumber(rng *rand.Rand, num int64) *types.Block {
header := testutils.RandomHeader(rng)
header.Number = big.NewInt(num)
return types.NewBlock(header, nil, nil, nil, trie.NewStackTrie(nil))
}
type blockRequest struct {
ctx context.Context
blockHash common.Hash
}
type stubBlockSource struct {
requests []blockRequest
nextErr error
nextResult *types.Block
}
func (s *stubBlockSource) BlockByHash(ctx context.Context, blockHash common.Hash) (*types.Block, error) {
s.requests = append(s.requests, blockRequest{
ctx: ctx,
blockHash: blockHash,
})
return s.nextResult, s.nextErr
}
type callContextRequest struct {
ctx context.Context
method string
args []interface{}
}
type stubCallContext struct {
nextResult any
nextErr error
requests []callContextRequest
}
func (c *stubCallContext) CallContext(ctx context.Context, result any, method string, args ...interface{}) error {
if result != nil && reflect.TypeOf(result).Kind() != reflect.Ptr {
return fmt.Errorf("call result parameter must be pointer or nil interface: %v", result)
}
c.requests = append(c.requests, callContextRequest{ctx: ctx, method: method, args: args})
if c.nextErr != nil {
return c.nextErr
}
res, err := json.Marshal(c.nextResult)
if err != nil {
return fmt.Errorf("json marshal: %w", err)
}
err = json.Unmarshal(res, result)
if err != nil {
return fmt.Errorf("json unmarshal: %w", err)
}
return nil
} }
func newFetcher(blockSource BlockSource, callContext CallContext) *FetchingL2Oracle { func newFetcher(blockSource BlockSource, callContext CallContext) *FetchingL2Oracle {
rng := rand.New(rand.NewSource(int64(1)))
head := testutils.MakeBlockInfo(func(i *testutils.MockBlockInfo) {
i.InfoNum = headBlockNumber
})(rng)
return &FetchingL2Oracle{ return &FetchingL2Oracle{
ctx: context.Background(),
logger: log.New(), logger: log.New(),
head: head,
blockSource: blockSource, blockSource: blockSource,
callContext: callContext, callContext: callContext,
} }
......
...@@ -19,7 +19,7 @@ func NewFetchingEngine(ctx context.Context, logger log.Logger, cfg *config.Confi ...@@ -19,7 +19,7 @@ func NewFetchingEngine(ctx context.Context, logger log.Logger, cfg *config.Confi
if err != nil { if err != nil {
return nil, err return nil, err
} }
oracle, err := NewFetchingL2Oracle(ctx, logger, cfg.L2URL) oracle, err := NewFetchingL2Oracle(ctx, logger, cfg.L2URL, cfg.L2Head)
if err != nil { if err != nil {
return nil, fmt.Errorf("connect l2 oracle: %w", err) return nil, fmt.Errorf("connect l2 oracle: %w", err)
} }
......
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