Commit f1cb42df authored by Danyal Prout's avatar Danyal Prout

Canyon: P2P / Testing

parent 9b59013c
......@@ -107,8 +107,11 @@ type DeployConfig struct {
L2GenesisBlockBaseFeePerGas *hexutil.Big `json:"l2GenesisBlockBaseFeePerGas"`
// L2GenesisRegolithTimeOffset is the number of seconds after genesis block that Regolith hard fork activates.
// Set it to 0 to activate at genesis. Nil to disable regolith.
// Set it to 0 to activate at genesis. Nil to disable Regolith.
L2GenesisRegolithTimeOffset *hexutil.Uint64 `json:"l2GenesisRegolithTimeOffset,omitempty"`
// L2GenesisCanyonTimeOffset is the number of seconds after genesis block that Canyon hard fork activates.
// Set it to 0 to activate at genesis. Nil to disable Canyon.
L2GenesisCanyonTimeOffset *hexutil.Uint64 `json:"L2GenesisCanyonTimeOffset,omitempty"`
// L2GenesisSpanBatchTimeOffset is the number of seconds after genesis block that Span Batch hard fork activates.
// Set it to 0 to activate at genesis. Nil to disable SpanBatch.
L2GenesisSpanBatchTimeOffset *hexutil.Uint64 `json:"l2GenesisSpanBatchTimeOffset,omitempty"`
......@@ -444,6 +447,17 @@ func (d *DeployConfig) RegolithTime(genesisTime uint64) *uint64 {
return &v
}
func (d *DeployConfig) CanyonTime(genesisTime uint64) *uint64 {
if d.L2GenesisCanyonTimeOffset == nil {
return nil
}
v := uint64(0)
if offset := *d.L2GenesisCanyonTimeOffset; offset > 0 {
v = genesisTime + uint64(offset)
}
return &v
}
func (d *DeployConfig) SpanBatchTime(genesisTime uint64) *uint64 {
if d.L2GenesisSpanBatchTimeOffset == nil {
return nil
......@@ -492,6 +506,7 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *types.Block, l2GenesisBlockHas
DepositContractAddress: d.OptimismPortalProxy,
L1SystemConfigAddress: d.SystemConfigProxy,
RegolithTime: d.RegolithTime(l1StartBlock.Time()),
CanyonTime: d.CanyonTime(l1StartBlock.Time()),
SpanBatchTime: d.SpanBatchTime(l1StartBlock.Time()),
}, nil
}
......
......@@ -49,6 +49,18 @@ func TestRegolithTimeAsOffset(t *testing.T) {
require.Equal(t, uint64(1500+5000), *config.RegolithTime(5000))
}
func TestCanyonTimeZero(t *testing.T) {
canyonOffset := hexutil.Uint64(0)
config := &DeployConfig{L2GenesisCanyonTimeOffset: &canyonOffset}
require.Equal(t, uint64(0), *config.CanyonTime(1234))
}
func TestCanyonTimeOffset(t *testing.T) {
canyonOffset := hexutil.Uint64(1500)
config := &DeployConfig{L2GenesisCanyonTimeOffset: &canyonOffset}
require.Equal(t, uint64(1234+1500), *config.CanyonTime(1234))
}
// TestCopy will copy a DeployConfig and ensure that the copy is equal to the original.
func TestCopy(t *testing.T) {
b, err := os.ReadFile("testdata/test-deploy-config-full.json")
......
......@@ -58,6 +58,8 @@ func NewL2Genesis(config *DeployConfig, block *types.Block) (*core.Genesis, erro
TerminalTotalDifficultyPassed: true,
BedrockBlock: new(big.Int).SetUint64(uint64(config.L2GenesisBlockNumber)),
RegolithTime: config.RegolithTime(block.Time()),
CanyonTime: config.CanyonTime(block.Time()),
ShanghaiTime: config.CanyonTime(block.Time()),
Optimism: &params.OptimismConfig{
EIP1559Denominator: eip1559Denom,
EIP1559Elasticity: eip1559Elasticity,
......
......@@ -46,7 +46,7 @@ func TestL2EngineAPI(gt *testing.T) {
chainA, _ := core.GenerateChain(sd.L2Cfg.Config, genesisBlock, consensus, db, 1, func(i int, gen *core.BlockGen) {
gen.SetCoinbase(common.Address{'A'})
})
payloadA, err := eth.BlockAsPayload(chainA[0])
payloadA, err := eth.BlockAsPayload(chainA[0], sd.RollupCfg.CanyonTime)
require.NoError(t, err)
// apply the payload
......@@ -69,7 +69,7 @@ func TestL2EngineAPI(gt *testing.T) {
chainB, _ := core.GenerateChain(sd.L2Cfg.Config, genesisBlock, consensus, db, 1, func(i int, gen *core.BlockGen) {
gen.SetCoinbase(common.Address{'B'})
})
payloadB, err := eth.BlockAsPayload(chainB[0])
payloadB, err := eth.BlockAsPayload(chainB[0], sd.RollupCfg.CanyonTime)
require.NoError(t, err)
// apply the payload
......@@ -125,18 +125,26 @@ func TestL2EngineAPIBlockBuilding(gt *testing.T) {
l2Cl, err := sources.NewEngineClient(engine.RPCClient(), log, nil, sources.EngineClientDefaultConfig(sd.RollupCfg))
require.NoError(t, err)
nextBlockTime := eth.Uint64Quantity(parent.Time) + 2
var w *eth.Withdrawals
if sd.RollupCfg.IsCanyon(uint64(nextBlockTime)) {
w = &eth.Withdrawals{}
}
// Now let's ask the engine to build a block
fcRes, err := l2Cl.ForkchoiceUpdate(t.Ctx(), &eth.ForkchoiceState{
HeadBlockHash: parent.Hash(),
SafeBlockHash: genesisBlock.Hash(),
FinalizedBlockHash: genesisBlock.Hash(),
}, &eth.PayloadAttributes{
Timestamp: eth.Uint64Quantity(parent.Time) + 2,
Timestamp: nextBlockTime,
PrevRandao: eth.Bytes32{},
SuggestedFeeRecipient: common.Address{'C'},
Transactions: nil,
NoTxPool: false,
GasLimit: (*eth.Uint64Quantity)(&sd.RollupCfg.Genesis.SystemConfig.GasLimit),
Withdrawals: w,
})
require.NoError(t, err)
require.Equal(t, fcRes.PayloadStatus.Status, eth.ExecutionValid)
......
......@@ -58,6 +58,7 @@ func MakeDeployParams(t require.TestingT, tp *TestParams) *DeployParams {
deployConfig.ChannelTimeout = tp.ChannelTimeout
deployConfig.L1BlockTime = tp.L1BlockTime
deployConfig.L2GenesisRegolithTimeOffset = nil
deployConfig.L2GenesisCanyonTimeOffset = CanyonTimeOffset()
deployConfig.L2GenesisSpanBatchTimeOffset = SpanBatchTimeOffset()
require.NoError(t, deployConfig.Check())
......@@ -157,6 +158,7 @@ func Setup(t require.TestingT, deployParams *DeployParams, alloc *AllocParams) *
DepositContractAddress: deployConf.OptimismPortalProxy,
L1SystemConfigAddress: deployConf.SystemConfigProxy,
RegolithTime: deployConf.RegolithTime(uint64(deployConf.L1GenesisBlockTimestamp)),
CanyonTime: deployConf.CanyonTime(uint64(deployConf.L1GenesisBlockTimestamp)),
SpanBatchTime: deployConf.SpanBatchTime(uint64(deployConf.L1GenesisBlockTimestamp)),
}
......@@ -191,3 +193,11 @@ func SpanBatchTimeOffset() *hexutil.Uint64 {
}
return nil
}
func CanyonTimeOffset() *hexutil.Uint64 {
if os.Getenv("OP_E2E_USE_CANYON") == "true" {
offset := hexutil.Uint64(0)
return &offset
}
return nil
}
......@@ -105,7 +105,7 @@ func NewOpGeth(t *testing.T, ctx context.Context, cfg *SystemConfig) (*OpGeth, e
l2Client, err := ethclient.Dial(node.HTTPEndpoint())
require.Nil(t, err)
genesisPayload, err := eth.BlockAsPayload(l2GenesisBlock)
genesisPayload, err := eth.BlockAsPayload(l2GenesisBlock, cfg.DeployConfig.CanyonTime(l2GenesisBlock.Time()))
require.Nil(t, err)
return &OpGeth{
......@@ -209,11 +209,18 @@ func (d *OpGeth) CreatePayloadAttributes(txs ...*types.Transaction) (*eth.Payloa
}
txBytes = append(txBytes, bin)
}
var withdrawals *eth.Withdrawals
if d.L2ChainConfig.IsCanyon(uint64(timestamp)) {
withdrawals = &eth.Withdrawals{}
}
attrs := eth.PayloadAttributes{
Timestamp: timestamp,
Transactions: txBytes,
NoTxPool: true,
GasLimit: (*eth.Uint64Quantity)(&d.SystemConfig.GasLimit),
Withdrawals: withdrawals,
}
return &attrs, nil
}
......@@ -2,12 +2,13 @@ package op_e2e
import (
"context"
"fmt"
"math/big"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
......@@ -16,9 +17,8 @@ import (
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestMissingGasLimit tests that op-geth cannot build a block without gas limit while optimism is active in the chain config.
......@@ -719,3 +719,145 @@ func TestRegolith(t *testing.T) {
})
}
}
func TestPreCanyon(t *testing.T) {
InitParallel(t)
futureTimestamp := hexutil.Uint64(4)
tests := []struct {
name string
canyonTime *hexutil.Uint64
}{
{name: "CanyonNotScheduled"},
{name: "CanyonNotYetActive", canyonTime: &futureTimestamp},
}
for _, test := range tests {
test := test
t.Run(fmt.Sprintf("ReturnsNilWithdrawals_%s", test.name), func(t *testing.T) {
InitParallel(t)
cfg := DefaultSystemConfig(t)
cfg.DeployConfig.L2GenesisCanyonTimeOffset = test.canyonTime
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.Withdrawals, "should not have withdrawals")
l1Block, err := opGeth.L2Client.BlockByNumber(ctx, nil)
require.Nil(t, err)
assert.Equal(t, types.Withdrawals(nil), l1Block.Withdrawals())
})
t.Run(fmt.Sprintf("RejectPushZeroTx_%s", test.name), func(t *testing.T) {
InitParallel(t)
cfg := DefaultSystemConfig(t)
cfg.DeployConfig.L2GenesisCanyonTimeOffset = test.canyonTime
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
opGeth, err := NewOpGeth(t, ctx, &cfg)
require.NoError(t, err)
defer opGeth.Close()
pushZeroContractCreateTxn := types.NewTx(&types.DepositTx{
From: cfg.Secrets.Addresses().Alice,
Value: big.NewInt(params.Ether),
Gas: 1000001,
Data: []byte{
byte(vm.PUSH0),
},
IsSystemTransaction: false,
})
_, err = opGeth.AddL2Block(ctx, pushZeroContractCreateTxn)
require.NoError(t, err)
receipt, err := opGeth.L2Client.TransactionReceipt(ctx, pushZeroContractCreateTxn.Hash())
require.NoError(t, err)
assert.Equal(t, types.ReceiptStatusFailed, receipt.Status)
})
}
}
func TestCanyon(t *testing.T) {
InitParallel(t)
tests := []struct {
name string
canyonTime hexutil.Uint64
activeCanyon func(ctx context.Context, opGeth *OpGeth)
}{
{name: "ActivateAtGenesis", canyonTime: 0, activeCanyon: func(ctx context.Context, opGeth *OpGeth) {}},
{name: "ActivateAfterGenesis", canyonTime: 2, activeCanyon: 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("ReturnsEmptyWithdrawals_%s", test.name), func(t *testing.T) {
InitParallel(t)
cfg := DefaultSystemConfig(t)
s := hexutil.Uint64(0)
cfg.DeployConfig.L2GenesisRegolithTimeOffset = &s
cfg.DeployConfig.L2GenesisCanyonTimeOffset = &test.canyonTime
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.activeCanyon(ctx, opGeth)
b, err := opGeth.AddL2Block(ctx)
require.NoError(t, err)
assert.Equal(t, *b.Withdrawals, eth.Withdrawals{})
l1Block, err := opGeth.L2Client.BlockByNumber(ctx, nil)
require.Nil(t, err)
assert.Equal(t, l1Block.Withdrawals(), types.Withdrawals{})
})
t.Run(fmt.Sprintf("AcceptsPushZeroTxn_%s", test.name), func(t *testing.T) {
InitParallel(t)
cfg := DefaultSystemConfig(t)
cfg.DeployConfig.L2GenesisCanyonTimeOffset = &test.canyonTime
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
opGeth, err := NewOpGeth(t, ctx, &cfg)
require.NoError(t, err)
defer opGeth.Close()
pushZeroContractCreateTxn := types.NewTx(&types.DepositTx{
From: cfg.Secrets.Addresses().Alice,
Value: big.NewInt(params.Ether),
Gas: 1000001,
Data: []byte{
byte(vm.PUSH0),
},
IsSystemTransaction: false,
})
_, err = opGeth.AddL2Block(ctx, pushZeroContractCreateTxn)
require.NoError(t, err)
receipt, err := opGeth.L2Client.TransactionReceipt(ctx, pushZeroContractCreateTxn.Hash())
require.NoError(t, err)
assert.Equal(t, types.ReceiptStatusSuccessful, receipt.Status)
})
}
}
......@@ -86,6 +86,7 @@ func DefaultSystemConfig(t *testing.T) SystemConfig {
require.NoError(t, err)
deployConfig := config.DeployConfig.Copy()
deployConfig.L1GenesisBlockTimestamp = hexutil.Uint64(time.Now().Unix())
deployConfig.L2GenesisCanyonTimeOffset = e2eutils.CanyonTimeOffset()
deployConfig.L2GenesisSpanBatchTimeOffset = e2eutils.SpanBatchTimeOffset()
require.NoError(t, deployConfig.Check(), "Deploy config is invalid, do you need to run make devnet-allocs?")
l1Deployments := config.L1Deployments.Copy()
......@@ -425,6 +426,7 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste
DepositContractAddress: cfg.DeployConfig.OptimismPortalProxy,
L1SystemConfigAddress: cfg.DeployConfig.SystemConfigProxy,
RegolithTime: cfg.DeployConfig.RegolithTime(uint64(cfg.DeployConfig.L1GenesisBlockTimestamp)),
CanyonTime: cfg.DeployConfig.CanyonTime(uint64(cfg.DeployConfig.L1GenesisBlockTimestamp)),
SpanBatchTime: cfg.DeployConfig.SpanBatchTime(uint64(cfg.DeployConfig.L1GenesisBlockTimestamp)),
ProtocolVersionsAddress: cfg.L1Deployments.ProtocolVersionsProxy,
}
......
......@@ -491,7 +491,7 @@ func TestSystemMockP2P(t *testing.T) {
verifierPeerID := sys.RollupNodes["verifier"].P2P().Host().ID()
check := func() bool {
sequencerBlocksTopicPeers := sys.RollupNodes["sequencer"].P2P().GossipOut().BlocksTopicPeers()
sequencerBlocksTopicPeers := sys.RollupNodes["sequencer"].P2P().GossipOut().AllBlockTopicsPeers()
return slices.Contains[[]peer.ID](sequencerBlocksTopicPeers, verifierPeerID)
}
......
......@@ -510,6 +510,7 @@ func (n *OpNode) OnUnsafeL2Payload(ctx context.Context, from peer.ID, payload *e
// Pass on the event to the L2 Engine
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()
if err := n.l2Driver.OnUnsafeL2Payload(ctx, payload); err != nil {
n.log.Warn("failed to notify engine driver of new L2 payload", "err", err, "id", payload.ID())
}
......
......@@ -70,10 +70,14 @@ func blocksTopicV1(cfg *rollup.Config) string {
return fmt.Sprintf("/optimism/%s/0/blocks", cfg.L2ChainID.String())
}
func blocksTopicV2(cfg *rollup.Config) string {
return fmt.Sprintf("/optimism/%s/1/blocks", cfg.L2ChainID.String())
}
// BuildSubscriptionFilter builds a simple subscription filter,
// to help protect against peers spamming useless subscriptions.
func BuildSubscriptionFilter(cfg *rollup.Config) pubsub.SubscriptionFilter {
return pubsub.NewAllowlistSubscriptionFilter(blocksTopicV1(cfg)) // add more topics here in the future, if any.
return pubsub.NewAllowlistSubscriptionFilter(blocksTopicV1(cfg), blocksTopicV2(cfg)) // add more topics here in the future, if any.
}
var msgBufPool = sync.Pool{New: func() any {
......@@ -239,7 +243,7 @@ func (sb *seenBlocks) markSeen(h common.Hash) {
sb.blockHashes = append(sb.blockHashes, h)
}
func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig) pubsub.ValidatorEx {
func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, blockVersion eth.BlockVersion) pubsub.ValidatorEx {
// Seen block hashes per block height
// uint64 -> *seenBlocks
......@@ -284,7 +288,7 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti
// [REJECT] if the block encoding is not valid
var payload eth.ExecutionPayload
if err := payload.UnmarshalSSZ(uint32(len(payloadBytes)), bytes.NewReader(payloadBytes)); err != nil {
if err := payload.UnmarshalSSZ(blockVersion, uint32(len(payloadBytes)), bytes.NewReader(payloadBytes)); err != nil {
log.Warn("invalid payload", "err", err, "peer", id)
return pubsub.ValidationReject
}
......@@ -310,6 +314,18 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti
return pubsub.ValidationReject
}
// [REJECT] if a V1 Block has withdrawals
if blockVersion == eth.BlockV1 && payload.Withdrawals != nil {
log.Warn("payload is on v1 topic, but has withdrawals", "bad_hash", payload.BlockHash.String())
return pubsub.ValidationReject
}
// [REJECT] if a V2 Block does not have withdrawals
if blockVersion == eth.BlockV2 && payload.Withdrawals == nil {
log.Warn("payload is on v2 topic, but does not have withdrawals", "bad_hash", payload.BlockHash.String())
return pubsub.ValidationReject
}
seen, ok := blockHeightLRU.Get(uint64(payload.BlockNumber))
if !ok {
seen = new(seenBlocks)
......@@ -370,7 +386,9 @@ type GossipIn interface {
}
type GossipTopicInfo interface {
BlocksTopicPeers() []peer.ID
AllBlockTopicsPeers() []peer.ID
BlocksTopicV1Peers() []peer.ID
BlocksTopicV2Peers() []peer.ID
}
type GossipOut interface {
......@@ -379,6 +397,21 @@ type GossipOut interface {
Close() error
}
type blockTopic struct {
// blocks topic, main handle on block gossip
topic *pubsub.Topic
// block events handler, to be cancelled before closing the blocks topic.
events *pubsub.TopicEventHandler
// block subscriptions, to be cancelled before closing blocks topic.
sub *pubsub.Subscription
}
func (bt *blockTopic) Close() error {
bt.events.Cancel()
bt.sub.Cancel()
return bt.topic.Close()
}
type publisher struct {
log log.Logger
cfg *rollup.Config
......@@ -388,20 +421,39 @@ type publisher struct {
// thus we have to stop it ourselves this way.
p2pCancel context.CancelFunc
// blocks topic, main handle on block gossip
blocksTopic *pubsub.Topic
// block events handler, to be cancelled before closing the blocks topic.
blocksEvents *pubsub.TopicEventHandler
// block subscriptions, to be cancelled before closing blocks topic.
blocksSub *pubsub.Subscription
blocksV1 *blockTopic
blocksV2 *blockTopic
runCfg GossipRuntimeConfig
}
var _ GossipOut = (*publisher)(nil)
func (p *publisher) BlocksTopicPeers() []peer.ID {
return p.blocksTopic.ListPeers()
func combinePeers(allPeers ...[]peer.ID) []peer.ID {
var seen = make(map[peer.ID]bool)
var res []peer.ID
for _, peers := range allPeers {
for _, p := range peers {
if _, ok := seen[p]; ok {
continue
}
res = append(res, p)
seen[p] = true
}
}
return res
}
func (p *publisher) AllBlockTopicsPeers() []peer.ID {
return combinePeers(p.BlocksTopicV1Peers(), p.BlocksTopicV2Peers())
}
func (p *publisher) BlocksTopicV1Peers() []peer.ID {
return p.blocksV1.topic.ListPeers()
}
func (p *publisher) BlocksTopicV2Peers() []peer.ID {
return p.blocksV2.topic.ListPeers()
}
func (p *publisher) PublishL2Payload(ctx context.Context, payload *eth.ExecutionPayload, signer Signer) error {
......@@ -428,55 +480,84 @@ func (p *publisher) PublishL2Payload(ctx context.Context, payload *eth.Execution
// This also copies the data, freeing up the original buffer to go back into the pool
out := snappy.Encode(nil, data)
return p.blocksTopic.Publish(ctx, out)
if p.cfg.IsCanyon(uint64(payload.Timestamp)) {
return p.blocksV2.topic.Publish(ctx, out)
} else {
return p.blocksV1.topic.Publish(ctx, out)
}
}
func (p *publisher) Close() error {
p.p2pCancel()
p.blocksEvents.Cancel()
p.blocksSub.Cancel()
return p.blocksTopic.Close()
e1 := p.blocksV1.Close()
e2 := p.blocksV2.Close()
return errors.Join(e1, e2)
}
func JoinGossip(self peer.ID, ps *pubsub.PubSub, log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, gossipIn GossipIn) (GossipOut, error) {
val := guardGossipValidator(log, logValidationResult(self, "validated block", log, BuildBlocksValidator(log, cfg, runCfg)))
blocksTopicName := blocksTopicV1(cfg)
err := ps.RegisterTopicValidator(blocksTopicName,
val,
p2pCtx, p2pCancel := context.WithCancel(context.Background())
v1Logger := log.New("topic", "blocksV1")
blocksV1Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv1", v1Logger, BuildBlocksValidator(v1Logger, cfg, runCfg, eth.BlockV1)))
blocksV1, err := newBlockTopic(p2pCtx, blocksTopicV1(cfg), ps, v1Logger, gossipIn, blocksV1Validator)
if err != nil {
p2pCancel()
return nil, fmt.Errorf("failed to setup blocks v1 p2p: %w", err)
}
v2Logger := log.New("topic", "blocksV2")
blocksV2Validator := guardGossipValidator(log, logValidationResult(self, "validated blockv2", v2Logger, BuildBlocksValidator(v2Logger, cfg, runCfg, eth.BlockV2)))
blocksV2, err := newBlockTopic(p2pCtx, blocksTopicV2(cfg), ps, v2Logger, gossipIn, blocksV2Validator)
if err != nil {
p2pCancel()
return nil, fmt.Errorf("failed to setup blocks v2 p2p: %w", err)
}
return &publisher{
log: log,
cfg: cfg,
p2pCancel: p2pCancel,
blocksV1: blocksV1,
blocksV2: blocksV2,
runCfg: runCfg,
}, nil
}
func newBlockTopic(ctx context.Context, topicId string, ps *pubsub.PubSub, log log.Logger, gossipIn GossipIn, validator pubsub.ValidatorEx) (*blockTopic, error) {
err := ps.RegisterTopicValidator(topicId,
validator,
pubsub.WithValidatorTimeout(3*time.Second),
pubsub.WithValidatorConcurrency(4))
if err != nil {
return nil, fmt.Errorf("failed to register blocks gossip topic: %w", err)
return nil, fmt.Errorf("failed to register gossip topic: %w", err)
}
blocksTopic, err := ps.Join(blocksTopicName)
blocksTopic, err := ps.Join(topicId)
if err != nil {
return nil, fmt.Errorf("failed to join blocks gossip topic: %w", err)
return nil, fmt.Errorf("failed to join gossip topic: %w", err)
}
blocksTopicEvents, err := blocksTopic.EventHandler()
if err != nil {
return nil, fmt.Errorf("failed to create blocks gossip topic handler: %w", err)
}
p2pCtx, p2pCancel := context.WithCancel(context.Background())
go LogTopicEvents(p2pCtx, log.New("topic", "blocks"), blocksTopicEvents)
go LogTopicEvents(ctx, log, blocksTopicEvents)
subscription, err := blocksTopic.Subscribe()
if err != nil {
p2pCancel()
err = errors.Join(err, blocksTopic.Close())
return nil, fmt.Errorf("failed to subscribe to blocks gossip topic: %w", err)
}
subscriber := MakeSubscriber(log, BlocksHandler(gossipIn.OnUnsafeL2Payload))
go subscriber(p2pCtx, subscription)
go subscriber(ctx, subscription)
return &publisher{
log: log,
cfg: cfg,
blocksTopic: blocksTopic,
blocksEvents: blocksTopicEvents,
blocksSub: subscription,
p2pCancel: p2pCancel,
runCfg: runCfg,
return &blockTopic{
topic: blocksTopic,
events: blocksTopicEvents,
sub: subscription,
}, nil
}
......
......@@ -40,6 +40,11 @@ func TestGuardGossipValidator(t *testing.T) {
require.Equal(t, pubsub.ValidationIgnore, val(context.Background(), "bob", nil))
}
func TestCombinePeers(t *testing.T) {
res := combinePeers([]peer.ID{"foo", "bar"}, []peer.ID{"bar", "baz"})
require.Equal(t, []peer.ID{"foo", "bar", "baz"}, res)
}
func TestVerifyBlockSignature(t *testing.T) {
logger := testlog.Logger(t, log.LvlCrit)
cfg := &rollup.Config{
......
......@@ -194,7 +194,7 @@ func (s *APIBackend) Peers(ctx context.Context, connected bool) (*PeerDump, erro
dump.TotalConnected += 1
}
}
for _, id := range s.node.GossipOut().BlocksTopicPeers() {
for _, id := range s.node.GossipOut().AllBlockTopicsPeers() {
if p, ok := dump.Peers[id.String()]; ok {
p.GossipBlocks = true
}
......@@ -208,11 +208,12 @@ func (s *APIBackend) Peers(ctx context.Context, connected bool) (*PeerDump, erro
}
type PeerStats struct {
Connected uint `json:"connected"`
Table uint `json:"table"`
BlocksTopic uint `json:"blocksTopic"`
Banned uint `json:"banned"`
Known uint `json:"known"`
Connected uint `json:"connected"`
Table uint `json:"table"`
BlocksTopic uint `json:"blocksTopic"`
BlocksTopicV2 uint `json:"blocksTopicV2"`
Banned uint `json:"banned"`
Known uint `json:"known"`
}
func (s *APIBackend) PeerStats(_ context.Context) (*PeerStats, error) {
......@@ -223,11 +224,12 @@ func (s *APIBackend) PeerStats(_ context.Context) (*PeerStats, error) {
pstore := h.Peerstore()
stats := &PeerStats{
Connected: uint(len(nw.Peers())),
Table: 0,
BlocksTopic: uint(len(s.node.GossipOut().BlocksTopicPeers())),
Banned: 0,
Known: uint(len(pstore.Peers())),
Connected: uint(len(nw.Peers())),
Table: 0,
BlocksTopic: uint(len(s.node.GossipOut().BlocksTopicV1Peers())),
BlocksTopicV2: uint(len(s.node.GossipOut().BlocksTopicV2Peers())),
Banned: 0,
Known: uint(len(pstore.Peers())),
}
if gater := s.node.ConnectionGater(); gater != nil {
stats.Banned = uint(len(gater.ListBlockedPeers()))
......
......@@ -571,7 +571,7 @@ func (r requestResultErr) ResultCode() byte {
return byte(r)
}
func (s *SyncClient) doRequest(ctx context.Context, id peer.ID, n uint64) error {
func (s *SyncClient) doRequest(ctx context.Context, id peer.ID, expectedBlockNum uint64) error {
// open stream to peer
reqCtx, reqCancel := context.WithTimeout(ctx, streamTimeout)
str, err := s.newStreamFn(reqCtx, id, s.payloadByNumber)
......@@ -582,8 +582,8 @@ func (s *SyncClient) doRequest(ctx context.Context, id peer.ID, n uint64) error
defer str.Close()
// set write timeout (if available)
_ = str.SetWriteDeadline(time.Now().Add(clientWriteRequestTimeout))
if err := binary.Write(str, binary.LittleEndian, n); err != nil {
return fmt.Errorf("failed to write request (%d): %w", n, err)
if err := binary.Write(str, binary.LittleEndian, expectedBlockNum); err != nil {
return fmt.Errorf("failed to write request (%d): %w", expectedBlockNum, err)
}
if err := str.CloseWrite(); err != nil {
return fmt.Errorf("failed to close writer side while making request: %w", err)
......@@ -620,14 +620,20 @@ func (s *SyncClient) doRequest(ctx context.Context, id peer.ID, n uint64) error
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
blockVersion := eth.BlockV1
if s.cfg.IsCanyon(expectedBlockNum) {
blockVersion = eth.BlockV2
}
var res eth.ExecutionPayload
if err := res.UnmarshalSSZ(uint32(len(data)), bytes.NewReader(data)); err != nil {
if err := res.UnmarshalSSZ(blockVersion, uint32(len(data)), bytes.NewReader(data)); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
if err := str.CloseRead(); err != nil {
return fmt.Errorf("failed to close reading side")
}
if err := verifyBlock(&res, n); err != nil {
if err := verifyBlock(&res, expectedBlockNum); err != nil {
return fmt.Errorf("received execution payload is invalid: %w", err)
}
select {
......
......@@ -109,6 +109,11 @@ func (ba *FetchingAttributesBuilder) PreparePayloadAttributes(ctx context.Contex
txs = append(txs, l1InfoTx)
txs = append(txs, depositTxs...)
var withdrawals *eth.Withdrawals
if ba.cfg.IsCanyon(nextL2Time) {
withdrawals = &eth.Withdrawals{}
}
return &eth.PayloadAttributes{
Timestamp: hexutil.Uint64(nextL2Time),
PrevRandao: eth.Bytes32(l1Info.MixDigest()),
......@@ -116,5 +121,6 @@ func (ba *FetchingAttributesBuilder) PreparePayloadAttributes(ctx context.Contex
Transactions: txs,
NoTxPool: true,
GasLimit: (*eth.Uint64Quantity)(&sysConfig.GasLimit),
Withdrawals: withdrawals,
}, nil
}
......@@ -182,6 +182,7 @@ func (eq *EngineQueue) AddUnsafePayload(payload *eth.ExecutionPayload) {
eq.log.Warn("cannot add nil unsafe payload")
return
}
if err := eq.unsafePayloads.Push(payload); err != nil {
eq.log.Warn("Could not add unsafe payload", "id", payload.ID(), "timestamp", uint64(payload.Timestamp), "err", err)
return
......
......@@ -48,15 +48,19 @@ func (o *OracleEngine) L2OutputRoot() (eth.Bytes32, error) {
}
func (o *OracleEngine) GetPayload(ctx context.Context, payloadId eth.PayloadID) (*eth.ExecutionPayload, error) {
return o.api.GetPayloadV1(ctx, payloadId)
res, err := o.api.GetPayloadV2(ctx, payloadId)
if err != nil {
return nil, err
}
return res.ExecutionPayload, nil
}
func (o *OracleEngine) ForkchoiceUpdate(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) {
return o.api.ForkchoiceUpdatedV1(ctx, state, attr)
return o.api.ForkchoiceUpdatedV2(ctx, state, attr)
}
func (o *OracleEngine) NewPayload(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error) {
return o.api.NewPayloadV1(ctx, payload)
return o.api.NewPayloadV2(ctx, payload)
}
func (o *OracleEngine) PayloadByHash(ctx context.Context, hash common.Hash) (*eth.ExecutionPayload, error) {
......@@ -64,7 +68,7 @@ func (o *OracleEngine) PayloadByHash(ctx context.Context, hash common.Hash) (*et
if block == nil {
return nil, ErrNotFound
}
return eth.BlockAsPayload(block)
return eth.BlockAsPayload(block, o.rollupCfg.CanyonTime)
}
func (o *OracleEngine) PayloadByNumber(ctx context.Context, n uint64) (*eth.ExecutionPayload, error) {
......
......@@ -29,7 +29,7 @@ func TestPayloadByHash(t *testing.T) {
block := stub.head
payload, err := engine.PayloadByHash(ctx, block.Hash())
require.NoError(t, err)
expected, err := eth.BlockAsPayload(block)
expected, err := eth.BlockAsPayload(block, engine.rollupCfg.CanyonTime)
require.NoError(t, err)
require.Equal(t, expected, payload)
})
......@@ -51,7 +51,7 @@ func TestPayloadByNumber(t *testing.T) {
block := stub.head
payload, err := engine.PayloadByNumber(ctx, block.NumberU64())
require.NoError(t, err)
expected, err := eth.BlockAsPayload(block)
expected, err := eth.BlockAsPayload(block, engine.rollupCfg.CanyonTime)
require.NoError(t, err)
require.Equal(t, expected, payload)
})
......@@ -124,7 +124,7 @@ func TestSystemConfigByL2Hash(t *testing.T) {
engine, stub := createOracleEngine(t)
t.Run("KnownBlock", func(t *testing.T) {
payload, err := eth.BlockAsPayload(stub.safe)
payload, err := eth.BlockAsPayload(stub.safe, engine.rollupCfg.CanyonTime)
require.NoError(t, err)
expected, err := derive.PayloadToSystemConfig(payload, engine.rollupCfg)
require.NoError(t, err)
......
......@@ -264,7 +264,7 @@ func (ea *L2EngineAPI) getPayload(ctx context.Context, payloadId eth.PayloadID)
ea.log.Error("failed to finish block building", "err", err)
return nil, engine.UnknownPayload
}
return eth.BlockAsPayload(bl)
return eth.BlockAsPayload(bl, ea.config().CanyonTime)
}
func (ea *L2EngineAPI) forkchoiceUpdated(ctx context.Context, state *eth.ForkchoiceState, attr *eth.PayloadAttributes) (*eth.ForkchoiceUpdatedResult, error) {
......@@ -350,6 +350,25 @@ func (ea *L2EngineAPI) forkchoiceUpdated(ctx context.Context, state *eth.Forkcho
return valid(nil), nil
}
func toGethWithdrawals(payload *eth.ExecutionPayload) []*types.Withdrawal {
if payload.Withdrawals == nil {
return nil
}
result := []*types.Withdrawal{}
for _, w := range *payload.Withdrawals {
result = append(result, &types.Withdrawal{
Index: w.Index,
Validator: w.Validator,
Address: w.Address,
Amount: w.Amount,
})
}
return result
}
func (ea *L2EngineAPI) newPayload(ctx context.Context, payload *eth.ExecutionPayload) (*eth.PayloadStatusV1, error) {
ea.log.Trace("L2Engine API request received", "method", "ExecutePayload", "number", payload.BlockNumber, "hash", payload.BlockHash)
txs := make([][]byte, len(payload.Transactions))
......@@ -371,6 +390,7 @@ func (ea *L2EngineAPI) newPayload(ctx context.Context, payload *eth.ExecutionPay
BaseFeePerGas: payload.BaseFeePerGas.ToBig(),
BlockHash: payload.BlockHash,
Transactions: txs,
Withdrawals: toGethWithdrawals(payload),
}, nil, nil)
if err != nil {
log.Debug("Invalid NewPayload params", "params", payload, "error", err)
......
......@@ -55,17 +55,25 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
txRlp, err := tx.MarshalBinary()
api.assert.NoError(err)
result, err := api.engine.ForkchoiceUpdatedV1(api.ctx, &eth.ForkchoiceState{
nextBlockTime := eth.Uint64Quantity(genesis.Time + 1)
var w *eth.Withdrawals
if api.backend.Config().IsCanyon(uint64(nextBlockTime)) {
w = &eth.Withdrawals{}
}
result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
HeadBlockHash: genesis.Hash(),
SafeBlockHash: genesis.Hash(),
FinalizedBlockHash: genesis.Hash(),
}, &eth.PayloadAttributes{
Timestamp: eth.Uint64Quantity(genesis.Time + 1),
Timestamp: nextBlockTime,
PrevRandao: eth.Bytes32(genesis.MixDigest),
SuggestedFeeRecipient: feeRecipient,
Transactions: []eth.Data{txRlp},
NoTxPool: true,
GasLimit: &gasLimit,
Withdrawals: w,
})
api.assert.Error(err)
api.assert.Equal(eth.ExecutionInvalid, result.PayloadStatus.Status)
......@@ -103,9 +111,16 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
t.Run("RejectInvalidBlockHash", func(t *testing.T) {
api := newTestHelper(t, createBackend)
var w *eth.Withdrawals
if api.backend.Config().IsCanyon(uint64(0)) {
w = &eth.Withdrawals{}
}
// Invalid because BlockHash won't be correct (among many other reasons)
block := &eth.ExecutionPayload{}
r, err := api.engine.NewPayloadV1(api.ctx, block)
block := &eth.ExecutionPayload{
Withdrawals: w,
}
r, err := api.engine.NewPayloadV2(api.ctx, block)
api.assert.NoError(err)
api.assert.Equal(eth.ExecutionInvalidBlockHash, r.Status)
})
......@@ -122,7 +137,7 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
newBlock.StateRoot = eth.Bytes32(genesis.TxHash)
updateBlockHash(newBlock)
r, err := api.engine.NewPayloadV1(api.ctx, newBlock)
r, err := api.engine.NewPayloadV2(api.ctx, newBlock)
api.assert.NoError(err)
api.assert.Equal(eth.ExecutionInvalid, r.Status)
})
......@@ -139,7 +154,7 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
newBlock.Timestamp = eth.Uint64Quantity(genesis.Time)
updateBlockHash(newBlock)
r, err := api.engine.NewPayloadV1(api.ctx, newBlock)
r, err := api.engine.NewPayloadV2(api.ctx, newBlock)
api.assert.NoError(err)
api.assert.Equal(eth.ExecutionInvalid, r.Status)
})
......@@ -156,7 +171,7 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
newBlock.Timestamp = eth.Uint64Quantity(genesis.Time - 1)
updateBlockHash(newBlock)
r, err := api.engine.NewPayloadV1(api.ctx, newBlock)
r, err := api.engine.NewPayloadV2(api.ctx, newBlock)
api.assert.NoError(err)
api.assert.Equal(eth.ExecutionInvalid, r.Status)
})
......@@ -165,7 +180,7 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
api := newTestHelper(t, createBackend)
genesis := api.backend.CurrentHeader()
result, err := api.engine.ForkchoiceUpdatedV1(api.ctx, &eth.ForkchoiceState{
result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
HeadBlockHash: genesis.Hash(),
SafeBlockHash: genesis.Hash(),
FinalizedBlockHash: genesis.Hash(),
......@@ -185,7 +200,7 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
api := newTestHelper(t, createBackend)
genesis := api.backend.CurrentHeader()
result, err := api.engine.ForkchoiceUpdatedV1(api.ctx, &eth.ForkchoiceState{
result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
HeadBlockHash: genesis.Hash(),
SafeBlockHash: genesis.Hash(),
FinalizedBlockHash: genesis.Hash(),
......@@ -207,7 +222,7 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
gasLimit := eth.Uint64Quantity(params.MaxGasLimit + 1)
result, err := api.engine.ForkchoiceUpdatedV1(api.ctx, &eth.ForkchoiceState{
result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
HeadBlockHash: genesis.Hash(),
SafeBlockHash: genesis.Hash(),
FinalizedBlockHash: genesis.Hash(),
......@@ -246,7 +261,7 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
chainB1 := api.addBlockWithParent(genesis, eth.Uint64Quantity(genesis.Time+3))
result, err := api.engine.ForkchoiceUpdatedV1(api.ctx, &eth.ForkchoiceState{
result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
HeadBlockHash: chainA3.BlockHash,
SafeBlockHash: chainB1.BlockHash,
FinalizedBlockHash: chainA2.BlockHash,
......@@ -266,7 +281,7 @@ func RunEngineAPITests(t *testing.T, createBackend func(t *testing.T) engineapi.
chainB1 := api.addBlockWithParent(genesis, eth.Uint64Quantity(genesis.Time+3))
result, err := api.engine.ForkchoiceUpdatedV1(api.ctx, &eth.ForkchoiceState{
result, err := api.engine.ForkchoiceUpdatedV2(api.ctx, &eth.ForkchoiceState{
HeadBlockHash: chainA3.BlockHash,
SafeBlockHash: chainA2.BlockHash,
FinalizedBlockHash: chainB1.BlockHash,
......@@ -349,7 +364,7 @@ func (h *testHelper) addBlockWithParent(head *types.Header, timestamp eth.Uint64
func (h *testHelper) forkChoiceUpdated(head common.Hash, safe common.Hash, finalized common.Hash) {
h.Log("forkChoiceUpdated", "head", head, "safe", safe, "finalized", finalized)
result, err := h.engine.ForkchoiceUpdatedV1(h.ctx, &eth.ForkchoiceState{
result, err := h.engine.ForkchoiceUpdatedV2(h.ctx, &eth.ForkchoiceState{
HeadBlockHash: head,
SafeBlockHash: safe,
FinalizedBlockHash: finalized,
......@@ -368,7 +383,14 @@ func (h *testHelper) startBlockBuilding(head *types.Header, newBlockTimestamp et
h.assert.NoError(err, "Failed to marshall tx %v", tx)
txData = append(txData, rlp)
}
result, err := h.engine.ForkchoiceUpdatedV1(h.ctx, &eth.ForkchoiceState{
canyonTime := h.backend.Config().CanyonTime
var w *eth.Withdrawals
if canyonTime != nil && *canyonTime <= uint64(newBlockTimestamp) {
w = &eth.Withdrawals{}
}
result, err := h.engine.ForkchoiceUpdatedV2(h.ctx, &eth.ForkchoiceState{
HeadBlockHash: head.Hash(),
SafeBlockHash: head.Hash(),
FinalizedBlockHash: head.Hash(),
......@@ -379,6 +401,7 @@ func (h *testHelper) startBlockBuilding(head *types.Header, newBlockTimestamp et
Transactions: txData,
NoTxPool: true,
GasLimit: &gasLimit,
Withdrawals: w,
})
h.assert.NoError(err)
h.assert.Equal(eth.ExecutionValid, result.PayloadStatus.Status)
......@@ -389,15 +412,16 @@ func (h *testHelper) startBlockBuilding(head *types.Header, newBlockTimestamp et
func (h *testHelper) getPayload(id *eth.PayloadID) *eth.ExecutionPayload {
h.Log("getPayload", "id", id)
block, err := h.engine.GetPayloadV1(h.ctx, *id)
envelope, err := h.engine.GetPayloadV2(h.ctx, *id)
h.assert.NoError(err)
h.assert.NotNil(block)
return block
h.assert.NotNil(envelope)
h.assert.NotNil(envelope.ExecutionPayload)
return envelope.ExecutionPayload
}
func (h *testHelper) newPayload(block *eth.ExecutionPayload) {
h.Log("newPayload", "hash", block.BlockHash)
r, err := h.engine.NewPayloadV1(h.ctx, block)
r, err := h.engine.NewPayloadV2(h.ctx, block)
h.assert.NoError(err)
h.assert.Equal(eth.ExecutionValid, r.Status)
h.assert.Nil(r.ValidationError)
......
......@@ -15,5 +15,6 @@ generate-mocks:
fuzz:
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzExecutionPayloadUnmarshal ./eth
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzExecutionPayloadMarshalUnmarshal ./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 FuzzOBP01 ./eth
......@@ -9,16 +9,33 @@ import (
"sync"
)
type BlockVersion int
const ( // iota is reset to 0
BlockV1 BlockVersion = iota
BlockV2 = iota
)
// ExecutionPayload is the only SSZ type we have to marshal/unmarshal,
// so instead of importing a SSZ lib we implement the bare minimum.
// This is more efficient than RLP, and matches the L1 consensus-layer encoding of ExecutionPayload.
// All fields (4s are offsets to dynamic data)
const executionPayloadFixedPart = 32 + 20 + 32 + 32 + 256 + 32 + 8 + 8 + 8 + 8 + 4 + 32 + 32 + 4
const blockV1FixedPart = 32 + 20 + 32 + 32 + 256 + 32 + 8 + 8 + 8 + 8 + 4 + 32 + 32 + 4
// V1 + Withdrawals offset
const blockV2FixedPart = blockV1FixedPart + 4
const withdrawalSize = 8 + 8 + 32 + 8
// MAX_TRANSACTIONS_PER_PAYLOAD in consensus spec
// https://github.com/ethereum/consensus-specs/blob/ef434e87165e9a4c82a99f54ffd4974ae113f732/specs/bellatrix/beacon-chain.md#execution
const maxTransactionsPerPayload = 1 << 20
// MAX_WITHDRAWALS_PER_PAYLOAD in consensus spec
// https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#execution
const maxWithdrawalsPerPayload = 1 << 4
// ErrExtraDataTooLarge occurs when the ExecutionPayload's ExtraData field
// is too large to be properly represented in SSZ.
var ErrExtraDataTooLarge = errors.New("extra data too large")
......@@ -31,16 +48,44 @@ var payloadBufPool = sync.Pool{New: func() any {
}}
var ErrBadTransactionOffset = errors.New("transactions offset is smaller than extra data offset, aborting")
var ErrBadWithdrawalsOffset = errors.New("withdrawals offset is smaller than transaction offset, aborting")
func executionPayloadFixedPart(version BlockVersion) uint32 {
if version == BlockV2 {
return blockV2FixedPart
} else {
return blockV1FixedPart
}
}
func (payload *ExecutionPayload) inferVersion() BlockVersion {
if payload.Withdrawals != nil {
return BlockV2
} else {
return BlockV1
}
}
func (payload *ExecutionPayload) SizeSSZ() (full uint32) {
full = executionPayloadFixedPart + uint32(len(payload.ExtraData))
return executionPayloadFixedPart(payload.inferVersion()) + uint32(len(payload.ExtraData)) + payload.transactionSize() + payload.withdrawalSize()
}
func (payload *ExecutionPayload) withdrawalSize() uint32 {
if payload.Withdrawals == nil {
return 0
}
return uint32(len(*payload.Withdrawals) * withdrawalSize)
}
func (payload *ExecutionPayload) transactionSize() uint32 {
// One offset to each transaction
full += uint32(len(payload.Transactions)) * 4
result := uint32(len(payload.Transactions)) * 4
// Each transaction
for _, tx := range payload.Transactions {
full += uint32(len(tx))
result += uint32(len(tx))
}
return full
return result
}
// marshalBytes32LE returns the value of z as a 32-byte little-endian array.
......@@ -62,9 +107,13 @@ func unmarshalBytes32LE(in []byte, z *Uint256Quantity) {
// MarshalSSZ encodes the ExecutionPayload as SSZ type
func (payload *ExecutionPayload) MarshalSSZ(w io.Writer) (n int, err error) {
fixedSize := executionPayloadFixedPart(payload.inferVersion())
transactionSize := payload.transactionSize()
// Cast to uint32 to enable 32-bit MIPS support where math.MaxUint32-executionPayloadFixedPart is too big for int
// In that case, len(payload.ExtraData) can't be longer than an int so this is always false anyway.
if uint32(len(payload.ExtraData)) > math.MaxUint32-uint32(executionPayloadFixedPart) {
extraDataSize := uint32(len(payload.ExtraData))
if extraDataSize > math.MaxUint32-fixedSize {
return 0, ErrExtraDataTooLarge
}
......@@ -100,26 +149,58 @@ func (payload *ExecutionPayload) MarshalSSZ(w io.Writer) (n int, err error) {
binary.LittleEndian.PutUint64(buf[offset:offset+8], uint64(payload.Timestamp))
offset += 8
// offset to ExtraData
binary.LittleEndian.PutUint32(buf[offset:offset+4], executionPayloadFixedPart)
binary.LittleEndian.PutUint32(buf[offset:offset+4], fixedSize)
offset += 4
marshalBytes32LE(buf[offset:offset+32], &payload.BaseFeePerGas)
offset += 32
copy(buf[offset:offset+32], payload.BlockHash[:])
offset += 32
// offset to Transactions
binary.LittleEndian.PutUint32(buf[offset:offset+4], executionPayloadFixedPart+uint32(len(payload.ExtraData)))
binary.LittleEndian.PutUint32(buf[offset:offset+4], fixedSize+extraDataSize)
offset += 4
if offset != executionPayloadFixedPart {
panic("fixed part size is inconsistent")
if payload.Withdrawals == nil && offset != fixedSize {
panic("transactions - fixed part size is inconsistent")
}
if payload.Withdrawals != nil {
binary.LittleEndian.PutUint32(buf[offset:offset+4], fixedSize+extraDataSize+transactionSize)
offset += 4
if offset != fixedSize {
panic("withdrawals - fixed part size is inconsistent")
}
}
// dynamic value 1: ExtraData
copy(buf[offset:offset+uint32(len(payload.ExtraData))], payload.ExtraData[:])
offset += uint32(len(payload.ExtraData))
copy(buf[offset:offset+extraDataSize], payload.ExtraData[:])
offset += extraDataSize
// dynamic value 2: Transactions
marshalTransactions(buf[offset:], payload.Transactions)
marshalTransactions(buf[offset:offset+transactionSize], payload.Transactions)
offset += transactionSize
// dyanmic value 3: Withdrawals
if payload.Withdrawals != nil {
marshalWithdrawals(buf[offset:], payload.Withdrawals)
}
return w.Write(buf)
}
func marshalWithdrawals(out []byte, withdrawals *Withdrawals) {
offset := uint32(0)
for _, withdrawal := range *withdrawals {
binary.LittleEndian.PutUint64(out[offset:offset+8], withdrawal.Index)
offset += 8
binary.LittleEndian.PutUint64(out[offset:offset+8], withdrawal.Validator)
offset += 8
copy(out[offset:offset+32], withdrawal.Address[:])
offset += 32
binary.LittleEndian.PutUint64(out[offset:offset+8], withdrawal.Amount)
offset += 8
}
}
func marshalTransactions(out []byte, txs []Data) {
offset := uint32(0)
txOffset := uint32(len(txs)) * 4
......@@ -133,8 +214,10 @@ func marshalTransactions(out []byte, txs []Data) {
}
// UnmarshalSSZ decodes the ExecutionPayload as SSZ type
func (payload *ExecutionPayload) UnmarshalSSZ(scope uint32, r io.Reader) error {
if scope < executionPayloadFixedPart {
func (payload *ExecutionPayload) UnmarshalSSZ(version BlockVersion, scope uint32, r io.Reader) error {
fixedSize := executionPayloadFixedPart(version)
if scope < fixedSize {
return fmt.Errorf("scope too small to decode execution payload: %d", scope)
}
......@@ -171,36 +254,99 @@ func (payload *ExecutionPayload) UnmarshalSSZ(scope uint32, r io.Reader) error {
payload.Timestamp = Uint64Quantity(binary.LittleEndian.Uint64(buf[offset : offset+8]))
offset += 8
extraDataOffset := binary.LittleEndian.Uint32(buf[offset : offset+4])
if extraDataOffset != executionPayloadFixedPart {
return fmt.Errorf("unexpected extra data offset: %d <> %d", extraDataOffset, executionPayloadFixedPart)
if extraDataOffset != fixedSize {
return fmt.Errorf("unexpected extra data offset: %d <> %d", extraDataOffset, fixedSize)
}
offset += 4
unmarshalBytes32LE(buf[offset:offset+32], &payload.BaseFeePerGas)
offset += 32
copy(payload.BlockHash[:], buf[offset:offset+32])
offset += 32
transactionsOffset := binary.LittleEndian.Uint32(buf[offset : offset+4])
if transactionsOffset < extraDataOffset {
return ErrBadTransactionOffset
}
offset += 4
if offset != executionPayloadFixedPart {
if version == BlockV1 && offset != fixedSize {
panic("fixed part size is inconsistent")
}
withdrawalsOffset := scope
if version == BlockV2 {
withdrawalsOffset = binary.LittleEndian.Uint32(buf[offset : offset+4])
// No offset increment, due to this being the last field
if withdrawalsOffset < transactionsOffset {
return ErrBadWithdrawalsOffset
}
}
if transactionsOffset > extraDataOffset+32 || transactionsOffset > scope {
return fmt.Errorf("extra-data is too large: %d", transactionsOffset-extraDataOffset)
}
extraDataSize := transactionsOffset - extraDataOffset
payload.ExtraData = make(BytesMax32, extraDataSize)
copy(payload.ExtraData, buf[extraDataOffset:transactionsOffset])
txs, err := unmarshalTransactions(buf[transactionsOffset:])
txs, err := unmarshalTransactions(buf[transactionsOffset:withdrawalsOffset])
if err != nil {
return fmt.Errorf("failed to unmarshal transactions list: %w", err)
}
payload.Transactions = txs
if version == BlockV2 {
if withdrawalsOffset > scope {
return fmt.Errorf("withdrawals offset is too large: %d", withdrawalsOffset)
}
withdrawals, err := unmarshalWithdrawals(buf[withdrawalsOffset:])
if err != nil {
return fmt.Errorf("failed to unmarshal withdrawals list: %w", err)
}
payload.Withdrawals = withdrawals
}
return nil
}
func unmarshalWithdrawals(in []byte) (*Withdrawals, error) {
result := &Withdrawals{}
if len(in)%withdrawalSize != 0 {
return nil, errors.New("invalid withdrawals data")
}
withdrawalCount := len(in) / withdrawalSize
if withdrawalCount > maxWithdrawalsPerPayload {
return nil, fmt.Errorf("too many withdrawals: %d > %d", withdrawalCount, maxWithdrawalsPerPayload)
}
offset := 0
for i := 0; i < withdrawalCount; i++ {
withdrawal := Withdrawal{}
withdrawal.Index = binary.LittleEndian.Uint64(in[offset : offset+8])
offset += 8
withdrawal.Validator = binary.LittleEndian.Uint64(in[offset : offset+8])
offset += 8
copy(withdrawal.Address[:], in[offset:offset+32])
offset += 32
withdrawal.Amount = binary.LittleEndian.Uint64(in[offset : offset+8])
offset += 8
*result = append(*result, withdrawal)
}
return result, nil
}
func unmarshalTransactions(in []byte) (txs []Data, err error) {
scope := uint32(len(in))
if scope == 0 { // empty txs list
......
......@@ -3,29 +3,41 @@ package eth
import (
"bytes"
"encoding/binary"
"fmt"
"math"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/google/go-cmp/cmp"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
)
// FuzzExecutionPayloadUnmarshal checks that our SSZ decoding never panics
func FuzzExecutionPayloadUnmarshal(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
var payload ExecutionPayload
err := payload.UnmarshalSSZ(uint32(len(data)), bytes.NewReader(data))
if err != nil {
// not every input is a valid ExecutionPayload, that's ok. Should just not panic.
return
{
var payload ExecutionPayload
err := payload.UnmarshalSSZ(BlockV1, uint32(len(data)), bytes.NewReader(data))
if err != nil {
// not every input is a valid ExecutionPayload, that's ok. Should just not panic.
return
}
}
{
var payload ExecutionPayload
err := payload.UnmarshalSSZ(BlockV2, uint32(len(data)), bytes.NewReader(data))
if err != nil {
// not every input is a valid ExecutionPayload, that's ok. Should just not panic.
return
}
}
})
}
// FuzzExecutionPayloadMarshalUnmarshal checks that our SSZ encoding>decoding round trips properly
func FuzzExecutionPayloadMarshalUnmarshal(f *testing.F) {
func FuzzExecutionPayloadMarshalUnmarshalV1(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte, a, b, c, d uint64, extraData []byte, txs uint16, txsData []byte) {
if len(data) < 32+20+32+32+256+32+32+32 {
return
......@@ -72,7 +84,77 @@ func FuzzExecutionPayloadMarshalUnmarshal(f *testing.F) {
t.Fatalf("failed to marshal ExecutionPayload: %v", err)
}
var roundTripped ExecutionPayload
err := roundTripped.UnmarshalSSZ(uint32(len(buf.Bytes())), bytes.NewReader(buf.Bytes()))
err := roundTripped.UnmarshalSSZ(BlockV1, 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 FuzzExecutionPayloadMarshalUnmarshalV2(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte, a, b, c, d uint64, extraData []byte, txs uint16, txsData []byte, wCount uint16) {
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)
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(Withdrawals, wCount)
for i := 0; i < int(wCount); i++ {
withdrawals[i] = 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(BlockV2, uint32(len(buf.Bytes())), bytes.NewReader(buf.Bytes()))
if err != nil {
t.Fatalf("failed to decode previously marshalled payload: %v", err)
}
......@@ -99,7 +181,7 @@ func FuzzOBP01(f *testing.F) {
binary.LittleEndian.PutUint32(clone[504:508], txOffset)
var unmarshalled ExecutionPayload
err = unmarshalled.UnmarshalSSZ(uint32(len(clone)), bytes.NewReader(clone))
err = unmarshalled.UnmarshalSSZ(BlockV1, uint32(len(clone)), bytes.NewReader(clone))
if err == nil {
t.Fatalf("expected a failure, but didn't get one")
}
......@@ -122,7 +204,7 @@ func TestOPB01(t *testing.T) {
copy(data[504:508], make([]byte, 4))
var unmarshalled ExecutionPayload
err = unmarshalled.UnmarshalSSZ(uint32(len(data)), bytes.NewReader(data))
err = unmarshalled.UnmarshalSSZ(BlockV1, uint32(len(data)), bytes.NewReader(data))
require.Equal(t, ErrBadTransactionOffset, err)
}
......@@ -130,20 +212,126 @@ func TestOPB01(t *testing.T) {
// properly returns an error when the ExtraData field
// cannot be represented in the outputted SSZ.
func TestOPB04(t *testing.T) {
data := make([]byte, math.MaxUint32)
var buf bytes.Buffer
// First, test the maximum len - which in this case is the max uint32
// minus the execution payload fixed part.
payload := &ExecutionPayload{
ExtraData: make([]byte, math.MaxUint32-executionPayloadFixedPart),
ExtraData: data[:math.MaxUint32-executionPayloadFixedPart(BlockV1)],
Withdrawals: nil,
}
var buf bytes.Buffer
_, err := payload.MarshalSSZ(&buf)
require.NoError(t, err)
buf.Reset()
payload = &ExecutionPayload{
ExtraData: make([]byte, math.MaxUint32-executionPayloadFixedPart+1),
tests := []struct {
version BlockVersion
withdrawals *Withdrawals
}{
{BlockV1, nil},
{BlockV2, &Withdrawals{}},
}
for _, test := range tests {
payload := &ExecutionPayload{
ExtraData: data[:math.MaxUint32-executionPayloadFixedPart(test.version)+1],
Withdrawals: test.withdrawals,
}
_, err := payload.MarshalSSZ(&buf)
require.Error(t, err)
require.Equal(t, ErrExtraDataTooLarge, err)
}
}
func createPayloadWithWithdrawals(w *Withdrawals) *ExecutionPayload {
return &ExecutionPayload{
ParentHash: common.HexToHash("0x123"),
FeeRecipient: common.HexToAddress("0x456"),
StateRoot: Bytes32(common.HexToHash("0x789")),
ReceiptsRoot: Bytes32(common.HexToHash("0xabc")),
LogsBloom: Bytes256{byte(13), byte(14), byte(15)},
PrevRandao: Bytes32(common.HexToHash("0x111")),
BlockNumber: Uint64Quantity(222),
GasLimit: Uint64Quantity(333),
GasUsed: Uint64Quantity(444),
Timestamp: Uint64Quantity(555),
ExtraData: common.Hex2Bytes("0x666"),
BaseFeePerGas: *uint256.NewInt(777),
BlockHash: common.HexToHash("0x888"),
Withdrawals: w,
Transactions: []Data{common.Hex2Bytes("0x999")},
}
}
func TestMarshalUnmarshalWithdrawals(t *testing.T) {
emptyWithdrawal := &Withdrawals{}
withdrawals := &Withdrawals{
{
Index: 987,
Validator: 654,
Address: common.HexToAddress("0x898"),
Amount: 321,
},
}
maxWithdrawals := make(Withdrawals, maxWithdrawalsPerPayload)
for i := 0; i < maxWithdrawalsPerPayload; i++ {
maxWithdrawals[i] = Withdrawal{
Index: 987,
Validator: 654,
Address: common.HexToAddress("0x898"),
Amount: 321,
}
}
tooManyWithdrawals := make(Withdrawals, maxWithdrawalsPerPayload+1)
for i := 0; i < maxWithdrawalsPerPayload+1; i++ {
tooManyWithdrawals[i] = Withdrawal{
Index: 987,
Validator: 654,
Address: common.HexToAddress("0x898"),
Amount: 321,
}
}
tests := []struct {
name string
version BlockVersion
hasError bool
withdrawals *Withdrawals
}{
{"ZeroWithdrawalsSucceeds", BlockV2, false, emptyWithdrawal},
{"ZeroWithdrawalsFailsToDeserialize", BlockV1, true, emptyWithdrawal},
{"WithdrawalsSucceeds", BlockV2, false, withdrawals},
{"WithdrawalsFailsToDeserialize", BlockV1, true, withdrawals},
{"MaxWithdrawalsSucceeds", BlockV2, false, &maxWithdrawals},
{"TooManyWithdrawalsErrors", BlockV2, true, &tooManyWithdrawals},
}
for _, test := range tests {
test := test
t.Run(fmt.Sprintf("TestWithdrawalUnmarshalMarshal_%s", test.name), func(t *testing.T) {
input := createPayloadWithWithdrawals(test.withdrawals)
var buf bytes.Buffer
_, err := input.MarshalSSZ(&buf)
require.NoError(t, err)
data := buf.Bytes()
output := &ExecutionPayload{}
err = output.UnmarshalSSZ(test.version, uint32(len(data)), bytes.NewReader(data))
if test.hasError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, input, output)
if test.withdrawals != nil {
require.Equal(t, len(*test.withdrawals), len(*output.Withdrawals))
}
}
})
}
_, err = payload.MarshalSSZ(&buf)
require.Error(t, err)
require.Equal(t, ErrExtraDataTooLarge, err)
}
......@@ -6,13 +6,13 @@ import (
"math/big"
"reflect"
"github.com/holiman/uint256"
"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/holiman/uint256"
)
type ErrorCode int
......@@ -143,7 +143,7 @@ type ExecutionPayload struct {
ExtraData BytesMax32 `json:"extraData"`
BaseFeePerGas Uint256Quantity `json:"baseFeePerGas"`
BlockHash common.Hash `json:"blockHash"`
Withdrawals *[]Withdrawal `json:"withdrawals,omitempty"`
Withdrawals *Withdrawals `json:"withdrawals,omitempty"`
// Array of transaction objects, each object is a byte list (DATA) representing
// TransactionType || TransactionPayload or LegacyTransaction as defined in EIP-2718
Transactions []Data `json:"transactions"`
......@@ -168,6 +168,10 @@ func (s rawTransactions) EncodeIndex(i int, w *bytes.Buffer) {
w.Write(s[i])
}
func (payload *ExecutionPayload) CanyonBlock() bool {
return payload.Withdrawals != nil
}
// CheckBlockHash recomputes the block hash and returns if the embedded block hash matches.
func (payload *ExecutionPayload) CheckBlockHash() (actual common.Hash, ok bool) {
hasher := trie.NewStackTrie(nil)
......@@ -191,11 +195,17 @@ func (payload *ExecutionPayload) CheckBlockHash() (actual common.Hash, ok bool)
Nonce: types.BlockNonce{}, // zeroed, proof-of-work legacy
BaseFee: payload.BaseFeePerGas.ToBig(),
}
if payload.CanyonBlock() {
withdrawalHash := types.DeriveSha(*payload.Withdrawals, hasher)
header.WithdrawalsHash = &withdrawalHash
}
blockHash := header.Hash()
return blockHash, blockHash == payload.BlockHash
}
func BlockAsPayload(bl *types.Block) (*ExecutionPayload, error) {
func BlockAsPayload(bl *types.Block, canyonForkTime *uint64) (*ExecutionPayload, error) {
baseFee, overflow := uint256.FromBig(bl.BaseFee())
if overflow {
return nil, fmt.Errorf("invalid base fee in block: %s", bl.BaseFee())
......@@ -208,7 +218,8 @@ func BlockAsPayload(bl *types.Block) (*ExecutionPayload, error) {
}
opaqueTxs[i] = otx
}
return &ExecutionPayload{
payload := &ExecutionPayload{
ParentHash: bl.ParentHash(),
FeeRecipient: bl.Coinbase(),
StateRoot: Bytes32(bl.Root()),
......@@ -220,10 +231,16 @@ func BlockAsPayload(bl *types.Block) (*ExecutionPayload, error) {
GasUsed: Uint64Quantity(bl.GasUsed()),
Timestamp: Uint64Quantity(bl.Time()),
ExtraData: bl.Extra(),
BaseFeePerGas: Uint256Quantity(*baseFee),
BaseFeePerGas: *baseFee,
BlockHash: bl.Hash(),
Transactions: opaqueTxs,
}, nil
}
if canyonForkTime != nil && uint64(payload.Timestamp) >= *canyonForkTime {
payload.Withdrawals = &Withdrawals{}
}
return payload, nil
}
type PayloadAttributes struct {
......@@ -234,7 +251,7 @@ type PayloadAttributes struct {
// suggested value for the coinbase field of the new payload
SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient"`
// Withdrawals to include into the block -- should be nil or empty depending on Shanghai enablement
Withdrawals *[]Withdrawal `json:"withdrawals,omitempty"`
Withdrawals *Withdrawals `json:"withdrawals,omitempty"`
// Transactions to force into the block (always at the start of the transactions list).
Transactions []Data `json:"transactions,omitempty"`
// NoTxPool to disable adding any transactions from the transaction-pool.
......@@ -302,9 +319,23 @@ type SystemConfig struct {
}
// Withdrawal represents a validator withdrawal from the consensus layer.
// https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#withdrawal
type Withdrawal struct {
Index uint64 `json:"index"` // monotonically increasing identifier issued by consensus layer
Validator uint64 `json:"validatorIndex"` // index of validator associated with withdrawal
Address common.Address `json:"address"` // target address for withdrawn ether
Amount uint64 `json:"amount"` // value of withdrawal in Gwei
}
// Withdrawals implements DerivableList for withdrawals.
type Withdrawals []Withdrawal
// Len returns the length of s.
func (s Withdrawals) Len() int { return len(s) }
// EncodeIndex encodes the i'th withdrawal to w. Note that this does not check for errors
// because we assume that *Withdrawal will only ever contain valid withdrawals that were either
// constructed by decoding or via public API in this package.
func (s Withdrawals) EncodeIndex(i int, w *bytes.Buffer) {
_ = rlp.Encode(w, s[i])
}
......@@ -181,6 +181,7 @@ func (hdr *rpcHeader) Info(trustCache bool, mustBePostMerge bool) (eth.BlockInfo
type rpcBlock struct {
rpcHeader
Transactions []*types.Transaction `json:"transactions"`
Withdrawals *eth.Withdrawals `json:"withdrawals,omitempty"`
}
func (block *rpcBlock) verify() error {
......@@ -252,6 +253,7 @@ func (block *rpcBlock) ExecutionPayload(trustCache bool) (*eth.ExecutionPayload,
BaseFeePerGas: baseFee,
BlockHash: block.Hash,
Transactions: opaqueTxs,
Withdrawals: block.Withdrawals,
}, nil
}
......
......@@ -51,7 +51,8 @@ and are adopted by several other blockchains, most notably the [L1 consensus lay
- [Topic configuration](#topic-configuration)
- [Topic validation](#topic-validation)
- [Gossip Topics](#gossip-topics)
- [`blocks`](#blocks)
- [`blocksv1`](#blocksv1)
- [`blocksv2`](#blocksv2)
- [Block encoding](#block-encoding)
- [Block signatures](#block-signatures)
- [Block validation](#block-validation)
......@@ -247,9 +248,15 @@ The extended validator emits one of the following validation signals:
## Gossip Topics
### `blocks`
There are two topics for distributing blocks to other nodes faster than proxying through L1 would. These are:
The primary topic of the L2, to distribute blocks to other nodes faster than proxying through L1 would.
### `blocksv1`
Pre-Canyon/Shanghai blocks are broadcast on `/optimism/<chainId>/0/blocks`.
### `blocksv2`
Post-Canyon/Shanghai blocks are broadcast on `/optimism/<chainId>/1/blocks`.
#### Block encoding
......@@ -282,6 +289,8 @@ An [extended-validator] checks the incoming messages as follows, in order of ope
(graceful boundary for worst-case propagation and clock skew)
- `[REJECT]` if the `payload.timestamp` is more than 5 seconds into the future
- `[REJECT]` if the `block_hash` in the `payload` is not valid
- `[REJECT]` if the block is on the V1 topic and has withdrawals
- `[REJECT]` if the block is on the V2 topic and does not have withdrawals
- `[REJECT]` if more than 5 different blocks have been seen with the same block height
- `[IGNORE]` if the block has already been seen
- `[REJECT]` if the signature by the sequencer is not valid
......
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