Commit dbf29ae2 authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge pull request #4957 from ethereum-optimism/aj/l2geth-e2e

op-e2e: Introduce op-geth e2e test util
parents 096d9c2b 49c57bcf
package op_e2e
import (
"context"
"errors"
"fmt"
"math/big"
"reflect"
"testing"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
gn "github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/require"
)
var (
// ErrForkChoiceUpdatedNotValid is returned when a forkChoiceUpdated returns a status other than Valid
ErrForkChoiceUpdatedNotValid = errors.New("forkChoiceUpdated status was not valid")
// ErrNewPayloadNotValid is returned when a newPayload call returns a status other than Valid, indicating the new block is invalid
ErrNewPayloadNotValid = errors.New("newPayload status was not valid")
)
// OpGeth is an actor that functions as a l2 op-geth node
// It provides useful functions for advancing and querying the chain
type OpGeth struct {
node *gn.Node
l2Engine *sources.EngineClient
L2Client *ethclient.Client
SystemConfig eth.SystemConfig
L1ChainConfig *params.ChainConfig
L2ChainConfig *params.ChainConfig
L1Head eth.BlockInfo
L2Head *eth.ExecutionPayload
sequenceNum uint64
}
func NewOpGeth(t *testing.T, ctx context.Context, cfg *SystemConfig) (*OpGeth, error) {
logger := testlog.Logger(t, log.LvlCrit)
l1Genesis, err := genesis.BuildL1DeveloperGenesis(cfg.DeployConfig)
require.Nil(t, err)
l1Block := l1Genesis.ToBlock()
l2Genesis, err := genesis.BuildL2DeveloperGenesis(cfg.DeployConfig, l1Block)
require.Nil(t, err)
l2GenesisBlock := l2Genesis.ToBlock()
rollupGenesis := rollup.Genesis{
L1: eth.BlockID{
Hash: l1Block.Hash(),
Number: l1Block.NumberU64(),
},
L2: eth.BlockID{
Hash: l2GenesisBlock.Hash(),
Number: l2GenesisBlock.NumberU64(),
},
L2Time: l2GenesisBlock.Time(),
SystemConfig: e2eutils.SystemConfigFromDeployConfig(cfg.DeployConfig),
}
node, _, err := initL2Geth("l2", big.NewInt(int64(cfg.DeployConfig.L2ChainID)), l2Genesis, cfg.JWTFilePath)
require.Nil(t, err)
require.Nil(t, node.Start())
auth := rpc.WithHTTPAuth(gn.NewJWTAuth(cfg.JWTSecret))
l2Node, err := client.NewRPC(ctx, logger, node.WSAuthEndpoint(), auth)
require.Nil(t, err)
// Finally create the engine client
l2Engine, err := sources.NewEngineClient(
l2Node,
logger,
nil,
sources.EngineClientDefaultConfig(&rollup.Config{Genesis: rollupGenesis}),
)
require.Nil(t, err)
l2Client, err := ethclient.Dial(node.HTTPEndpoint())
require.Nil(t, err)
genesisPayload, err := eth.BlockAsPayload(l2GenesisBlock)
require.Nil(t, err)
return &OpGeth{
node: node,
L2Client: l2Client,
l2Engine: l2Engine,
SystemConfig: rollupGenesis.SystemConfig,
L1ChainConfig: l1Genesis.Config,
L2ChainConfig: l2Genesis.Config,
L1Head: l1Block,
L2Head: genesisPayload,
}, nil
}
func (d *OpGeth) Close() {
_ = d.node.Close()
d.l2Engine.Close()
d.L2Client.Close()
}
// AddL2Block Appends a new L2 block to the current chain including the specified transactions
// The L1Info transaction is automatically prepended to the created block
func (d *OpGeth) AddL2Block(ctx context.Context, txs ...*types.Transaction) (*eth.ExecutionPayload, error) {
attrs, err := d.CreatePayloadAttributes(txs...)
if err != nil {
return nil, err
}
res, err := d.StartBlockBuilding(ctx, attrs)
if err != nil {
return nil, err
}
payload, err := d.l2Engine.GetPayload(ctx, *res.PayloadID)
if err != nil {
return nil, err
}
if !reflect.DeepEqual(payload.Transactions, attrs.Transactions) {
return nil, errors.New("required transactions were not included")
}
status, err := d.l2Engine.NewPayload(ctx, payload)
if err != nil {
return nil, err
}
if status.Status != eth.ExecutionValid {
return nil, fmt.Errorf("%w: %s", ErrNewPayloadNotValid, status.Status)
}
fc := eth.ForkchoiceState{
HeadBlockHash: payload.BlockHash,
SafeBlockHash: payload.BlockHash,
}
res, err = d.l2Engine.ForkchoiceUpdate(ctx, &fc, nil)
if err != nil {
return nil, err
}
if res.PayloadStatus.Status != eth.ExecutionValid {
return nil, fmt.Errorf("%w: %s", ErrForkChoiceUpdatedNotValid, res.PayloadStatus.Status)
}
d.L2Head = payload
d.sequenceNum = d.sequenceNum + 1
return payload, nil
}
// StartBlockBuilding begins block building for the specified PayloadAttributes by sending a engine_forkChoiceUpdated call.
// The current L2Head is used as the parent of the new block.
// ErrForkChoiceUpdatedNotValid is returned if the forkChoiceUpdate call returns a status other than valid.
func (d *OpGeth) StartBlockBuilding(ctx context.Context, attrs *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) {
fc := eth.ForkchoiceState{
HeadBlockHash: d.L2Head.BlockHash,
SafeBlockHash: d.L2Head.BlockHash,
}
res, err := d.l2Engine.ForkchoiceUpdate(ctx, &fc, attrs)
if err != nil {
return nil, err
}
if res.PayloadStatus.Status != eth.ExecutionValid {
return nil, fmt.Errorf("%w: %s", ErrForkChoiceUpdatedNotValid, res.PayloadStatus.Status)
}
if res.PayloadID == nil {
return nil, errors.New("forkChoiceUpdated returned nil PayloadID")
}
return res, nil
}
// CreatePayloadAttributes creates a valid PayloadAttributes containing a L1Info deposit transaction followed by the supplied transactions.
func (d *OpGeth) CreatePayloadAttributes(txs ...*types.Transaction) (*eth.PayloadAttributes, error) {
timestamp := d.L2Head.Timestamp + 2
l1Info, err := derive.L1InfoDepositBytes(d.sequenceNum, d.L1Head, d.SystemConfig, false)
if err != nil {
return nil, err
}
var txBytes []hexutil.Bytes
txBytes = append(txBytes, l1Info)
for _, tx := range txs {
bin, err := tx.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("tx marshalling failed: %w", err)
}
txBytes = append(txBytes, bin)
}
attrs := eth.PayloadAttributes{
Timestamp: timestamp,
Transactions: txBytes,
NoTxPool: true,
GasLimit: (*eth.Uint64Quantity)(&d.SystemConfig.GasLimit),
}
return &attrs, nil
}
......@@ -6,91 +6,29 @@ import (
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
gn "github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/require"
)
// TestMissingGasLimit tests that op-geth cannot build a block without gas limit while optimism is active in the chain config.
func TestMissingGasLimit(t *testing.T) {
// Setup an L2 EE and create a client connection to the engine.
// We also need to setup a L1 Genesis to create the rollup genesis.
log := testlog.Logger(t, log.LvlCrit)
cfg := DefaultSystemConfig(t)
cfg.DeployConfig.FundDevAccounts = false
l1Genesis, err := genesis.BuildL1DeveloperGenesis(cfg.DeployConfig)
require.Nil(t, err)
l1Block := l1Genesis.ToBlock()
l2Genesis, err := genesis.BuildL2DeveloperGenesis(cfg.DeployConfig, l1Block)
require.Nil(t, err)
l2GenesisBlock := l2Genesis.ToBlock()
rollupGenesis := rollup.Genesis{
L1: eth.BlockID{
Hash: l1Block.Hash(),
Number: l1Block.NumberU64(),
},
L2: eth.BlockID{
Hash: l2GenesisBlock.Hash(),
Number: l2GenesisBlock.NumberU64(),
},
L2Time: l2GenesisBlock.Time(),
SystemConfig: e2eutils.SystemConfigFromDeployConfig(cfg.DeployConfig),
}
node, _, err := initL2Geth("l2", big.NewInt(int64(cfg.DeployConfig.L2ChainID)), l2Genesis, writeDefaultJWT(t))
require.Nil(t, err)
require.Nil(t, node.Start())
defer node.Close()
auth := rpc.WithHTTPAuth(gn.NewJWTAuth(testingJWTSecret))
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
l2Node, err := client.NewRPC(ctx, log, node.WSAuthEndpoint(), auth)
require.Nil(t, err)
// Finally create the engine client
client, err := sources.NewEngineClient(
l2Node,
log,
nil,
sources.EngineClientDefaultConfig(&rollup.Config{Genesis: rollupGenesis}),
)
require.Nil(t, err)
opGeth, err := NewOpGeth(t, ctx, &cfg)
require.NoError(t, err)
defer opGeth.Close()
attrs := eth.PayloadAttributes{
Timestamp: hexutil.Uint64(l2GenesisBlock.Time() + 2),
Transactions: []hexutil.Bytes{},
NoTxPool: true,
GasLimit: nil, // no gas limit
}
ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
attrs, err := opGeth.CreatePayloadAttributes()
require.NoError(t, err)
// Remove the GasLimit from the otherwise valid attributes
attrs.GasLimit = nil
fc := eth.ForkchoiceState{
HeadBlockHash: l2GenesisBlock.Hash(),
SafeBlockHash: l2GenesisBlock.Hash(),
}
res, err := client.ForkchoiceUpdate(ctx, &fc, &attrs)
res, err := opGeth.StartBlockBuilding(ctx, attrs)
require.ErrorIs(t, err, eth.InputError{})
require.Equal(t, eth.InvalidPayloadAttributes, err.(eth.InputError).Code)
require.Nil(t, res)
......@@ -99,109 +37,36 @@ func TestMissingGasLimit(t *testing.T) {
// TestInvalidDepositInFCU runs an invalid deposit through a FCU/GetPayload/NewPayload/FCU set of calls.
// This tests that deposits must always allow the block to be built even if they are invalid.
func TestInvalidDepositInFCU(t *testing.T) {
// Setup an L2 EE and create a client connection to the engine.
// We also need to setup a L1 Genesis to create the rollup genesis.
log := testlog.Logger(t, log.LvlCrit)
cfg := DefaultSystemConfig(t)
cfg.DeployConfig.FundDevAccounts = false
l1Genesis, err := genesis.BuildL1DeveloperGenesis(cfg.DeployConfig)
require.Nil(t, err)
l1Block := l1Genesis.ToBlock()
l2Genesis, err := genesis.BuildL2DeveloperGenesis(cfg.DeployConfig, l1Block)
require.Nil(t, err)
l2GenesisBlock := l2Genesis.ToBlock()
rollupGenesis := rollup.Genesis{
L1: eth.BlockID{
Hash: l1Block.Hash(),
Number: l1Block.NumberU64(),
},
L2: eth.BlockID{
Hash: l2GenesisBlock.Hash(),
Number: l2GenesisBlock.NumberU64(),
},
L2Time: l2GenesisBlock.Time(),
SystemConfig: e2eutils.SystemConfigFromDeployConfig(cfg.DeployConfig),
}
node, _, err := initL2Geth("l2", big.NewInt(int64(cfg.DeployConfig.L2ChainID)), l2Genesis, writeDefaultJWT(t))
require.Nil(t, err)
require.Nil(t, node.Start())
defer node.Close()
auth := rpc.WithHTTPAuth(gn.NewJWTAuth(testingJWTSecret))
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
l2Node, err := client.NewRPC(ctx, log, node.WSAuthEndpoint(), auth)
require.Nil(t, err)
// Finally create the engine client
client, err := sources.NewEngineClient(
l2Node,
log,
nil,
sources.EngineClientDefaultConfig(&rollup.Config{Genesis: rollupGenesis}),
)
require.Nil(t, err)
// Create the test data (L1 Info Tx and then always failing deposit)
l1Info, err := derive.L1InfoDepositBytes(1, l1Block, rollupGenesis.SystemConfig, false)
require.Nil(t, err)
opGeth, err := NewOpGeth(t, ctx, &cfg)
require.NoError(t, err)
defer opGeth.Close()
// Create a deposit from alice that will always fail (not enough funds)
fromAddr := cfg.Secrets.Addresses().Alice
l2Client, err := ethclient.Dial(node.HTTPEndpoint())
require.Nil(t, err)
balance, err := l2Client.BalanceAt(ctx, fromAddr, nil)
balance, err := opGeth.L2Client.BalanceAt(ctx, fromAddr, nil)
require.Nil(t, err)
require.Equal(t, 0, balance.Cmp(common.Big0))
badDepositTx := types.NewTx(&types.DepositTx{
// TODO: Source Hash
SourceHash: opGeth.L1Head.Hash(),
From: fromAddr,
To: &fromAddr, // send it to ourselves
Value: big.NewInt(params.Ether),
Gas: 25000,
IsSystemTransaction: false,
})
badDeposit, err := badDepositTx.MarshalBinary()
require.Nil(t, err)
attrs := eth.PayloadAttributes{
Timestamp: hexutil.Uint64(l2GenesisBlock.Time() + 2),
Transactions: []hexutil.Bytes{l1Info, badDeposit},
NoTxPool: true,
GasLimit: (*eth.Uint64Quantity)(&rollupGenesis.SystemConfig.GasLimit),
}
// Go through the flow of FCU, GetPayload, NewPayload, FCU
// We are inserting a block with an invalid deposit.
// The invalid deposit should still remain in the block.
ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_, err = opGeth.AddL2Block(ctx, badDepositTx)
require.NoError(t, err)
fc := eth.ForkchoiceState{
HeadBlockHash: l2GenesisBlock.Hash(),
SafeBlockHash: l2GenesisBlock.Hash(),
}
res, err := client.ForkchoiceUpdate(ctx, &fc, &attrs)
// Deposit tx was included, but Alice still shouldn't have any ETH
balance, err = opGeth.L2Client.BalanceAt(ctx, fromAddr, nil)
require.Nil(t, err)
require.Equal(t, eth.ExecutionValid, res.PayloadStatus.Status)
require.NotNil(t, res.PayloadID)
payload, err := client.GetPayload(ctx, *res.PayloadID)
require.Nil(t, err)
require.NotNil(t, payload)
require.Equal(t, payload.Transactions, attrs.Transactions) // Ensure we don't drop the transactions
status, err := client.NewPayload(ctx, payload)
require.Nil(t, err)
require.Equal(t, eth.ExecutionValid, status.Status)
fc.HeadBlockHash = payload.BlockHash
res, err = client.ForkchoiceUpdate(ctx, &fc, nil)
require.Nil(t, err)
require.Equal(t, eth.ExecutionValid, res.PayloadStatus.Status)
require.Equal(t, 0, balance.Cmp(common.Big0))
}
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