go-ethereum/beacon/engine/ssz/payload.go
2026-06-09 14:05:22 +02:00

251 lines
9.1 KiB
Go

// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package ssz
import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
"github.com/karalabe/ssz"
)
// Fork-boundary filters. Fields gated by these only participate in size,
// encode, decode and hash when Codec.fork is in range — see forkmap.go for how
// the geth fork maps onto ssz.Fork. Grouping the filters here (rather than one
// flag per field) keeps the "field-group" structure visible: each filter is a
// group boundary, and DefineSSZ applies it to every field the group owns.
var (
// withdrawals group, introduced in Shanghai (Shapella).
fromShanghai = ssz.ForkFilter{Added: ssz.ForkShapella}
// blob-gas group, introduced in Cancun (Dencun).
fromCancun = ssz.ForkFilter{Added: ssz.ForkDencun}
// beacon-root attribute, introduced in Cancun (Dencun).
// (shares the Cancun boundary but kept named for clarity at call sites)
// bal + slot group, introduced in Amsterdam.
fromAmsterdam = ssz.ForkFilter{Added: forkAmsterdam}
)
// ExecutionPayload is the monolithic execution payload spanning every fork from
// Paris onward. Fork-specific fields are gated with ...OnFork; the in-memory
// struct is a superset and the fork passed to the codec selects the wire shape.
//
// Field groups (see the filter vars above):
// - base (Paris): ParentHash … Transactions
// - withdrawals (Shanghai): Withdrawals
// - blobGas (Cancun): BlobGasUsed, ExcessBlobGas
// - bal (Amsterdam): BlockAccessList, SlotNumber
//
// Gated scalars are pointers so the codec can distinguish "absent for this
// fork" (nil) from "present and zero"; Validate(fork) enforces that invariant.
type ExecutionPayload struct {
// base group (Paris)
ParentHash common.Hash
FeeRecipient common.Address
StateRoot common.Hash
ReceiptsRoot common.Hash
LogsBloom [256]byte
PrevRandao common.Hash
BlockNumber uint64
GasLimit uint64
GasUsed uint64
Timestamp uint64
ExtraData []byte
BaseFeePerGas *uint256.Int
BlockHash common.Hash
Transactions [][]byte
// withdrawals group (Shanghai)
Withdrawals []*Withdrawal
// blobGas group (Cancun)
BlobGasUsed *uint64
ExcessBlobGas *uint64
// bal group (Amsterdam)
BlockAccessList []byte
SlotNumber *uint64
}
func (p *ExecutionPayload) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
// Fixed portion is fork-dependent; let the codec primitives account for it
// via the active fork rather than hand-summing magic byte constants.
var size uint32
// base fixed: 5 hashes(32) + addr(20) + bloom(256) + 4 u64s + uint256(32)
// + 2 offsets(extra, txs)(4)
size += 5*32 + 20 + 256 + 4*8 + 32 + 2*4
if siz.Fork() >= ssz.ForkShapella {
size += 4 // withdrawals offset
}
if siz.Fork() >= ssz.ForkDencun {
size += 2 * 8 // blob_gas_used, excess_blob_gas
}
if siz.Fork() >= forkAmsterdam {
size += 4 + 8 // block_access_list offset, slot_number
}
if fixed {
return size
}
size += ssz.SizeDynamicBytes(siz, p.ExtraData)
size += ssz.SizeSliceOfDynamicBytes(siz, p.Transactions)
if siz.Fork() >= ssz.ForkShapella {
size += ssz.SizeSliceOfStaticObjects(siz, p.Withdrawals)
}
if siz.Fork() >= forkAmsterdam {
size += ssz.SizeDynamicBytes(siz, p.BlockAccessList)
}
return size
}
func (p *ExecutionPayload) DefineSSZ(c *ssz.Codec) {
// --- offset phase ---
ssz.DefineStaticBytes(c, &p.ParentHash)
ssz.DefineStaticBytes(c, &p.FeeRecipient)
ssz.DefineStaticBytes(c, &p.StateRoot)
ssz.DefineStaticBytes(c, &p.ReceiptsRoot)
ssz.DefineStaticBytes(c, &p.LogsBloom)
ssz.DefineStaticBytes(c, &p.PrevRandao)
ssz.DefineUint64(c, &p.BlockNumber)
ssz.DefineUint64(c, &p.GasLimit)
ssz.DefineUint64(c, &p.GasUsed)
ssz.DefineUint64(c, &p.Timestamp)
ssz.DefineDynamicBytesOffset(c, &p.ExtraData, MaxExtraDataBytes)
ssz.DefineUint256(c, &p.BaseFeePerGas)
ssz.DefineStaticBytes(c, &p.BlockHash)
ssz.DefineSliceOfDynamicBytesOffset(c, &p.Transactions, MaxTxsPerPayload, MaxBytesPerTx)
ssz.DefineSliceOfStaticObjectsOffsetOnFork(c, &p.Withdrawals, MaxWithdrawalsPerPayload, fromShanghai)
ssz.DefineUint64PointerOnFork(c, &p.BlobGasUsed, fromCancun)
ssz.DefineUint64PointerOnFork(c, &p.ExcessBlobGas, fromCancun)
ssz.DefineDynamicBytesOffsetOnFork(c, &p.BlockAccessList, MaxBalBytes, fromAmsterdam)
ssz.DefineUint64PointerOnFork(c, &p.SlotNumber, fromAmsterdam)
// --- content phase ---
ssz.DefineDynamicBytesContent(c, &p.ExtraData, MaxExtraDataBytes)
ssz.DefineSliceOfDynamicBytesContent(c, &p.Transactions, MaxTxsPerPayload, MaxBytesPerTx)
ssz.DefineSliceOfStaticObjectsContentOnFork(c, &p.Withdrawals, MaxWithdrawalsPerPayload, fromShanghai)
ssz.DefineDynamicBytesContentOnFork(c, &p.BlockAccessList, MaxBalBytes, fromAmsterdam)
}
// Validate checks that the in-memory field set matches what the given fork's
// wire shape carries: gated scalars must be present iff active, and absent
// fields must be empty so the codec doesn't silently drop populated data.
func (p *ExecutionPayload) Validate(fork ssz.Fork) error {
// withdrawals (Shanghai+)
if fork < ssz.ForkShapella && len(p.Withdrawals) > 0 {
return fmt.Errorf("withdrawals set but fork %d predates Shanghai", fork)
}
// blobGas (Cancun+)
if fork >= ssz.ForkDencun {
if p.BlobGasUsed == nil || p.ExcessBlobGas == nil {
return fmt.Errorf("blob gas fields required at fork %d", fork)
}
} else if p.BlobGasUsed != nil || p.ExcessBlobGas != nil {
return fmt.Errorf("blob gas fields set but fork %d predates Cancun", fork)
}
// bal + slot (Amsterdam+)
if fork >= forkAmsterdam {
if p.SlotNumber == nil {
return fmt.Errorf("slot_number required at fork %d", fork)
}
} else {
if len(p.BlockAccessList) > 0 {
return fmt.Errorf("block_access_list set but fork %d predates Amsterdam", fork)
}
if p.SlotNumber != nil {
return fmt.Errorf("slot_number set but fork %d predates Amsterdam", fork)
}
}
return nil
}
// PayloadAttributes is the monolithic build-attributes container spanning every
// fork from Paris onward.
//
// Field groups:
// - base (Paris): Timestamp, PrevRandao, SuggestedFeeRecipient
// - withdrawals (Shanghai): Withdrawals
// - beaconRoot (Cancun): ParentBeaconBlockRoot
// - amsterdam (Amsterdam): SlotNumber, TargetGasLimit
type PayloadAttributes struct {
Timestamp uint64
PrevRandao common.Hash
SuggestedFeeRecipient common.Address
Withdrawals []*Withdrawal // Shanghai+
ParentBeaconBlockRoot *common.Hash // Cancun+
SlotNumber *uint64 // Amsterdam+
TargetGasLimit *uint64 // Amsterdam+
}
func (a *PayloadAttributes) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 {
// base fixed: timestamp(8) + randao(32) + fee_recipient(20)
var size uint32 = 8 + 32 + 20
if siz.Fork() >= ssz.ForkShapella {
size += 4 // withdrawals offset
}
if siz.Fork() >= ssz.ForkDencun {
size += 32 // parent_beacon_block_root
}
if siz.Fork() >= forkAmsterdam {
size += 8 + 8 // slot_number, target_gas_limit
}
if fixed {
return size
}
if siz.Fork() >= ssz.ForkShapella {
size += ssz.SizeSliceOfStaticObjects(siz, a.Withdrawals)
}
return size
}
func (a *PayloadAttributes) DefineSSZ(c *ssz.Codec) {
// offset phase
ssz.DefineUint64(c, &a.Timestamp)
ssz.DefineStaticBytes(c, &a.PrevRandao)
ssz.DefineStaticBytes(c, &a.SuggestedFeeRecipient)
ssz.DefineSliceOfStaticObjectsOffsetOnFork(c, &a.Withdrawals, MaxWithdrawalsPerPayload, fromShanghai)
ssz.DefineStaticBytesPointerOnFork(c, &a.ParentBeaconBlockRoot, fromCancun)
ssz.DefineUint64PointerOnFork(c, &a.SlotNumber, fromAmsterdam)
ssz.DefineUint64PointerOnFork(c, &a.TargetGasLimit, fromAmsterdam)
// content phase
ssz.DefineSliceOfStaticObjectsContentOnFork(c, &a.Withdrawals, MaxWithdrawalsPerPayload, fromShanghai)
}
// Validate enforces presence/absence of gated fields for the given fork.
func (a *PayloadAttributes) Validate(fork ssz.Fork) error {
if fork < ssz.ForkShapella && len(a.Withdrawals) > 0 {
return fmt.Errorf("withdrawals set but fork %d predates Shanghai", fork)
}
if fork >= ssz.ForkDencun {
if a.ParentBeaconBlockRoot == nil {
return fmt.Errorf("parent_beacon_block_root required at fork %d", fork)
}
} else if a.ParentBeaconBlockRoot != nil {
return fmt.Errorf("parent_beacon_block_root set but fork %d predates Cancun", fork)
}
if fork >= forkAmsterdam {
if a.SlotNumber == nil || a.TargetGasLimit == nil {
return fmt.Errorf("slot_number and target_gas_limit required at fork %d", fork)
}
} else if a.SlotNumber != nil || a.TargetGasLimit != nil {
return fmt.Errorf("amsterdam attributes set but fork %d predates Amsterdam", fork)
}
return nil
}