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