Commit 705db877 authored by Evan Richard's avatar Evan Richard Committed by GitHub

Ecotone/Dencun implementation changes (#8707)

* op-node: L2 Dencun implementation updates

init branch

Update reference to Eclipse/Ecotone in specs.

Pull in Danyal's deposit source and add additional tests.

Add notion of ParentBeaconRoot and build a contract deploy tx in attributes.go.

Add a test for activating dencun l2 fork after genesis.

Add draft ecotone setup.

Add first pass of Eclipse upgrade txns

Fix tests/compile

Code review feedback

Obey linter.

Move ecotone setup to helpers.go; get the ParentBeaconBlockRoot from the l1Info in attributes.go.

chore(op-node): Add tests for Ecotone deposit transactions (#8746)

* Source hash teEvanJRichard <evan@oplabs.co>

dencun review fixes

derive: ecotone upgradeTo abi encoding

op-e2e: test L2 exclusion of blob-txs in Ecotone

op-node/rollup: deduplicate ecotone activation helper func, fix rollup config var name

op-chain-ops: clarify 4788 contract nonce

op-node/rollup: add setEcotone to ecotone upgrade txs

dencun review fixes

Dencun: P2P / EngineAPI / ExecutionPayloadEnvelope changes

Includes:
- Pass through execution payload (Envelope type everywhere) by Danyal,
  extended by Proto
- Fix ecotone upgrade txns, by Danyal
- ci fixes by Danyal
- P2P Req/Resp (version based encoding/decoding) by Danyal
- EngineAPI v3 usage by Danyl, rebased by Proto on EngineController (from
  trianglesphere)
- Block v3 Gossip validation, by Danyal
- Block v3 Gossip publishing, by Proto

Rebased on updated Ecotone / Dencun base branch

op-e2e: fix upgrade-txs count in test

op-node: fix l1 info scalar migration, implement dencun review suggestions

op-node: more dencun review nit fixes

op-node: rabbit suggestions, but fixed

Fix nil pointer in p2p sync for Ecotone blocks

dencun: fix more nits

op-e2e: fix lint
Co-authored-by: default avatarDanyal Prout <me@dany.al>
Co-authored-by: default avatarprotolambda <proto@protolambda.com>
Co-authored-by: default avatarEvanJRichard <evan@oplabs.co>

* Add tests for attribute matching

* Provide a no-op blob fetcher to prevent derivation error

Fail tests when there is an unknown error

Fix typo / empty array to nil

Update L2 tests to ecotone / add additional checks

* op-e2e: dencun action-test setup fixes

* op-node: op-conductor dencun todo

* dencun: fix review nit about parent beacon block root gossip check style

---------
Co-authored-by: default avatarDanyal Prout <me@dany.al>
Co-authored-by: default avatarprotolambda <proto@protolambda.com>
parent cceb207a
......@@ -24,7 +24,7 @@ import (
type gossipNoop struct{}
func (g *gossipNoop) OnUnsafeL2Payload(_ context.Context, _ peer.ID, _ *eth.ExecutionPayload) error {
func (g *gossipNoop) OnUnsafeL2Payload(_ context.Context, _ peer.ID, _ *eth.ExecutionPayloadEnvelope) error {
return nil
}
......@@ -36,7 +36,7 @@ func (g *gossipConfig) P2PSequencerAddress() common.Address {
type l2Chain struct{}
func (l *l2Chain) PayloadByNumber(_ context.Context, _ uint64) (*eth.ExecutionPayload, error) {
func (l *l2Chain) PayloadByNumber(_ context.Context, _ uint64) (*eth.ExecutionPayloadEnvelope, error) {
return nil, nil
}
......
......@@ -64,8 +64,8 @@ func NewL2Genesis(config *DeployConfig, block *types.Block) (*core.Genesis, erro
RegolithTime: config.RegolithTime(block.Time()),
CanyonTime: config.CanyonTime(block.Time()),
ShanghaiTime: config.CanyonTime(block.Time()),
EcotoneTime: config.EcotoneTime(block.Time()), // separate from Dencun, for Blob-fees support activation
CancunTime: nil, // no Dencun on L2 yet.
CancunTime: config.EcotoneTime(block.Time()),
EcotoneTime: config.EcotoneTime(block.Time()),
InteropTime: config.InteropTime(block.Time()),
Optimism: &params.OptimismConfig{
EIP1559Denominator: eip1559Denom,
......
// Code generated by mockery v2.28.1. DO NOT EDIT.
// Code generated by mockery v2.39.1. DO NOT EDIT.
package mocks
......@@ -29,6 +29,10 @@ func (_m *SequencerControl) EXPECT() *SequencerControl_Expecter {
func (_m *SequencerControl) LatestUnsafeBlock(ctx context.Context) (eth.BlockInfo, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for LatestUnsafeBlock")
}
var r0 eth.BlockInfo
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) (eth.BlockInfo, error)); ok {
......@@ -80,11 +84,15 @@ func (_c *SequencerControl_LatestUnsafeBlock_Call) RunAndReturn(run func(context
}
// PostUnsafePayload provides a mock function with given fields: ctx, payload
func (_m *SequencerControl) PostUnsafePayload(ctx context.Context, payload *eth.ExecutionPayload) error {
func (_m *SequencerControl) PostUnsafePayload(ctx context.Context, payload *eth.ExecutionPayloadEnvelope) error {
ret := _m.Called(ctx, payload)
if len(ret) == 0 {
panic("no return value specified for PostUnsafePayload")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *eth.ExecutionPayload) error); ok {
if rf, ok := ret.Get(0).(func(context.Context, *eth.ExecutionPayloadEnvelope) error); ok {
r0 = rf(ctx, payload)
} else {
r0 = ret.Error(0)
......@@ -100,14 +108,14 @@ type SequencerControl_PostUnsafePayload_Call struct {
// PostUnsafePayload is a helper method to define mock.On call
// - ctx context.Context
// - payload *eth.ExecutionPayload
// - payload *eth.ExecutionPayloadEnvelope
func (_e *SequencerControl_Expecter) PostUnsafePayload(ctx interface{}, payload interface{}) *SequencerControl_PostUnsafePayload_Call {
return &SequencerControl_PostUnsafePayload_Call{Call: _e.mock.On("PostUnsafePayload", ctx, payload)}
}
func (_c *SequencerControl_PostUnsafePayload_Call) Run(run func(ctx context.Context, payload *eth.ExecutionPayload)) *SequencerControl_PostUnsafePayload_Call {
func (_c *SequencerControl_PostUnsafePayload_Call) Run(run func(ctx context.Context, payload *eth.ExecutionPayloadEnvelope)) *SequencerControl_PostUnsafePayload_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*eth.ExecutionPayload))
run(args[0].(context.Context), args[1].(*eth.ExecutionPayloadEnvelope))
})
return _c
}
......@@ -117,7 +125,7 @@ func (_c *SequencerControl_PostUnsafePayload_Call) Return(_a0 error) *SequencerC
return _c
}
func (_c *SequencerControl_PostUnsafePayload_Call) RunAndReturn(run func(context.Context, *eth.ExecutionPayload) error) *SequencerControl_PostUnsafePayload_Call {
func (_c *SequencerControl_PostUnsafePayload_Call) RunAndReturn(run func(context.Context, *eth.ExecutionPayloadEnvelope) error) *SequencerControl_PostUnsafePayload_Call {
_c.Call.Return(run)
return _c
}
......@@ -126,6 +134,10 @@ func (_c *SequencerControl_PostUnsafePayload_Call) RunAndReturn(run func(context
func (_m *SequencerControl) SequencerActive(ctx context.Context) (bool, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for SequencerActive")
}
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) (bool, error)); ok {
......@@ -178,6 +190,10 @@ func (_c *SequencerControl_SequencerActive_Call) RunAndReturn(run func(context.C
func (_m *SequencerControl) StartSequencer(ctx context.Context, hash common.Hash) error {
ret := _m.Called(ctx, hash)
if len(ret) == 0 {
panic("no return value specified for StartSequencer")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, common.Hash) error); ok {
r0 = rf(ctx, hash)
......@@ -221,6 +237,10 @@ func (_c *SequencerControl_StartSequencer_Call) RunAndReturn(run func(context.Co
func (_m *SequencerControl) StopSequencer(ctx context.Context) (common.Hash, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for StopSequencer")
}
var r0 common.Hash
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) (common.Hash, error)); ok {
......@@ -271,13 +291,12 @@ func (_c *SequencerControl_StopSequencer_Call) RunAndReturn(run func(context.Con
return _c
}
type mockConstructorTestingTNewSequencerControl interface {
// NewSequencerControl creates a new instance of SequencerControl. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewSequencerControl(t interface {
mock.TestingT
Cleanup(func())
}
// NewSequencerControl creates a new instance of SequencerControl. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewSequencerControl(t mockConstructorTestingTNewSequencerControl) *SequencerControl {
}) *SequencerControl {
mock := &SequencerControl{}
mock.Mock.Test(t)
......
......@@ -17,7 +17,7 @@ type SequencerControl interface {
StopSequencer(ctx context.Context) (common.Hash, error)
SequencerActive(ctx context.Context) (bool, error)
LatestUnsafeBlock(ctx context.Context) (eth.BlockInfo, error)
PostUnsafePayload(ctx context.Context, payload *eth.ExecutionPayload) error
PostUnsafePayload(ctx context.Context, payload *eth.ExecutionPayloadEnvelope) error
}
// NewSequencerControl creates a new SequencerControl instance.
......@@ -56,6 +56,6 @@ func (s *sequencerController) SequencerActive(ctx context.Context) (bool, error)
}
// PostUnsafePayload implements SequencerControl.
func (s *sequencerController) PostUnsafePayload(ctx context.Context, payload *eth.ExecutionPayload) error {
func (s *sequencerController) PostUnsafePayload(ctx context.Context, payload *eth.ExecutionPayloadEnvelope) error {
return s.node.PostUnsafePayload(ctx, payload)
}
......@@ -559,7 +559,9 @@ func (oc *OpConductor) startSequencer() error {
if uint64(unsafeInCons.BlockNumber)-unsafeInNode.NumberU64() == 1 {
// tries to post the unsafe head to op-node when head is only 1 block behind (most likely due to gossip delay)
if err = oc.ctrl.PostUnsafePayload(context.Background(), unsafeInCons); err != nil {
// TODO(ethereum-optimism/optimism#9064): op-conductor Dencun changes.
envelope := &eth.ExecutionPayloadEnvelope{ExecutionPayload: unsafeInCons}
if err = oc.ctrl.PostUnsafePayload(context.Background(), envelope); err != nil {
oc.log.Error("failed to post unsafe head payload to op-node", "err", err)
}
}
......
// Code generated by mockery v2.28.1. DO NOT EDIT.
// Code generated by mockery v2.39.1. DO NOT EDIT.
package mocks
......@@ -24,6 +24,10 @@ func (_m *Consensus) EXPECT() *Consensus_Expecter {
func (_m *Consensus) AddNonVoter(id string, addr string) error {
ret := _m.Called(id, addr)
if len(ret) == 0 {
panic("no return value specified for AddNonVoter")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(id, addr)
......@@ -67,6 +71,10 @@ func (_c *Consensus_AddNonVoter_Call) RunAndReturn(run func(string, string) erro
func (_m *Consensus) AddVoter(id string, addr string) error {
ret := _m.Called(id, addr)
if len(ret) == 0 {
panic("no return value specified for AddVoter")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(id, addr)
......@@ -110,6 +118,10 @@ func (_c *Consensus_AddVoter_Call) RunAndReturn(run func(string, string) error)
func (_m *Consensus) CommitUnsafePayload(payload *eth.ExecutionPayload) error {
ret := _m.Called(payload)
if len(ret) == 0 {
panic("no return value specified for CommitUnsafePayload")
}
var r0 error
if rf, ok := ret.Get(0).(func(*eth.ExecutionPayload) error); ok {
r0 = rf(payload)
......@@ -152,6 +164,10 @@ func (_c *Consensus_CommitUnsafePayload_Call) RunAndReturn(run func(*eth.Executi
func (_m *Consensus) DemoteVoter(id string) error {
ret := _m.Called(id)
if len(ret) == 0 {
panic("no return value specified for DemoteVoter")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(id)
......@@ -194,6 +210,10 @@ func (_c *Consensus_DemoteVoter_Call) RunAndReturn(run func(string) error) *Cons
func (_m *Consensus) LatestUnsafePayload() *eth.ExecutionPayload {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for LatestUnsafePayload")
}
var r0 *eth.ExecutionPayload
if rf, ok := ret.Get(0).(func() *eth.ExecutionPayload); ok {
r0 = rf()
......@@ -237,6 +257,10 @@ func (_c *Consensus_LatestUnsafePayload_Call) RunAndReturn(run func() *eth.Execu
func (_m *Consensus) Leader() bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Leader")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
......@@ -278,6 +302,10 @@ func (_c *Consensus_Leader_Call) RunAndReturn(run func() bool) *Consensus_Leader
func (_m *Consensus) LeaderCh() <-chan bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for LeaderCh")
}
var r0 <-chan bool
if rf, ok := ret.Get(0).(func() <-chan bool); ok {
r0 = rf()
......@@ -321,6 +349,10 @@ func (_c *Consensus_LeaderCh_Call) RunAndReturn(run func() <-chan bool) *Consens
func (_m *Consensus) LeaderWithID() (string, string) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for LeaderWithID")
}
var r0 string
var r1 string
if rf, ok := ret.Get(0).(func() (string, string)); ok {
......@@ -372,6 +404,10 @@ func (_c *Consensus_LeaderWithID_Call) RunAndReturn(run func() (string, string))
func (_m *Consensus) RemoveServer(id string) error {
ret := _m.Called(id)
if len(ret) == 0 {
panic("no return value specified for RemoveServer")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(id)
......@@ -414,6 +450,10 @@ func (_c *Consensus_RemoveServer_Call) RunAndReturn(run func(string) error) *Con
func (_m *Consensus) ServerID() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for ServerID")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
......@@ -455,6 +495,10 @@ func (_c *Consensus_ServerID_Call) RunAndReturn(run func() string) *Consensus_Se
func (_m *Consensus) Shutdown() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Shutdown")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
......@@ -496,6 +540,10 @@ func (_c *Consensus_Shutdown_Call) RunAndReturn(run func() error) *Consensus_Shu
func (_m *Consensus) TransferLeader() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for TransferLeader")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
......@@ -537,6 +585,10 @@ func (_c *Consensus_TransferLeader_Call) RunAndReturn(run func() error) *Consens
func (_m *Consensus) TransferLeaderTo(id string, addr string) error {
ret := _m.Called(id, addr)
if len(ret) == 0 {
panic("no return value specified for TransferLeaderTo")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(id, addr)
......@@ -576,13 +628,12 @@ func (_c *Consensus_TransferLeaderTo_Call) RunAndReturn(run func(string, string)
return _c
}
type mockConstructorTestingTNewConsensus interface {
// NewConsensus creates a new instance of Consensus. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewConsensus(t interface {
mock.TestingT
Cleanup(func())
}
// NewConsensus creates a new instance of Consensus. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewConsensus(t mockConstructorTestingTNewConsensus) *Consensus {
}) *Consensus {
mock := &Consensus{}
mock.Mock.Test(t)
......
// Code generated by mockery v2.28.1. DO NOT EDIT.
// Code generated by mockery v2.39.1. DO NOT EDIT.
package mocks
......@@ -21,6 +21,10 @@ func (_m *HealthMonitor) EXPECT() *HealthMonitor_Expecter {
func (_m *HealthMonitor) Start() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Start")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
......@@ -62,6 +66,10 @@ func (_c *HealthMonitor_Start_Call) RunAndReturn(run func() error) *HealthMonito
func (_m *HealthMonitor) Stop() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Stop")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
......@@ -103,6 +111,10 @@ func (_c *HealthMonitor_Stop_Call) RunAndReturn(run func() error) *HealthMonitor
func (_m *HealthMonitor) Subscribe() <-chan bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Subscribe")
}
var r0 <-chan bool
if rf, ok := ret.Get(0).(func() <-chan bool); ok {
r0 = rf()
......@@ -142,13 +154,12 @@ func (_c *HealthMonitor_Subscribe_Call) RunAndReturn(run func() <-chan bool) *He
return _c
}
type mockConstructorTestingTNewHealthMonitor interface {
// NewHealthMonitor creates a new instance of HealthMonitor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewHealthMonitor(t interface {
mock.TestingT
Cleanup(func())
}
// NewHealthMonitor creates a new instance of HealthMonitor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewHealthMonitor(t mockConstructorTestingTNewHealthMonitor) *HealthMonitor {
}) *HealthMonitor {
mock := &HealthMonitor{}
mock.Mock.Test(t)
......
package actions
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"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/log"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions"
"github.com/ethereum-optimism/optimism/op-service/testlog"
)
......@@ -99,3 +102,158 @@ func TestDencunL1ForkAtGenesis(gt *testing.T) {
require.Equal(t, l1Head.Hash(), verifier.SyncStatus().SafeL2.L1Origin.Hash, "verifier synced L1 chain that includes Cancun headers")
require.Equal(t, sequencer.SyncStatus().UnsafeL2, verifier.SyncStatus().UnsafeL2, "verifier and sequencer agree")
}
func verifyPreEcotoneBlock(gt *testing.T, header *types.Header) {
require.Nil(gt, header.ParentBeaconRoot)
require.Nil(gt, header.ExcessBlobGas)
require.Nil(gt, header.BlobGasUsed)
}
func verifyEcotoneBlock(gt *testing.T, header *types.Header) {
require.NotNil(gt, header.ParentBeaconRoot)
require.NotNil(gt, header.ExcessBlobGas)
require.Equal(gt, *header.ExcessBlobGas, uint64(0))
require.NotNil(gt, header.BlobGasUsed)
require.Equal(gt, *header.BlobGasUsed, uint64(0))
}
func TestDencunL2ForkAfterGenesis(gt *testing.T) {
t := NewDefaultTesting(gt)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
cancunOffset := hexutil.Uint64(0)
dp.DeployConfig.L1CancunTimeOffset = &cancunOffset
// This test wil fork on the second block
offset := hexutil.Uint64(dp.DeployConfig.L2BlockTime * 2)
dp.DeployConfig.L2GenesisCanyonTimeOffset = &offset
dp.DeployConfig.L2GenesisDeltaTimeOffset = &offset
dp.DeployConfig.L2GenesisEcotoneTimeOffset = &offset
sd := e2eutils.Setup(t, dp, defaultAlloc)
log := testlog.Logger(t, log.LvlDebug)
_, _, _, sequencer, engine, verifier, _, _ := setupReorgTestActors(t, dp, sd, log)
// start op-nodes
sequencer.ActL2PipelineFull(t)
verifier.ActL2PipelineFull(t)
// Genesis block is pre-ecotone
verifyPreEcotoneBlock(gt, engine.l2Chain.CurrentBlock())
// Block before fork block
sequencer.ActL2StartBlock(t)
sequencer.ActL2EndBlock(t)
verifyPreEcotoneBlock(gt, engine.l2Chain.CurrentBlock())
// Fork block is ecotone
sequencer.ActL2StartBlock(t)
sequencer.ActL2EndBlock(t)
verifyEcotoneBlock(gt, engine.l2Chain.CurrentBlock())
// Blocks post fork have Ecotone properties
sequencer.ActL2StartBlock(t)
sequencer.ActL2EndBlock(t)
verifyEcotoneBlock(gt, engine.l2Chain.CurrentBlock())
}
func TestDencunL2ForkAtGenesis(gt *testing.T) {
t := NewDefaultTesting(gt)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
offset := hexutil.Uint64(0)
dp.DeployConfig.L2GenesisRegolithTimeOffset = &offset
dp.DeployConfig.L1CancunTimeOffset = &offset
dp.DeployConfig.L2GenesisCanyonTimeOffset = &offset
dp.DeployConfig.L2GenesisDeltaTimeOffset = &offset
dp.DeployConfig.L2GenesisEcotoneTimeOffset = &offset
sd := e2eutils.Setup(t, dp, defaultAlloc)
log := testlog.Logger(t, log.LvlDebug)
_, _, _, sequencer, engine, verifier, _, _ := setupReorgTestActors(t, dp, sd, log)
// start op-nodes
sequencer.ActL2PipelineFull(t)
verifier.ActL2PipelineFull(t)
// Genesis block has ecotone properties
verifyEcotoneBlock(gt, engine.l2Chain.CurrentBlock())
// Blocks post fork have Ecotone properties
sequencer.ActL2StartBlock(t)
sequencer.ActL2EndBlock(t)
verifyEcotoneBlock(gt, engine.l2Chain.CurrentBlock())
}
func aliceSimpleBlobTx(t Testing, dp *e2eutils.DeployParams) *types.Transaction {
txData := transactions.CreateEmptyBlobTx(true, dp.DeployConfig.L2ChainID)
// Manual signer creation, so we can sign a blob tx on the chain,
// even though we have disabled cancun signer support in Ecotone.
signer := types.NewCancunSigner(txData.ChainID.ToBig())
tx, err := types.SignNewTx(dp.Secrets.Alice, signer, txData)
require.NoError(t, err, "must sign tx")
return tx
}
func newEngine(t Testing, sd *e2eutils.SetupData, log log.Logger) *L2Engine {
jwtPath := e2eutils.WriteDefaultJWT(t)
return NewL2Engine(t, log, sd.L2Cfg, sd.RollupCfg.Genesis.L1, jwtPath)
}
// TestDencunBlobTxRPC tries to send a Blob tx to the L2 engine via RPC, it should not be accepted.
func TestDencunBlobTxRPC(gt *testing.T) {
t := NewDefaultTesting(gt)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
offset := hexutil.Uint64(0)
dp.DeployConfig.L2GenesisRegolithTimeOffset = &offset
dp.DeployConfig.L2GenesisCanyonTimeOffset = &offset
dp.DeployConfig.L2GenesisDeltaTimeOffset = &offset
dp.DeployConfig.L2GenesisEcotoneTimeOffset = &offset
sd := e2eutils.Setup(t, dp, defaultAlloc)
log := testlog.Logger(t, log.LvlDebug)
engine := newEngine(t, sd, log)
cl := engine.EthClient()
tx := aliceSimpleBlobTx(t, dp)
err := cl.SendTransaction(context.Background(), tx)
require.ErrorContains(t, err, "transaction type not supported")
}
// TestDencunBlobTxInTxPool tries to insert a blob tx directly into the tx pool, it should not be accepted.
func TestDencunBlobTxInTxPool(gt *testing.T) {
t := NewDefaultTesting(gt)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
offset := hexutil.Uint64(0)
dp.DeployConfig.L2GenesisRegolithTimeOffset = &offset
dp.DeployConfig.L2GenesisCanyonTimeOffset = &offset
dp.DeployConfig.L2GenesisDeltaTimeOffset = &offset
dp.DeployConfig.L2GenesisEcotoneTimeOffset = &offset
sd := e2eutils.Setup(t, dp, defaultAlloc)
log := testlog.Logger(t, log.LvlDebug)
engine := newEngine(t, sd, log)
tx := aliceSimpleBlobTx(t, dp)
errs := engine.eth.TxPool().Add([]*types.Transaction{tx}, true, true)
require.ErrorContains(t, errs[0], "transaction type not supported")
}
// TestDencunBlobTxInclusion tries to send a Blob tx to the L2 engine, it should not be accepted.
func TestDencunBlobTxInclusion(gt *testing.T) {
t := NewDefaultTesting(gt)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
offset := hexutil.Uint64(0)
dp.DeployConfig.L2GenesisRegolithTimeOffset = &offset
dp.DeployConfig.L2GenesisCanyonTimeOffset = &offset
dp.DeployConfig.L2GenesisDeltaTimeOffset = &offset
dp.DeployConfig.L2GenesisEcotoneTimeOffset = &offset
sd := e2eutils.Setup(t, dp, defaultAlloc)
log := testlog.Logger(t, log.LvlDebug)
_, engine, sequencer := setupSequencerTest(t, sd, log)
sequencer.ActL2PipelineFull(t)
tx := aliceSimpleBlobTx(t, dp)
sequencer.ActL2StartBlock(t)
err := engine.engineApi.IncludeTx(tx, dp.Addresses.Alice)
require.ErrorContains(t, err, "invalid L2 block (tx 1): failed to apply transaction to L2 block (tx 1): transaction type not supported")
}
......@@ -50,7 +50,7 @@ func TestL2EngineAPI(gt *testing.T) {
require.NoError(t, err)
// apply the payload
status, err := l2Cl.NewPayload(t.Ctx(), payloadA)
status, err := l2Cl.NewPayload(t.Ctx(), payloadA, nil)
require.NoError(t, err)
require.Equal(t, status.Status, eth.ExecutionValid)
require.Equal(t, genesisBlock.Hash(), engine.l2Chain.CurrentBlock().Hash(), "processed payloads are not immediately canonical")
......@@ -73,7 +73,7 @@ func TestL2EngineAPI(gt *testing.T) {
require.NoError(t, err)
// apply the payload
status, err = l2Cl.NewPayload(t.Ctx(), payloadB)
status, err = l2Cl.NewPayload(t.Ctx(), payloadB, nil)
require.NoError(t, err)
require.Equal(t, status.Status, eth.ExecutionValid)
require.Equal(t, payloadA.BlockHash, engine.l2Chain.CurrentBlock().Hash(), "processed payloads are not immediately canonical")
......@@ -154,12 +154,13 @@ func TestL2EngineAPIBlockBuilding(gt *testing.T) {
engine.ActL2IncludeTx(dp.Addresses.Alice)(t)
}
payload, err := l2Cl.GetPayload(t.Ctx(), *fcRes.PayloadID)
envelope, err := l2Cl.GetPayload(t.Ctx(), *fcRes.PayloadID)
payload := envelope.ExecutionPayload
require.NoError(t, err)
require.Equal(t, parent.Hash(), payload.ParentHash, "block builds on parent block")
// apply the payload
status, err := l2Cl.NewPayload(t.Ctx(), payload)
status, err := l2Cl.NewPayload(t.Ctx(), payload, nil)
require.NoError(t, err)
require.Equal(t, status.Status, eth.ExecutionValid)
require.Equal(t, parent.Hash(), engine.l2Chain.CurrentBlock().Hash(), "processed payloads are not immediately canonical")
......
......@@ -28,6 +28,18 @@ func (m *MockL1OriginSelector) FindL1Origin(ctx context.Context, l2Head eth.L2Bl
return m.actual.FindL1Origin(ctx, l2Head)
}
// emptyL1BlobsFetcher is a no-op blobs provider. The actions test batcher currently only supports using calldata.
type emptyL1BlobsFetcher struct {
t Testing
}
var _ derive.L1BlobsFetcher = &emptyL1BlobsFetcher{}
func (e *emptyL1BlobsFetcher) GetBlobs(ctx context.Context, ref eth.L1BlockRef, hashes []eth.IndexedBlobHash) ([]*eth.Blob, error) {
e.t.Fatal("actions test do not support blobs")
return nil, nil
}
// L2Sequencer is an actor that functions like a rollup node,
// without the full P2P/API/Node stack, but just the derivation state, and simplified driver with sequencing ability.
type L2Sequencer struct {
......@@ -41,7 +53,8 @@ type L2Sequencer struct {
}
func NewL2Sequencer(t Testing, log log.Logger, l1 derive.L1Fetcher, eng L2API, cfg *rollup.Config, seqConfDepth uint64) *L2Sequencer {
ver := NewL2Verifier(t, log, l1, eng, cfg, &sync.Config{})
mockBlobFetcher := &emptyL1BlobsFetcher{t: t}
ver := NewL2Verifier(t, log, l1, mockBlobFetcher, eng, cfg, &sync.Config{})
attrBuilder := derive.NewFetchingAttributesBuilder(cfg, l1, eng)
seqConfDepthL1 := driver.NewConfDepth(seqConfDepth, ver.l1State.L1Head, l1)
l1OriginSelector := &MockL1OriginSelector{
......
......@@ -58,17 +58,9 @@ type L2API interface {
OutputV0AtBlock(ctx context.Context, blockHash common.Hash) (*eth.OutputV0, error)
}
type EmptyBlobsSource struct {
}
func (b *EmptyBlobsSource) GetBlobs(ctx context.Context, ref eth.L1BlockRef, hashes []eth.IndexedBlobHash) ([]*eth.Blob, error) {
return nil, nil
}
func NewL2Verifier(t Testing, log log.Logger, l1 derive.L1Fetcher, eng L2API, cfg *rollup.Config, syncCfg *sync.Config) *L2Verifier {
func NewL2Verifier(t Testing, log log.Logger, l1 derive.L1Fetcher, blobsSrc derive.L1BlobsFetcher, eng L2API, cfg *rollup.Config, syncCfg *sync.Config) *L2Verifier {
metrics := &testutils.TestDerivationMetrics{}
engine := derive.NewEngineController(eng, log, metrics, cfg, syncCfg.SyncMode)
blobsSrc := &EmptyBlobsSource{}
pipeline := derive.NewDerivationPipeline(log, cfg, l1, blobsSrc, eng, engine, metrics, syncCfg)
pipeline.Reset()
......@@ -138,7 +130,7 @@ func (s *l2VerifierBackend) SequencerActive(ctx context.Context) (bool, error) {
return false, nil
}
func (s *l2VerifierBackend) OnUnsafeL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error {
func (s *l2VerifierBackend) OnUnsafeL2Payload(ctx context.Context, envelope *eth.ExecutionPayloadEnvelope) error {
return nil
}
......@@ -242,6 +234,8 @@ func (s *L2Verifier) ActL2PipelineStep(t Testing) {
return
} else if err != nil && errors.Is(err, derive.ErrCritical) {
t.Fatalf("derivation failed critically: %v", err)
} else if err != nil {
t.Fatalf("derivation failed: %v", err)
} else {
return
}
......@@ -255,7 +249,7 @@ func (s *L2Verifier) ActL2PipelineFull(t Testing) {
}
// ActL2UnsafeGossipReceive creates an action that can receive an unsafe execution payload, like gossipsub
func (s *L2Verifier) ActL2UnsafeGossipReceive(payload *eth.ExecutionPayload) Action {
func (s *L2Verifier) ActL2UnsafeGossipReceive(payload *eth.ExecutionPayloadEnvelope) Action {
return func(t Testing) {
s.derivation.AddUnsafePayload(payload)
}
......
......@@ -17,7 +17,8 @@ func setupVerifier(t Testing, sd *e2eutils.SetupData, log log.Logger, l1F derive
jwtPath := e2eutils.WriteDefaultJWT(t)
engine := NewL2Engine(t, log, sd.L2Cfg, sd.RollupCfg.Genesis.L1, jwtPath)
engCl := engine.EngineClient(t, sd.RollupCfg)
verifier := NewL2Verifier(t, log, l1F, engCl, sd.RollupCfg, syncCfg)
mockBlobFetcher := &emptyL1BlobsFetcher{t: t}
verifier := NewL2Verifier(t, log, l1F, mockBlobFetcher, engCl, sd.RollupCfg, syncCfg)
return engine, verifier
}
......
......@@ -135,9 +135,9 @@ func BatcherKeyRotation(gt *testing.T, deltaTimeOffset *hexutil.Uint64) {
// 12 new L2 blocks: 5 with origin before L1 block with batch, 6 with origin of L1 block
// with batch, 1 with new origin that changed the batcher
for i := 0; i <= 12; i++ {
payload, err := engCl.PayloadByNumber(t.Ctx(), sequencer.L2Safe().Number+uint64(i))
envelope, err := engCl.PayloadByNumber(t.Ctx(), sequencer.L2Safe().Number+uint64(i))
require.NoError(t, err)
ref, err := derive.PayloadToBlockRef(sd.RollupCfg, payload)
ref, err := derive.PayloadToBlockRef(sd.RollupCfg, envelope.ExecutionPayload)
require.NoError(t, err)
if i < 6 {
require.Equal(t, ref.L1Origin.Number, cfgChangeL1BlockNum-2)
......@@ -148,7 +148,7 @@ func BatcherKeyRotation(gt *testing.T, deltaTimeOffset *hexutil.Uint64) {
} else {
require.Equal(t, ref.L1Origin.Number, cfgChangeL1BlockNum)
require.Equal(t, ref.SequenceNumber, uint64(0), "first L2 block with this origin")
sysCfg, err := derive.PayloadToSystemConfig(sd.RollupCfg, payload)
sysCfg, err := derive.PayloadToSystemConfig(sd.RollupCfg, envelope.ExecutionPayload)
require.NoError(t, err)
require.Equal(t, dp.Addresses.Bob, sysCfg.BatcherAddr, "bob should be batcher now")
}
......@@ -305,9 +305,9 @@ func GPOParamsChange(gt *testing.T, deltaTimeOffset *hexutil.Uint64) {
sequencer.ActBuildToL1HeadExcl(t)
engCl := seqEngine.EngineClient(t, sd.RollupCfg)
payload, err := engCl.PayloadByLabel(t.Ctx(), eth.Unsafe)
envelope, err := engCl.PayloadByLabel(t.Ctx(), eth.Unsafe)
require.NoError(t, err)
sysCfg, err := derive.PayloadToSystemConfig(sd.RollupCfg, payload)
sysCfg, err := derive.PayloadToSystemConfig(sd.RollupCfg, envelope.ExecutionPayload)
require.NoError(t, err)
require.Equal(t, sd.RollupCfg.Genesis.SystemConfig, sysCfg, "still have genesis system config before we adopt the L1 block with GPO change")
......@@ -318,9 +318,9 @@ func GPOParamsChange(gt *testing.T, deltaTimeOffset *hexutil.Uint64) {
seqEngine.ActL2IncludeTx(dp.Addresses.Alice)(t)
sequencer.ActL2EndBlock(t)
payload, err = engCl.PayloadByLabel(t.Ctx(), eth.Unsafe)
envelope, err = engCl.PayloadByLabel(t.Ctx(), eth.Unsafe)
require.NoError(t, err)
sysCfg, err = derive.PayloadToSystemConfig(sd.RollupCfg, payload)
sysCfg, err = derive.PayloadToSystemConfig(sd.RollupCfg, envelope.ExecutionPayload)
require.NoError(t, err)
require.Equal(t, eth.Bytes32(common.BigToHash(big.NewInt(1000))), sysCfg.Overhead, "overhead changed")
require.Equal(t, eth.Bytes32(common.BigToHash(big.NewInt(2_300_000))), sysCfg.Scalar, "scalar changed")
......
package transactions
import (
"crypto/ecdsa"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
......@@ -30,7 +28,7 @@ func init() {
// with thanks to fjl
// https://github.com/ethereum/go-ethereum/commit/2a6beb6a39d7cb3c5906dd4465d65da6efcc73cd
func CreateEmptyBlobTx(key *ecdsa.PrivateKey, withSidecar bool, chainID uint64) *types.BlobTx {
func CreateEmptyBlobTx(withSidecar bool, chainID uint64) *types.BlobTx {
sidecar := &types.BlobTxSidecar{
Blobs: []kzg4844.Blob{emptyBlob},
Commitments: []kzg4844.Commitment{emptyBlobCommit},
......
......@@ -18,6 +18,7 @@ import (
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum-optimism/optimism/op-service/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"
......@@ -135,7 +136,7 @@ func (d *OpGeth) 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) {
func (d *OpGeth) AddL2Block(ctx context.Context, txs ...*types.Transaction) (*eth.ExecutionPayloadEnvelope, error) {
attrs, err := d.CreatePayloadAttributes(txs...)
if err != nil {
return nil, err
......@@ -145,7 +146,9 @@ func (d *OpGeth) AddL2Block(ctx context.Context, txs ...*types.Transaction) (*et
return nil, err
}
payload, err := d.l2Engine.GetPayload(ctx, *res.PayloadID)
envelope, err := d.l2Engine.GetPayload(ctx, *res.PayloadID)
payload := envelope.ExecutionPayload
if err != nil {
return nil, err
}
......@@ -153,7 +156,7 @@ func (d *OpGeth) AddL2Block(ctx context.Context, txs ...*types.Transaction) (*et
return nil, errors.New("required transactions were not included")
}
status, err := d.l2Engine.NewPayload(ctx, payload)
status, err := d.l2Engine.NewPayload(ctx, payload, envelope.ParentBeaconBlockRoot)
if err != nil {
return nil, err
}
......@@ -174,7 +177,7 @@ func (d *OpGeth) AddL2Block(ctx context.Context, txs ...*types.Transaction) (*et
}
d.L2Head = payload
d.sequenceNum = d.sequenceNum + 1
return payload, nil
return envelope, nil
}
// StartBlockBuilding begins block building for the specified PayloadAttributes by sending a engine_forkChoiceUpdated call.
......@@ -221,12 +224,18 @@ func (d *OpGeth) CreatePayloadAttributes(txs ...*types.Transaction) (*eth.Payloa
withdrawals = &types.Withdrawals{}
}
var parentBeaconBlockRoot *common.Hash
if d.L2ChainConfig.IsEcotone(uint64(timestamp)) {
parentBeaconBlockRoot = d.L1Head.ParentBeaconRoot()
}
attrs := eth.PayloadAttributes{
Timestamp: timestamp,
Transactions: txBytes,
NoTxPool: true,
GasLimit: (*eth.Uint64Quantity)(&d.SystemConfig.GasLimit),
Withdrawals: withdrawals,
Timestamp: timestamp,
Transactions: txBytes,
NoTxPool: true,
GasLimit: (*eth.Uint64Quantity)(&d.SystemConfig.GasLimit),
Withdrawals: withdrawals,
ParentBeaconBlockRoot: parentBeaconBlockRoot,
}
return &attrs, nil
}
......@@ -196,14 +196,16 @@ func TestGethOnlyPendingBlockIsLatest(t *testing.T) {
time.Sleep(time.Second * 4) // conservatively wait 4 seconds, CI might lag during block building.
// retrieve the block
payload, err := opGeth.l2Engine.GetPayload(ctx, *res.PayloadID)
envelope, err := opGeth.l2Engine.GetPayload(ctx, *res.PayloadID)
require.NoError(t, err)
payload := envelope.ExecutionPayload
checkPending("retrieved", 0)
require.Len(t, payload.Transactions, 2, "must include L1 info tx and tx from alice")
checkPendingBalance()
// process the block
status, err := opGeth.l2Engine.NewPayload(ctx, payload)
status, err := opGeth.l2Engine.NewPayload(ctx, payload, envelope.ParentBeaconBlockRoot)
require.NoError(t, err)
require.Equal(t, eth.ExecutionValid, status.Status)
checkPending("processed", 0)
......@@ -260,11 +262,11 @@ func TestPreregolith(t *testing.T) {
IsSystemTransaction: false,
})
block, err := opGeth.AddL2Block(ctx, depositTx)
envelope, err := opGeth.AddL2Block(ctx, depositTx)
require.NoError(t, err)
// L1Info tx should report 0 gas used
infoTx, err := opGeth.L2Client.TransactionInBlock(ctx, block.BlockHash, 0)
infoTx, err := opGeth.L2Client.TransactionInBlock(ctx, envelope.ExecutionPayload.BlockHash, 0)
require.NoError(t, err)
infoRcpt, err := opGeth.L2Client.TransactionReceipt(ctx, infoTx.Hash())
require.NoError(t, err)
......@@ -450,11 +452,11 @@ func TestRegolith(t *testing.T) {
IsSystemTransaction: false,
})
block, err := opGeth.AddL2Block(ctx, depositTx)
envelope, err := opGeth.AddL2Block(ctx, depositTx)
require.NoError(t, err)
// L1Info tx should report actual gas used, not 0 or the tx gas limit
infoTx, err := opGeth.L2Client.TransactionInBlock(ctx, block.BlockHash, 0)
infoTx, err := opGeth.L2Client.TransactionInBlock(ctx, envelope.ExecutionPayload.BlockHash, 0)
require.NoError(t, err)
infoRcpt, err := opGeth.L2Client.TransactionReceipt(ctx, infoTx.Hash())
require.NoError(t, err)
......@@ -751,7 +753,7 @@ func TestPreCanyon(t *testing.T) {
b, err := opGeth.AddL2Block(ctx)
require.NoError(t, err)
assert.Nil(t, b.Withdrawals, "should not have withdrawals")
assert.Nil(t, b.ExecutionPayload.Withdrawals, "should not have withdrawals")
l1Block, err := opGeth.L2Client.BlockByNumber(ctx, nil)
require.Nil(t, err)
......@@ -788,19 +790,18 @@ func TestPreCanyon(t *testing.T) {
assert.Equal(t, types.ReceiptStatusFailed, receipt.Status)
})
}
}
func TestCanyon(t *testing.T) {
InitParallel(t)
tests := []struct {
name string
canyonTime hexutil.Uint64
activeCanyon func(ctx context.Context, opGeth *OpGeth)
name string
canyonTime hexutil.Uint64
activateCanyon func(ctx context.Context, opGeth *OpGeth)
}{
{name: "ActivateAtGenesis", canyonTime: 0, activeCanyon: func(ctx context.Context, opGeth *OpGeth) {}},
{name: "ActivateAfterGenesis", canyonTime: 2, activeCanyon: func(ctx context.Context, opGeth *OpGeth) {
{name: "ActivateAtGenesis", canyonTime: 0, activateCanyon: func(ctx context.Context, opGeth *OpGeth) {}},
{name: "ActivateAfterGenesis", canyonTime: 2, activateCanyon: func(ctx context.Context, opGeth *OpGeth) {
// Adding this block advances us to the fork time.
_, err := opGeth.AddL2Block(ctx)
require.NoError(t, err)
......@@ -814,6 +815,7 @@ func TestCanyon(t *testing.T) {
s := hexutil.Uint64(0)
cfg.DeployConfig.L2GenesisRegolithTimeOffset = &s
cfg.DeployConfig.L2GenesisCanyonTimeOffset = &test.canyonTime
cfg.DeployConfig.L2GenesisEcotoneTimeOffset = nil
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
......@@ -822,11 +824,11 @@ func TestCanyon(t *testing.T) {
require.NoError(t, err)
defer opGeth.Close()
test.activeCanyon(ctx, opGeth)
test.activateCanyon(ctx, opGeth)
b, err := opGeth.AddL2Block(ctx)
require.NoError(t, err)
assert.Equal(t, *b.Withdrawals, types.Withdrawals{})
assert.Equal(t, *b.ExecutionPayload.Withdrawals, types.Withdrawals{})
l1Block, err := opGeth.L2Client.BlockByNumber(ctx, nil)
require.Nil(t, err)
......@@ -837,6 +839,7 @@ func TestCanyon(t *testing.T) {
InitParallel(t)
cfg := DefaultSystemConfig(t)
cfg.DeployConfig.L2GenesisCanyonTimeOffset = &test.canyonTime
cfg.DeployConfig.L2GenesisEcotoneTimeOffset = nil
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
......@@ -864,3 +867,158 @@ func TestCanyon(t *testing.T) {
})
}
}
func TestPreEcotone(t *testing.T) {
InitParallel(t)
futureTimestamp := hexutil.Uint64(4)
tests := []struct {
name string
ecotoneTime *hexutil.Uint64
}{
{name: "EcotoneNotScheduled"},
{name: "EcotoneNotYetActive", ecotoneTime: &futureTimestamp},
}
for _, test := range tests {
test := test
t.Run(fmt.Sprintf("NilParentBeaconRoot_%s", test.name), func(t *testing.T) {
InitParallel(t)
cfg := DefaultSystemConfig(t)
cfg.DeployConfig.L2GenesisCanyonTimeOffset = test.ecotoneTime
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
opGeth, err := NewOpGeth(t, ctx, &cfg)
require.NoError(t, err)
defer opGeth.Close()
b, err := opGeth.AddL2Block(ctx)
require.NoError(t, err)
assert.Nil(t, b.ParentBeaconBlockRoot)
l2Block, err := opGeth.L2Client.BlockByNumber(ctx, nil)
require.NoError(t, err)
assert.Nil(t, l2Block.Header().ParentBeaconRoot)
})
t.Run(fmt.Sprintf("RejectTstoreTxn%s", test.name), func(t *testing.T) {
InitParallel(t)
cfg := DefaultSystemConfig(t)
cfg.DeployConfig.L2GenesisCanyonTimeOffset = test.ecotoneTime
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
opGeth, err := NewOpGeth(t, ctx, &cfg)
require.NoError(t, err)
defer opGeth.Close()
tstoreTxn := types.NewTx(&types.DepositTx{
From: cfg.Secrets.Addresses().Alice,
Value: big.NewInt(params.Ether),
Gas: 1000001,
Data: []byte{
byte(vm.PUSH1),
byte(vm.PUSH2),
byte(vm.TSTORE),
},
IsSystemTransaction: false,
})
_, err = opGeth.AddL2Block(ctx, tstoreTxn)
require.NoError(t, err)
receipt, err := opGeth.L2Client.TransactionReceipt(ctx, tstoreTxn.Hash())
require.NoError(t, err)
assert.Equal(t, types.ReceiptStatusFailed, receipt.Status)
})
}
}
func TestEcotone(t *testing.T) {
InitParallel(t)
tests := []struct {
name string
ecotoneTime hexutil.Uint64
activateEcotone func(ctx context.Context, opGeth *OpGeth)
}{
{name: "ActivateAtGenesis", ecotoneTime: 0, activateEcotone: func(ctx context.Context, opGeth *OpGeth) {}},
{name: "ActivateAfterGenesis", ecotoneTime: 2, activateEcotone: func(ctx context.Context, opGeth *OpGeth) {
// Adding this block advances us to the fork time.
_, err := opGeth.AddL2Block(ctx)
require.NoError(t, err)
}},
}
for _, test := range tests {
test := test
t.Run(fmt.Sprintf("HashParentBeaconBlockRoot_%s", test.name), func(t *testing.T) {
InitParallel(t)
cfg := DefaultSystemConfig(t)
s := hexutil.Uint64(0)
cfg.DeployConfig.L2GenesisCanyonTimeOffset = &s
cfg.DeployConfig.L2GenesisDeltaTimeOffset = &s
cfg.DeployConfig.L2GenesisEcotoneTimeOffset = &test.ecotoneTime
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
opGeth, err := NewOpGeth(t, ctx, &cfg)
require.NoError(t, err)
defer opGeth.Close()
test.activateEcotone(ctx, opGeth)
b, err := opGeth.AddL2Block(ctx)
require.NoError(t, err)
require.NotNil(t, b.ParentBeaconBlockRoot)
assert.Equal(t, b.ParentBeaconBlockRoot, opGeth.L1Head.ParentBeaconRoot())
l2Block, err := opGeth.L2Client.BlockByNumber(ctx, nil)
require.NoError(t, err)
assert.NotNil(t, l2Block.Header().ParentBeaconRoot)
assert.Equal(t, l2Block.Header().ParentBeaconRoot, opGeth.L1Head.ParentBeaconRoot())
})
t.Run(fmt.Sprintf("TstoreTxn%s", test.name), func(t *testing.T) {
InitParallel(t)
cfg := DefaultSystemConfig(t)
s := hexutil.Uint64(0)
cfg.DeployConfig.L2GenesisCanyonTimeOffset = &s
cfg.DeployConfig.L2GenesisDeltaTimeOffset = &s
cfg.DeployConfig.L2GenesisEcotoneTimeOffset = &test.ecotoneTime
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
opGeth, err := NewOpGeth(t, ctx, &cfg)
require.NoError(t, err)
defer opGeth.Close()
tstoreTxn := types.NewTx(&types.DepositTx{
From: cfg.Secrets.Addresses().Alice,
Value: big.NewInt(params.Ether),
Gas: 1000001,
Data: []byte{
byte(vm.PUSH1), 0x01,
byte(vm.PUSH1), 0x01,
byte(vm.TSTORE),
byte(vm.PUSH0),
},
IsSystemTransaction: false,
})
_, err = opGeth.AddL2Block(ctx, tstoreTxn)
require.NoError(t, err)
_, err = opGeth.AddL2Block(ctx, tstoreTxn)
require.NoError(t, err)
receipt, err := opGeth.L2Client.TransactionReceipt(ctx, tstoreTxn.Hash())
require.NoError(t, err)
assert.Equal(t, types.ReceiptStatusSuccessful, receipt.Status)
})
}
}
......@@ -200,7 +200,7 @@ func TestPostUnsafePayload(t *testing.T) {
require.NoError(t, err)
payload, err := eth.BlockAsPayload(blockNumberOne, sys.RollupConfig.CanyonTime)
require.NoError(t, err)
err = rollupClient.PostUnsafePayload(ctx, payload)
err = rollupClient.PostUnsafePayload(ctx, &eth.ExecutionPayloadEnvelope{ExecutionPayload: payload})
require.NoError(t, err)
require.NoError(t, wait.ForUnsafeBlock(ctx, rollupClient, 1), "Chain did not advance after posting payload")
......@@ -210,7 +210,7 @@ func TestPostUnsafePayload(t *testing.T) {
payload, err = eth.BlockAsPayload(blockNumberTwo, sys.RollupConfig.CanyonTime)
require.NoError(t, err)
payload.BlockHash = common.Hash{0xaa}
err = rollupClient.PostUnsafePayload(ctx, payload)
err = rollupClient.PostUnsafePayload(ctx, &eth.ExecutionPayloadEnvelope{ExecutionPayload: payload})
require.ErrorContains(t, err, "payload has bad block hash")
}
......
......@@ -188,7 +188,7 @@ func TestSystemE2EDencunAtGenesisWithBlobs(t *testing.T) {
// send a blob-containing txn on l1
ethPrivKey := sys.Cfg.Secrets.Alice
txData := transactions.CreateEmptyBlobTx(ethPrivKey, true, sys.Cfg.L1ChainIDBig().Uint64())
txData := transactions.CreateEmptyBlobTx(true, sys.Cfg.L1ChainIDBig().Uint64())
tx := types.MustSignNewTx(ethPrivKey, types.LatestSignerForChainID(cfg.L1ChainIDBig()), txData)
// send blob-containing txn
sendCtx, sendCancel := context.WithTimeout(context.Background(), 15*time.Second)
......@@ -545,11 +545,11 @@ func TestSystemMockP2P(t *testing.T) {
var published, received []common.Hash
seqTracer, verifTracer := new(FnTracer), new(FnTracer)
seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayload) {
published = append(published, payload.BlockHash)
seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayloadEnvelope) {
published = append(published, payload.ExecutionPayload.BlockHash)
}
verifTracer.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) {
received = append(received, payload.BlockHash)
verifTracer.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) {
received = append(received, payload.ExecutionPayload.BlockHash)
}
cfg.Nodes["sequencer"].Tracer = seqTracer
cfg.Nodes["verifier"].Tracer = verifTracer
......@@ -646,8 +646,8 @@ func TestSystemP2PAltSync(t *testing.T) {
var published []string
seqTracer := new(FnTracer)
// The sequencer still publishes the blocks to the tracer, even if they do not reach the network due to disabled P2P
seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayload) {
published = append(published, payload.ID().String())
seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayloadEnvelope) {
published = append(published, payload.ExecutionPayload.ID().String())
}
// Blocks are now received via the RPC based alt-sync method
cfg.Nodes["sequencer"].Tracer = seqTracer
......@@ -700,8 +700,8 @@ func TestSystemP2PAltSync(t *testing.T) {
Pprof: oppprof.CLIConfig{},
L1EpochPollInterval: time.Second * 10,
Tracer: &FnTracer{
OnUnsafeL2PayloadFn: func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) {
syncedPayloads = append(syncedPayloads, payload.ID().String())
OnUnsafeL2PayloadFn: func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) {
syncedPayloads = append(syncedPayloads, payload.ExecutionPayload.ID().String())
},
},
}
......@@ -790,17 +790,17 @@ func TestSystemDenseTopology(t *testing.T) {
var published, received1, received2, received3 []common.Hash
seqTracer, verifTracer, verifTracer2, verifTracer3 := new(FnTracer), new(FnTracer), new(FnTracer), new(FnTracer)
seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayload) {
published = append(published, payload.BlockHash)
seqTracer.OnPublishL2PayloadFn = func(ctx context.Context, payload *eth.ExecutionPayloadEnvelope) {
published = append(published, payload.ExecutionPayload.BlockHash)
}
verifTracer.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) {
received1 = append(received1, payload.BlockHash)
verifTracer.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) {
received1 = append(received1, payload.ExecutionPayload.BlockHash)
}
verifTracer2.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) {
received2 = append(received2, payload.BlockHash)
verifTracer2.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) {
received2 = append(received2, payload.ExecutionPayload.BlockHash)
}
verifTracer3.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) {
received3 = append(received3, payload.BlockHash)
verifTracer3.OnUnsafeL2PayloadFn = func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) {
received3 = append(received3, payload.ExecutionPayload.BlockHash)
}
cfg.Nodes["sequencer"].Tracer = seqTracer
cfg.Nodes["verifier"].Tracer = verifTracer
......
......@@ -11,8 +11,8 @@ import (
type FnTracer struct {
OnNewL1HeadFn func(ctx context.Context, sig eth.L1BlockRef)
OnUnsafeL2PayloadFn func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload)
OnPublishL2PayloadFn func(ctx context.Context, payload *eth.ExecutionPayload)
OnUnsafeL2PayloadFn func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope)
OnPublishL2PayloadFn func(ctx context.Context, payload *eth.ExecutionPayloadEnvelope)
}
func (n *FnTracer) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) {
......@@ -21,13 +21,13 @@ func (n *FnTracer) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) {
}
}
func (n *FnTracer) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) {
func (n *FnTracer) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) {
if n.OnUnsafeL2PayloadFn != nil {
n.OnUnsafeL2PayloadFn(ctx, from, payload)
}
}
func (n *FnTracer) OnPublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload) {
func (n *FnTracer) OnPublishL2Payload(ctx context.Context, payload *eth.ExecutionPayloadEnvelope) {
if n.OnPublishL2PayloadFn != nil {
n.OnPublishL2PayloadFn(ctx, payload)
}
......
......@@ -41,7 +41,7 @@ type Metricer interface {
RecordSequencingError()
RecordPublishingError()
RecordDerivationError()
RecordReceivedUnsafePayload(payload *eth.ExecutionPayload)
RecordReceivedUnsafePayload(payload *eth.ExecutionPayloadEnvelope)
RecordRef(layer string, name string, num uint64, timestamp uint64, h common.Hash)
RecordL1Ref(name string, ref eth.L1BlockRef)
RecordL2Ref(name string, ref eth.L2BlockRef)
......@@ -443,9 +443,9 @@ func (m *Metrics) RecordDerivationError() {
m.DerivationErrors.Record()
}
func (m *Metrics) RecordReceivedUnsafePayload(payload *eth.ExecutionPayload) {
func (m *Metrics) RecordReceivedUnsafePayload(payload *eth.ExecutionPayloadEnvelope) {
m.UnsafePayloads.Record()
m.RecordRef("l2", "received_payload", uint64(payload.BlockNumber), uint64(payload.Timestamp), payload.BlockHash)
m.RecordRef("l2", "received_payload", uint64(payload.ExecutionPayload.BlockNumber), uint64(payload.ExecutionPayload.Timestamp), payload.ExecutionPayload.BlockHash)
}
func (m *Metrics) RecordUnsafePayloadsBuffer(length uint64, memSize uint64, next eth.BlockID) {
......@@ -640,7 +640,7 @@ func (n *noopMetricer) RecordPublishingError() {
func (n *noopMetricer) RecordDerivationError() {
}
func (n *noopMetricer) RecordReceivedUnsafePayload(payload *eth.ExecutionPayload) {
func (n *noopMetricer) RecordReceivedUnsafePayload(payload *eth.ExecutionPayloadEnvelope) {
}
func (n *noopMetricer) RecordRef(layer string, name string, num uint64, timestamp uint64, h common.Hash) {
......
......@@ -30,7 +30,7 @@ type driverClient interface {
StartSequencer(ctx context.Context, blockHash common.Hash) error
StopSequencer(context.Context) (common.Hash, error)
SequencerActive(context.Context) (bool, error)
OnUnsafeL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error
OnUnsafeL2Payload(ctx context.Context, payload *eth.ExecutionPayloadEnvelope) error
}
type adminAPI struct {
......@@ -71,16 +71,18 @@ func (n *adminAPI) SequencerActive(ctx context.Context) (bool, error) {
// PostUnsafePayload is a special API that allow posting an unsafe payload to the L2 derivation pipeline.
// It should only be used by op-conductor for sequencer failover scenarios.
func (n *adminAPI) PostUnsafePayload(ctx context.Context, payload *eth.ExecutionPayload) error {
// TODO(ethereum-optimism/optimism#9064): op-conductor Dencun changes.
func (n *adminAPI) PostUnsafePayload(ctx context.Context, envelope *eth.ExecutionPayloadEnvelope) error {
recordDur := n.M.RecordRPCServerRequest("admin_postUnsafePayload")
defer recordDur()
if actual, ok := payload.CheckBlockHash(); !ok {
payload := envelope.ExecutionPayload
if actual, ok := envelope.CheckBlockHash(); !ok {
log.Error("payload has bad block hash", "bad_hash", payload.BlockHash.String(), "actual", actual.String())
return fmt.Errorf("payload has bad block hash: %s, actual block hash is: %s", payload.BlockHash.String(), actual.String())
}
return n.dr.OnUnsafeL2Payload(ctx, payload)
return n.dr.OnUnsafeL2Payload(ctx, envelope)
}
type nodeAPI struct {
......
......@@ -11,17 +11,17 @@ import (
// Tracer configures the OpNode to share events
type Tracer interface {
OnNewL1Head(ctx context.Context, sig eth.L1BlockRef)
OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload)
OnPublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload)
OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope)
OnPublishL2Payload(ctx context.Context, payload *eth.ExecutionPayloadEnvelope)
}
type noOpTracer struct{}
func (n noOpTracer) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) {}
func (n noOpTracer) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) {
func (n noOpTracer) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) {
}
func (n noOpTracer) OnPublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload) {}
func (n noOpTracer) OnPublishL2Payload(ctx context.Context, payload *eth.ExecutionPayloadEnvelope) {}
var _ Tracer = (*noOpTracer)(nil)
......@@ -481,37 +481,38 @@ func (n *OpNode) OnNewL1Finalized(ctx context.Context, sig eth.L1BlockRef) {
}
}
func (n *OpNode) PublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error {
n.tracer.OnPublishL2Payload(ctx, payload)
func (n *OpNode) PublishL2Payload(ctx context.Context, envelope *eth.ExecutionPayloadEnvelope) error {
n.tracer.OnPublishL2Payload(ctx, envelope)
// publish to p2p, if we are running p2p at all
if n.p2pNode != nil {
payload := envelope.ExecutionPayload
if n.p2pSigner == nil {
return fmt.Errorf("node has no p2p signer, payload %s cannot be published", payload.ID())
}
n.log.Info("Publishing signed execution payload on p2p", "id", payload.ID())
return n.p2pNode.GossipOut().PublishL2Payload(ctx, payload, n.p2pSigner)
return n.p2pNode.GossipOut().PublishL2Payload(ctx, envelope, n.p2pSigner)
}
// if p2p is not enabled then we just don't publish the payload
return nil
}
func (n *OpNode) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) error {
func (n *OpNode) OnUnsafeL2Payload(ctx context.Context, from peer.ID, envelope *eth.ExecutionPayloadEnvelope) error {
// ignore if it's from ourselves
if n.p2pNode != nil && from == n.p2pNode.Host().ID() {
return nil
}
n.tracer.OnUnsafeL2Payload(ctx, from, payload)
n.tracer.OnUnsafeL2Payload(ctx, from, envelope)
n.log.Info("Received signed execution payload from p2p", "id", payload.ID(), "peer", from)
n.log.Info("Received signed execution payload from p2p", "id", envelope.ExecutionPayload.ID(), "peer", from)
// Pass on the event to the L2 Engine
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()
if err := n.l2Driver.OnUnsafeL2Payload(ctx, payload); err != nil {
n.log.Warn("failed to notify engine driver of new L2 payload", "err", err, "id", payload.ID())
if err := n.l2Driver.OnUnsafeL2Payload(ctx, envelope); err != nil {
n.log.Warn("failed to notify engine driver of new L2 payload", "err", err, "id", envelope.ExecutionPayload.ID())
}
return nil
......
......@@ -228,6 +228,6 @@ func (c *mockDriverClient) SequencerActive(ctx context.Context) (bool, error) {
return c.Mock.MethodCalled("SequencerActive").Get(0).(bool), nil
}
func (c *mockDriverClient) OnUnsafeL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error {
func (c *mockDriverClient) OnUnsafeL2Payload(ctx context.Context, payload *eth.ExecutionPayloadEnvelope) error {
return c.Mock.MethodCalled("OnUnsafeL2Payload").Get(0).(error)
}
......@@ -74,10 +74,14 @@ func blocksTopicV2(cfg *rollup.Config) string {
return fmt.Sprintf("/optimism/%s/1/blocks", cfg.L2ChainID.String())
}
func blocksTopicV3(cfg *rollup.Config) string {
return fmt.Sprintf("/optimism/%s/2/blocks", cfg.L2ChainID.String())
}
// BuildSubscriptionFilter builds a simple subscription filter,
// to help protect against peers spamming useless subscriptions.
func BuildSubscriptionFilter(cfg *rollup.Config) pubsub.SubscriptionFilter {
return pubsub.NewAllowlistSubscriptionFilter(blocksTopicV1(cfg), blocksTopicV2(cfg)) // add more topics here in the future, if any.
return pubsub.NewAllowlistSubscriptionFilter(blocksTopicV1(cfg), blocksTopicV2(cfg), blocksTopicV3(cfg)) // add more topics here in the future, if any.
}
var msgBufPool = sync.Pool{New: func() any {
......@@ -286,13 +290,25 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti
return result
}
var envelope eth.ExecutionPayloadEnvelope
// [REJECT] if the block encoding is not valid
var payload eth.ExecutionPayload
if err := payload.UnmarshalSSZ(blockVersion, uint32(len(payloadBytes)), bytes.NewReader(payloadBytes)); err != nil {
log.Warn("invalid payload", "err", err, "peer", id)
return pubsub.ValidationReject
if blockVersion == eth.BlockV3 {
if err := envelope.UnmarshalSSZ(uint32(len(payloadBytes)), bytes.NewReader(payloadBytes)); err != nil {
log.Warn("invalid envelope payload", "err", err, "peer", id)
return pubsub.ValidationReject
}
} else {
var payload eth.ExecutionPayload
if err := payload.UnmarshalSSZ(blockVersion, uint32(len(payloadBytes)), bytes.NewReader(payloadBytes)); err != nil {
log.Warn("invalid execution payload", "err", err, "peer", id)
return pubsub.ValidationReject
}
envelope = eth.ExecutionPayloadEnvelope{ExecutionPayload: &payload}
}
payload := envelope.ExecutionPayload
// rounding down to seconds is fine here.
now := uint64(time.Now().Unix())
......@@ -309,26 +325,58 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti
}
// [REJECT] if the `block_hash` in the `payload` is not valid
if actual, ok := payload.CheckBlockHash(); !ok {
if actual, ok := envelope.CheckBlockHash(); !ok {
log.Warn("payload has bad block hash", "bad_hash", payload.BlockHash.String(), "actual", actual.String())
return pubsub.ValidationReject
}
// [REJECT] if a V1 Block has withdrawals
if blockVersion == eth.BlockV1 && payload.Withdrawals != nil {
if !blockVersion.HasWithdrawals() && payload.Withdrawals != nil {
log.Warn("payload is on v1 topic, but has withdrawals", "bad_hash", payload.BlockHash.String())
return pubsub.ValidationReject
}
// [REJECT] if a V2 Block does not have withdrawals
if blockVersion == eth.BlockV2 && payload.Withdrawals == nil {
log.Warn("payload is on v2 topic, but does not have withdrawals", "bad_hash", payload.BlockHash.String())
if blockVersion.HasWithdrawals() && payload.Withdrawals == nil {
log.Warn("payload is on v2/v3 topic, but does not have withdrawals", "bad_hash", payload.BlockHash.String())
return pubsub.ValidationReject
}
// [REJECT] if a V2 Block has non-empty withdrawals
if blockVersion == eth.BlockV2 && len(*payload.Withdrawals) != 0 {
log.Warn("payload is on v2 topic, but has non-empty withdrawals", "bad_hash", payload.BlockHash.String(), "withdrawal_count", len(*payload.Withdrawals))
if blockVersion.HasWithdrawals() && len(*payload.Withdrawals) != 0 {
log.Warn("payload is on v2/v3 topic, but has non-empty withdrawals", "bad_hash", payload.BlockHash.String(), "withdrawal_count", len(*payload.Withdrawals))
return pubsub.ValidationReject
}
// [REJECT] if the block is on a topic <= V2 and has a blob gas value set
if !blockVersion.HasBlobProperties() && payload.BlobGasUsed != nil {
log.Warn("payload is on v1/v2 topic, but has blob gas used", "bad_hash", payload.BlockHash.String())
return pubsub.ValidationReject
}
// [REJECT] if the block is on a topic <= V2 and has an excess blob gas value set
if !blockVersion.HasBlobProperties() && payload.ExcessBlobGas != nil {
log.Warn("payload is on v1/v2 topic, but has excess blob gas", "bad_hash", payload.BlockHash.String())
return pubsub.ValidationReject
}
if blockVersion.HasBlobProperties() {
// [REJECT] if the block is on a topic >= V3 and has a blob gas used value that is not zero
if payload.BlobGasUsed == nil || (payload.BlobGasUsed != nil && *payload.BlobGasUsed != 0) {
log.Warn("payload is on v3 topic, but has non-zero blob gas used", "bad_hash", payload.BlockHash.String(), "blob_gas_used", payload.BlobGasUsed)
return pubsub.ValidationReject
}
// [REJECT] if the block is on a topic >= V3 and has an excess blob gas value that is not zero
if payload.ExcessBlobGas == nil || (payload.ExcessBlobGas != nil && *payload.ExcessBlobGas != 0) {
log.Warn("payload is on v3 topic, but has non-zero excess blob gas", "bad_hash", payload.BlockHash.String(), "excess_blob_gas", payload.ExcessBlobGas)
return pubsub.ValidationReject
}
}
// [REJECT] if the block is on a topic >= V3 and the parent beacon block root is nil
if blockVersion.HasParentBeaconBlockRoot() && envelope.ParentBeaconBlockRoot == nil {
log.Warn("payload is on v3 topic, but has nil parent beacon block root", "bad_hash", payload.BlockHash.String())
return pubsub.ValidationReject
}
......@@ -353,7 +401,7 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti
seen.markSeen(payload.BlockHash)
// remember the decoded payload for later usage in topic subscriber.
message.ValidatorData = &payload
message.ValidatorData = &envelope
return pubsub.ValidationAccept
}
}
......@@ -388,18 +436,19 @@ func verifyBlockSignature(log log.Logger, cfg *rollup.Config, runCfg GossipRunti
}
type GossipIn interface {
OnUnsafeL2Payload(ctx context.Context, from peer.ID, msg *eth.ExecutionPayload) error
OnUnsafeL2Payload(ctx context.Context, from peer.ID, msg *eth.ExecutionPayloadEnvelope) error
}
type GossipTopicInfo interface {
AllBlockTopicsPeers() []peer.ID
BlocksTopicV1Peers() []peer.ID
BlocksTopicV2Peers() []peer.ID
BlocksTopicV3Peers() []peer.ID
}
type GossipOut interface {
GossipTopicInfo
PublishL2Payload(ctx context.Context, msg *eth.ExecutionPayload, signer Signer) error
PublishL2Payload(ctx context.Context, msg *eth.ExecutionPayloadEnvelope, signer Signer) error
Close() error
}
......@@ -429,6 +478,7 @@ type publisher struct {
blocksV1 *blockTopic
blocksV2 *blockTopic
blocksV3 *blockTopic
runCfg GossipRuntimeConfig
}
......@@ -451,7 +501,7 @@ func combinePeers(allPeers ...[]peer.ID) []peer.ID {
}
func (p *publisher) AllBlockTopicsPeers() []peer.ID {
return combinePeers(p.BlocksTopicV1Peers(), p.BlocksTopicV2Peers())
return combinePeers(p.BlocksTopicV1Peers(), p.BlocksTopicV2Peers(), p.BlocksTopicV3Peers())
}
func (p *publisher) BlocksTopicV1Peers() []peer.ID {
......@@ -462,7 +512,11 @@ func (p *publisher) BlocksTopicV2Peers() []peer.ID {
return p.blocksV2.topic.ListPeers()
}
func (p *publisher) PublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload, signer Signer) error {
func (p *publisher) BlocksTopicV3Peers() []peer.ID {
return p.blocksV3.topic.ListPeers()
}
func (p *publisher) PublishL2Payload(ctx context.Context, envelope *eth.ExecutionPayloadEnvelope, signer Signer) error {
res := msgBufPool.Get().(*[]byte)
buf := bytes.NewBuffer((*res)[:0])
defer func() {
......@@ -471,9 +525,17 @@ func (p *publisher) PublishL2Payload(ctx context.Context, payload *eth.Execution
}()
buf.Write(make([]byte, 65))
if _, err := payload.MarshalSSZ(buf); err != nil {
return fmt.Errorf("failed to encoded execution payload to publish: %w", err)
if envelope.ParentBeaconBlockRoot != nil {
if _, err := envelope.MarshalSSZ(buf); err != nil {
return fmt.Errorf("failed to encoded execution payload envelope to publish: %w", err)
}
} else {
if _, err := envelope.ExecutionPayload.MarshalSSZ(buf); err != nil {
return fmt.Errorf("failed to encoded execution payload to publish: %w", err)
}
}
data := buf.Bytes()
payloadData := data[65:]
sig, err := signer.Sign(ctx, SigningDomainBlocksV1, p.cfg.L2ChainID, payloadData)
......@@ -486,7 +548,9 @@ func (p *publisher) PublishL2Payload(ctx context.Context, payload *eth.Execution
// This also copies the data, freeing up the original buffer to go back into the pool
out := snappy.Encode(nil, data)
if p.cfg.IsCanyon(uint64(payload.Timestamp)) {
if p.cfg.IsEcotone(uint64(envelope.ExecutionPayload.Timestamp)) {
return p.blocksV3.topic.Publish(ctx, out)
} else if p.cfg.IsCanyon(uint64(envelope.ExecutionPayload.Timestamp)) {
return p.blocksV2.topic.Publish(ctx, out)
} else {
return p.blocksV1.topic.Publish(ctx, out)
......@@ -519,12 +583,21 @@ func JoinGossip(self peer.ID, ps *pubsub.PubSub, log log.Logger, cfg *rollup.Con
return nil, fmt.Errorf("failed to setup blocks v2 p2p: %w", err)
}
v3Logger := log.New("topic", "blocksV3")
blocksV3Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv3", v3Logger, BuildBlocksValidator(v3Logger, cfg, runCfg, eth.BlockV3)))
blocksV3, err := newBlockTopic(p2pCtx, blocksTopicV3(cfg), ps, v3Logger, gossipIn, blocksV3Validator)
if err != nil {
p2pCancel()
return nil, fmt.Errorf("failed to setup blocks v3 p2p: %w", err)
}
return &publisher{
log: log,
cfg: cfg,
p2pCancel: p2pCancel,
blocksV1: blocksV1,
blocksV2: blocksV2,
blocksV3: blocksV3,
runCfg: runCfg,
}, nil
}
......@@ -570,9 +643,9 @@ func newBlockTopic(ctx context.Context, topicId string, ps *pubsub.PubSub, log l
type TopicSubscriber func(ctx context.Context, sub *pubsub.Subscription)
type MessageHandler func(ctx context.Context, from peer.ID, msg any) error
func BlocksHandler(onBlock func(ctx context.Context, from peer.ID, msg *eth.ExecutionPayload) error) MessageHandler {
func BlocksHandler(onBlock func(ctx context.Context, from peer.ID, msg *eth.ExecutionPayloadEnvelope) error) MessageHandler {
return func(ctx context.Context, from peer.ID, msg any) error {
payload, ok := msg.(*eth.ExecutionPayload)
payload, ok := msg.(*eth.ExecutionPayloadEnvelope)
if !ok {
return fmt.Errorf("expected topic validator to parse and validate data into execution payload, but got %T", msg)
}
......
......@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"io"
"math/big"
"testing"
"time"
......@@ -100,7 +101,11 @@ func TestVerifyBlockSignature(t *testing.T) {
})
}
func createSignedP2Payload(payload *eth.ExecutionPayload, signer Signer, l2ChainID *big.Int) ([]byte, error) {
type MarshalSSZ interface {
MarshalSSZ(w io.Writer) (n int, err error)
}
func createSignedP2Payload(payload MarshalSSZ, signer Signer, l2ChainID *big.Int) ([]byte, error) {
var buf bytes.Buffer
buf.Write(make([]byte, 65))
if _, err := payload.MarshalSSZ(&buf); err != nil {
......@@ -119,6 +124,22 @@ func createSignedP2Payload(payload *eth.ExecutionPayload, signer Signer, l2Chain
return snappy.Encode(nil, data), nil
}
func createExecutionPayload(w types.Withdrawals, excessGas, gasUsed *uint64) *eth.ExecutionPayload {
return &eth.ExecutionPayload{
Timestamp: hexutil.Uint64(time.Now().Unix()),
Withdrawals: &w,
ExcessBlobGas: (*eth.Uint64Quantity)(excessGas),
BlobGasUsed: (*eth.Uint64Quantity)(gasUsed),
}
}
func createEnvelope(h *common.Hash, w types.Withdrawals, excessGas, gasUsed *uint64) *eth.ExecutionPayloadEnvelope {
return &eth.ExecutionPayloadEnvelope{
ExecutionPayload: createExecutionPayload(w, excessGas, gasUsed),
ParentBeaconBlockRoot: h,
}
}
// TestBlockValidator does some very basic tests of the p2p block validation logic
func TestBlockValidator(t *testing.T) {
// Params Set 1: Create the validation function
......@@ -129,35 +150,61 @@ func TestBlockValidator(t *testing.T) {
require.NoError(t, err)
runCfg := &testutils.MockRuntimeConfig{P2PSeqAddress: crypto.PubkeyToAddress(secrets.SequencerP2P.PublicKey)}
signer := &PreparedSigner{Signer: NewLocalSigner(secrets.SequencerP2P)}
// valFnV1 := BuildBlocksValidator(testlog.Logger(t, log.LvlCrit), rollupCfg, runCfg, eth.BlockV1)
valFnV2 := BuildBlocksValidator(testlog.Logger(t, log.LvlCrit), cfg, runCfg, eth.BlockV2)
// Params Set 2: Call the validation function
peerID := peer.ID("foo")
// Valid Case
payload := eth.ExecutionPayload{
Timestamp: hexutil.Uint64(time.Now().Unix()),
Withdrawals: &types.Withdrawals{},
v2Validator := BuildBlocksValidator(testlog.Logger(t, log.LvlCrit), cfg, runCfg, eth.BlockV2)
v3Validator := BuildBlocksValidator(testlog.Logger(t, log.LvlCrit), cfg, runCfg, eth.BlockV3)
zero, one := uint64(0), uint64(1)
beaconHash := common.HexToHash("0x1234")
payloadTests := []struct {
name string
validator pubsub.ValidatorEx
result pubsub.ValidationResult
payload *eth.ExecutionPayload
}{
{"V2Valid", v2Validator, pubsub.ValidationAccept, createExecutionPayload(types.Withdrawals{}, nil, nil)},
{"V2NonZeroWithdrawals", v2Validator, pubsub.ValidationReject, createExecutionPayload(types.Withdrawals{&types.Withdrawal{Index: 1, Validator: 1}}, nil, nil)},
{"V2NonZeroBlobProperties", v2Validator, pubsub.ValidationReject, createExecutionPayload(types.Withdrawals{}, &zero, &zero)},
{"V3RejectExecutionPayload", v3Validator, pubsub.ValidationReject, createExecutionPayload(types.Withdrawals{}, &zero, &zero)},
}
payload.BlockHash, _ = payload.CheckBlockHash() // hack to generate the block hash easily.
data, err := createSignedP2Payload(&payload, signer, cfg.L2ChainID)
require.NoError(t, err)
message := &pubsub.Message{Message: &pubsub_pb.Message{Data: data}}
res := valFnV2(context.TODO(), peerID, message)
require.Equal(t, res, pubsub.ValidationAccept)
// Invalid because non-empty withdrawals when Canyon is active
payload = eth.ExecutionPayload{
Timestamp: hexutil.Uint64(time.Now().Unix()),
Withdrawals: &types.Withdrawals{&types.Withdrawal{Index: 1, Validator: 1}},
for _, tt := range payloadTests {
test := tt
t.Run(fmt.Sprintf("ExecutionPayload_%s", test.name), func(t *testing.T) {
e := &eth.ExecutionPayloadEnvelope{ExecutionPayload: test.payload}
test.payload.BlockHash, _ = e.CheckBlockHash() // hack to generate the block hash easily.
data, err := createSignedP2Payload(test.payload, signer, cfg.L2ChainID)
require.NoError(t, err)
message := &pubsub.Message{Message: &pubsub_pb.Message{Data: data}}
res := test.validator(context.TODO(), peerID, message)
require.Equal(t, res, test.result)
})
}
envelopeTests := []struct {
name string
validator pubsub.ValidatorEx
result pubsub.ValidationResult
payload *eth.ExecutionPayloadEnvelope
}{
{"V3RejectNonZeroExcessGas", v3Validator, pubsub.ValidationReject, createEnvelope(&beaconHash, types.Withdrawals{}, &one, &zero)},
{"V3RejectNonZeroBlobGasUsed", v3Validator, pubsub.ValidationReject, createEnvelope(&beaconHash, types.Withdrawals{}, &zero, &one)},
{"V3RejectNonZeroBlobGasUsed", v3Validator, pubsub.ValidationReject, createEnvelope(&beaconHash, types.Withdrawals{}, &zero, &one)},
{"V3Valid", v3Validator, pubsub.ValidationAccept, createEnvelope(&beaconHash, types.Withdrawals{}, &zero, &zero)},
}
payload.BlockHash, _ = payload.CheckBlockHash()
data, err = createSignedP2Payload(&payload, signer, cfg.L2ChainID)
require.NoError(t, err)
message = &pubsub.Message{Message: &pubsub_pb.Message{Data: data}}
res = valFnV2(context.TODO(), peerID, message)
require.Equal(t, res, pubsub.ValidationReject)
for _, tt := range envelopeTests {
test := tt
t.Run(fmt.Sprintf("ExecutionPayloadEnvelope_%s", test.name), func(t *testing.T) {
test.payload.ExecutionPayload.BlockHash, _ = test.payload.CheckBlockHash() // hack to generate the block hash easily.
data, err := createSignedP2Payload(test.payload, signer, cfg.L2ChainID)
require.NoError(t, err)
message := &pubsub.Message{Message: &pubsub_pb.Message{Data: data}}
res := test.validator(context.TODO(), peerID, message)
require.Equal(t, res, test.result)
})
}
}
......@@ -73,10 +73,10 @@ func TestP2PSimple(t *testing.T) {
}
type mockGossipIn struct {
OnUnsafeL2PayloadFn func(ctx context.Context, from peer.ID, msg *eth.ExecutionPayload) error
OnUnsafeL2PayloadFn func(ctx context.Context, from peer.ID, msg *eth.ExecutionPayloadEnvelope) error
}
func (m *mockGossipIn) OnUnsafeL2Payload(ctx context.Context, from peer.ID, msg *eth.ExecutionPayload) error {
func (m *mockGossipIn) OnUnsafeL2Payload(ctx context.Context, from peer.ID, msg *eth.ExecutionPayloadEnvelope) error {
if m.OnUnsafeL2PayloadFn != nil {
return m.OnUnsafeL2PayloadFn(ctx, from, msg)
}
......
......@@ -204,6 +204,7 @@ type PeerStats struct {
Table uint `json:"table"`
BlocksTopic uint `json:"blocksTopic"`
BlocksTopicV2 uint `json:"blocksTopicV2"`
BlocksTopicV3 uint `json:"blocksTopicV3"`
Banned uint `json:"banned"`
Known uint `json:"known"`
}
......@@ -220,6 +221,7 @@ func (s *APIBackend) PeerStats(_ context.Context) (*PeerStats, error) {
Table: 0,
BlocksTopic: uint(len(s.node.GossipOut().BlocksTopicV1Peers())),
BlocksTopicV2: uint(len(s.node.GossipOut().BlocksTopicV2Peers())),
BlocksTopicV3: uint(len(s.node.GossipOut().BlocksTopicV3Peers())),
Banned: 0,
Known: uint(len(pstore.Peers())),
}
......
......@@ -82,7 +82,7 @@ func MakeStreamHandler(resourcesCtx context.Context, log log.Logger, fn requestH
type newStreamFn func(ctx context.Context, peerId peer.ID, protocolId ...protocol.ID) (network.Stream, error)
type receivePayloadFn func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) error
type receivePayloadFn func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) error
type rangeRequest struct {
start uint64
......@@ -90,7 +90,7 @@ type rangeRequest struct {
}
type syncResult struct {
payload *eth.ExecutionPayload
payload *eth.ExecutionPayloadEnvelope
peer peer.ID
}
......@@ -428,14 +428,14 @@ func (s *SyncClient) onRangeRequest(ctx context.Context, req rangeRequest) {
}
func (s *SyncClient) onQuarantineEvict(key common.Hash, value syncResult) {
delete(s.quarantineByNum, uint64(value.payload.BlockNumber))
delete(s.quarantineByNum, uint64(value.payload.ExecutionPayload.BlockNumber))
s.metrics.PayloadsQuarantineSize(s.quarantine.Len())
if !s.trusted.Contains(key) {
s.log.Debug("evicting untrusted payload from quarantine", "id", value.payload.ID(), "peer", value.peer)
s.log.Debug("evicting untrusted payload from quarantine", "id", value.payload.ExecutionPayload.ID(), "peer", value.peer)
// Down-score peer for having provided us a bad block that never turned out to be canonical
s.appScorer.onRejectedPayload(value.peer)
} else {
s.log.Debug("evicting trusted payload from quarantine", "id", value.payload.ID(), "peer", value.peer)
s.log.Debug("evicting trusted payload from quarantine", "id", value.payload.ExecutionPayload.ID(), "peer", value.peer)
}
}
......@@ -455,27 +455,28 @@ func (s *SyncClient) tryPromote(h common.Hash) {
}
func (s *SyncClient) promote(ctx context.Context, res syncResult) {
s.log.Debug("promoting p2p sync result", "payload", res.payload.ID(), "peer", res.peer)
s.log.Debug("promoting p2p sync result", "payload", res.payload.ExecutionPayload.ID(), "peer", res.peer)
if err := s.receivePayload(ctx, res.peer, res.payload); err != nil {
s.log.Warn("failed to promote payload, receiver error", "err", err)
return
}
s.trusted.Add(res.payload.BlockHash, struct{}{})
if s.quarantine.Remove(res.payload.BlockHash) {
s.log.Debug("promoted previously p2p-synced block from quarantine to main", "id", res.payload.ID())
s.trusted.Add(res.payload.ExecutionPayload.BlockHash, struct{}{})
if s.quarantine.Remove(res.payload.ExecutionPayload.BlockHash) {
s.log.Debug("promoted previously p2p-synced block from quarantine to main", "id", res.payload.ExecutionPayload.ID())
} else {
s.log.Debug("promoted new p2p-synced block to main", "id", res.payload.ID())
s.log.Debug("promoted new p2p-synced block to main", "id", res.payload.ExecutionPayload.ID())
}
// Mark parent block as trusted, so that we can promote it once we receive it / find it
s.trusted.Add(res.payload.ParentHash, struct{}{})
s.trusted.Add(res.payload.ExecutionPayload.ParentHash, struct{}{})
// Try to promote the parent block too, if any: previous unverifiable data may now be canonical
s.tryPromote(res.payload.ParentHash)
s.tryPromote(res.payload.ExecutionPayload.ParentHash)
// In case we don't have the parent, and what we have in quarantine is wrong,
// clear what we buffered in favor of fetching something else.
if h, ok := s.quarantineByNum[uint64(res.payload.BlockNumber)-1]; ok {
if h, ok := s.quarantineByNum[uint64(res.payload.ExecutionPayload.BlockNumber)-1]; ok {
s.quarantine.Remove(h)
}
}
......@@ -483,15 +484,16 @@ func (s *SyncClient) promote(ctx context.Context, res syncResult) {
// onResult is exclusively called by the main loop, and has thus direct access to the request bookkeeping state.
// This function verifies if the result is canonical, and either promotes the result or moves the result into quarantine.
func (s *SyncClient) onResult(ctx context.Context, res syncResult) {
s.log.Debug("processing p2p sync result", "payload", res.payload.ID(), "peer", res.peer)
payload := res.payload.ExecutionPayload
s.log.Debug("processing p2p sync result", "payload", payload.ID(), "peer", res.peer)
// Clean up the in-flight request, we have a result now.
delete(s.inFlight, uint64(res.payload.BlockNumber))
delete(s.inFlight, uint64(payload.BlockNumber))
// Always put it in quarantine first. If promotion fails because the receiver is too busy, this functions as cache.
s.quarantine.Add(res.payload.BlockHash, res)
s.quarantineByNum[uint64(res.payload.BlockNumber)] = res.payload.BlockHash
s.quarantine.Add(payload.BlockHash, res)
s.quarantineByNum[uint64(payload.BlockNumber)] = payload.BlockHash
s.metrics.PayloadsQuarantineSize(s.quarantine.Len())
// If we know this block is canonical, then promote it
if s.trusted.Contains(res.payload.BlockHash) {
if s.trusted.Contains(payload.BlockHash) {
s.promote(ctx, res)
}
}
......@@ -608,8 +610,8 @@ func (s *SyncClient) doRequest(ctx context.Context, id peer.ID, expectedBlockNum
return fmt.Errorf("failed to read version part of response: %w", err)
}
version := binary.LittleEndian.Uint32(versionData[:])
if version != 0 {
return fmt.Errorf("unrecognized ExecutionPayload version: %d", version)
if version != 0 && version != 1 {
return fmt.Errorf("unrecognized version: %d", version)
}
// payload is SSZ encoded with Snappy framed compression
r = snappy.NewReader(r)
......@@ -621,37 +623,58 @@ func (s *SyncClient) doRequest(ctx context.Context, id peer.ID, expectedBlockNum
return fmt.Errorf("failed to read response: %w", err)
}
expectedBlockTime := s.cfg.TimestampForBlock(expectedBlockNum)
envelope := &eth.ExecutionPayloadEnvelope{}
blockVersion := eth.BlockV1
if s.cfg.IsCanyon(expectedBlockTime) {
blockVersion = eth.BlockV2
}
var res eth.ExecutionPayload
if err := res.UnmarshalSSZ(blockVersion, uint32(len(data)), bytes.NewReader(data)); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
if version == 0 {
expectedBlockTime := s.cfg.TimestampForBlock(expectedBlockNum)
envelope, err = s.readExecutionPayload(data, expectedBlockTime)
if err != nil {
return err
}
} else if version == 1 {
if err := envelope.UnmarshalSSZ(uint32(len(data)), bytes.NewReader(data)); err != nil {
return fmt.Errorf("failed to decode execution payload envelope response: %w", err)
}
} else {
panic(fmt.Errorf("should have already filtered by version, but got: %d", version))
}
if err := str.CloseRead(); err != nil {
return fmt.Errorf("failed to close reading side")
}
if err := verifyBlock(&res, expectedBlockNum); err != nil {
if err := verifyBlock(envelope, expectedBlockNum); err != nil {
return fmt.Errorf("received execution payload is invalid: %w", err)
}
select {
case s.results <- syncResult{payload: &res, peer: id}:
case s.results <- syncResult{payload: envelope, peer: id}:
case <-ctx.Done():
return fmt.Errorf("failed to process response, sync client is too busy: %w", err)
}
return nil
}
func verifyBlock(payload *eth.ExecutionPayload, expectedNum uint64) error {
func (s *SyncClient) readExecutionPayload(data []byte, expectedTime uint64) (*eth.ExecutionPayloadEnvelope, error) {
blockVersion := eth.BlockV1
if s.cfg.IsCanyon(expectedTime) {
blockVersion = eth.BlockV2
}
var res eth.ExecutionPayload
if err := res.UnmarshalSSZ(blockVersion, uint32(len(data)), bytes.NewReader(data)); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &eth.ExecutionPayloadEnvelope{ExecutionPayload: &res}, nil
}
func verifyBlock(envelope *eth.ExecutionPayloadEnvelope, expectedNum uint64) error {
payload := envelope.ExecutionPayload
// verify L2 block
if expectedNum != uint64(payload.BlockNumber) {
return fmt.Errorf("received execution payload for block %d, but expected block %d", payload.BlockNumber, expectedNum)
}
actual, ok := payload.CheckBlockHash()
actual, ok := envelope.CheckBlockHash()
if !ok { // payload itself contains bad block hash
return fmt.Errorf("received execution payload for block %d with bad block hash %s, expected %s", expectedNum, payload.BlockHash, actual)
}
......@@ -665,7 +688,7 @@ type peerStat struct {
}
type L2Chain interface {
PayloadByNumber(ctx context.Context, number uint64) (*eth.ExecutionPayload, error)
PayloadByNumber(ctx context.Context, number uint64) (*eth.ExecutionPayloadEnvelope, error)
}
type ReqRespServerMetrics interface {
......@@ -791,7 +814,7 @@ func (srv *ReqRespServer) handleSyncRequest(ctx context.Context, stream network.
return req, fmt.Errorf("cannot serve request for L2 block %d after max expected block (%v): %w", req, max, invalidRequestErr)
}
payload, err := srv.l2.PayloadByNumber(ctx, req)
envelope, err := srv.l2.PayloadByNumber(ctx, req)
if err != nil {
if errors.Is(err, ethereum.NotFound) {
return req, fmt.Errorf("peer requested unknown block by number: %w", err)
......@@ -803,18 +826,33 @@ func (srv *ReqRespServer) handleSyncRequest(ctx context.Context, stream network.
// We set write deadline, if available, to safely write without blocking on a throttling peer connection
_ = stream.SetWriteDeadline(time.Now().Add(serverWriteChunkTimeout))
// 0 - resultCode: success = 0
// 1:5 - version: 0
var tmp [5]byte
if _, err := stream.Write(tmp[:]); err != nil {
return req, fmt.Errorf("failed to write response header data: %w", err)
}
w := snappy.NewBufferedWriter(stream)
if _, err := payload.MarshalSSZ(w); err != nil {
return req, fmt.Errorf("failed to write payload to sync response: %w", err)
if srv.cfg.IsEcotone(uint64(envelope.ExecutionPayload.Timestamp)) {
// 0 - resultCode: success = 0
// 1:5 - version: 1 (little endian)
tmp := [5]byte{0, 1, 0, 0, 0}
if _, err := stream.Write(tmp[:]); err != nil {
return req, fmt.Errorf("failed to write response header data: %w", err)
}
if _, err := envelope.MarshalSSZ(w); err != nil {
return req, fmt.Errorf("failed to write payload to sync response: %w", err)
}
} else {
// 0 - resultCode: success = 0
// 1:5 - version: 0
var tmp [5]byte
if _, err := stream.Write(tmp[:]); err != nil {
return req, fmt.Errorf("failed to write response header data: %w", err)
}
if _, err := envelope.ExecutionPayload.MarshalSSZ(w); err != nil {
return req, fmt.Errorf("failed to write payload to sync response: %w", err)
}
}
if err := w.Close(); err != nil {
return req, fmt.Errorf("failed to finishing writing payload to sync response: %w", err)
}
return req, nil
}
......@@ -15,6 +15,7 @@ import (
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-node/metrics"
......@@ -23,9 +24,9 @@ import (
"github.com/ethereum-optimism/optimism/op-service/testlog"
)
type mockPayloadFn func(n uint64) (*eth.ExecutionPayload, error)
type mockPayloadFn func(n uint64) (*eth.ExecutionPayloadEnvelope, error)
func (fn mockPayloadFn) PayloadByNumber(_ context.Context, number uint64) (*eth.ExecutionPayload, error) {
func (fn mockPayloadFn) PayloadByNumber(_ context.Context, number uint64) (*eth.ExecutionPayloadEnvelope, error) {
return fn(number)
}
......@@ -33,10 +34,10 @@ var _ L2Chain = mockPayloadFn(nil)
type syncTestData struct {
sync.RWMutex
payloads map[uint64]*eth.ExecutionPayload
payloads map[uint64]*eth.ExecutionPayloadEnvelope
}
func (s *syncTestData) getPayload(i uint64) (payload *eth.ExecutionPayload, ok bool) {
func (s *syncTestData) getPayload(i uint64) (payload *eth.ExecutionPayloadEnvelope, ok bool) {
s.RLock()
defer s.RUnlock()
payload, ok = s.payloads[i]
......@@ -49,20 +50,20 @@ func (s *syncTestData) deletePayload(i uint64) {
delete(s.payloads, i)
}
func (s *syncTestData) addPayload(payload *eth.ExecutionPayload) {
func (s *syncTestData) addPayload(payload *eth.ExecutionPayloadEnvelope) {
s.Lock()
defer s.Unlock()
s.payloads[uint64(payload.BlockNumber)] = payload
s.payloads[uint64(payload.ExecutionPayload.BlockNumber)] = payload
}
func (s *syncTestData) getBlockRef(i uint64) eth.L2BlockRef {
s.RLock()
defer s.RUnlock()
return eth.L2BlockRef{
Hash: s.payloads[i].BlockHash,
Number: uint64(s.payloads[i].BlockNumber),
ParentHash: s.payloads[i].ParentHash,
Time: uint64(s.payloads[i].Timestamp),
Hash: s.payloads[i].ExecutionPayload.BlockHash,
Number: uint64(s.payloads[i].ExecutionPayload.BlockNumber),
ParentHash: s.payloads[i].ExecutionPayload.ParentHash,
Time: uint64(s.payloads[i].ExecutionPayload.Timestamp),
}
}
......@@ -78,19 +79,42 @@ func setupSyncTestData(length uint64) (*rollup.Config, *syncTestData) {
L2ChainID: big.NewInt(1234),
}
ecotoneBlock := length / 2
ecotoneTime := cfg.Genesis.L2Time + ecotoneBlock*cfg.BlockTime
cfg.EcotoneTime = &ecotoneTime
// create some simple fake test blocks
payloads := make(map[uint64]*eth.ExecutionPayload)
payloads[0] = &eth.ExecutionPayload{
Timestamp: eth.Uint64Quantity(cfg.Genesis.L2Time),
payloads := make(map[uint64]*eth.ExecutionPayloadEnvelope)
payloads[0] = &eth.ExecutionPayloadEnvelope{
ExecutionPayload: &eth.ExecutionPayload{
Timestamp: eth.Uint64Quantity(cfg.Genesis.L2Time),
},
}
payloads[0].BlockHash, _ = payloads[0].CheckBlockHash()
payloads[0].ExecutionPayload.BlockHash, _ = payloads[0].CheckBlockHash()
for i := uint64(1); i <= length; i++ {
payload := &eth.ExecutionPayload{
ParentHash: payloads[i-1].BlockHash,
BlockNumber: eth.Uint64Quantity(i),
Timestamp: eth.Uint64Quantity(cfg.Genesis.L2Time + i*cfg.BlockTime),
timestamp := cfg.Genesis.L2Time + i*cfg.BlockTime
payload := &eth.ExecutionPayloadEnvelope{
ExecutionPayload: &eth.ExecutionPayload{
ParentHash: payloads[i-1].ExecutionPayload.BlockHash,
BlockNumber: eth.Uint64Quantity(i),
Timestamp: eth.Uint64Quantity(timestamp),
},
}
if cfg.IsEcotone(timestamp) {
hash := common.BigToHash(big.NewInt(int64(i)))
payload.ParentBeaconBlockRoot = &hash
zero := eth.Uint64Quantity(0)
payload.ExecutionPayload.ExcessBlobGas = &zero
payload.ExecutionPayload.BlobGasUsed = &zero
w := types.Withdrawals{}
payload.ExecutionPayload.Withdrawals = &w
}
payload.BlockHash, _ = payload.CheckBlockHash()
payload.ExecutionPayload.BlockHash, _ = payload.CheckBlockHash()
payloads[i] = payload
}
......@@ -105,7 +129,7 @@ func TestSinglePeerSync(t *testing.T) {
cfg, payloads := setupSyncTestData(25)
// Serving payloads: just load them from the map, if they exist
servePayload := mockPayloadFn(func(n uint64) (*eth.ExecutionPayload, error) {
servePayload := mockPayloadFn(func(n uint64) (*eth.ExecutionPayloadEnvelope, error) {
p, ok := payloads.getPayload(n)
if !ok {
return nil, ethereum.NotFound
......@@ -114,8 +138,8 @@ func TestSinglePeerSync(t *testing.T) {
})
// collect received payloads in a buffered channel, so we can verify we get everything
received := make(chan *eth.ExecutionPayload, 100)
receivePayload := receivePayloadFn(func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) error {
received := make(chan *eth.ExecutionPayloadEnvelope, 100)
receivePayload := receivePayloadFn(func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) error {
received <- payload
return nil
})
......@@ -150,10 +174,17 @@ func TestSinglePeerSync(t *testing.T) {
// and wait for the sync results to come in (in reverse order)
for i := uint64(19); i > 10; i-- {
p := <-received
require.Equal(t, uint64(p.BlockNumber), i, "expecting payloads in order")
exp, ok := payloads.getPayload(uint64(p.BlockNumber))
require.Equal(t, uint64(p.ExecutionPayload.BlockNumber), i, "expecting payloads in order")
exp, ok := payloads.getPayload(uint64(p.ExecutionPayload.BlockNumber))
require.True(t, ok, "expecting known payload")
require.Equal(t, exp.BlockHash, p.BlockHash, "expecting the correct payload")
require.Equal(t, exp.ExecutionPayload.BlockHash, p.ExecutionPayload.BlockHash, "expecting the correct payload")
require.Equal(t, exp.ParentBeaconBlockRoot, p.ParentBeaconBlockRoot)
if cfg.IsEcotone(uint64(p.ExecutionPayload.Timestamp)) {
require.NotNil(t, p.ParentBeaconBlockRoot)
} else {
require.Nil(t, p.ParentBeaconBlockRoot)
}
}
}
......@@ -167,9 +198,9 @@ func TestMultiPeerSync(t *testing.T) {
// Buffered channel of all blocks requested from any client.
requested := make(chan uint64, 100)
setupPeer := func(ctx context.Context, h host.Host) (*SyncClient, chan *eth.ExecutionPayload) {
setupPeer := func(ctx context.Context, h host.Host) (*SyncClient, chan *eth.ExecutionPayloadEnvelope) {
// Serving payloads: just load them from the map, if they exist
servePayload := mockPayloadFn(func(n uint64) (*eth.ExecutionPayload, error) {
servePayload := mockPayloadFn(func(n uint64) (*eth.ExecutionPayloadEnvelope, error) {
requested <- n
p, ok := payloads.getPayload(n)
if !ok {
......@@ -179,8 +210,8 @@ func TestMultiPeerSync(t *testing.T) {
})
// collect received payloads in a buffered channel, so we can verify we get everything
received := make(chan *eth.ExecutionPayload, 100)
receivePayload := receivePayloadFn(func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) error {
received := make(chan *eth.ExecutionPayloadEnvelope, 100)
receivePayload := receivePayloadFn(func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) error {
received <- payload
return nil
})
......@@ -229,10 +260,11 @@ func TestMultiPeerSync(t *testing.T) {
// With such large range to request we are going to hit the rate-limits of B and C,
// but that means we'll balance the work between the peers.
for i := uint64(89); i > 10; i-- { // wait for all payloads
p := <-recvA
e := <-recvA
p := e.ExecutionPayload
exp, ok := payloads.getPayload(uint64(p.BlockNumber))
require.True(t, ok, "expecting known payload")
require.Equal(t, exp.BlockHash, p.BlockHash, "expecting the correct payload")
require.Equal(t, exp.ExecutionPayload.BlockHash, p.BlockHash, "expecting the correct payload")
}
// now see if B can sync a range, and fill the gap with a re-request
......@@ -241,9 +273,9 @@ func TestMultiPeerSync(t *testing.T) {
require.NoError(t, clB.RequestL2Range(ctx, payloads.getBlockRef(20), payloads.getBlockRef(30)))
for i := uint64(29); i > 25; i-- {
p := <-recvB
exp, ok := payloads.getPayload(uint64(p.BlockNumber))
exp, ok := payloads.getPayload(uint64(p.ExecutionPayload.BlockNumber))
require.True(t, ok, "expecting known payload")
require.Equal(t, exp.BlockHash, p.BlockHash, "expecting the correct payload")
require.Equal(t, exp.ExecutionPayload.BlockHash, p.ExecutionPayload.BlockHash, "expecting the correct payload")
}
// Wait for the request for block 25 to be made
ctx, cancelFunc := context.WithTimeout(context.Background(), 30*time.Second)
......@@ -283,9 +315,15 @@ func TestMultiPeerSync(t *testing.T) {
require.NoError(t, clB.RequestL2Range(ctx, payloads.getBlockRef(20), payloads.getBlockRef(26)))
for i := uint64(25); i > 20; i-- {
p := <-recvB
exp, ok := payloads.getPayload(uint64(p.BlockNumber))
exp, ok := payloads.getPayload(uint64(p.ExecutionPayload.BlockNumber))
require.True(t, ok, "expecting known payload")
require.Equal(t, exp.BlockHash, p.BlockHash, "expecting the correct payload")
require.Equal(t, exp.ExecutionPayload.BlockHash, p.ExecutionPayload.BlockHash, "expecting the correct payload")
require.Equal(t, exp.ParentBeaconBlockRoot, p.ParentBeaconBlockRoot)
if cfg.IsEcotone(uint64(p.ExecutionPayload.Timestamp)) {
require.NotNil(t, p.ParentBeaconBlockRoot)
} else {
require.Nil(t, p.ParentBeaconBlockRoot)
}
}
}
......@@ -304,7 +342,7 @@ func TestNetworkNotifyAddPeerAndRemovePeer(t *testing.T) {
require.NoError(t, err, "failed to launch host B")
defer hostB.Close()
syncCl := NewSyncClient(log, cfg, hostA.NewStream, func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayload) error {
syncCl := NewSyncClient(log, cfg, hostA.NewStream, func(ctx context.Context, from peer.ID, payload *eth.ExecutionPayloadEnvelope) error {
return nil
}, metrics.NoopMetrics, &NoopApplicationScorer{})
......@@ -340,5 +378,4 @@ func TestNetworkNotifyAddPeerAndRemovePeer(t *testing.T) {
<-waitChan
_, peerBExist3 := syncCl.peers[hostB.ID()]
require.True(t, !peerBExist3, "peerB should not exist in syncClient")
}
......@@ -123,6 +123,14 @@ func (ba *FetchingAttributesBuilder) PreparePayloadAttributes(ctx context.Contex
withdrawals = &types.Withdrawals{}
}
var parentBeaconRoot *common.Hash
if ba.rollupCfg.IsEcotone(nextL2Time) {
parentBeaconRoot = l1Info.ParentBeaconRoot()
if parentBeaconRoot == nil {
return nil, NewCriticalError(fmt.Errorf("cannot build Ecotone (L2 Dencun) block without L1 Dencun info, at L2 timestamp %d", nextL2Time))
}
}
return &eth.PayloadAttributes{
Timestamp: hexutil.Uint64(nextL2Time),
PrevRandao: eth.Bytes32(l1Info.MixDigest()),
......@@ -131,5 +139,6 @@ func (ba *FetchingAttributesBuilder) PreparePayloadAttributes(ctx context.Contex
NoTxPool: true,
GasLimit: (*eth.Uint64Quantity)(&sysConfig.GasLimit),
Withdrawals: withdrawals,
ParentBeaconBlockRoot: parentBeaconRoot,
}, nil
}
......@@ -34,7 +34,7 @@ type NextBatchProvider interface {
type SafeBlockFetcher interface {
L2BlockRefByNumber(context.Context, uint64) (eth.L2BlockRef, error)
PayloadByNumber(context.Context, uint64) (*eth.ExecutionPayload, error)
PayloadByNumber(context.Context, uint64) (*eth.ExecutionPayloadEnvelope, error)
}
// BatchQueue contains a set of batches for every L1 block.
......
......@@ -96,15 +96,17 @@ func l1InfoDepositTx(t *testing.T, l1BlockNum uint64) hexutil.Bytes {
return txData
}
func singularBatchToPayload(t *testing.T, batch *SingularBatch, blockNumber uint64) eth.ExecutionPayload {
func singularBatchToPayload(t *testing.T, batch *SingularBatch, blockNumber uint64) eth.ExecutionPayloadEnvelope {
txs := []hexutil.Bytes{l1InfoDepositTx(t, uint64(batch.EpochNum))}
txs = append(txs, batch.Transactions...)
return eth.ExecutionPayload{
BlockHash: mockHash(batch.Timestamp, 2),
ParentHash: batch.ParentHash,
BlockNumber: hexutil.Uint64(blockNumber),
Timestamp: hexutil.Uint64(batch.Timestamp),
Transactions: txs,
return eth.ExecutionPayloadEnvelope{
ExecutionPayload: &eth.ExecutionPayload{
BlockHash: mockHash(batch.Timestamp, 2),
ParentHash: batch.ParentHash,
BlockNumber: hexutil.Uint64(blockNumber),
Timestamp: hexutil.Uint64(batch.Timestamp),
Transactions: txs,
},
}
}
......
......@@ -344,7 +344,7 @@ func checkSpanBatch(ctx context.Context, cfg *rollup.Config, log log.Logger, l1B
// unable to validate the batch for now. retry later.
return BatchUndecided
}
safeBlockTxs := safeBlockPayload.Transactions
safeBlockTxs := safeBlockPayload.ExecutionPayload.Transactions
batchTxs := batch.GetBlockTransactions(int(i))
// execution payload has deposit TXs, but batch does not.
depositCount := 0
......@@ -363,9 +363,9 @@ func checkSpanBatch(ctx context.Context, cfg *rollup.Config, log log.Logger, l1B
return BatchDrop
}
}
safeBlockRef, err := PayloadToBlockRef(cfg, safeBlockPayload)
safeBlockRef, err := PayloadToBlockRef(cfg, safeBlockPayload.ExecutionPayload)
if err != nil {
log.Error("failed to extract L2BlockRef from execution payload", "hash", safeBlockPayload.BlockHash, "err", err)
log.Error("failed to extract L2BlockRef from execution payload", "hash", safeBlockPayload.ExecutionPayload.BlockHash, "err", err)
return BatchDrop
}
if safeBlockRef.L1Origin.Number != batch.GetBlockEpochNum(int(i)) {
......
......@@ -15,7 +15,6 @@ import (
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum-optimism/optimism/op-service/testutils"
"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/log"
......@@ -42,9 +41,6 @@ func (th *TestLogHandler) Log(r *log.Record) error {
return th.handler.Log(r)
}
var HashA = common.Hash{0x0a}
var HashB = common.Hash{0x0b}
func TestValidBatch(t *testing.T) {
defaultConf := rollup.Config{
Genesis: rollup.Genesis{
......@@ -1515,18 +1511,20 @@ func TestValidBatch(t *testing.T) {
// will return an error for block #99 (parent of l2A0)
l2Client.Mock.On("L2BlockRefByNumber", l2A0.Number-1).Return(eth.L2BlockRef{}, &tempErr)
// will return an error for l2A3
l2Client.Mock.On("PayloadByNumber", l2A3.Number).Return(&eth.ExecutionPayload{}, &tempErr)
l2Client.Mock.On("PayloadByNumber", l2A3.Number).Return(&eth.ExecutionPayloadEnvelope{}, &tempErr)
// make payloads for L2 blocks and set as expected return value of MockL2Client
for _, l2Block := range []eth.L2BlockRef{l2A0, l2A1, l2A2, l2B0} {
l2Client.ExpectL2BlockRefByNumber(l2Block.Number, l2Block, nil)
txData := l1InfoDepositTx(t, l2Block.L1Origin.Number)
payload := eth.ExecutionPayload{
ParentHash: l2Block.ParentHash,
BlockNumber: hexutil.Uint64(l2Block.Number),
Timestamp: hexutil.Uint64(l2Block.Time),
BlockHash: l2Block.Hash,
Transactions: []hexutil.Bytes{txData},
payload := eth.ExecutionPayloadEnvelope{
ExecutionPayload: &eth.ExecutionPayload{
ParentHash: l2Block.ParentHash,
BlockNumber: hexutil.Uint64(l2Block.Number),
Timestamp: hexutil.Uint64(l2Block.Time),
BlockHash: l2Block.Hash,
Transactions: []hexutil.Bytes{txData},
},
}
l2Client.Mock.On("L2BlockRefByNumber", l2Block.Number).Return(l2Block, &nilErr)
l2Client.Mock.On("PayloadByNumber", l2Block.Number).Return(&payload, &nilErr)
......@@ -1574,12 +1572,14 @@ func TestValidBatch(t *testing.T) {
txData := l1InfoDepositTx(t, l2B1.L1Origin.Number)
randTx = testutils.RandomTx(rng, new(big.Int).SetUint64(rng.Uint64()), signer)
randTxData, _ = randTx.MarshalBinary()
payload := eth.ExecutionPayload{
ParentHash: l2B0.Hash,
BlockNumber: hexutil.Uint64(l2B1.Number),
Timestamp: hexutil.Uint64(l2B1.Time),
BlockHash: l2B1.Hash,
Transactions: []hexutil.Bytes{txData, randTxData},
payload := eth.ExecutionPayloadEnvelope{
ExecutionPayload: &eth.ExecutionPayload{
ParentHash: l2B0.Hash,
BlockNumber: hexutil.Uint64(l2B1.Number),
Timestamp: hexutil.Uint64(l2B1.Time),
BlockHash: l2B1.Hash,
Transactions: []hexutil.Bytes{txData, randTxData},
},
}
l2Client.Mock.On("PayloadByNumber", l2B1.Number).Return(&payload, &nilErr).Once()
......@@ -1618,13 +1618,15 @@ func TestValidBatch(t *testing.T) {
})
// ====== Test invalid TX for overlapping batches ======
payload = eth.ExecutionPayload{
ParentHash: l2B0.Hash,
BlockNumber: hexutil.Uint64(l2B1.Number),
Timestamp: hexutil.Uint64(l2B1.Time),
BlockHash: l2B1.Hash,
// First TX is not a deposit TX. it will make error when extracting L2BlockRef from the payload
Transactions: []hexutil.Bytes{randTxData},
payload = eth.ExecutionPayloadEnvelope{
ExecutionPayload: &eth.ExecutionPayload{
ParentHash: l2B0.Hash,
BlockNumber: hexutil.Uint64(l2B1.Number),
Timestamp: hexutil.Uint64(l2B1.Time),
BlockHash: l2B1.Hash,
// First TX is not a deposit TX. it will make error when extracting L2BlockRef from the payload
Transactions: []hexutil.Bytes{randTxData},
},
}
l2Client.Mock.On("PayloadByNumber", l2B1.Number).Return(&payload, &nilErr).Once()
......
......@@ -15,7 +15,9 @@ import (
// AttributesMatchBlock checks if the L2 attributes pre-inputs match the output
// nil if it is a match. If err is not nil, the error contains the reason for the mismatch
func AttributesMatchBlock(rollupCfg *rollup.Config, attrs *eth.PayloadAttributes, parentHash common.Hash, block *eth.ExecutionPayload, l log.Logger) error {
func AttributesMatchBlock(rollupCfg *rollup.Config, attrs *eth.PayloadAttributes, parentHash common.Hash, envelope *eth.ExecutionPayloadEnvelope, l log.Logger) error {
block := envelope.ExecutionPayload
if parentHash != block.ParentHash {
return fmt.Errorf("parent hash field does not match. expected: %v. got: %v", parentHash, block.ParentHash)
}
......@@ -45,6 +47,9 @@ func AttributesMatchBlock(rollupCfg *rollup.Config, attrs *eth.PayloadAttributes
if withdrawalErr := checkWithdrawalsMatch(attrs.Withdrawals, block.Withdrawals); withdrawalErr != nil {
return withdrawalErr
}
if envelope.ParentBeaconBlockRoot != attrs.ParentBeaconBlockRoot {
return fmt.Errorf("parent beacon block root does not match. expected %v. got: %v", attrs.ParentBeaconBlockRoot, envelope.ParentBeaconBlockRoot)
}
return nil
}
......
......@@ -3,11 +3,161 @@ package derive
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
var (
validParentHash = common.HexToHash("0x123")
validTimestamp = eth.Uint64Quantity(123)
validParentBeaconRoot = common.HexToHash("0x456")
validPrevRandao = eth.Bytes32(common.HexToHash("0x789"))
validGasLimit = eth.Uint64Quantity(1000)
validWithdrawals = types.Withdrawals{}
)
type args struct {
envelope *eth.ExecutionPayloadEnvelope
attrs *eth.PayloadAttributes
parentHash common.Hash
}
func ecotoneArgs() args {
return args{
envelope: &eth.ExecutionPayloadEnvelope{
ParentBeaconBlockRoot: &validParentBeaconRoot,
ExecutionPayload: &eth.ExecutionPayload{
ParentHash: validParentHash,
Timestamp: validTimestamp,
PrevRandao: validPrevRandao,
GasLimit: validGasLimit,
Withdrawals: &validWithdrawals,
},
},
attrs: &eth.PayloadAttributes{
Timestamp: validTimestamp,
PrevRandao: validPrevRandao,
GasLimit: &validGasLimit,
ParentBeaconBlockRoot: &validParentBeaconRoot,
Withdrawals: &validWithdrawals,
},
parentHash: validParentHash,
}
}
func canyonArgs() args {
args := ecotoneArgs()
args.attrs.ParentBeaconBlockRoot = nil
args.envelope.ParentBeaconBlockRoot = nil
return args
}
func bedrockArgs() args {
args := ecotoneArgs()
args.attrs.Withdrawals = nil
args.envelope.ExecutionPayload.Withdrawals = nil
return args
}
func ecotoneNoParentBeaconBlockRoot() args {
args := ecotoneArgs()
args.envelope.ParentBeaconBlockRoot = nil
return args
}
func mismatchedParentHashArgs() args {
args := ecotoneArgs()
args.parentHash = common.HexToHash("0xabc")
return args
}
func createMistmatchedPrevRandao() args {
args := ecotoneArgs()
args.attrs.PrevRandao = eth.Bytes32(common.HexToHash("0xabc"))
return args
}
func createMismatchedGasLimit() args {
args := ecotoneArgs()
val := eth.Uint64Quantity(2000)
args.attrs.GasLimit = &val
return args
}
func createNilGasLimit() args {
args := ecotoneArgs()
args.attrs.GasLimit = nil
return args
}
func createMistmatchedTimestamp() args {
args := ecotoneArgs()
val := eth.Uint64Quantity(2000)
args.attrs.Timestamp = val
return args
}
func TestAttributesMatch(t *testing.T) {
rollupCfg := &rollup.Config{}
tests := []struct {
shouldMatch bool
args args
}{
{
shouldMatch: true,
args: ecotoneArgs(),
},
{
shouldMatch: true,
args: canyonArgs(),
},
{
shouldMatch: true,
args: bedrockArgs(),
},
{
shouldMatch: false,
args: mismatchedParentHashArgs(),
},
{
shouldMatch: false,
args: ecotoneNoParentBeaconBlockRoot(),
},
{
shouldMatch: false,
args: createMistmatchedPrevRandao(),
},
{
shouldMatch: false,
args: createMismatchedGasLimit(),
},
{
shouldMatch: false,
args: createNilGasLimit(),
},
{
shouldMatch: false,
args: createMistmatchedTimestamp(),
},
}
for _, test := range tests {
err := AttributesMatchBlock(rollupCfg, test.args.attrs, test.args.parentHash, test.args.envelope, testlog.Logger(t, log.LvlInfo))
if test.shouldMatch {
require.NoError(t, err)
} else {
require.Error(t, err)
}
}
}
func TestWithdrawalsMatch(t *testing.T) {
tests := []struct {
attrs *types.Withdrawals
......
......@@ -18,9 +18,9 @@ var _ EngineControl = (*EngineController)(nil)
var _ LocalEngineControl = (*EngineController)(nil)
type ExecEngine interface {
GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayload, error)
GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayloadEnvelope, error)
ForkchoiceUpdate(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error)
NewPayload(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error)
NewPayload(ctx context.Context, payload *eth.ExecutionPayload, parentBeaconBlockRoot *common.Hash) (*eth.PayloadStatusV1, error)
}
type EngineController struct {
......@@ -139,7 +139,7 @@ func (e *EngineController) StartPayload(ctx context.Context, parent eth.L2BlockR
return BlockInsertOK, nil
}
func (e *EngineController) ConfirmPayload(ctx context.Context) (out *eth.ExecutionPayload, errTyp BlockInsertionErrType, err error) {
func (e *EngineController) ConfirmPayload(ctx context.Context) (out *eth.ExecutionPayloadEnvelope, errTyp BlockInsertionErrType, err error) {
if e.buildingID == (eth.PayloadID{}) {
return nil, BlockInsertPrestateErr, fmt.Errorf("cannot complete payload building: not currently building a payload")
}
......@@ -153,11 +153,11 @@ func (e *EngineController) ConfirmPayload(ctx context.Context) (out *eth.Executi
}
// Update the safe head if the payload is built with the last attributes in the batch.
updateSafe := e.buildingSafe && e.safeAttrs != nil && e.safeAttrs.isLastInSpan
payload, errTyp, err := confirmPayload(ctx, e.log, e.engine, fc, e.buildingID, updateSafe)
envelope, errTyp, err := confirmPayload(ctx, e.log, e.engine, fc, e.buildingID, updateSafe)
if err != nil {
return nil, errTyp, fmt.Errorf("failed to complete building on top of L2 chain %s, id: %s, error (%d): %w", e.buildingOnto, e.buildingID, errTyp, err)
}
ref, err := PayloadToBlockRef(e.rollupCfg, payload)
ref, err := PayloadToBlockRef(e.rollupCfg, envelope.ExecutionPayload)
if err != nil {
return nil, BlockInsertPayloadErr, NewResetError(fmt.Errorf("failed to decode L2 block ref from payload: %w", err))
}
......@@ -175,7 +175,7 @@ func (e *EngineController) ConfirmPayload(ctx context.Context) (out *eth.Executi
}
e.resetBuildingState()
return payload, BlockInsertOK, nil
return envelope, BlockInsertOK, nil
}
func (e *EngineController) CancelPayload(ctx context.Context, force bool) error {
......@@ -256,19 +256,20 @@ func (e *EngineController) TryUpdateEngine(ctx context.Context) error {
return nil
}
func (e *EngineController) InsertUnsafePayload(ctx context.Context, payload *eth.ExecutionPayload, ref eth.L2BlockRef) error {
status, err := e.engine.NewPayload(ctx, payload)
func (e *EngineController) InsertUnsafePayload(ctx context.Context, envelope *eth.ExecutionPayloadEnvelope, ref eth.L2BlockRef) error {
status, err := e.engine.NewPayload(ctx, envelope.ExecutionPayload, envelope.ParentBeaconBlockRoot)
if err != nil {
return NewTemporaryError(fmt.Errorf("failed to update insert payload: %w", err))
}
if !e.checkNewPayloadStatus(status.Status) {
payload := envelope.ExecutionPayload
return NewTemporaryError(fmt.Errorf("cannot process unsafe payload: new - %v; parent: %v; err: %w",
payload.ID(), payload.ParentID(), eth.NewPayloadErr(payload, status)))
}
// Mark the new payload as valid
fc := eth.ForkchoiceState{
HeadBlockHash: payload.BlockHash,
HeadBlockHash: envelope.ExecutionPayload.BlockHash,
SafeBlockHash: e.safeHead.Hash,
FinalizedBlockHash: e.finalizedHead.Hash,
}
......@@ -287,6 +288,7 @@ func (e *EngineController) InsertUnsafePayload(ctx context.Context, payload *eth
}
}
if !e.checkForkchoiceUpdatedStatus(fcRes.PayloadStatus.Status) {
payload := envelope.ExecutionPayload
return NewTemporaryError(fmt.Errorf("cannot prepare unsafe chain for new payload: new - %v; parent: %v; err: %w",
payload.ID(), payload.ParentID(), eth.ForkchoiceUpdateErr(fcRes.PayloadStatus)))
}
......@@ -307,6 +309,6 @@ func (e *EngineController) ForkchoiceUpdate(ctx context.Context, state *eth.Fork
}
// NewPayload implements LocalEngineControl.
func (e *EngineController) NewPayload(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error) {
return e.engine.NewPayload(ctx, payload)
func (e *EngineController) NewPayload(ctx context.Context, payload *eth.ExecutionPayload, parentBeaconBlockRoot *common.Hash) (*eth.PayloadStatusV1, error) {
return e.engine.NewPayload(ctx, payload, parentBeaconBlockRoot)
}
......@@ -37,8 +37,8 @@ type NextAttributesProvider interface {
}
type L2Source interface {
PayloadByHash(context.Context, common.Hash) (*eth.ExecutionPayload, error)
PayloadByNumber(context.Context, uint64) (*eth.ExecutionPayload, error)
PayloadByHash(context.Context, common.Hash) (*eth.ExecutionPayloadEnvelope, error)
PayloadByNumber(context.Context, uint64) (*eth.ExecutionPayloadEnvelope, error)
L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error)
L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error)
L2BlockRefByNumber(ctx context.Context, num uint64) (eth.L2BlockRef, error)
......@@ -67,7 +67,7 @@ type EngineControl interface {
// If updateSafe, the resulting block will be marked as a safe block.
StartPayload(ctx context.Context, parent eth.L2BlockRef, attrs *AttributesWithParent, updateSafe bool) (errType BlockInsertionErrType, err error)
// ConfirmPayload requests the engine to complete the current block. If no block is being built, or if it fails, an error is returned.
ConfirmPayload(ctx context.Context) (out *eth.ExecutionPayload, errTyp BlockInsertionErrType, err error)
ConfirmPayload(ctx context.Context) (out *eth.ExecutionPayloadEnvelope, errTyp BlockInsertionErrType, err error)
// CancelPayload requests the engine to stop building the current block without making it canonical.
// This is optional, as the engine expires building jobs that are left uncompleted, but can still save resources.
CancelPayload(ctx context.Context, force bool) error
......@@ -80,7 +80,7 @@ type LocalEngineControl interface {
ResetBuildingState()
IsEngineSyncing() bool
TryUpdateEngine(ctx context.Context) error
InsertUnsafePayload(ctx context.Context, payload *eth.ExecutionPayload, ref eth.L2BlockRef) error
InsertUnsafePayload(ctx context.Context, payload *eth.ExecutionPayloadEnvelope, ref eth.L2BlockRef) error
PendingSafeL2Head() eth.L2BlockRef
......@@ -178,19 +178,19 @@ func (eq *EngineQueue) SystemConfig() eth.SystemConfig {
return eq.sysCfg
}
func (eq *EngineQueue) AddUnsafePayload(payload *eth.ExecutionPayload) {
if payload == nil {
func (eq *EngineQueue) AddUnsafePayload(envelope *eth.ExecutionPayloadEnvelope) {
if envelope == nil {
eq.log.Warn("cannot add nil unsafe payload")
return
}
if err := eq.unsafePayloads.Push(payload); err != nil {
eq.log.Warn("Could not add unsafe payload", "id", payload.ID(), "timestamp", uint64(payload.Timestamp), "err", err)
if err := eq.unsafePayloads.Push(envelope); err != nil {
eq.log.Warn("Could not add unsafe payload", "id", envelope.ExecutionPayload.ID(), "timestamp", uint64(envelope.ExecutionPayload.Timestamp), "err", err)
return
}
p := eq.unsafePayloads.Peek()
eq.metrics.RecordUnsafePayloadsBuffer(uint64(eq.unsafePayloads.Len()), eq.unsafePayloads.MemSize(), p.ID())
eq.log.Trace("Next unsafe payload to process", "next", p.ID(), "timestamp", uint64(p.Timestamp))
eq.metrics.RecordUnsafePayloadsBuffer(uint64(eq.unsafePayloads.Len()), eq.unsafePayloads.MemSize(), p.ExecutionPayload.ID())
eq.log.Trace("Next unsafe payload to process", "next", p.ExecutionPayload.ID(), "timestamp", uint64(p.ExecutionPayload.Timestamp))
}
func (eq *EngineQueue) Finalize(l1Origin eth.L1BlockRef) {
......@@ -229,7 +229,7 @@ func (eq *EngineQueue) LowestQueuedUnsafeBlock() eth.L2BlockRef {
if payload == nil {
return eth.L2BlockRef{}
}
ref, err := PayloadToBlockRef(eq.cfg, payload)
ref, err := PayloadToBlockRef(eq.cfg, payload.ExecutionPayload)
if err != nil {
return eth.L2BlockRef{}
}
......@@ -414,7 +414,8 @@ func (eq *EngineQueue) logSyncProgress(reason string) {
}
func (eq *EngineQueue) tryNextUnsafePayload(ctx context.Context) error {
first := eq.unsafePayloads.Peek()
firstEnvelope := eq.unsafePayloads.Peek()
first := firstEnvelope.ExecutionPayload
if uint64(first.BlockNumber) <= eq.ec.SafeL2Head().Number {
eq.log.Info("skipping unsafe payload, since it is older than safe head", "safe", eq.ec.SafeL2Head().ID(), "unsafe", first.ID(), "payload", first.ID())
......@@ -443,7 +444,7 @@ func (eq *EngineQueue) tryNextUnsafePayload(ctx context.Context) error {
return nil
}
if err := eq.ec.InsertUnsafePayload(ctx, first, ref); errors.Is(err, ErrTemporary) {
if err := eq.ec.InsertUnsafePayload(ctx, firstEnvelope, ref); errors.Is(err, ErrTemporary) {
eq.log.Debug("Temporary error while inserting unsafe payload", "hash", ref.Hash, "number", ref.Number, "timestamp", ref.Time, "l1Origin", ref.L1Origin)
return err
} else if err != nil {
......@@ -497,7 +498,7 @@ func (eq *EngineQueue) consolidateNextSafeAttributes(ctx context.Context) error
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
payload, err := eq.engine.PayloadByNumber(ctx, eq.ec.PendingSafeL2Head().Number+1)
envelope, err := eq.engine.PayloadByNumber(ctx, eq.ec.PendingSafeL2Head().Number+1)
if err != nil {
if errors.Is(err, ethereum.NotFound) {
// engine may have restarted, or inconsistent safe head. We need to reset
......@@ -505,12 +506,12 @@ func (eq *EngineQueue) consolidateNextSafeAttributes(ctx context.Context) error
}
return NewTemporaryError(fmt.Errorf("failed to get existing unsafe payload to compare against derived attributes from L1: %w", err))
}
if err := AttributesMatchBlock(eq.cfg, eq.safeAttributes.attributes, eq.ec.PendingSafeL2Head().Hash, payload, eq.log); err != nil {
if err := AttributesMatchBlock(eq.cfg, eq.safeAttributes.attributes, eq.ec.PendingSafeL2Head().Hash, envelope, eq.log); err != nil {
eq.log.Warn("L2 reorg: existing unsafe block does not match derived attributes from L1", "err", err, "unsafe", eq.ec.UnsafeL2Head(), "pending_safe", eq.ec.PendingSafeL2Head(), "safe", eq.ec.SafeL2Head())
// geth cannot wind back a chain without reorging to a new, previously non-canonical, block
return eq.forceNextSafeAttributes(ctx)
}
ref, err := PayloadToBlockRef(eq.cfg, payload)
ref, err := PayloadToBlockRef(eq.cfg, envelope.ExecutionPayload)
if err != nil {
return NewResetError(fmt.Errorf("failed to decode L2 block ref from payload: %w", err))
}
......@@ -535,7 +536,7 @@ func (eq *EngineQueue) forceNextSafeAttributes(ctx context.Context) error {
lastInSpan := eq.safeAttributes.isLastInSpan
errType, err := eq.StartPayload(ctx, eq.ec.PendingSafeL2Head(), eq.safeAttributes, true)
if err == nil {
_, errType, err = eq.ConfirmPayload(ctx)
_, errType, err = eq.ec.ConfirmPayload(ctx)
}
if err != nil {
switch errType {
......@@ -588,7 +589,7 @@ func (eq *EngineQueue) StartPayload(ctx context.Context, parent eth.L2BlockRef,
return eq.ec.StartPayload(ctx, parent, attrs, updateSafe)
}
func (eq *EngineQueue) ConfirmPayload(ctx context.Context) (out *eth.ExecutionPayload, errTyp BlockInsertionErrType, err error) {
func (eq *EngineQueue) ConfirmPayload(ctx context.Context) (out *eth.ExecutionPayloadEnvelope, errTyp BlockInsertionErrType, err error) {
return eq.ec.ConfirmPayload(ctx)
}
......@@ -660,7 +661,7 @@ func (eq *EngineQueue) Reset(ctx context.Context, _ eth.L1BlockRef, _ eth.System
// UnsafeL2SyncTarget retrieves the first queued-up L2 unsafe payload, or a zeroed reference if there is none.
func (eq *EngineQueue) UnsafeL2SyncTarget() eth.L2BlockRef {
if first := eq.unsafePayloads.Peek(); first != nil {
ref, err := PayloadToBlockRef(eq.cfg, first)
ref, err := PayloadToBlockRef(eq.cfg, first.ExecutionPayload)
if err != nil {
return eth.L2BlockRef{}
}
......
......@@ -979,8 +979,9 @@ func TestBlockBuildingRace(t *testing.T) {
a1InfoTx,
},
}
eng.ExpectGetPayload(id, payloadA1, nil)
eng.ExpectNewPayload(payloadA1, &eth.PayloadStatusV1{
envelope := &eth.ExecutionPayloadEnvelope{ExecutionPayload: payloadA1}
eng.ExpectGetPayload(id, envelope, nil)
eng.ExpectNewPayload(payloadA1, nil, &eth.PayloadStatusV1{
Status: eth.ExecutionValid,
LatestValidHash: &refA1.Hash,
ValidationError: nil,
......@@ -1169,7 +1170,7 @@ func TestEngineQueue_StepPopOlderUnsafe(t *testing.T) {
L1Origin: refA.ID(),
SequenceNumber: 2,
}
payloadA1 := &eth.ExecutionPayload{
payloadA1 := &eth.ExecutionPayloadEnvelope{ExecutionPayload: &eth.ExecutionPayload{
ParentHash: refA1.ParentHash,
FeeRecipient: common.Address{},
StateRoot: eth.Bytes32{},
......@@ -1184,7 +1185,7 @@ func TestEngineQueue_StepPopOlderUnsafe(t *testing.T) {
BaseFeePerGas: *uint256.NewInt(7),
BlockHash: refA1.Hash,
Transactions: []eth.Data{},
}
}}
prev := &fakeAttributesQueue{origin: refA}
......
......@@ -117,17 +117,18 @@ func startPayload(ctx context.Context, eng ExecEngine, fc eth.ForkchoiceState, a
// confirmPayload ends an execution payload building process in the provided Engine, and persists the payload as the canonical head.
// If updateSafe is true, then the payload will also be recognized as safe-head at the same time.
// The severity of the error is distinguished to determine whether the payload was valid and can become canonical.
func confirmPayload(ctx context.Context, log log.Logger, eng ExecEngine, fc eth.ForkchoiceState, id eth.PayloadID, updateSafe bool) (out *eth.ExecutionPayload, errTyp BlockInsertionErrType, err error) {
payload, err := eng.GetPayload(ctx, id)
func confirmPayload(ctx context.Context, log log.Logger, eng ExecEngine, fc eth.ForkchoiceState, id eth.PayloadID, updateSafe bool) (out *eth.ExecutionPayloadEnvelope, errTyp BlockInsertionErrType, err error) {
envelope, err := eng.GetPayload(ctx, id)
if err != nil {
// even if it is an input-error (unknown payload ID), it is temporary, since we will re-attempt the full payload building, not just the retrieval of the payload.
return nil, BlockInsertTemporaryErr, fmt.Errorf("failed to get execution payload: %w", err)
}
payload := envelope.ExecutionPayload
if err := sanityCheckPayload(payload); err != nil {
return nil, BlockInsertPayloadErr, err
}
status, err := eng.NewPayload(ctx, payload)
status, err := eng.NewPayload(ctx, payload, envelope.ParentBeaconBlockRoot)
if err != nil {
return nil, BlockInsertTemporaryErr, fmt.Errorf("failed to insert execution payload: %w", err)
}
......@@ -164,5 +165,5 @@ func confirmPayload(ctx context.Context, log log.Logger, eng ExecEngine, fc eth.
"state_root", payload.StateRoot, "timestamp", uint64(payload.Timestamp), "parent", payload.ParentHash,
"prev_randao", payload.PrevRandao, "fee_recipient", payload.FeeRecipient,
"txs", len(payload.Transactions), "update_safe", updateSafe)
return payload, BlockInsertOK, nil
return envelope, BlockInsertOK, nil
}
......@@ -11,8 +11,8 @@ import (
)
type payloadAndSize struct {
payload *eth.ExecutionPayload
size uint64
envelope *eth.ExecutionPayloadEnvelope
size uint64
}
// payloadsByNumber buffers payloads ordered by block number.
......@@ -26,7 +26,7 @@ var _ heap.Interface = (*payloadsByNumber)(nil)
func (pq payloadsByNumber) Len() int { return len(pq) }
func (pq payloadsByNumber) Less(i, j int) bool {
return pq[i].payload.BlockNumber < pq[j].payload.BlockNumber
return pq[i].envelope.ExecutionPayload.BlockNumber < pq[j].envelope.ExecutionPayload.BlockNumber
}
// Swap is a heap.Interface method. Do not use this method directly.
......@@ -50,19 +50,19 @@ func (pq *payloadsByNumber) Pop() any {
}
const (
// ~580 bytes per payload, with some margin for overhead like map data
payloadMemFixedCost uint64 = 800
// ~1000 bytes per payload, with some margin for overhead like map data
payloadMemFixedCost uint64 = 1000
// 24 bytes per tx overhead (size of slice header in memory)
payloadTxMemOverhead uint64 = 24
)
func payloadMemSize(p *eth.ExecutionPayload) uint64 {
func payloadMemSize(p *eth.ExecutionPayloadEnvelope) uint64 {
out := payloadMemFixedCost
if p == nil {
return out
}
// 24 byte overhead per tx
for _, tx := range p.Transactions {
for _, tx := range p.ExecutionPayload.Transactions {
out += uint64(len(tx)) + payloadTxMemOverhead
}
return out
......@@ -80,10 +80,10 @@ type PayloadsQueue struct {
currentSize uint64
MaxSize uint64
blockHashes map[common.Hash]struct{}
SizeFn func(p *eth.ExecutionPayload) uint64
SizeFn func(p *eth.ExecutionPayloadEnvelope) uint64
}
func NewPayloadsQueue(maxSize uint64, sizeFn func(p *eth.ExecutionPayload) uint64) *PayloadsQueue {
func NewPayloadsQueue(maxSize uint64, sizeFn func(p *eth.ExecutionPayloadEnvelope) uint64) *PayloadsQueue {
return &PayloadsQueue{
pq: nil,
currentSize: 0,
......@@ -108,48 +108,48 @@ func (upq *PayloadsQueue) MemSize() uint64 {
//
// We prefer higher block numbers over lower block numbers, since lower block numbers are more likely to be conflicts and/or read from L1 sooner.
// The higher payload block numbers can be preserved, and once L1 contents meets these, they can all be processed in order.
func (upq *PayloadsQueue) Push(p *eth.ExecutionPayload) error {
if p == nil {
func (upq *PayloadsQueue) Push(e *eth.ExecutionPayloadEnvelope) error {
if e == nil || e.ExecutionPayload == nil {
return errors.New("cannot add nil payload")
}
if _, ok := upq.blockHashes[p.BlockHash]; ok {
return fmt.Errorf("cannot add duplicate payload %s", p.ID())
if _, ok := upq.blockHashes[e.ExecutionPayload.BlockHash]; ok {
return fmt.Errorf("cannot add duplicate payload %s", e.ExecutionPayload.ID())
}
size := upq.SizeFn(p)
size := upq.SizeFn(e)
if size > upq.MaxSize {
return fmt.Errorf("cannot add payload %s, payload mem size %d is larger than max queue size %d", p.ID(), size, upq.MaxSize)
return fmt.Errorf("cannot add payload %s, payload mem size %d is larger than max queue size %d", e.ExecutionPayload.ID(), size, upq.MaxSize)
}
heap.Push(&upq.pq, payloadAndSize{
payload: p,
size: size,
envelope: e,
size: size,
})
upq.currentSize += size
for upq.currentSize > upq.MaxSize {
upq.Pop()
}
upq.blockHashes[p.BlockHash] = struct{}{}
upq.blockHashes[e.ExecutionPayload.BlockHash] = struct{}{}
return nil
}
// Peek retrieves the payload with the lowest block number from the queue in O(1), or nil if the queue is empty.
func (upq *PayloadsQueue) Peek() *eth.ExecutionPayload {
func (upq *PayloadsQueue) Peek() *eth.ExecutionPayloadEnvelope {
if len(upq.pq) == 0 {
return nil
}
// peek into the priority queue, the first element is the highest priority (lowest block number).
// This does not apply to other elements, those are structured like a heap.
return upq.pq[0].payload
return upq.pq[0].envelope
}
// Pop removes the payload with the lowest block number from the queue in O(log(N)),
// and may return nil if the queue is empty.
func (upq *PayloadsQueue) Pop() *eth.ExecutionPayload {
func (upq *PayloadsQueue) Pop() *eth.ExecutionPayloadEnvelope {
if len(upq.pq) == 0 {
return nil
}
ps := heap.Pop(&upq.pq).(payloadAndSize) // nosemgrep
upq.currentSize -= ps.size
// remove the key from the block hashes map
delete(upq.blockHashes, ps.payload.BlockHash)
return ps.payload
delete(upq.blockHashes, ps.envelope.ExecutionPayload.BlockHash)
return ps.envelope
}
......@@ -14,8 +14,10 @@ func TestPayloadsByNumber(t *testing.T) {
p := payloadsByNumber{}
mk := func(i uint64) payloadAndSize {
return payloadAndSize{
payload: &eth.ExecutionPayload{
BlockNumber: eth.Uint64Quantity(i),
envelope: &eth.ExecutionPayloadEnvelope{
ExecutionPayload: &eth.ExecutionPayload{
BlockNumber: eth.Uint64Quantity(i),
},
},
}
}
......@@ -62,30 +64,35 @@ func TestPayloadsByNumber(t *testing.T) {
func TestPayloadMemSize(t *testing.T) {
require.Equal(t, payloadMemFixedCost, payloadMemSize(nil), "nil is same fixed cost")
require.Equal(t, payloadMemFixedCost, payloadMemSize(&eth.ExecutionPayload{}), "empty payload fixed cost")
require.Equal(t, payloadMemFixedCost+payloadTxMemOverhead, payloadMemSize(&eth.ExecutionPayload{Transactions: []eth.Data{nil}}), "nil tx counts")
require.Equal(t, payloadMemFixedCost+payloadTxMemOverhead, payloadMemSize(&eth.ExecutionPayload{Transactions: []eth.Data{make([]byte, 0)}}), "empty tx counts")
require.Equal(t, payloadMemFixedCost, payloadMemSize(&eth.ExecutionPayloadEnvelope{ExecutionPayload: &eth.ExecutionPayload{}}), "empty payload fixed cost")
require.Equal(t, payloadMemFixedCost+payloadTxMemOverhead, payloadMemSize(&eth.ExecutionPayloadEnvelope{ExecutionPayload: &eth.ExecutionPayload{Transactions: []eth.Data{nil}}}), "nil tx counts")
require.Equal(t, payloadMemFixedCost+payloadTxMemOverhead, payloadMemSize(&eth.ExecutionPayloadEnvelope{ExecutionPayload: &eth.ExecutionPayload{Transactions: []eth.Data{make([]byte, 0)}}}), "empty tx counts")
require.Equal(t, payloadMemFixedCost+4*payloadTxMemOverhead+42+1337+0+1,
payloadMemSize(&eth.ExecutionPayload{Transactions: []eth.Data{
payloadMemSize(&eth.ExecutionPayloadEnvelope{ExecutionPayload: &eth.ExecutionPayload{Transactions: []eth.Data{
make([]byte, 42),
make([]byte, 1337),
make([]byte, 0),
make([]byte, 1),
}}), "mixed txs")
}}}), "mixed txs")
}
func envelope(payload *eth.ExecutionPayload) *eth.ExecutionPayloadEnvelope {
return &eth.ExecutionPayloadEnvelope{ExecutionPayload: payload}
}
func TestPayloadsQueue(t *testing.T) {
pq := NewPayloadsQueue(payloadMemFixedCost*3, payloadMemSize)
require.Equal(t, 0, pq.Len())
require.Equal(t, (*eth.ExecutionPayload)(nil), pq.Peek())
require.Equal(t, (*eth.ExecutionPayload)(nil), pq.Pop())
a := &eth.ExecutionPayload{BlockNumber: 3, BlockHash: common.Hash{3}}
b := &eth.ExecutionPayload{BlockNumber: 4, BlockHash: common.Hash{4}}
c := &eth.ExecutionPayload{BlockNumber: 5, BlockHash: common.Hash{5}}
d := &eth.ExecutionPayload{BlockNumber: 6, BlockHash: common.Hash{6}}
bAlt := &eth.ExecutionPayload{BlockNumber: 4, BlockHash: common.Hash{0xff}}
bDup := &eth.ExecutionPayload{BlockNumber: 4, BlockHash: common.Hash{4}}
require.Nil(t, pq.Peek())
require.Nil(t, pq.Pop())
a := envelope(&eth.ExecutionPayload{BlockNumber: 3, BlockHash: common.Hash{3}})
b := envelope(&eth.ExecutionPayload{BlockNumber: 4, BlockHash: common.Hash{4}})
c := envelope(&eth.ExecutionPayload{BlockNumber: 5, BlockHash: common.Hash{5}})
d := envelope(&eth.ExecutionPayload{BlockNumber: 6, BlockHash: common.Hash{6}})
bAlt := envelope(&eth.ExecutionPayload{BlockNumber: 4, BlockHash: common.Hash{0xff}})
bDup := envelope(&eth.ExecutionPayload{BlockNumber: 4, BlockHash: common.Hash{4}})
require.NoError(t, pq.Push(b))
require.Equal(t, pq.Len(), 1)
require.Equal(t, pq.Peek(), b)
......@@ -114,7 +121,7 @@ func TestPayloadsQueue(t *testing.T) {
require.Equal(t, pq.Pop(), c)
require.Equal(t, pq.Len(), 0, "expecting no items to remain")
e := &eth.ExecutionPayload{BlockNumber: 5, Transactions: []eth.Data{make([]byte, payloadMemFixedCost*3+1)}}
e := envelope(&eth.ExecutionPayload{BlockNumber: 5, Transactions: []eth.Data{make([]byte, payloadMemFixedCost*3+1)}})
require.Error(t, pq.Push(e), "cannot add payloads that are too large")
require.NoError(t, pq.Push(b))
......
......@@ -44,7 +44,7 @@ type EngineQueueStage interface {
SystemConfig() eth.SystemConfig
Finalize(l1Origin eth.L1BlockRef)
AddUnsafePayload(payload *eth.ExecutionPayload)
AddUnsafePayload(payload *eth.ExecutionPayloadEnvelope)
Step(context.Context) error
}
......@@ -128,7 +128,7 @@ func (dp *DerivationPipeline) FinalizedL1() eth.L1BlockRef {
}
// AddUnsafePayload schedules an execution payload to be processed, ahead of deriving it from L1
func (dp *DerivationPipeline) AddUnsafePayload(payload *eth.ExecutionPayload) {
func (dp *DerivationPipeline) AddUnsafePayload(payload *eth.ExecutionPayloadEnvelope) {
dp.eng.AddUnsafePayload(payload)
}
......
......@@ -18,7 +18,7 @@ type Metrics interface {
RecordPublishingError()
RecordDerivationError()
RecordReceivedUnsafePayload(payload *eth.ExecutionPayload)
RecordReceivedUnsafePayload(payload *eth.ExecutionPayloadEnvelope)
RecordL1Ref(name string, ref eth.L1BlockRef)
RecordL2Ref(name string, ref eth.L2BlockRef)
......@@ -55,7 +55,7 @@ type L2Chain interface {
type DerivationPipeline interface {
Reset()
Step(ctx context.Context) error
AddUnsafePayload(payload *eth.ExecutionPayload)
AddUnsafePayload(payload *eth.ExecutionPayloadEnvelope)
Finalize(ref eth.L1BlockRef)
FinalizedL1() eth.L1BlockRef
Origin() eth.L1BlockRef
......@@ -75,16 +75,16 @@ type L1StateIface interface {
type SequencerIface interface {
StartBuildingBlock(ctx context.Context) error
CompleteBuildingBlock(ctx context.Context) (*eth.ExecutionPayload, error)
CompleteBuildingBlock(ctx context.Context) (*eth.ExecutionPayloadEnvelope, error)
PlanNextSequencerAction() time.Duration
RunNextSequencerAction(ctx context.Context) (*eth.ExecutionPayload, error)
RunNextSequencerAction(ctx context.Context) (*eth.ExecutionPayloadEnvelope, error)
BuildingOnto() eth.L2BlockRef
CancelBuildingBlock(ctx context.Context)
}
type Network interface {
// PublishL2Payload is called by the driver whenever there is a new payload to publish, synchronously with the driver main loop.
PublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error
PublishL2Payload(ctx context.Context, payload *eth.ExecutionPayloadEnvelope) error
}
type AltSync interface {
......@@ -148,7 +148,7 @@ func NewDriver(driverCfg *Config, cfg *rollup.Config, l2 L2Chain, l1 L1Chain, l1
l1HeadSig: make(chan eth.L1BlockRef, 10),
l1SafeSig: make(chan eth.L1BlockRef, 10),
l1FinalizedSig: make(chan eth.L1BlockRef, 10),
unsafeL2Payloads: make(chan *eth.ExecutionPayload, 10),
unsafeL2Payloads: make(chan *eth.ExecutionPayloadEnvelope, 10),
altSync: altSync,
}
}
......@@ -60,7 +60,7 @@ func (m *MeteredEngine) StartPayload(ctx context.Context, parent eth.L2BlockRef,
return errType, err
}
func (m *MeteredEngine) ConfirmPayload(ctx context.Context) (out *eth.ExecutionPayload, errTyp derive.BlockInsertionErrType, err error) {
func (m *MeteredEngine) ConfirmPayload(ctx context.Context) (out *eth.ExecutionPayloadEnvelope, errTyp derive.BlockInsertionErrType, err error) {
sealingStart := time.Now()
// Actually execute the block and add it to the head of the chain.
payload, errType, err := m.inner.ConfirmPayload(ctx)
......@@ -73,12 +73,14 @@ func (m *MeteredEngine) ConfirmPayload(ctx context.Context) (out *eth.ExecutionP
buildTime := now.Sub(m.buildingStartTime)
m.metrics.RecordSequencerSealingTime(sealTime)
m.metrics.RecordSequencerBuildingDiffTime(buildTime - time.Duration(m.cfg.BlockTime)*time.Second)
m.metrics.CountSequencedTxs(len(payload.Transactions))
txnCount := len(payload.ExecutionPayload.Transactions)
m.metrics.CountSequencedTxs(txnCount)
ref := m.inner.UnsafeL2Head()
m.log.Debug("Processed new L2 block", "l2_unsafe", ref, "l1_origin", ref.L1Origin,
"txs", len(payload.Transactions), "time", ref.Time, "seal_time", sealTime, "build_time", buildTime)
"txs", txnCount, "time", ref.Time, "seal_time", sealTime, "build_time", buildTime)
return payload, errType, err
}
......
......@@ -113,12 +113,12 @@ func (d *Sequencer) StartBuildingBlock(ctx context.Context) error {
// CompleteBuildingBlock takes the current block that is being built, and asks the engine to complete the building, seal the block, and persist it as canonical.
// Warning: the safe and finalized L2 blocks as viewed during the initiation of the block building are reused for completion of the block building.
// The Execution engine should not change the safe and finalized blocks between start and completion of block building.
func (d *Sequencer) CompleteBuildingBlock(ctx context.Context) (*eth.ExecutionPayload, error) {
payload, errTyp, err := d.engine.ConfirmPayload(ctx)
func (d *Sequencer) CompleteBuildingBlock(ctx context.Context) (*eth.ExecutionPayloadEnvelope, error) {
envelope, errTyp, err := d.engine.ConfirmPayload(ctx)
if err != nil {
return nil, fmt.Errorf("failed to complete building block: error (%d): %w", errTyp, err)
}
return payload, nil
return envelope, nil
}
// CancelBuildingBlock cancels the current open block building job.
......@@ -203,7 +203,7 @@ func (d *Sequencer) BuildingOnto() eth.L2BlockRef {
// If the derivation pipeline does force a conflicting block, then an ongoing sequencer task might still finish,
// but the derivation can continue to reset until the chain is correct.
// If the engine is currently building safe blocks, then that building is not interrupted, and sequencing is delayed.
func (d *Sequencer) RunNextSequencerAction(ctx context.Context) (*eth.ExecutionPayload, error) {
func (d *Sequencer) RunNextSequencerAction(ctx context.Context) (*eth.ExecutionPayloadEnvelope, error) {
if onto, buildingID, safe := d.engine.BuildingPayload(); buildingID != (eth.PayloadID{}) {
if safe {
d.log.Warn("avoiding sequencing to not interrupt safe-head changes", "onto", onto, "onto_time", onto.Time)
......@@ -211,7 +211,7 @@ func (d *Sequencer) RunNextSequencerAction(ctx context.Context) (*eth.ExecutionP
d.nextAction = d.timeNow().Add(time.Second * time.Duration(d.rollupCfg.BlockTime))
return nil, nil
}
payload, err := d.CompleteBuildingBlock(ctx)
envelope, err := d.CompleteBuildingBlock(ctx)
if err != nil {
if errors.Is(err, derive.ErrCritical) {
return nil, err // bubble up critical errors.
......@@ -233,8 +233,9 @@ func (d *Sequencer) RunNextSequencerAction(ctx context.Context) (*eth.ExecutionP
}
return nil, nil
} else {
payload := envelope.ExecutionPayload
d.log.Info("sequencer successfully built a new block", "block", payload.ID(), "time", uint64(payload.Timestamp), "txs", len(payload.Transactions))
return payload, nil
return envelope, nil
}
} else {
err := d.StartBuildingBlock(ctx)
......
......@@ -73,7 +73,7 @@ func (m *FakeEngineControl) StartPayload(ctx context.Context, parent eth.L2Block
return derive.BlockInsertOK, nil
}
func (m *FakeEngineControl) ConfirmPayload(ctx context.Context) (out *eth.ExecutionPayload, errTyp derive.BlockInsertionErrType, err error) {
func (m *FakeEngineControl) ConfirmPayload(ctx context.Context) (out *eth.ExecutionPayloadEnvelope, errTyp derive.BlockInsertionErrType, err error) {
if m.err != nil {
return nil, m.errTyp, m.err
}
......@@ -92,7 +92,7 @@ func (m *FakeEngineControl) ConfirmPayload(ctx context.Context) (out *eth.Execut
m.resetBuildingState()
m.totalTxs += len(payload.Transactions)
return payload, derive.BlockInsertOK, nil
return &eth.ExecutionPayloadEnvelope{ExecutionPayload: payload}, derive.BlockInsertOK, nil
}
func (m *FakeEngineControl) CancelPayload(ctx context.Context, force bool) error {
......@@ -351,12 +351,12 @@ func TestSequencerChaosMonkey(t *testing.T) {
require.NoError(t, err)
}
if payload != nil {
require.Equal(t, engControl.UnsafeL2Head().ID(), payload.ID(), "head must stay in sync with emitted payloads")
require.Equal(t, engControl.UnsafeL2Head().ID(), payload.ExecutionPayload.ID(), "head must stay in sync with emitted payloads")
var tx types.Transaction
require.NoError(t, tx.UnmarshalBinary(payload.Transactions[0]))
info, err := derive.L1BlockInfoFromBytes(cfg, 0, tx.Data())
require.NoError(t, tx.UnmarshalBinary(payload.ExecutionPayload.Transactions[0]))
info, err := derive.L1BlockInfoFromBytes(cfg, uint64(payload.ExecutionPayload.Timestamp), tx.Data())
require.NoError(t, err)
require.GreaterOrEqual(t, uint64(payload.Timestamp), info.Time, "ensure L2 time >= L1 time")
require.GreaterOrEqual(t, uint64(payload.ExecutionPayload.Timestamp), info.Time, "ensure L2 time >= L1 time")
}
}
......
......@@ -80,7 +80,7 @@ type Driver struct {
// L2 Signals:
unsafeL2Payloads chan *eth.ExecutionPayload
unsafeL2Payloads chan *eth.ExecutionPayloadEnvelope
l1 L1Chain
l2 L2Chain
......@@ -160,11 +160,11 @@ func (s *Driver) OnL1Finalized(ctx context.Context, finalized eth.L1BlockRef) er
}
}
func (s *Driver) OnUnsafeL2Payload(ctx context.Context, payload *eth.ExecutionPayload) error {
func (s *Driver) OnUnsafeL2Payload(ctx context.Context, envelope *eth.ExecutionPayloadEnvelope) error {
select {
case <-ctx.Done():
return ctx.Err()
case s.unsafeL2Payloads <- payload:
case s.unsafeL2Payloads <- envelope:
return nil
}
}
......@@ -287,7 +287,7 @@ func (s *Driver) eventLoop() {
// Publishing of unsafe data via p2p is optional.
// Errors are not severe enough to change/halt sequencing but should be logged and metered.
if err := s.network.PublishL2Payload(s.driverCtx, payload); err != nil {
s.log.Warn("failed to publish newly created block", "id", payload.ID(), "err", err)
s.log.Warn("failed to publish newly created block", "id", payload.ExecutionPayload.ID(), "err", err)
s.metrics.RecordPublishingError()
}
}
......@@ -300,11 +300,11 @@ func (s *Driver) eventLoop() {
if err != nil {
s.log.Warn("failed to check for unsafe L2 blocks to sync", "err", err)
}
case payload := <-s.unsafeL2Payloads:
case envelope := <-s.unsafeL2Payloads:
s.snapshot("New unsafe payload")
s.log.Info("Optimistically queueing unsafe L2 execution payload", "id", payload.ID())
s.derivation.AddUnsafePayload(payload)
s.metrics.RecordReceivedUnsafePayload(payload)
s.log.Info("Optimistically queueing unsafe L2 execution payload", "id", envelope.ExecutionPayload.ID())
s.derivation.AddUnsafePayload(envelope)
s.metrics.RecordReceivedUnsafePayload(envelope)
reqStep()
case newL1Head := <-s.l1HeadSig:
......
......@@ -50,31 +50,42 @@ func (o *OracleEngine) L2OutputRoot(l2ClaimBlockNum uint64) (eth.Bytes32, error)
return rollup.ComputeL2OutputRootV0(eth.HeaderBlockInfo(outBlock), withdrawalsTrie.Hash())
}
func (o *OracleEngine) GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayload, error) {
res, err := o.api.GetPayloadV2(ctx, payloadId)
func (o *OracleEngine) GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayloadEnvelope, error) {
res, err := o.api.GetPayloadV3(ctx, payloadId)
if err != nil {
return nil, err
}
return res.ExecutionPayload, nil
return res, nil
}
func (o *OracleEngine) ForkchoiceUpdate(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) {
return o.api.ForkchoiceUpdatedV2(ctx, state, attr)
return o.api.ForkchoiceUpdatedV3(ctx, state, attr)
}
func (o *OracleEngine) NewPayload(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error) {
return o.api.NewPayloadV2(ctx, payload)
func (o *OracleEngine) NewPayload(ctx context.Context, payload *eth.ExecutionPayload, parentBeaconBlockRoot *common.Hash) (*eth.PayloadStatusV1, error) {
if o.rollupCfg.IsEcotone(uint64(payload.Timestamp)) {
return o.api.NewPayloadV3(ctx, payload, []common.Hash{}, parentBeaconBlockRoot)
} else {
return o.api.NewPayloadV2(ctx, payload)
}
}
func (o *OracleEngine) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayload, error) {
func (o *OracleEngine) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayloadEnvelope, error) {
block := o.backend.GetBlockByHash(hash)
if block == nil {
return nil, ErrNotFound
}
return eth.BlockAsPayload(block, o.rollupCfg.CanyonTime)
payload, err := eth.BlockAsPayload(block, o.rollupCfg.CanyonTime)
if err != nil {
return nil, err
}
return &eth.ExecutionPayloadEnvelope{
ParentBeaconBlockRoot: block.BeaconRoot(),
ExecutionPayload: payload,
}, nil
}
func (o *OracleEngine) PayloadByNumber(ctx context.Context, n uint64) (*eth.ExecutionPayload, error) {
func (o *OracleEngine) PayloadByNumber(ctx context.Context, n uint64) (*eth.ExecutionPayloadEnvelope, error) {
hash := o.backend.GetCanonicalHash(n)
if hash == (common.Hash{}) {
return nil, ErrNotFound
......@@ -125,5 +136,5 @@ func (o *OracleEngine) SystemConfigByL2Hash(ctx context.Context, hash common.Has
if err != nil {
return eth.SystemConfig{}, err
}
return derive.PayloadToSystemConfig(o.rollupCfg, payload)
return derive.PayloadToSystemConfig(o.rollupCfg, payload.ExecutionPayload)
}
......@@ -31,7 +31,7 @@ func TestPayloadByHash(t *testing.T) {
require.NoError(t, err)
expected, err := eth.BlockAsPayload(block, engine.rollupCfg.CanyonTime)
require.NoError(t, err)
require.Equal(t, expected, payload)
require.Equal(t, &eth.ExecutionPayloadEnvelope{ExecutionPayload: expected}, payload)
})
t.Run("UnknownBlock", func(t *testing.T) {
......@@ -53,7 +53,7 @@ func TestPayloadByNumber(t *testing.T) {
require.NoError(t, err)
expected, err := eth.BlockAsPayload(block, engine.rollupCfg.CanyonTime)
require.NoError(t, err)
require.Equal(t, expected, payload)
require.Equal(t, &eth.ExecutionPayloadEnvelope{ExecutionPayload: expected}, payload)
})
t.Run("NoCanonicalHash", func(t *testing.T) {
......
......@@ -41,15 +41,24 @@ type BlockProcessor struct {
func NewBlockProcessorFromPayloadAttributes(provider BlockDataProvider, parent common.Hash, params *eth.PayloadAttributes) (*BlockProcessor, error) {
header := &types.Header{
ParentHash: parent,
Coinbase: params.SuggestedFeeRecipient,
Difficulty: common.Big0,
GasLimit: uint64(*params.GasLimit),
Time: uint64(params.Timestamp),
Extra: nil,
MixDigest: common.Hash(params.PrevRandao),
Nonce: types.EncodeNonce(0),
ParentHash: parent,
Coinbase: params.SuggestedFeeRecipient,
Difficulty: common.Big0,
GasLimit: uint64(*params.GasLimit),
Time: uint64(params.Timestamp),
Extra: nil,
MixDigest: common.Hash(params.PrevRandao),
Nonce: types.EncodeNonce(0),
ParentBeaconRoot: params.ParentBeaconBlockRoot,
}
// Ecotone
if params.ParentBeaconBlockRoot != nil {
zero := uint64(0)
header.BlobGasUsed = &zero
header.ExcessBlobGas = &zero
}
return NewBlockProcessorFromHeader(provider, header)
}
......@@ -95,7 +104,7 @@ func (b *BlockProcessor) AddTx(tx *types.Transaction) error {
receipt, err := core.ApplyTransaction(b.dataProvider.Config(), b.dataProvider, &b.header.Coinbase,
b.gasPool, b.state, b.header, tx, &b.header.GasUsed, *b.dataProvider.GetVMConfig())
if err != nil {
return fmt.Errorf("failed to apply deposit transaction to L2 block (tx %d): %w", txIndex, err)
return fmt.Errorf("failed to apply transaction to L2 block (tx %d): %w", txIndex, err)
}
b.receipts = append(b.receipts, receipt)
b.transactions = append(b.transactions, tx)
......
......@@ -178,12 +178,19 @@ func (ea *L2EngineAPI) endBlock() (*types.Block, error) {
}
func (ea *L2EngineAPI) GetPayloadV1(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayload, error) {
return ea.getPayload(ctx, payloadId)
res, err := ea.getPayload(ctx, payloadId)
if err != nil {
return nil, err
}
return res.ExecutionPayload, nil
}
func (ea *L2EngineAPI) GetPayloadV2(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayloadEnvelope, error) {
payload, err := ea.getPayload(ctx, payloadId)
return &eth.ExecutionPayloadEnvelope{ExecutionPayload: payload}, err
return ea.getPayload(ctx, payloadId)
}
func (ea *L2EngineAPI) GetPayloadV3(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayloadEnvelope, error) {
return ea.getPayload(ctx, payloadId)
}
func (ea *L2EngineAPI) config() *params.ChainConfig {
......@@ -213,6 +220,17 @@ func (ea *L2EngineAPI) ForkchoiceUpdatedV2(ctx context.Context, state *eth.Forkc
return ea.forkchoiceUpdated(ctx, state, attr)
}
// Ported from: https://github.com/ethereum-optimism/op-geth/blob/c50337a60a1309a0f1dca3bf33ed1bb38c46cdd7/eth/catalyst/api.go#L197C1-L205C1
func (ea *L2EngineAPI) ForkchoiceUpdatedV3(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) {
if attr != nil {
if err := ea.verifyPayloadAttributes(attr); err != nil {
return STATUS_INVALID, engine.InvalidParams.With(err)
}
}
return ea.forkchoiceUpdated(ctx, state, attr)
}
// Ported from: https://github.com/ethereum-optimism/op-geth/blob/c50337a60a1309a0f1dca3bf33ed1bb38c46cdd7/eth/catalyst/api.go#L206-L218
func (ea *L2EngineAPI) verifyPayloadAttributes(attr *eth.PayloadAttributes) error {
c := ea.config()
......@@ -220,6 +238,11 @@ func (ea *L2EngineAPI) verifyPayloadAttributes(attr *eth.PayloadAttributes) erro
if err := checkAttribute(c.IsShanghai, attr.Withdrawals != nil, c.LondonBlock, uint64(attr.Timestamp)); err != nil {
return fmt.Errorf("invalid withdrawals: %w", err)
}
// Verify beacon root attribute for Cancun.
if err := checkAttribute(c.IsCancun, attr.ParentBeaconBlockRoot != nil, c.LondonBlock, uint64(attr.Timestamp)); err != nil {
return fmt.Errorf("invalid parent beacon block root: %w", err)
}
return nil
}
......@@ -238,7 +261,7 @@ func (ea *L2EngineAPI) NewPayloadV1(ctx context.Context, payload *eth.ExecutionP
return &eth.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.InvalidParams.With(errors.New("withdrawals not supported in V1"))
}
return ea.newPayload(ctx, payload)
return ea.newPayload(ctx, payload, nil, nil)
}
func (ea *L2EngineAPI) NewPayloadV2(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error) {
......@@ -250,10 +273,32 @@ func (ea *L2EngineAPI) NewPayloadV2(ctx context.Context, payload *eth.ExecutionP
return &eth.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.InvalidParams.With(errors.New("non-nil withdrawals pre-shanghai"))
}
return ea.newPayload(ctx, payload)
return ea.newPayload(ctx, payload, nil, nil)
}
func (ea *L2EngineAPI) getPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayload, error) {
// Ported from: https://github.com/ethereum-optimism/op-geth/blob/c50337a60a1309a0f1dca3bf33ed1bb38c46cdd7/eth/catalyst/api.go#L486C1-L507
func (ea *L2EngineAPI) NewPayloadV3(ctx context.Context, params *eth.ExecutionPayload, versionedHashes []common.Hash, beaconRoot *common.Hash) (*eth.PayloadStatusV1, error) {
if params.ExcessBlobGas == nil {
return &eth.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.InvalidParams.With(errors.New("nil excessBlobGas post-cancun"))
}
if params.BlobGasUsed == nil {
return &eth.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.InvalidParams.With(errors.New("nil params.BlobGasUsed post-cancun"))
}
if versionedHashes == nil {
return &eth.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.InvalidParams.With(errors.New("nil versionedHashes post-cancun"))
}
if beaconRoot == nil {
return &eth.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.InvalidParams.With(errors.New("nil parentBeaconBlockRoot post-cancun"))
}
if !ea.config().IsCancun(new(big.Int).SetUint64(uint64(params.BlockNumber)), uint64(params.Timestamp)) {
return &eth.PayloadStatusV1{Status: eth.ExecutionInvalid}, engine.UnsupportedFork.With(errors.New("newPayloadV3 called pre-cancun"))
}
return ea.newPayload(ctx, params, versionedHashes, beaconRoot)
}
func (ea *L2EngineAPI) getPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayloadEnvelope, error) {
ea.log.Trace("L2Engine API request received", "method", "GetPayload", "id", payloadId)
if ea.payloadID != payloadId {
ea.log.Warn("unexpected payload ID requested for block building", "expected", ea.payloadID, "got", payloadId)
......@@ -264,7 +309,13 @@ func (ea *L2EngineAPI) getPayload(ctx context.Context, payloadId eth.PayloadID)
ea.log.Error("failed to finish block building", "err", err)
return nil, engine.UnknownPayload
}
return eth.BlockAsPayload(bl, ea.config().CanyonTime)
payload, err := eth.BlockAsPayload(bl, ea.config().CanyonTime)
if err != nil {
return nil, err
}
return &eth.ExecutionPayloadEnvelope{ExecutionPayload: payload, ParentBeaconBlockRoot: bl.BeaconRoot()}, nil
}
func (ea *L2EngineAPI) forkchoiceUpdated(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) {
......@@ -369,7 +420,7 @@ func toGethWithdrawals(payload *eth.ExecutionPayload) []*types.Withdrawal {
return result
}
func (ea *L2EngineAPI) newPayload(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error) {
func (ea *L2EngineAPI) newPayload(ctx context.Context, payload *eth.ExecutionPayload, hashes []common.Hash, root *common.Hash) (*eth.PayloadStatusV1, error) {
ea.log.Trace("L2Engine API request received", "method", "ExecutePayload", "number", payload.BlockNumber, "hash", payload.BlockHash)
txs := make([][]byte, len(payload.Transactions))
for i, tx := range payload.Transactions {
......@@ -391,7 +442,10 @@ func (ea *L2EngineAPI) newPayload(ctx context.Context, payload *eth.ExecutionPay
BlockHash: payload.BlockHash,
Transactions: txs,
Withdrawals: toGethWithdrawals(payload),
}, nil, nil)
ExcessBlobGas: (*uint64)(payload.ExcessBlobGas),
BlobGasUsed: (*uint64)(payload.BlobGasUsed),
}, hashes, root)
if err != nil {
log.Debug("Invalid NewPayload params", "params", payload, "error", err)
return &eth.PayloadStatusV1{Status: eth.ExecutionInvalidBlockHash}, nil
......
......@@ -105,7 +105,8 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
payloadID := api.startBlockBuilding(genesis, eth.Uint64Quantity(genesis.Time+3))
api.assert.Equal(block.BlockHash, api.headHash(), "should not reset chain head when building starts")
payload := api.getPayload(payloadID)
envelope := api.getPayload(payloadID)
payload := envelope.ExecutionPayload
api.assert.Equal(genesis.Hash(), payload.ParentHash, "should have old block as parent")
api.newPayload(payload)
......@@ -136,11 +137,12 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
// Build a valid block
payloadID := api.startBlockBuilding(genesis, eth.Uint64Quantity(genesis.Time+2))
newBlock := api.getPayload(payloadID)
envelope := api.getPayload(payloadID)
newBlock := envelope.ExecutionPayload
// But then make it invalid by changing the state root
newBlock.StateRoot = eth.Bytes32(genesis.TxHash)
updateBlockHash(newBlock)
updateBlockHash(envelope)
r, err := api.engine.NewPayloadV2(api.ctx, newBlock)
api.assert.NoError(err)
......@@ -153,11 +155,12 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
// Start with a valid time
payloadID := api.startBlockBuilding(genesis, eth.Uint64Quantity(genesis.Time+1))
newBlock := api.getPayload(payloadID)
envelope := api.getPayload(payloadID)
newBlock := envelope.ExecutionPayload
// Then make it invalid to check NewPayload rejects it
newBlock.Timestamp = eth.Uint64Quantity(genesis.Time)
updateBlockHash(newBlock)
updateBlockHash(envelope)
r, err := api.engine.NewPayloadV2(api.ctx, newBlock)
api.assert.NoError(err)
......@@ -170,11 +173,12 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
// Start with a valid time
payloadID := api.startBlockBuilding(genesis, eth.Uint64Quantity(genesis.Time+1))
newBlock := api.getPayload(payloadID)
envelope := api.getPayload(payloadID)
newBlock := envelope.ExecutionPayload
// Then make it invalid to check NewPayload rejects it
newBlock.Timestamp = eth.Uint64Quantity(genesis.Time - 1)
updateBlockHash(newBlock)
updateBlockHash(envelope)
r, err := api.engine.NewPayloadV2(api.ctx, newBlock)
api.assert.NoError(err)
......@@ -298,10 +302,10 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
}
// Updates the block hash to the expected value based on the other fields in the payload
func updateBlockHash(newBlock *eth.ExecutionPayload) {
func updateBlockHash(envelope *eth.ExecutionPayloadEnvelope) {
// And fix up the block hash
newHash, _ := newBlock.CheckBlockHash()
newBlock.BlockHash = newHash
newHash, _ := envelope.CheckBlockHash()
envelope.ExecutionPayload.BlockHash = newHash
}
type testHelper struct {
......@@ -352,7 +356,9 @@ func (h *testHelper) addBlockWithParent(head *types.Header, timestamp eth.Uint64
prevHead := h.backend.CurrentHeader()
id := h.startBlockBuilding(head, timestamp, txs...)
block := h.getPayload(id)
envelope := h.getPayload(id)
block := envelope.ExecutionPayload
h.assert.Equal(timestamp, block.Timestamp, "should create block with correct timestamp")
h.assert.Equal(head.Hash(), block.ParentHash, "should have correct parent")
h.assert.Len(block.Transactions, len(txs))
......@@ -415,13 +421,13 @@ func (h *testHelper) startBlockBuilding(head *types.Header, newBlockTimestamp et
return id
}
func (h *testHelper) getPayload(id *eth.PayloadID) *eth.ExecutionPayload {
func (h *testHelper) getPayload(id *eth.PayloadID) *eth.ExecutionPayloadEnvelope {
h.Log("getPayload", "id", id)
envelope, err := h.engine.GetPayloadV2(h.ctx, *id)
h.assert.NoError(err)
h.assert.NotNil(envelope)
h.assert.NotNil(envelope.ExecutionPayload)
return envelope.ExecutionPayload
return envelope
}
func (h *testHelper) newPayload(block *eth.ExecutionPayload) {
......
......@@ -8,6 +8,7 @@ fuzz:
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzExecutionPayloadUnmarshal ./eth
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzExecutionPayloadMarshalUnmarshalV1 ./eth
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzExecutionPayloadMarshalUnmarshalV2 ./eth
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzExecutionPayloadMarshalUnmarshalV3 ./eth
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzOBP01 ./eth
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzEncodeDecodeBlob ./eth
......
......@@ -25,6 +25,7 @@ type BlockInfo interface {
ReceiptHash() common.Hash
GasUsed() uint64
GasLimit() uint64
ParentBeaconRoot() *common.Hash // Dencun extension
// HeaderRLP returns the RLP of the block header as per consensus rules
// Returns an error if the header RLP could not be written
......@@ -67,6 +68,10 @@ func (b blockInfo) HeaderRLP() ([]byte, error) {
return rlp.EncodeToBytes(b.Header())
}
func (b blockInfo) ParentBeaconRoot() *common.Hash {
return b.Block.BeaconRoot()
}
func BlockToInfo(b *types.Block) BlockInfo {
return blockInfo{b}
}
......@@ -124,6 +129,10 @@ func (h headerBlockInfo) GasLimit() uint64 {
return h.Header.GasLimit
}
func (h headerBlockInfo) ParentBeaconRoot() *common.Hash {
return h.Header.ParentBeaconRoot
}
func (h headerBlockInfo) HeaderRLP() ([]byte, error) {
return rlp.EncodeToBytes(h.Header)
}
......
......@@ -8,6 +8,7 @@ import (
"math"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
......@@ -16,46 +17,68 @@ type BlockVersion int
const ( // iota is reset to 0
BlockV1 BlockVersion = iota
BlockV2
BlockV3
)
// ExecutionPayload is the only SSZ type we have to marshal/unmarshal,
// ExecutionPayload and ExecutionPayloadEnvelope are the only SSZ types we have to marshal/unmarshal,
// so instead of importing a SSZ lib we implement the bare minimum.
// This is more efficient than RLP, and matches the L1 consensus-layer encoding of ExecutionPayload.
// All fields (4s are offsets to dynamic data)
const blockV1FixedPart = 32 + 20 + 32 + 32 + 256 + 32 + 8 + 8 + 8 + 8 + 4 + 32 + 32 + 4
var (
// The payloads are small enough to read and write at once.
// But this happens often enough that we want to avoid re-allocating buffers for this.
payloadBufPool = sync.Pool{New: func() any {
x := make([]byte, 0, 100_000)
return &x
}}
// V1 + Withdrawals offset
const blockV2FixedPart = blockV1FixedPart + 4
// ErrExtraDataTooLarge occurs when the ExecutionPayload's ExtraData field
// is too large to be properly represented in SSZ.
ErrExtraDataTooLarge = errors.New("extra data too large")
const withdrawalSize = 8 + 8 + 20 + 8
ErrBadTransactionOffset = errors.New("transactions offset is smaller than extra data offset, aborting")
ErrBadWithdrawalsOffset = errors.New("withdrawals offset is smaller than transaction offset, aborting")
// MAX_TRANSACTIONS_PER_PAYLOAD in consensus spec
// https://github.com/ethereum/consensus-specs/blob/ef434e87165e9a4c82a99f54ffd4974ae113f732/specs/bellatrix/beacon-chain.md#execution
const maxTransactionsPerPayload = 1 << 20
ErrMissingData = errors.New("execution payload envelope is missing data")
)
// MAX_WITHDRAWALS_PER_PAYLOAD in consensus spec
// https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#execution
const maxWithdrawalsPerPayload = 1 << 4
const (
// All fields (4s are offsets to dynamic data)
blockV1FixedPart = 32 + 20 + 32 + 32 + 256 + 32 + 8 + 8 + 8 + 8 + 4 + 32 + 32 + 4
// ErrExtraDataTooLarge occurs when the ExecutionPayload's ExtraData field
// is too large to be properly represented in SSZ.
var ErrExtraDataTooLarge = errors.New("extra data too large")
// V1 + Withdrawals offset
blockV2FixedPart = blockV1FixedPart + 4
// The payloads are small enough to read and write at once.
// But this happens often enough that we want to avoid re-allocating buffers for this.
var payloadBufPool = sync.Pool{New: func() any {
x := make([]byte, 0, 100_000)
return &x
}}
// V2 + BlobGasUed + ExcessBlobGas
blockV3FixedPart = blockV2FixedPart + 8 + 8
var (
ErrBadTransactionOffset = errors.New("transactions offset is smaller than extra data offset, aborting")
ErrBadWithdrawalsOffset = errors.New("withdrawals offset is smaller than transaction offset, aborting")
withdrawalSize = 8 + 8 + 20 + 8
// MAX_TRANSACTIONS_PER_PAYLOAD in consensus spec
// https://github.com/ethereum/consensus-specs/blob/ef434e87165e9a4c82a99f54ffd4974ae113f732/specs/bellatrix/beacon-chain.md#execution
maxTransactionsPerPayload = 1 << 20
// MAX_WITHDRAWALS_PER_PAYLOAD in consensus spec
// https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#execution
maxWithdrawalsPerPayload = 1 << 4
)
func (v BlockVersion) HasBlobProperties() bool {
return v == BlockV3
}
func (v BlockVersion) HasWithdrawals() bool {
return v == BlockV2 || v == BlockV3
}
func (v BlockVersion) HasParentBeaconBlockRoot() bool {
return v == BlockV3
}
func executionPayloadFixedPart(version BlockVersion) uint32 {
if version == BlockV2 {
if version == BlockV3 {
return blockV3FixedPart
} else if version == BlockV2 {
return blockV2FixedPart
} else {
return blockV1FixedPart
......@@ -63,7 +86,9 @@ func executionPayloadFixedPart(version BlockVersion) uint32 {
}
func (payload *ExecutionPayload) inferVersion() BlockVersion {
if payload.Withdrawals != nil {
if payload.ExcessBlobGas != nil && payload.BlobGasUsed != nil {
return BlockV3
} else if payload.Withdrawals != nil {
return BlockV2
} else {
return BlockV1
......@@ -170,10 +195,20 @@ func (payload *ExecutionPayload) MarshalSSZ(w io.Writer) (n int, err error) {
if payload.Withdrawals != nil {
binary.LittleEndian.PutUint32(buf[offset:offset+4], fixedSize+extraDataSize+transactionSize)
offset += 4
}
if offset != fixedSize {
panic("withdrawals - fixed part size is inconsistent")
if payload.inferVersion() == BlockV3 {
if payload.BlobGasUsed == nil || payload.ExcessBlobGas == nil {
return 0, errors.New("cannot encode ecotone payload without dencun header attributes")
}
binary.LittleEndian.PutUint64(buf[offset:offset+8], uint64(*payload.BlobGasUsed))
offset += 8
binary.LittleEndian.PutUint64(buf[offset:offset+8], uint64(*payload.ExcessBlobGas))
offset += 8
}
if payload.Withdrawals != nil && offset != fixedSize {
panic("withdrawals - fixed part size is inconsistent")
}
// dynamic value 1: ExtraData
......@@ -277,15 +312,24 @@ func (payload *ExecutionPayload) UnmarshalSSZ(version BlockVersion, scope uint32
}
withdrawalsOffset := scope
if version == BlockV2 {
if version.HasWithdrawals() {
withdrawalsOffset = binary.LittleEndian.Uint32(buf[offset : offset+4])
// No offset increment, due to this being the last field
offset += 4
if withdrawalsOffset < transactionsOffset {
return ErrBadWithdrawalsOffset
}
}
if version == BlockV3 {
blobGasUsed := binary.LittleEndian.Uint64(buf[offset : offset+8])
payload.BlobGasUsed = (*Uint64Quantity)(&blobGasUsed)
offset += 8
excessBlobGas := binary.LittleEndian.Uint64(buf[offset : offset+8])
payload.ExcessBlobGas = (*Uint64Quantity)(&excessBlobGas)
}
_ = offset // for future extensions: we keep the offset accurate for extensions
if transactionsOffset > extraDataOffset+32 || transactionsOffset > scope {
return fmt.Errorf("extra-data is too large: %d", transactionsOffset-extraDataOffset)
}
......@@ -300,7 +344,7 @@ func (payload *ExecutionPayload) UnmarshalSSZ(version BlockVersion, scope uint32
}
payload.Transactions = txs
if version == BlockV2 {
if version.HasWithdrawals() {
if withdrawalsOffset > scope {
return fmt.Errorf("withdrawals offset is too large: %d", withdrawalsOffset)
}
......@@ -390,3 +434,48 @@ func unmarshalTransactions(in []byte) (txs []Data, err error) {
}
return txs, nil
}
// UnmarshalSSZ decodes the ExecutionPayloadEnvelope as SSZ type
func (envelope *ExecutionPayloadEnvelope) UnmarshalSSZ(scope uint32, r io.Reader) error {
if scope < common.HashLength {
return fmt.Errorf("scope too small to decode execution payload envelope: %d", scope)
}
data := make([]byte, common.HashLength)
n, err := r.Read(data)
if err != nil || n != common.HashLength {
return err
}
envelope.ParentBeaconBlockRoot = &common.Hash{}
copy(envelope.ParentBeaconBlockRoot[:], data)
var payload ExecutionPayload
err = payload.UnmarshalSSZ(BlockV3, scope-32, r)
if err != nil {
return err
}
envelope.ExecutionPayload = &payload
return nil
}
// MarshalSSZ encodes the ExecutionPayload as SSZ type
func (envelope *ExecutionPayloadEnvelope) MarshalSSZ(w io.Writer) (n int, err error) {
if envelope.ExecutionPayload == nil || envelope.ParentBeaconBlockRoot == nil {
return 0, ErrMissingData
}
// write parent beacon block root
hashSize, err := w.Write(envelope.ParentBeaconBlockRoot[:])
if err != nil || hashSize != common.HashLength {
return 0, errors.New("unable to write parent beacon block hash")
}
payloadSize, err := envelope.ExecutionPayload.MarshalSSZ(w)
if err != nil {
return 0, err
}
return hashSize + payloadSize, nil
}
......@@ -3,12 +3,14 @@ package eth
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"math"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/holiman/uint256"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
......@@ -166,6 +168,78 @@ func FuzzExecutionPayloadMarshalUnmarshalV2(f *testing.F) {
})
}
func FuzzExecutionPayloadMarshalUnmarshalV3(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte, a, b, c, d uint64, extraData []byte, txs uint16, txsData []byte, wCount uint16, blobGasUsed, excessBlobGas uint64) {
if len(data) < 32+20+32+32+256+32+32+32 {
return
}
var payload ExecutionPayload
payload.ParentHash = *(*common.Hash)(data[:32])
data = data[32:]
payload.FeeRecipient = *(*common.Address)(data[:20])
data = data[20:]
payload.StateRoot = *(*Bytes32)(data[:32])
data = data[32:]
payload.ReceiptsRoot = *(*Bytes32)(data[:32])
data = data[32:]
payload.LogsBloom = *(*Bytes256)(data[:256])
data = data[256:]
payload.PrevRandao = *(*Bytes32)(data[:32])
data = data[32:]
payload.BlockNumber = Uint64Quantity(a)
payload.GasLimit = Uint64Quantity(a)
payload.GasUsed = Uint64Quantity(a)
payload.Timestamp = Uint64Quantity(a)
payload.BlobGasUsed = (*Uint64Quantity)(&blobGasUsed)
payload.ExcessBlobGas = (*Uint64Quantity)(&excessBlobGas)
if len(extraData) > 32 {
extraData = extraData[:32]
}
payload.ExtraData = extraData
payload.BaseFeePerGas.SetBytes(data[:32])
payload.BlockHash = *(*common.Hash)(data[:32])
payload.Transactions = make([]Data, txs)
for i := 0; i < int(txs); i++ {
if len(txsData) < 2 {
payload.Transactions[i] = make(Data, 0)
continue
}
txSize := binary.LittleEndian.Uint16(txsData[:2])
txsData = txsData[2:]
if int(txSize) > len(txsData) {
txSize = uint16(len(txsData))
}
payload.Transactions[i] = txsData[:txSize]
txsData = txsData[txSize:]
}
wCount = wCount % maxWithdrawalsPerPayload
withdrawals := make(types.Withdrawals, wCount)
for i := 0; i < int(wCount); i++ {
withdrawals[i] = &types.Withdrawal{
Index: a,
Validator: b,
Address: common.BytesToAddress(data[:20]),
Amount: c,
}
}
payload.Withdrawals = &withdrawals
var buf bytes.Buffer
if _, err := payload.MarshalSSZ(&buf); err != nil {
t.Fatalf("failed to marshal ExecutionPayload: %v", err)
}
var roundTripped ExecutionPayload
err := roundTripped.UnmarshalSSZ(BlockV3, uint32(len(buf.Bytes())), bytes.NewReader(buf.Bytes()))
if err != nil {
t.Fatalf("failed to decode previously marshalled payload: %v", err)
}
if diff := cmp.Diff(payload, roundTripped); diff != "" {
t.Fatalf("The data did not round trip correctly:\n%s", diff)
}
})
}
func FuzzOBP01(f *testing.F) {
payload := &ExecutionPayload{
ExtraData: make([]byte, 32),
......@@ -337,3 +411,74 @@ func TestMarshalUnmarshalWithdrawals(t *testing.T) {
})
}
}
func TestMarshalUnmarshalExecutionPayloadEnvelopes(t *testing.T) {
hash := common.HexToHash("0x123")
zero := uint64(0)
validInput := &ExecutionPayloadEnvelope{
ParentBeaconBlockRoot: &hash,
ExecutionPayload: createPayloadWithWithdrawals(&types.Withdrawals{}),
}
validInput.ExecutionPayload.ExcessBlobGas = (*Uint64Quantity)(&zero)
validInput.ExecutionPayload.BlobGasUsed = (*Uint64Quantity)(&zero)
missingHash := &ExecutionPayloadEnvelope{
ParentBeaconBlockRoot: nil,
ExecutionPayload: createPayloadWithWithdrawals(&types.Withdrawals{}),
}
missingExecutionPaylaod := &ExecutionPayloadEnvelope{
ParentBeaconBlockRoot: &hash,
ExecutionPayload: nil,
}
tests := []struct {
name string
input *ExecutionPayloadEnvelope
err error
}{
{"ValidInputSucceeds", validInput, nil},
{"MissingHashFailsToSerialize", missingHash, ErrMissingData},
{"MissingExecutionDataFailsToSerialize", missingExecutionPaylaod, ErrMissingData},
}
for _, test := range tests {
test := test
t.Run(fmt.Sprintf("TestExecutionPayloadEnvelopeMarshalUnmarshal_%s", test.name), func(t *testing.T) {
hash := common.HexToHash("0x123")
var buf bytes.Buffer
_, err := test.input.MarshalSSZ(&buf)
if test.err != nil {
require.ErrorIs(t, err, test.err)
return
} else {
require.NoError(t, err)
}
data := buf.Bytes()
output := &ExecutionPayloadEnvelope{}
err = output.UnmarshalSSZ(uint32(len(data)), bytes.NewReader(data))
require.NoError(t, err)
require.NotNil(t, output.ParentBeaconBlockRoot)
assert.Equal(t, hash, *output.ParentBeaconBlockRoot)
require.NotNil(t, output.ExecutionPayload)
if diff := cmp.Diff(*test.input.ExecutionPayload, *output.ExecutionPayload); diff != "" {
t.Fatalf("The data did not round trip correctly:\n%s", diff)
}
})
}
}
func TestFailsToDeserializeTooLittleData(t *testing.T) {
var payload ExecutionPayloadEnvelope
err := payload.UnmarshalSSZ(1, bytes.NewReader([]byte{0x00}))
assert.Equal(t, err, errors.New("scope too small to decode execution payload envelope: 1"))
}
......@@ -127,7 +127,8 @@ type Data = hexutil.Bytes
type PayloadID = engine.PayloadID
type ExecutionPayloadEnvelope struct {
ExecutionPayload *ExecutionPayload `json:"executionPayload"`
ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot,omitempty"`
ExecutionPayload *ExecutionPayload `json:"executionPayload"`
}
type ExecutionPayload struct {
......@@ -144,11 +145,15 @@ type ExecutionPayload struct {
ExtraData BytesMax32 `json:"extraData"`
BaseFeePerGas Uint256Quantity `json:"baseFeePerGas"`
BlockHash common.Hash `json:"blockHash"`
// nil if not present, pre-shanghai
Withdrawals *types.Withdrawals `json:"withdrawals,omitempty"`
// Array of transaction objects, each object is a byte list (DATA) representing
// TransactionType || TransactionPayload or LegacyTransaction as defined in EIP-2718
Transactions []Data `json:"transactions"`
// Nil if not present (Bedrock)
Withdrawals *types.Withdrawals `json:"withdrawals,omitempty"`
// Nil if not present (Bedrock, Canyon, Delta)
BlobGasUsed *Uint64Quantity `json:"blobGasUsed,omitempty"`
// Nil if not present (Bedrock, Canyon, Delta)
ExcessBlobGas *Uint64Quantity `json:"excessBlobGas,omitempty"`
}
func (payload *ExecutionPayload) ID() BlockID {
......@@ -175,27 +180,30 @@ func (payload *ExecutionPayload) CanyonBlock() bool {
}
// CheckBlockHash recomputes the block hash and returns if the embedded block hash matches.
func (payload *ExecutionPayload) CheckBlockHash() (actual common.Hash, ok bool) {
func (envelope *ExecutionPayloadEnvelope) CheckBlockHash() (actual common.Hash, ok bool) {
payload := envelope.ExecutionPayload
hasher := trie.NewStackTrie(nil)
txHash := types.DeriveSha(rawTransactions(payload.Transactions), hasher)
header := types.Header{
ParentHash: payload.ParentHash,
UncleHash: types.EmptyUncleHash,
Coinbase: payload.FeeRecipient,
Root: common.Hash(payload.StateRoot),
TxHash: txHash,
ReceiptHash: common.Hash(payload.ReceiptsRoot),
Bloom: types.Bloom(payload.LogsBloom),
Difficulty: common.Big0, // zeroed, proof-of-work legacy
Number: big.NewInt(int64(payload.BlockNumber)),
GasLimit: uint64(payload.GasLimit),
GasUsed: uint64(payload.GasUsed),
Time: uint64(payload.Timestamp),
Extra: payload.ExtraData,
MixDigest: common.Hash(payload.PrevRandao),
Nonce: types.BlockNonce{}, // zeroed, proof-of-work legacy
BaseFee: payload.BaseFeePerGas.ToBig(),
ParentHash: payload.ParentHash,
UncleHash: types.EmptyUncleHash,
Coinbase: payload.FeeRecipient,
Root: common.Hash(payload.StateRoot),
TxHash: txHash,
ReceiptHash: common.Hash(payload.ReceiptsRoot),
Bloom: types.Bloom(payload.LogsBloom),
Difficulty: common.Big0, // zeroed, proof-of-work legacy
Number: big.NewInt(int64(payload.BlockNumber)),
GasLimit: uint64(payload.GasLimit),
GasUsed: uint64(payload.GasUsed),
Time: uint64(payload.Timestamp),
Extra: payload.ExtraData,
MixDigest: common.Hash(payload.PrevRandao),
Nonce: types.BlockNonce{}, // zeroed, proof-of-work legacy
BaseFee: payload.BaseFeePerGas.ToBig(),
ParentBeaconRoot: envelope.ParentBeaconBlockRoot,
}
if payload.CanyonBlock() {
......@@ -236,6 +244,8 @@ func BlockAsPayload(bl *types.Block, canyonForkTime *uint64) (*ExecutionPayload,
BaseFeePerGas: *baseFee,
BlockHash: bl.Hash(),
Transactions: opaqueTxs,
ExcessBlobGas: (*Uint64Quantity)(bl.ExcessBlobGas()),
BlobGasUsed: (*Uint64Quantity)(bl.BlobGasUsed()),
}
if canyonForkTime != nil && uint64(payload.Timestamp) >= *canyonForkTime {
......@@ -260,6 +270,8 @@ type PayloadAttributes struct {
NoTxPool bool `json:"noTxPool,omitempty"`
// GasLimit override
GasLimit *Uint64Quantity `json:"gasLimit,omitempty"`
// parentBeaconBlockRoot optional extension in Dencun
ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot,omitempty"`
}
type ExecutePayloadStatus string
......
......@@ -5,15 +5,16 @@ import (
"fmt"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/eth/catalyst"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/sources/caching"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
)
type EngineClientConfig struct {
......@@ -57,7 +58,7 @@ func (s *EngineClient) ForkchoiceUpdate(ctx context.Context, fc *eth.ForkchoiceS
fcCtx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel()
var result eth.ForkchoiceUpdatedResult
err := s.client.CallContext(fcCtx, &result, "engine_forkchoiceUpdatedV2", fc, attributes)
err := s.client.CallContext(fcCtx, &result, "engine_forkchoiceUpdatedV3", fc, attributes)
if err == nil {
tlog.Trace("Shared forkchoice-updated signal")
if attributes != nil { // block building is optional, we only get a payload ID if we are building a block
......@@ -85,14 +86,21 @@ func (s *EngineClient) ForkchoiceUpdate(ctx context.Context, fc *eth.ForkchoiceS
// NewPayload executes a full block on the execution engine.
// This returns a PayloadStatusV1 which encodes any validation/processing error,
// and this type of error is kept separate from the returned `error` used for RPC errors, like timeouts.
func (s *EngineClient) NewPayload(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error) {
func (s *EngineClient) NewPayload(ctx context.Context, payload *eth.ExecutionPayload, parentBeaconBlockRoot *common.Hash) (*eth.PayloadStatusV1, error) {
e := s.log.New("block_hash", payload.BlockHash)
e.Trace("sending payload for execution")
execCtx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel()
var result eth.PayloadStatusV1
err := s.client.CallContext(execCtx, &result, "engine_newPayloadV2", payload)
var err error
if s.rollupCfg.IsEcotone(uint64(payload.Timestamp)) {
err = s.client.CallContext(execCtx, &result, "engine_newPayloadV3", payload, []common.Hash{}, parentBeaconBlockRoot)
} else {
err = s.client.CallContext(execCtx, &result, "engine_newPayloadV2", payload)
}
e.Trace("Received payload execution result", "status", result.Status, "latestValidHash", result.LatestValidHash, "message", result.ValidationError)
if err != nil {
e.Error("Payload execution failed", "err", err)
......@@ -105,11 +113,11 @@ func (s *EngineClient) NewPayload(ctx context.Context, payload *eth.ExecutionPay
// There may be two types of error:
// 1. `error` as eth.InputError: the payload ID may be unknown
// 2. Other types of `error`: temporary RPC errors, like timeouts.
func (s *EngineClient) GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayload, error) {
func (s *EngineClient) GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayloadEnvelope, error) {
e := s.log.New("payload_id", payloadId)
e.Trace("getting payload")
var result eth.ExecutionPayloadEnvelope
err := s.client.CallContext(ctx, &result, "engine_getPayloadV2", payloadId)
err := s.client.CallContext(ctx, &result, "engine_getPayloadV3", payloadId)
if err != nil {
e.Warn("Failed to get payload", "payload_id", payloadId, "err", err)
if rpcErr, ok := err.(rpc.Error); ok {
......@@ -127,7 +135,7 @@ func (s *EngineClient) GetPayload(ctx context.Context, payloadId eth.PayloadID)
return nil, err
}
e.Trace("Received payload")
return result.ExecutionPayload, nil
return &result, nil
}
func (s *EngineClient) SignalSuperchainV1(ctx context.Context, recommended, required params.ProtocolVersion) (params.ProtocolVersion, error) {
......
......@@ -125,7 +125,7 @@ type EthClient struct {
// cache payloads by hash
// common.Hash -> *eth.ExecutionPayload
payloadsCache *caching.LRUCache[common.Hash, *eth.ExecutionPayload]
payloadsCache *caching.LRUCache[common.Hash, *eth.ExecutionPayloadEnvelope]
}
// NewEthClient returns an [EthClient], wrapping an RPC with bindings to fetch ethereum data with added error logging,
......@@ -145,7 +145,7 @@ func NewEthClient(client client.RPC, log log.Logger, metrics caching.Metrics, co
log: log,
transactionsCache: caching.NewLRUCache[common.Hash, types.Transactions](metrics, "txs", config.TransactionsCacheSize),
headersCache: caching.NewLRUCache[common.Hash, eth.BlockInfo](metrics, "headers", config.HeadersCacheSize),
payloadsCache: caching.NewLRUCache[common.Hash, *eth.ExecutionPayload](metrics, "payloads", config.PayloadsCacheSize),
payloadsCache: caching.NewLRUCache[common.Hash, *eth.ExecutionPayloadEnvelope](metrics, "payloads", config.PayloadsCacheSize),
}, nil
}
......@@ -227,7 +227,7 @@ func (s *EthClient) blockCall(ctx context.Context, method string, id rpcBlockID)
return info, txs, nil
}
func (s *EthClient) payloadCall(ctx context.Context, method string, id rpcBlockID) (*eth.ExecutionPayload, error) {
func (s *EthClient) payloadCall(ctx context.Context, method string, id rpcBlockID) (*eth.ExecutionPayloadEnvelope, error) {
var block *rpcBlock
err := s.client.CallContext(ctx, &block, method, id.Arg(), true)
if err != nil {
......@@ -236,15 +236,15 @@ func (s *EthClient) payloadCall(ctx context.Context, method string, id rpcBlockI
if block == nil {
return nil, ethereum.NotFound
}
payload, err := block.ExecutionPayload(s.trustRPC)
envelope, err := block.ExecutionPayloadEnvelope(s.trustRPC)
if err != nil {
return nil, err
}
if err := id.CheckID(payload.ID()); err != nil {
if err := id.CheckID(envelope.ExecutionPayload.ID()); err != nil {
return nil, fmt.Errorf("fetched payload does not match requested ID: %w", err)
}
s.payloadsCache.Add(payload.BlockHash, payload)
return payload, nil
s.payloadsCache.Add(envelope.ExecutionPayload.BlockHash, envelope)
return envelope, nil
}
// ChainID fetches the chain id of the internal RPC.
......@@ -293,18 +293,18 @@ func (s *EthClient) InfoAndTxsByLabel(ctx context.Context, label eth.BlockLabel)
return s.blockCall(ctx, "eth_getBlockByNumber", label)
}
func (s *EthClient) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayload, error) {
func (s *EthClient) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayloadEnvelope, error) {
if payload, ok := s.payloadsCache.Get(hash); ok {
return payload, nil
}
return s.payloadCall(ctx, "eth_getBlockByHash", hashID(hash))
}
func (s *EthClient) PayloadByNumber(ctx context.Context, number uint64) (*eth.ExecutionPayload, error) {
func (s *EthClient) PayloadByNumber(ctx context.Context, number uint64) (*eth.ExecutionPayloadEnvelope, error) {
return s.payloadCall(ctx, "eth_getBlockByNumber", numberID(number))
}
func (s *EthClient) PayloadByLabel(ctx context.Context, label eth.BlockLabel) (*eth.ExecutionPayload, error) {
func (s *EthClient) PayloadByLabel(ctx context.Context, label eth.BlockLabel) (*eth.ExecutionPayloadEnvelope, error) {
return s.payloadCall(ctx, "eth_getBlockByNumber", label)
}
......
......@@ -269,6 +269,6 @@ func newEthClientWithCaches(metrics caching.Metrics, cacheSize int) *EthClient {
return &EthClient{
transactionsCache: caching.NewLRUCache[common.Hash, types.Transactions](metrics, "txs", cacheSize),
headersCache: caching.NewLRUCache[common.Hash, eth.BlockInfo](metrics, "headers", cacheSize),
payloadsCache: caching.NewLRUCache[common.Hash, *eth.ExecutionPayload](metrics, "payloads", cacheSize),
payloadsCache: caching.NewLRUCache[common.Hash, *eth.ExecutionPayloadEnvelope](metrics, "payloads", cacheSize),
}
}
......@@ -98,7 +98,7 @@ func (s *L2Client) RollupConfig() *rollup.Config {
// L2BlockRefByLabel returns the [eth.L2BlockRef] for the given block label.
func (s *L2Client) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error) {
payload, err := s.PayloadByLabel(ctx, label)
envelope, err := s.PayloadByLabel(ctx, label)
if err != nil {
// Both geth and erigon like to serve non-standard errors for the safe and finalized heads, correct that.
// This happens when the chain just started and nothing is marked as safe/finalized yet.
......@@ -108,7 +108,7 @@ func (s *L2Client) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel)
// w%: wrap to preserve ethereum.NotFound case
return eth.L2BlockRef{}, fmt.Errorf("failed to determine L2BlockRef of %s, could not get payload: %w", label, err)
}
ref, err := derive.PayloadToBlockRef(s.rollupCfg, payload)
ref, err := derive.PayloadToBlockRef(s.rollupCfg, envelope.ExecutionPayload)
if err != nil {
return eth.L2BlockRef{}, err
}
......@@ -118,12 +118,12 @@ func (s *L2Client) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel)
// L2BlockRefByNumber returns the [eth.L2BlockRef] for the given block number.
func (s *L2Client) L2BlockRefByNumber(ctx context.Context, num uint64) (eth.L2BlockRef, error) {
payload, err := s.PayloadByNumber(ctx, num)
envelope, err := s.PayloadByNumber(ctx, num)
if err != nil {
// w%: wrap to preserve ethereum.NotFound case
return eth.L2BlockRef{}, fmt.Errorf("failed to determine L2BlockRef of height %v, could not get payload: %w", num, err)
}
ref, err := derive.PayloadToBlockRef(s.rollupCfg, payload)
ref, err := derive.PayloadToBlockRef(s.rollupCfg, envelope.ExecutionPayload)
if err != nil {
return eth.L2BlockRef{}, err
}
......@@ -138,12 +138,12 @@ func (s *L2Client) L2BlockRefByHash(ctx context.Context, hash common.Hash) (eth.
return ref, nil
}
payload, err := s.PayloadByHash(ctx, hash)
envelope, err := s.PayloadByHash(ctx, hash)
if err != nil {
// w%: wrap to preserve ethereum.NotFound case
return eth.L2BlockRef{}, fmt.Errorf("failed to determine block-hash of hash %v, could not get payload: %w", hash, err)
}
ref, err := derive.PayloadToBlockRef(s.rollupCfg, payload)
ref, err := derive.PayloadToBlockRef(s.rollupCfg, envelope.ExecutionPayload)
if err != nil {
return eth.L2BlockRef{}, err
}
......@@ -158,12 +158,12 @@ func (s *L2Client) SystemConfigByL2Hash(ctx context.Context, hash common.Hash) (
return ref, nil
}
payload, err := s.PayloadByHash(ctx, hash)
envelope, err := s.PayloadByHash(ctx, hash)
if err != nil {
// w%: wrap to preserve ethereum.NotFound case
return eth.SystemConfig{}, fmt.Errorf("failed to determine block-hash of hash %v, could not get payload: %w", hash, err)
}
cfg, err := derive.PayloadToSystemConfig(s.rollupCfg, payload)
cfg, err := derive.PayloadToSystemConfig(s.rollupCfg, envelope.ExecutionPayload)
if err != nil {
return eth.SystemConfig{}, err
}
......
......@@ -60,7 +60,7 @@ func (r *RollupClient) SequencerActive(ctx context.Context) (bool, error) {
return result, err
}
func (r *RollupClient) PostUnsafePayload(ctx context.Context, payload *eth.ExecutionPayload) error {
func (r *RollupClient) PostUnsafePayload(ctx context.Context, payload *eth.ExecutionPayloadEnvelope) error {
return r.rpc.CallContext(ctx, nil, "admin_postUnsafePayload", payload)
}
......
......@@ -90,6 +90,10 @@ func (h headerInfo) GasLimit() uint64 {
return h.Header.GasLimit
}
func (h headerInfo) ParentBeaconRoot() *common.Hash {
return h.Header.ParentBeaconRoot
}
func (h headerInfo) HeaderRLP() ([]byte, error) {
return rlp.EncodeToBytes(h.Header)
}
......@@ -264,7 +268,7 @@ func (block *rpcBlock) Info(trustCache bool, mustBePostMerge bool) (eth.BlockInf
return info, block.Transactions, nil
}
func (block *rpcBlock) ExecutionPayload(trustCache bool) (*eth.ExecutionPayload, error) {
func (block *rpcBlock) ExecutionPayloadEnvelope(trustCache bool) (*eth.ExecutionPayloadEnvelope, error) {
if err := block.checkPostMerge(); err != nil {
return nil, err
}
......@@ -287,7 +291,7 @@ func (block *rpcBlock) ExecutionPayload(trustCache bool) (*eth.ExecutionPayload,
opaqueTxs[i] = data
}
return &eth.ExecutionPayload{
payload := &eth.ExecutionPayload{
ParentHash: block.ParentHash,
FeeRecipient: block.Coinbase,
StateRoot: eth.Bytes32(block.Root),
......@@ -303,6 +307,11 @@ func (block *rpcBlock) ExecutionPayload(trustCache bool) (*eth.ExecutionPayload,
BlockHash: block.Hash,
Transactions: opaqueTxs,
Withdrawals: block.Withdrawals,
}
return &eth.ExecutionPayloadEnvelope{
ParentBeaconBlockRoot: block.ParentBeaconRoot,
ExecutionPayload: payload,
}, nil
}
......
......@@ -28,6 +28,8 @@ type MockBlockInfo struct {
InfoGasUsed uint64
InfoGasLimit uint64
InfoHeaderRLP []byte
InfoParentBeaconRoot *common.Hash
}
func (l *MockBlockInfo) Hash() common.Hash {
......@@ -82,6 +84,10 @@ func (l *MockBlockInfo) ID() eth.BlockID {
return eth.BlockID{Hash: l.InfoHash, Number: l.InfoNum}
}
func (l *MockBlockInfo) ParentBeaconRoot() *common.Hash {
return l.InfoParentBeaconRoot
}
func (l *MockBlockInfo) HeaderRLP() ([]byte, error) {
if l.InfoHeaderRLP == nil {
return nil, errors.New("header rlp not available")
......
......@@ -3,6 +3,8 @@ package testutils
import (
"context"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-service/eth"
)
......@@ -10,12 +12,12 @@ type MockEngine struct {
MockL2Client
}
func (m *MockEngine) GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayload, error) {
func (m *MockEngine) GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayloadEnvelope, error) {
out := m.Mock.Called(payloadId)
return out.Get(0).(*eth.ExecutionPayload), out.Error(1)
return out.Get(0).(*eth.ExecutionPayloadEnvelope), out.Error(1)
}
func (m *MockEngine) ExpectGetPayload(payloadId eth.PayloadID, payload *eth.ExecutionPayload, err error) {
func (m *MockEngine) ExpectGetPayload(payloadId eth.PayloadID, payload *eth.ExecutionPayloadEnvelope, err error) {
m.Mock.On("GetPayload", payloadId).Once().Return(payload, err)
}
......@@ -28,11 +30,11 @@ func (m *MockEngine) ExpectForkchoiceUpdate(state *eth.ForkchoiceState, attr *et
m.Mock.On("ForkchoiceUpdate", state, attr).Once().Return(result, err)
}
func (m *MockEngine) NewPayload(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error) {
out := m.Mock.Called(payload)
func (m *MockEngine) NewPayload(ctx context.Context, payload *eth.ExecutionPayload, parentBeaconBlockRoot *common.Hash) (*eth.PayloadStatusV1, error) {
out := m.Mock.Called(payload, parentBeaconBlockRoot)
return out.Get(0).(*eth.PayloadStatusV1), out.Error(1)
}
func (m *MockEngine) ExpectNewPayload(payload *eth.ExecutionPayload, result *eth.PayloadStatusV1, err error) {
m.Mock.On("NewPayload", payload).Once().Return(result, err)
func (m *MockEngine) ExpectNewPayload(payload *eth.ExecutionPayload, parentBeaconBlockRoot *common.Hash, result *eth.PayloadStatusV1, err error) {
m.Mock.On("NewPayload", payload, parentBeaconBlockRoot).Once().Return(result, err)
}
......@@ -70,30 +70,30 @@ func (m *MockEthClient) ExpectInfoAndTxsByLabel(label eth.BlockLabel, info eth.B
m.Mock.On("InfoAndTxsByLabel", label).Once().Return(info, transactions, err)
}
func (m *MockEthClient) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayload, error) {
func (m *MockEthClient) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayloadEnvelope, error) {
out := m.Mock.Called(hash)
return out.Get(0).(*eth.ExecutionPayload), out.Error(1)
return out.Get(0).(*eth.ExecutionPayloadEnvelope), out.Error(1)
}
func (m *MockEthClient) ExpectPayloadByHash(hash common.Hash, payload *eth.ExecutionPayload, err error) {
func (m *MockEthClient) ExpectPayloadByHash(hash common.Hash, payload *eth.ExecutionPayloadEnvelope, err error) {
m.Mock.On("PayloadByHash", hash).Once().Return(payload, err)
}
func (m *MockEthClient) PayloadByNumber(ctx context.Context, n uint64) (*eth.ExecutionPayload, error) {
func (m *MockEthClient) PayloadByNumber(ctx context.Context, n uint64) (*eth.ExecutionPayloadEnvelope, error) {
out := m.Mock.MethodCalled("PayloadByNumber", n)
return out[0].(*eth.ExecutionPayload), *out[1].(*error)
return out[0].(*eth.ExecutionPayloadEnvelope), *out[1].(*error)
}
func (m *MockEthClient) ExpectPayloadByNumber(n uint64, payload *eth.ExecutionPayload, err error) {
func (m *MockEthClient) ExpectPayloadByNumber(n uint64, payload *eth.ExecutionPayloadEnvelope, err error) {
m.Mock.On("PayloadByNumber", n).Once().Return(payload, &err)
}
func (m *MockEthClient) PayloadByLabel(ctx context.Context, label eth.BlockLabel) (*eth.ExecutionPayload, error) {
func (m *MockEthClient) PayloadByLabel(ctx context.Context, label eth.BlockLabel) (*eth.ExecutionPayloadEnvelope, error) {
out := m.Mock.Called(label)
return out.Get(0).(*eth.ExecutionPayload), out.Error(1)
return out.Get(0).(*eth.ExecutionPayloadEnvelope), out.Error(1)
}
func (m *MockEthClient) ExpectPayloadByLabel(label eth.BlockLabel, payload *eth.ExecutionPayload, err error) {
func (m *MockEthClient) ExpectPayloadByLabel(label eth.BlockLabel, payload *eth.ExecutionPayloadEnvelope, err error) {
m.Mock.On("PayloadByLabel", label).Once().Return(payload, 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