mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-24 08:49:29 +00:00
1284 lines
36 KiB
Go
1284 lines
36 KiB
Go
// Copyright 2025 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 engine
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
)
|
|
|
|
// SSZ status codes for PayloadStatusSSZ (EIP-8161).
|
|
const (
|
|
SSZStatusValid uint8 = 0
|
|
SSZStatusInvalid uint8 = 1
|
|
SSZStatusSyncing uint8 = 2
|
|
SSZStatusAccepted uint8 = 3
|
|
SSZStatusInvalidBlockHash uint8 = 4
|
|
)
|
|
|
|
// EngineStatusToSSZ converts a string engine status to the SSZ uint8 representation.
|
|
func EngineStatusToSSZ(status string) uint8 {
|
|
switch status {
|
|
case VALID:
|
|
return SSZStatusValid
|
|
case INVALID:
|
|
return SSZStatusInvalid
|
|
case SYNCING:
|
|
return SSZStatusSyncing
|
|
case ACCEPTED:
|
|
return SSZStatusAccepted
|
|
case "INVALID_BLOCK_HASH":
|
|
return SSZStatusInvalidBlockHash
|
|
default:
|
|
return SSZStatusInvalid
|
|
}
|
|
}
|
|
|
|
// SSZToEngineStatus converts an SSZ uint8 status to the string engine status.
|
|
func SSZToEngineStatus(status uint8) string {
|
|
switch status {
|
|
case SSZStatusValid:
|
|
return VALID
|
|
case SSZStatusInvalid:
|
|
return INVALID
|
|
case SSZStatusSyncing:
|
|
return SYNCING
|
|
case SSZStatusAccepted:
|
|
return ACCEPTED
|
|
case SSZStatusInvalidBlockHash:
|
|
return "INVALID_BLOCK_HASH"
|
|
default:
|
|
return INVALID
|
|
}
|
|
}
|
|
|
|
// --- PayloadStatus SSZ ---
|
|
|
|
const payloadStatusFixedSize = 9 // status(1) + hash_offset(4) + err_offset(4)
|
|
|
|
// EncodePayloadStatusSSZ encodes a PayloadStatusV1 to SSZ bytes per EIP-8161.
|
|
// Uses List[Hash32, 1] encoding: 0 bytes if nil, 32 bytes if present.
|
|
func EncodePayloadStatusSSZ(ps *PayloadStatusV1) []byte {
|
|
var hashData []byte
|
|
if ps.LatestValidHash != nil {
|
|
hashData = ps.LatestValidHash[:]
|
|
}
|
|
|
|
var errorBytes []byte
|
|
if ps.ValidationError != nil {
|
|
errorBytes = []byte(*ps.ValidationError)
|
|
}
|
|
|
|
buf := make([]byte, payloadStatusFixedSize+len(hashData)+len(errorBytes))
|
|
buf[0] = EngineStatusToSSZ(ps.Status)
|
|
binary.LittleEndian.PutUint32(buf[1:5], uint32(payloadStatusFixedSize))
|
|
binary.LittleEndian.PutUint32(buf[5:9], uint32(payloadStatusFixedSize+len(hashData)))
|
|
|
|
copy(buf[payloadStatusFixedSize:], hashData)
|
|
copy(buf[payloadStatusFixedSize+len(hashData):], errorBytes)
|
|
return buf
|
|
}
|
|
|
|
// DecodePayloadStatusSSZ decodes SSZ bytes into a PayloadStatusV1.
|
|
func DecodePayloadStatusSSZ(buf []byte) (*PayloadStatusV1, error) {
|
|
if len(buf) < payloadStatusFixedSize {
|
|
return nil, fmt.Errorf("PayloadStatusSSZ: buffer too short (%d < %d)", len(buf), payloadStatusFixedSize)
|
|
}
|
|
|
|
ps := &PayloadStatusV1{
|
|
Status: SSZToEngineStatus(buf[0]),
|
|
}
|
|
|
|
hashOffset := binary.LittleEndian.Uint32(buf[1:5])
|
|
errOffset := binary.LittleEndian.Uint32(buf[5:9])
|
|
|
|
if hashOffset > uint32(len(buf)) || errOffset > uint32(len(buf)) || hashOffset > errOffset {
|
|
return nil, fmt.Errorf("PayloadStatusSSZ: offsets out of bounds")
|
|
}
|
|
|
|
// Decode List[Hash32, 1]: 0 bytes = nil, 32 bytes = hash present
|
|
hashData := buf[hashOffset:errOffset]
|
|
switch len(hashData) {
|
|
case 0:
|
|
ps.LatestValidHash = nil
|
|
case 32:
|
|
hash := common.BytesToHash(hashData)
|
|
ps.LatestValidHash = &hash
|
|
default:
|
|
return nil, fmt.Errorf("PayloadStatusSSZ: invalid hash list length %d (expected 0 or 32)", len(hashData))
|
|
}
|
|
|
|
// Decode validation_error
|
|
if errOffset < uint32(len(buf)) {
|
|
errLen := uint32(len(buf)) - errOffset
|
|
if errLen > 1024 {
|
|
return nil, fmt.Errorf("PayloadStatusSSZ: validation error too long (%d > 1024)", errLen)
|
|
}
|
|
s := string(buf[errOffset:])
|
|
ps.ValidationError = &s
|
|
}
|
|
|
|
return ps, nil
|
|
}
|
|
|
|
// --- ForkchoiceState SSZ ---
|
|
|
|
// EncodeForkchoiceStateSSZ encodes a ForkchoiceStateV1 to SSZ bytes (96 bytes fixed).
|
|
func EncodeForkchoiceStateSSZ(fcs *ForkchoiceStateV1) []byte {
|
|
buf := make([]byte, 96)
|
|
copy(buf[0:32], fcs.HeadBlockHash[:])
|
|
copy(buf[32:64], fcs.SafeBlockHash[:])
|
|
copy(buf[64:96], fcs.FinalizedBlockHash[:])
|
|
return buf
|
|
}
|
|
|
|
// DecodeForkchoiceStateSSZ decodes SSZ bytes into a ForkchoiceStateV1.
|
|
func DecodeForkchoiceStateSSZ(buf []byte) (*ForkchoiceStateV1, error) {
|
|
if len(buf) < 96 {
|
|
return nil, fmt.Errorf("ForkchoiceState: buffer too short (%d < 96)", len(buf))
|
|
}
|
|
fcs := &ForkchoiceStateV1{}
|
|
copy(fcs.HeadBlockHash[:], buf[0:32])
|
|
copy(fcs.SafeBlockHash[:], buf[32:64])
|
|
copy(fcs.FinalizedBlockHash[:], buf[64:96])
|
|
return fcs, nil
|
|
}
|
|
|
|
// --- ForkchoiceUpdated Response SSZ ---
|
|
|
|
const forkchoiceUpdatedResponseFixedSize = 8
|
|
|
|
// EncodeForkChoiceResponseSSZ encodes a ForkChoiceResponse to SSZ bytes.
|
|
func EncodeForkChoiceResponseSSZ(resp *ForkChoiceResponse) []byte {
|
|
psBytes := EncodePayloadStatusSSZ(&resp.PayloadStatus)
|
|
|
|
// Build List[Bytes8, 1] for payload ID: 0 bytes if nil, 8 bytes if present
|
|
var pidData []byte
|
|
if resp.PayloadID != nil {
|
|
pidData = resp.PayloadID[:]
|
|
}
|
|
|
|
buf := make([]byte, forkchoiceUpdatedResponseFixedSize+len(psBytes)+len(pidData))
|
|
binary.LittleEndian.PutUint32(buf[0:4], uint32(forkchoiceUpdatedResponseFixedSize))
|
|
binary.LittleEndian.PutUint32(buf[4:8], uint32(forkchoiceUpdatedResponseFixedSize+len(psBytes)))
|
|
|
|
copy(buf[forkchoiceUpdatedResponseFixedSize:], psBytes)
|
|
copy(buf[forkchoiceUpdatedResponseFixedSize+len(psBytes):], pidData)
|
|
return buf
|
|
}
|
|
|
|
// DecodeForkChoiceResponseSSZ decodes SSZ bytes into a ForkChoiceResponse.
|
|
func DecodeForkChoiceResponseSSZ(buf []byte) (*ForkChoiceResponse, error) {
|
|
if len(buf) < forkchoiceUpdatedResponseFixedSize {
|
|
return nil, fmt.Errorf("ForkChoiceResponseSSZ: buffer too short (%d < %d)", len(buf), forkchoiceUpdatedResponseFixedSize)
|
|
}
|
|
|
|
psOffset := binary.LittleEndian.Uint32(buf[0:4])
|
|
pidOffset := binary.LittleEndian.Uint32(buf[4:8])
|
|
|
|
if psOffset > uint32(len(buf)) || pidOffset > uint32(len(buf)) || psOffset > pidOffset {
|
|
return nil, fmt.Errorf("ForkChoiceResponseSSZ: offsets out of bounds")
|
|
}
|
|
|
|
resp := &ForkChoiceResponse{}
|
|
|
|
ps, err := DecodePayloadStatusSSZ(buf[psOffset:pidOffset])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.PayloadStatus = *ps
|
|
|
|
// Decode List[Bytes8, 1]: 0 bytes = nil, 8 bytes = payload ID present
|
|
pidData := buf[pidOffset:]
|
|
switch len(pidData) {
|
|
case 0:
|
|
resp.PayloadID = nil
|
|
case 8:
|
|
var pid PayloadID
|
|
copy(pid[:], pidData)
|
|
resp.PayloadID = &pid
|
|
default:
|
|
return nil, fmt.Errorf("ForkChoiceResponseSSZ: invalid payload_id list length %d (expected 0 or 8)", len(pidData))
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// --- Capabilities SSZ ---
|
|
|
|
// EncodeCapabilitiesSSZ encodes a list of capability strings to SSZ.
|
|
func EncodeCapabilitiesSSZ(capabilities []string) []byte {
|
|
var totalSize int
|
|
for _, cap := range capabilities {
|
|
totalSize += 4 + len(cap)
|
|
}
|
|
|
|
buf := make([]byte, 4+totalSize)
|
|
binary.LittleEndian.PutUint32(buf[0:4], uint32(len(capabilities)))
|
|
|
|
offset := 4
|
|
for _, cap := range capabilities {
|
|
capBytes := []byte(cap)
|
|
binary.LittleEndian.PutUint32(buf[offset:offset+4], uint32(len(capBytes)))
|
|
offset += 4
|
|
copy(buf[offset:], capBytes)
|
|
offset += len(capBytes)
|
|
}
|
|
|
|
return buf
|
|
}
|
|
|
|
// DecodeCapabilitiesSSZ decodes a list of capability strings from SSZ bytes.
|
|
func DecodeCapabilitiesSSZ(buf []byte) ([]string, error) {
|
|
if len(buf) < 4 {
|
|
return nil, fmt.Errorf("Capabilities: buffer too short")
|
|
}
|
|
|
|
count := binary.LittleEndian.Uint32(buf[0:4])
|
|
if count > 128 {
|
|
return nil, fmt.Errorf("Capabilities: too many capabilities (%d > 128)", count)
|
|
}
|
|
|
|
capabilities := make([]string, 0, count)
|
|
offset := uint32(4)
|
|
|
|
for i := uint32(0); i < count; i++ {
|
|
if offset+4 > uint32(len(buf)) {
|
|
return nil, fmt.Errorf("Capabilities: unexpected end of buffer")
|
|
}
|
|
capLen := binary.LittleEndian.Uint32(buf[offset : offset+4])
|
|
offset += 4
|
|
if capLen > 64 || offset+capLen > uint32(len(buf)) {
|
|
return nil, fmt.Errorf("Capabilities: capability too long or truncated")
|
|
}
|
|
capabilities = append(capabilities, string(buf[offset:offset+capLen]))
|
|
offset += capLen
|
|
}
|
|
|
|
return capabilities, nil
|
|
}
|
|
|
|
// --- ClientVersion SSZ ---
|
|
|
|
// EncodeClientVersionSSZ encodes a ClientVersionV1 to SSZ.
|
|
func EncodeClientVersionSSZ(cv *ClientVersionV1) []byte {
|
|
codeBytes := []byte(cv.Code)
|
|
nameBytes := []byte(cv.Name)
|
|
versionBytes := []byte(cv.Version)
|
|
commitBytes := []byte(cv.Commit)
|
|
|
|
totalLen := 4 + len(codeBytes) + 4 + len(nameBytes) + 4 + len(versionBytes) + 4 + len(commitBytes)
|
|
buf := make([]byte, totalLen)
|
|
|
|
offset := 0
|
|
for _, field := range [][]byte{codeBytes, nameBytes, versionBytes, commitBytes} {
|
|
binary.LittleEndian.PutUint32(buf[offset:offset+4], uint32(len(field)))
|
|
offset += 4
|
|
copy(buf[offset:], field)
|
|
offset += len(field)
|
|
}
|
|
|
|
return buf
|
|
}
|
|
|
|
// DecodeClientVersionSSZ decodes a ClientVersionV1 from SSZ bytes.
|
|
func DecodeClientVersionSSZ(buf []byte) (*ClientVersionV1, error) {
|
|
if len(buf) < 16 {
|
|
return nil, fmt.Errorf("ClientVersion: buffer too short")
|
|
}
|
|
|
|
cv := &ClientVersionV1{}
|
|
offset := uint32(0)
|
|
|
|
readString := func(maxLen uint32) (string, error) {
|
|
if offset+4 > uint32(len(buf)) {
|
|
return "", fmt.Errorf("ClientVersion: unexpected end of buffer")
|
|
}
|
|
strLen := binary.LittleEndian.Uint32(buf[offset : offset+4])
|
|
offset += 4
|
|
if strLen > maxLen || offset+strLen > uint32(len(buf)) {
|
|
return "", fmt.Errorf("ClientVersion: string too long or truncated")
|
|
}
|
|
s := string(buf[offset : offset+strLen])
|
|
offset += strLen
|
|
return s, nil
|
|
}
|
|
|
|
var err error
|
|
if cv.Code, err = readString(8); err != nil {
|
|
return nil, err
|
|
}
|
|
if cv.Name, err = readString(64); err != nil {
|
|
return nil, err
|
|
}
|
|
if cv.Version, err = readString(64); err != nil {
|
|
return nil, err
|
|
}
|
|
if cv.Commit, err = readString(64); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cv, nil
|
|
}
|
|
|
|
// EncodeClientVersionsSSZ encodes a list of ClientVersionV1 to SSZ.
|
|
func EncodeClientVersionsSSZ(versions []ClientVersionV1) []byte {
|
|
var parts [][]byte
|
|
for i := range versions {
|
|
parts = append(parts, EncodeClientVersionSSZ(&versions[i]))
|
|
}
|
|
|
|
totalLen := 4
|
|
for _, p := range parts {
|
|
totalLen += 4 + len(p)
|
|
}
|
|
|
|
buf := make([]byte, totalLen)
|
|
binary.LittleEndian.PutUint32(buf[0:4], uint32(len(versions)))
|
|
|
|
offset := 4
|
|
for _, p := range parts {
|
|
binary.LittleEndian.PutUint32(buf[offset:offset+4], uint32(len(p)))
|
|
offset += 4
|
|
copy(buf[offset:], p)
|
|
offset += len(p)
|
|
}
|
|
|
|
return buf
|
|
}
|
|
|
|
// DecodeClientVersionsSSZ decodes a list of ClientVersionV1 from SSZ bytes.
|
|
func DecodeClientVersionsSSZ(buf []byte) ([]ClientVersionV1, error) {
|
|
if len(buf) < 4 {
|
|
return nil, fmt.Errorf("ClientVersions: buffer too short")
|
|
}
|
|
|
|
count := binary.LittleEndian.Uint32(buf[0:4])
|
|
if count > 16 {
|
|
return nil, fmt.Errorf("ClientVersions: too many versions (%d > 16)", count)
|
|
}
|
|
|
|
versions := make([]ClientVersionV1, 0, count)
|
|
offset := uint32(4)
|
|
|
|
for i := uint32(0); i < count; i++ {
|
|
if offset+4 > uint32(len(buf)) {
|
|
return nil, fmt.Errorf("ClientVersions: unexpected end of buffer")
|
|
}
|
|
cvLen := binary.LittleEndian.Uint32(buf[offset : offset+4])
|
|
offset += 4
|
|
if offset+cvLen > uint32(len(buf)) {
|
|
return nil, fmt.Errorf("ClientVersions: truncated")
|
|
}
|
|
cv, err := DecodeClientVersionSSZ(buf[offset : offset+cvLen])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
versions = append(versions, *cv)
|
|
offset += cvLen
|
|
}
|
|
|
|
return versions, nil
|
|
}
|
|
|
|
// --- ExecutionPayload SSZ ---
|
|
|
|
// engineVersionToPayloadVersion maps Engine API versions to ExecutionPayload SSZ versions.
|
|
func engineVersionToPayloadVersion(engineVersion int) int {
|
|
if engineVersion == 4 {
|
|
return 3 // Electra uses Deneb payload layout
|
|
}
|
|
if engineVersion >= 5 {
|
|
return 4 // Amsterdam and beyond use extended layout
|
|
}
|
|
return engineVersion
|
|
}
|
|
|
|
// executionPayloadFixedSize returns the fixed part size for a given version.
|
|
func executionPayloadFixedSize(version int) int {
|
|
size := 508 // V1 base
|
|
if version >= 2 {
|
|
size += 4 // withdrawals_offset
|
|
}
|
|
if version >= 3 {
|
|
size += 8 + 8 // blob_gas_used + excess_blob_gas
|
|
}
|
|
if version >= 4 {
|
|
size += 8 + 4 // slot_number + block_access_list_offset
|
|
}
|
|
return size
|
|
}
|
|
|
|
// uint256ToSSZBytes converts a big.Int to 32-byte little-endian SSZ representation.
|
|
func uint256ToSSZBytes(val *big.Int) []byte {
|
|
buf := make([]byte, 32)
|
|
if val == nil {
|
|
return buf
|
|
}
|
|
b := val.Bytes() // big-endian, minimal
|
|
for i, v := range b {
|
|
buf[len(b)-1-i] = v
|
|
}
|
|
return buf
|
|
}
|
|
|
|
// sszBytesToUint256 converts 32-byte little-endian SSZ bytes to a big.Int.
|
|
func sszBytesToUint256(buf []byte) *big.Int {
|
|
be := make([]byte, 32)
|
|
for i := 0; i < 32; i++ {
|
|
be[31-i] = buf[i]
|
|
}
|
|
return new(big.Int).SetBytes(be)
|
|
}
|
|
|
|
// encodeTransactionsSSZ encodes a list of transactions as SSZ list of variable-length items.
|
|
func encodeTransactionsSSZ(txs [][]byte) []byte {
|
|
if len(txs) == 0 {
|
|
return nil
|
|
}
|
|
offsetsSize := len(txs) * 4
|
|
dataSize := 0
|
|
for _, tx := range txs {
|
|
dataSize += len(tx)
|
|
}
|
|
buf := make([]byte, offsetsSize+dataSize)
|
|
|
|
dataStart := offsetsSize
|
|
for i, tx := range txs {
|
|
binary.LittleEndian.PutUint32(buf[i*4:(i+1)*4], uint32(dataStart))
|
|
dataStart += len(tx)
|
|
}
|
|
pos := offsetsSize
|
|
for _, tx := range txs {
|
|
copy(buf[pos:], tx)
|
|
pos += len(tx)
|
|
}
|
|
return buf
|
|
}
|
|
|
|
// decodeTransactionsSSZ decodes SSZ-encoded list of variable-length transactions.
|
|
func decodeTransactionsSSZ(buf []byte) ([][]byte, error) {
|
|
if len(buf) == 0 {
|
|
return nil, nil
|
|
}
|
|
if len(buf) < 4 {
|
|
return nil, fmt.Errorf("transactions SSZ: buffer too short")
|
|
}
|
|
firstOffset := binary.LittleEndian.Uint32(buf[0:4])
|
|
if firstOffset%4 != 0 {
|
|
return nil, fmt.Errorf("transactions SSZ: first offset not aligned (%d)", firstOffset)
|
|
}
|
|
count := firstOffset / 4
|
|
if count == 0 {
|
|
return nil, nil
|
|
}
|
|
if firstOffset > uint32(len(buf)) {
|
|
return nil, fmt.Errorf("transactions SSZ: first offset out of bounds")
|
|
}
|
|
|
|
offsets := make([]uint32, count)
|
|
for i := uint32(0); i < count; i++ {
|
|
offsets[i] = binary.LittleEndian.Uint32(buf[i*4 : (i+1)*4])
|
|
}
|
|
|
|
txs := make([][]byte, count)
|
|
for i := uint32(0); i < count; i++ {
|
|
start := offsets[i]
|
|
var end uint32
|
|
if i+1 < count {
|
|
end = offsets[i+1]
|
|
} else {
|
|
end = uint32(len(buf))
|
|
}
|
|
if start > uint32(len(buf)) || end > uint32(len(buf)) || start > end {
|
|
return nil, fmt.Errorf("transactions SSZ: invalid offset at index %d", i)
|
|
}
|
|
tx := make([]byte, end-start)
|
|
copy(tx, buf[start:end])
|
|
txs[i] = tx
|
|
}
|
|
return txs, nil
|
|
}
|
|
|
|
// Withdrawal SSZ: index(8) + validator_index(8) + address(20) + amount(8) = 44 bytes
|
|
const withdrawalSSZSize = 44
|
|
|
|
func encodeWithdrawalsSSZ(withdrawals []*types.Withdrawal) []byte {
|
|
if withdrawals == nil {
|
|
return nil
|
|
}
|
|
buf := make([]byte, len(withdrawals)*withdrawalSSZSize)
|
|
for i, w := range withdrawals {
|
|
off := i * withdrawalSSZSize
|
|
binary.LittleEndian.PutUint64(buf[off:off+8], w.Index)
|
|
binary.LittleEndian.PutUint64(buf[off+8:off+16], w.Validator)
|
|
copy(buf[off+16:off+36], w.Address[:])
|
|
binary.LittleEndian.PutUint64(buf[off+36:off+44], w.Amount)
|
|
}
|
|
return buf
|
|
}
|
|
|
|
func decodeWithdrawalsSSZ(buf []byte) ([]*types.Withdrawal, error) {
|
|
if len(buf) == 0 {
|
|
return []*types.Withdrawal{}, nil
|
|
}
|
|
if len(buf)%withdrawalSSZSize != 0 {
|
|
return nil, fmt.Errorf("withdrawals SSZ: buffer length %d not divisible by %d", len(buf), withdrawalSSZSize)
|
|
}
|
|
count := len(buf) / withdrawalSSZSize
|
|
withdrawals := make([]*types.Withdrawal, count)
|
|
for i := 0; i < count; i++ {
|
|
off := i * withdrawalSSZSize
|
|
withdrawals[i] = &types.Withdrawal{
|
|
Index: binary.LittleEndian.Uint64(buf[off : off+8]),
|
|
Validator: binary.LittleEndian.Uint64(buf[off+8 : off+16]),
|
|
Amount: binary.LittleEndian.Uint64(buf[off+36 : off+44]),
|
|
}
|
|
copy(withdrawals[i].Address[:], buf[off+16:off+36])
|
|
}
|
|
return withdrawals, nil
|
|
}
|
|
|
|
// EncodeExecutableDataSSZ encodes an ExecutableData to SSZ bytes.
|
|
// version: 1=Bellatrix, 2=Capella, 3=Deneb, 4=Amsterdam
|
|
func EncodeExecutableDataSSZ(ep *ExecutableData, version int) []byte {
|
|
fixedSize := executionPayloadFixedSize(version)
|
|
|
|
extraData := ep.ExtraData
|
|
txData := encodeTransactionsSSZ(ep.Transactions)
|
|
var withdrawalData []byte
|
|
if version >= 2 {
|
|
withdrawalData = encodeWithdrawalsSSZ(ep.Withdrawals)
|
|
}
|
|
|
|
totalVarSize := len(extraData) + len(txData)
|
|
if version >= 2 {
|
|
totalVarSize += len(withdrawalData)
|
|
}
|
|
|
|
buf := make([]byte, fixedSize+totalVarSize)
|
|
pos := 0
|
|
|
|
// Fixed fields
|
|
copy(buf[pos:pos+32], ep.ParentHash[:])
|
|
pos += 32
|
|
copy(buf[pos:pos+20], ep.FeeRecipient[:])
|
|
pos += 20
|
|
copy(buf[pos:pos+32], ep.StateRoot[:])
|
|
pos += 32
|
|
copy(buf[pos:pos+32], ep.ReceiptsRoot[:])
|
|
pos += 32
|
|
if len(ep.LogsBloom) >= 256 {
|
|
copy(buf[pos:pos+256], ep.LogsBloom[:256])
|
|
}
|
|
pos += 256
|
|
copy(buf[pos:pos+32], ep.Random[:])
|
|
pos += 32
|
|
binary.LittleEndian.PutUint64(buf[pos:pos+8], ep.Number)
|
|
pos += 8
|
|
binary.LittleEndian.PutUint64(buf[pos:pos+8], ep.GasLimit)
|
|
pos += 8
|
|
binary.LittleEndian.PutUint64(buf[pos:pos+8], ep.GasUsed)
|
|
pos += 8
|
|
binary.LittleEndian.PutUint64(buf[pos:pos+8], ep.Timestamp)
|
|
pos += 8
|
|
|
|
// extra_data offset
|
|
extraDataOffset := fixedSize
|
|
binary.LittleEndian.PutUint32(buf[pos:pos+4], uint32(extraDataOffset))
|
|
pos += 4
|
|
|
|
// base_fee_per_gas (uint256, 32 bytes LE)
|
|
copy(buf[pos:pos+32], uint256ToSSZBytes(ep.BaseFeePerGas))
|
|
pos += 32
|
|
|
|
copy(buf[pos:pos+32], ep.BlockHash[:])
|
|
pos += 32
|
|
|
|
// transactions offset
|
|
txOffset := extraDataOffset + len(extraData)
|
|
binary.LittleEndian.PutUint32(buf[pos:pos+4], uint32(txOffset))
|
|
pos += 4
|
|
|
|
if version >= 2 {
|
|
wdOffset := txOffset + len(txData)
|
|
binary.LittleEndian.PutUint32(buf[pos:pos+4], uint32(wdOffset))
|
|
pos += 4
|
|
}
|
|
|
|
if version >= 3 {
|
|
var blobGasUsed, excessBlobGas uint64
|
|
if ep.BlobGasUsed != nil {
|
|
blobGasUsed = *ep.BlobGasUsed
|
|
}
|
|
if ep.ExcessBlobGas != nil {
|
|
excessBlobGas = *ep.ExcessBlobGas
|
|
}
|
|
binary.LittleEndian.PutUint64(buf[pos:pos+8], blobGasUsed)
|
|
pos += 8
|
|
binary.LittleEndian.PutUint64(buf[pos:pos+8], excessBlobGas)
|
|
pos += 8
|
|
}
|
|
|
|
if version >= 4 {
|
|
var slotNumber uint64
|
|
if ep.SlotNumber != nil {
|
|
slotNumber = *ep.SlotNumber
|
|
}
|
|
binary.LittleEndian.PutUint64(buf[pos:pos+8], slotNumber)
|
|
pos += 8
|
|
// Note: For V4 we'd have block_access_list offset here, but Geth doesn't have that field.
|
|
// We write the end of data as the offset (no block_access_list data).
|
|
balOffset := extraDataOffset + len(extraData) + len(txData)
|
|
if version >= 2 {
|
|
balOffset += len(withdrawalData)
|
|
}
|
|
binary.LittleEndian.PutUint32(buf[pos:pos+4], uint32(balOffset))
|
|
}
|
|
|
|
// Variable part
|
|
copy(buf[extraDataOffset:], extraData)
|
|
copy(buf[txOffset:], txData)
|
|
if version >= 2 {
|
|
wdOffset := txOffset + len(txData)
|
|
copy(buf[wdOffset:], withdrawalData)
|
|
}
|
|
|
|
return buf
|
|
}
|
|
|
|
// DecodeExecutableDataSSZ decodes SSZ bytes into an ExecutableData.
|
|
func DecodeExecutableDataSSZ(buf []byte, version int) (*ExecutableData, error) {
|
|
fixedSize := executionPayloadFixedSize(version)
|
|
if len(buf) < fixedSize {
|
|
return nil, fmt.Errorf("ExecutableData SSZ: buffer too short (%d < %d)", len(buf), fixedSize)
|
|
}
|
|
|
|
ep := &ExecutableData{}
|
|
pos := 0
|
|
|
|
copy(ep.ParentHash[:], buf[pos:pos+32])
|
|
pos += 32
|
|
copy(ep.FeeRecipient[:], buf[pos:pos+20])
|
|
pos += 20
|
|
copy(ep.StateRoot[:], buf[pos:pos+32])
|
|
pos += 32
|
|
copy(ep.ReceiptsRoot[:], buf[pos:pos+32])
|
|
pos += 32
|
|
ep.LogsBloom = make([]byte, 256)
|
|
copy(ep.LogsBloom, buf[pos:pos+256])
|
|
pos += 256
|
|
copy(ep.Random[:], buf[pos:pos+32])
|
|
pos += 32
|
|
ep.Number = binary.LittleEndian.Uint64(buf[pos : pos+8])
|
|
pos += 8
|
|
ep.GasLimit = binary.LittleEndian.Uint64(buf[pos : pos+8])
|
|
pos += 8
|
|
ep.GasUsed = binary.LittleEndian.Uint64(buf[pos : pos+8])
|
|
pos += 8
|
|
ep.Timestamp = binary.LittleEndian.Uint64(buf[pos : pos+8])
|
|
pos += 8
|
|
|
|
extraDataOffset := binary.LittleEndian.Uint32(buf[pos : pos+4])
|
|
pos += 4
|
|
|
|
ep.BaseFeePerGas = sszBytesToUint256(buf[pos : pos+32])
|
|
pos += 32
|
|
|
|
copy(ep.BlockHash[:], buf[pos:pos+32])
|
|
pos += 32
|
|
|
|
txOffset := binary.LittleEndian.Uint32(buf[pos : pos+4])
|
|
pos += 4
|
|
|
|
var wdOffset uint32
|
|
if version >= 2 {
|
|
wdOffset = binary.LittleEndian.Uint32(buf[pos : pos+4])
|
|
pos += 4
|
|
}
|
|
|
|
if version >= 3 {
|
|
blobGasUsed := binary.LittleEndian.Uint64(buf[pos : pos+8])
|
|
ep.BlobGasUsed = &blobGasUsed
|
|
pos += 8
|
|
excessBlobGas := binary.LittleEndian.Uint64(buf[pos : pos+8])
|
|
ep.ExcessBlobGas = &excessBlobGas
|
|
pos += 8
|
|
}
|
|
|
|
var balOffset uint32
|
|
if version >= 4 {
|
|
slotNumber := binary.LittleEndian.Uint64(buf[pos : pos+8])
|
|
ep.SlotNumber = &slotNumber
|
|
pos += 8
|
|
balOffset = binary.LittleEndian.Uint32(buf[pos : pos+4])
|
|
}
|
|
|
|
// Decode variable-length fields
|
|
if extraDataOffset > uint32(len(buf)) || txOffset > uint32(len(buf)) || extraDataOffset > txOffset {
|
|
return nil, fmt.Errorf("ExecutableData SSZ: invalid extra_data/transactions offsets")
|
|
}
|
|
ep.ExtraData = make([]byte, txOffset-extraDataOffset)
|
|
copy(ep.ExtraData, buf[extraDataOffset:txOffset])
|
|
|
|
var txEnd uint32
|
|
if version >= 2 {
|
|
txEnd = wdOffset
|
|
} else {
|
|
txEnd = uint32(len(buf))
|
|
}
|
|
if txOffset > txEnd {
|
|
return nil, fmt.Errorf("ExecutableData SSZ: transactions offset > end")
|
|
}
|
|
|
|
txBuf := buf[txOffset:txEnd]
|
|
txs, err := decodeTransactionsSSZ(txBuf)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ExecutableData SSZ: %w", err)
|
|
}
|
|
ep.Transactions = txs
|
|
if ep.Transactions == nil {
|
|
ep.Transactions = [][]byte{}
|
|
}
|
|
|
|
if version >= 2 {
|
|
var wdEnd uint32
|
|
if version >= 4 {
|
|
wdEnd = balOffset
|
|
} else {
|
|
wdEnd = uint32(len(buf))
|
|
}
|
|
if wdOffset > wdEnd || wdEnd > uint32(len(buf)) {
|
|
return nil, fmt.Errorf("ExecutableData SSZ: invalid withdrawals offset")
|
|
}
|
|
wds, err := decodeWithdrawalsSSZ(buf[wdOffset:wdEnd])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ExecutableData SSZ: %w", err)
|
|
}
|
|
ep.Withdrawals = wds
|
|
}
|
|
|
|
return ep, nil
|
|
}
|
|
|
|
// --- NewPayload request SSZ ---
|
|
|
|
// EncodeNewPayloadRequestSSZ encodes a newPayload request to SSZ.
|
|
func EncodeNewPayloadRequestSSZ(
|
|
ep *ExecutableData,
|
|
versionedHashes []common.Hash,
|
|
parentBeaconBlockRoot *common.Hash,
|
|
executionRequests [][]byte,
|
|
version int,
|
|
) []byte {
|
|
payloadVersion := engineVersionToPayloadVersion(version)
|
|
if version <= 2 {
|
|
return EncodeExecutableDataSSZ(ep, payloadVersion)
|
|
}
|
|
|
|
epBytes := EncodeExecutableDataSSZ(ep, payloadVersion)
|
|
blobHashBytes := make([]byte, len(versionedHashes)*32)
|
|
for i, h := range versionedHashes {
|
|
copy(blobHashBytes[i*32:(i+1)*32], h[:])
|
|
}
|
|
|
|
if version == 3 {
|
|
fixedSize := 40
|
|
buf := make([]byte, fixedSize+len(epBytes)+len(blobHashBytes))
|
|
binary.LittleEndian.PutUint32(buf[0:4], uint32(fixedSize))
|
|
binary.LittleEndian.PutUint32(buf[4:8], uint32(fixedSize+len(epBytes)))
|
|
if parentBeaconBlockRoot != nil {
|
|
copy(buf[8:40], parentBeaconBlockRoot[:])
|
|
}
|
|
copy(buf[fixedSize:], epBytes)
|
|
copy(buf[fixedSize+len(epBytes):], blobHashBytes)
|
|
return buf
|
|
}
|
|
|
|
// V4+
|
|
reqBytes := encodeStructuredExecutionRequestsSSZ(executionRequests)
|
|
|
|
fixedSize := 44
|
|
buf := make([]byte, fixedSize+len(epBytes)+len(blobHashBytes)+len(reqBytes))
|
|
binary.LittleEndian.PutUint32(buf[0:4], uint32(fixedSize))
|
|
binary.LittleEndian.PutUint32(buf[4:8], uint32(fixedSize+len(epBytes)))
|
|
if parentBeaconBlockRoot != nil {
|
|
copy(buf[8:40], parentBeaconBlockRoot[:])
|
|
}
|
|
binary.LittleEndian.PutUint32(buf[40:44], uint32(fixedSize+len(epBytes)+len(blobHashBytes)))
|
|
|
|
copy(buf[fixedSize:], epBytes)
|
|
copy(buf[fixedSize+len(epBytes):], blobHashBytes)
|
|
copy(buf[fixedSize+len(epBytes)+len(blobHashBytes):], reqBytes)
|
|
return buf
|
|
}
|
|
|
|
// DecodeNewPayloadRequestSSZ decodes a newPayload request from SSZ.
|
|
func DecodeNewPayloadRequestSSZ(buf []byte, version int) (
|
|
ep *ExecutableData,
|
|
versionedHashes []common.Hash,
|
|
parentBeaconBlockRoot *common.Hash,
|
|
executionRequests [][]byte,
|
|
err error,
|
|
) {
|
|
payloadVersion := engineVersionToPayloadVersion(version)
|
|
if version <= 2 {
|
|
ep, err = DecodeExecutableDataSSZ(buf, payloadVersion)
|
|
return
|
|
}
|
|
|
|
if version == 3 {
|
|
if len(buf) < 40 {
|
|
err = fmt.Errorf("NewPayloadV3 SSZ: buffer too short (%d < 40)", len(buf))
|
|
return
|
|
}
|
|
epOffset := binary.LittleEndian.Uint32(buf[0:4])
|
|
blobHashOffset := binary.LittleEndian.Uint32(buf[4:8])
|
|
root := common.BytesToHash(buf[8:40])
|
|
parentBeaconBlockRoot = &root
|
|
|
|
if epOffset > uint32(len(buf)) || blobHashOffset > uint32(len(buf)) || epOffset > blobHashOffset {
|
|
err = fmt.Errorf("NewPayloadV3 SSZ: invalid offsets")
|
|
return
|
|
}
|
|
ep, err = DecodeExecutableDataSSZ(buf[epOffset:blobHashOffset], payloadVersion)
|
|
if err != nil {
|
|
return
|
|
}
|
|
blobHashBuf := buf[blobHashOffset:]
|
|
if len(blobHashBuf)%32 != 0 {
|
|
err = fmt.Errorf("NewPayloadV3 SSZ: blob hashes not aligned")
|
|
return
|
|
}
|
|
versionedHashes = make([]common.Hash, len(blobHashBuf)/32)
|
|
for i := range versionedHashes {
|
|
copy(versionedHashes[i][:], blobHashBuf[i*32:(i+1)*32])
|
|
}
|
|
return
|
|
}
|
|
|
|
// V4+
|
|
if len(buf) < 44 {
|
|
err = fmt.Errorf("NewPayloadV4 SSZ: buffer too short (%d < 44)", len(buf))
|
|
return
|
|
}
|
|
epOffset := binary.LittleEndian.Uint32(buf[0:4])
|
|
blobHashOffset := binary.LittleEndian.Uint32(buf[4:8])
|
|
root := common.BytesToHash(buf[8:40])
|
|
parentBeaconBlockRoot = &root
|
|
reqOffset := binary.LittleEndian.Uint32(buf[40:44])
|
|
|
|
if epOffset > uint32(len(buf)) || blobHashOffset > uint32(len(buf)) || reqOffset > uint32(len(buf)) {
|
|
err = fmt.Errorf("NewPayloadV4 SSZ: offsets out of bounds")
|
|
return
|
|
}
|
|
ep, err = DecodeExecutableDataSSZ(buf[epOffset:blobHashOffset], payloadVersion)
|
|
if err != nil {
|
|
return
|
|
}
|
|
blobHashBuf := buf[blobHashOffset:reqOffset]
|
|
if len(blobHashBuf)%32 != 0 {
|
|
err = fmt.Errorf("NewPayloadV4 SSZ: blob hashes not aligned")
|
|
return
|
|
}
|
|
versionedHashes = make([]common.Hash, len(blobHashBuf)/32)
|
|
for i := range versionedHashes {
|
|
copy(versionedHashes[i][:], blobHashBuf[i*32:(i+1)*32])
|
|
}
|
|
|
|
executionRequests, err = decodeStructuredExecutionRequestsSSZ(buf[reqOffset:])
|
|
return
|
|
}
|
|
|
|
// --- Execution Requests SSZ (structured container for Prysm compatibility) ---
|
|
|
|
func encodeStructuredExecutionRequestsSSZ(reqs [][]byte) []byte {
|
|
var depositsData, withdrawalsData, consolidationsData []byte
|
|
for _, r := range reqs {
|
|
if len(r) < 1 {
|
|
continue
|
|
}
|
|
switch r[0] {
|
|
case 0x00:
|
|
depositsData = append(depositsData, r[1:]...)
|
|
case 0x01:
|
|
withdrawalsData = append(withdrawalsData, r[1:]...)
|
|
case 0x02:
|
|
consolidationsData = append(consolidationsData, r[1:]...)
|
|
}
|
|
}
|
|
|
|
fixedSize := 12
|
|
totalVar := len(depositsData) + len(withdrawalsData) + len(consolidationsData)
|
|
buf := make([]byte, fixedSize+totalVar)
|
|
|
|
depositsOffset := fixedSize
|
|
withdrawalsOffset := depositsOffset + len(depositsData)
|
|
consolidationsOffset := withdrawalsOffset + len(withdrawalsData)
|
|
|
|
binary.LittleEndian.PutUint32(buf[0:4], uint32(depositsOffset))
|
|
binary.LittleEndian.PutUint32(buf[4:8], uint32(withdrawalsOffset))
|
|
binary.LittleEndian.PutUint32(buf[8:12], uint32(consolidationsOffset))
|
|
|
|
copy(buf[depositsOffset:], depositsData)
|
|
copy(buf[withdrawalsOffset:], withdrawalsData)
|
|
copy(buf[consolidationsOffset:], consolidationsData)
|
|
|
|
return buf
|
|
}
|
|
|
|
func decodeStructuredExecutionRequestsSSZ(buf []byte) ([][]byte, error) {
|
|
if len(buf) == 0 {
|
|
return [][]byte{}, nil
|
|
}
|
|
if len(buf) < 12 {
|
|
return nil, fmt.Errorf("structured execution requests SSZ: buffer too short (%d < 12)", len(buf))
|
|
}
|
|
|
|
depositsOffset := binary.LittleEndian.Uint32(buf[0:4])
|
|
withdrawalsOffset := binary.LittleEndian.Uint32(buf[4:8])
|
|
consolidationsOffset := binary.LittleEndian.Uint32(buf[8:12])
|
|
|
|
if depositsOffset > uint32(len(buf)) || withdrawalsOffset > uint32(len(buf)) || consolidationsOffset > uint32(len(buf)) {
|
|
return nil, fmt.Errorf("structured execution requests SSZ: offsets out of bounds")
|
|
}
|
|
if depositsOffset > withdrawalsOffset || withdrawalsOffset > consolidationsOffset {
|
|
return nil, fmt.Errorf("structured execution requests SSZ: offsets not in order")
|
|
}
|
|
|
|
reqs := make([][]byte, 0, 3)
|
|
|
|
depositsData := buf[depositsOffset:withdrawalsOffset]
|
|
if len(depositsData) > 0 {
|
|
r := make([]byte, 1+len(depositsData))
|
|
r[0] = 0x00
|
|
copy(r[1:], depositsData)
|
|
reqs = append(reqs, r)
|
|
}
|
|
|
|
withdrawalsData := buf[withdrawalsOffset:consolidationsOffset]
|
|
if len(withdrawalsData) > 0 {
|
|
r := make([]byte, 1+len(withdrawalsData))
|
|
r[0] = 0x01
|
|
copy(r[1:], withdrawalsData)
|
|
reqs = append(reqs, r)
|
|
}
|
|
|
|
consolidationsData := buf[consolidationsOffset:]
|
|
if len(consolidationsData) > 0 {
|
|
r := make([]byte, 1+len(consolidationsData))
|
|
r[0] = 0x02
|
|
copy(r[1:], consolidationsData)
|
|
reqs = append(reqs, r)
|
|
}
|
|
|
|
return reqs, nil
|
|
}
|
|
|
|
// --- GetPayload response SSZ ---
|
|
|
|
const getPayloadResponseFixedSize = 45
|
|
|
|
// EncodeExecutionPayloadEnvelopeSSZ encodes a GetPayload response to SSZ.
|
|
func EncodeExecutionPayloadEnvelopeSSZ(resp *ExecutionPayloadEnvelope, version int) []byte {
|
|
if version == 1 {
|
|
return EncodeExecutableDataSSZ(resp.ExecutionPayload, 1)
|
|
}
|
|
|
|
payloadVersion := engineVersionToPayloadVersion(version)
|
|
epBytes := EncodeExecutableDataSSZ(resp.ExecutionPayload, payloadVersion)
|
|
blobsBytes := encodeBlobsBundleSSZ(resp.BlobsBundle)
|
|
reqBytes := encodeStructuredExecutionRequestsSSZ(resp.Requests)
|
|
|
|
buf := make([]byte, getPayloadResponseFixedSize+len(epBytes)+len(blobsBytes)+len(reqBytes))
|
|
|
|
// ep offset
|
|
binary.LittleEndian.PutUint32(buf[0:4], uint32(getPayloadResponseFixedSize))
|
|
|
|
// block_value (uint256 LE)
|
|
if resp.BlockValue != nil {
|
|
copy(buf[4:36], uint256ToSSZBytes(resp.BlockValue))
|
|
}
|
|
|
|
// blobs_bundle offset
|
|
blobsOffset := getPayloadResponseFixedSize + len(epBytes)
|
|
binary.LittleEndian.PutUint32(buf[36:40], uint32(blobsOffset))
|
|
|
|
// should_override_builder
|
|
if resp.Override {
|
|
buf[40] = 1
|
|
}
|
|
|
|
// execution_requests offset
|
|
reqOffset := blobsOffset + len(blobsBytes)
|
|
binary.LittleEndian.PutUint32(buf[41:45], uint32(reqOffset))
|
|
|
|
// Variable data
|
|
copy(buf[getPayloadResponseFixedSize:], epBytes)
|
|
copy(buf[blobsOffset:], blobsBytes)
|
|
copy(buf[reqOffset:], reqBytes)
|
|
|
|
return buf
|
|
}
|
|
|
|
// DecodeExecutionPayloadEnvelopeSSZ decodes SSZ bytes into an ExecutionPayloadEnvelope.
|
|
func DecodeExecutionPayloadEnvelopeSSZ(buf []byte, version int) (*ExecutionPayloadEnvelope, error) {
|
|
if version == 1 {
|
|
ep, err := DecodeExecutableDataSSZ(buf, 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ExecutionPayloadEnvelope{ExecutionPayload: ep}, nil
|
|
}
|
|
|
|
if len(buf) < getPayloadResponseFixedSize {
|
|
return nil, fmt.Errorf("ExecutionPayloadEnvelope SSZ: buffer too short (%d < %d)", len(buf), getPayloadResponseFixedSize)
|
|
}
|
|
|
|
resp := &ExecutionPayloadEnvelope{}
|
|
|
|
epOffset := binary.LittleEndian.Uint32(buf[0:4])
|
|
resp.BlockValue = sszBytesToUint256(buf[4:36])
|
|
blobsOffset := binary.LittleEndian.Uint32(buf[36:40])
|
|
resp.Override = buf[40] == 1
|
|
reqOffset := binary.LittleEndian.Uint32(buf[41:45])
|
|
|
|
if epOffset > uint32(len(buf)) || blobsOffset > uint32(len(buf)) {
|
|
return nil, fmt.Errorf("ExecutionPayloadEnvelope SSZ: offsets out of bounds")
|
|
}
|
|
payloadVersion := engineVersionToPayloadVersion(version)
|
|
ep, err := DecodeExecutableDataSSZ(buf[epOffset:blobsOffset], payloadVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.ExecutionPayload = ep
|
|
|
|
if blobsOffset > reqOffset || reqOffset > uint32(len(buf)) {
|
|
return nil, fmt.Errorf("ExecutionPayloadEnvelope SSZ: invalid blobs/requests offsets")
|
|
}
|
|
bundle, err := decodeBlobsBundleSSZ(buf[blobsOffset:reqOffset])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.BlobsBundle = bundle
|
|
|
|
if reqOffset < uint32(len(buf)) {
|
|
reqs, err := decodeStructuredExecutionRequestsSSZ(buf[reqOffset:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.Requests = reqs
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// --- BlobsBundle SSZ ---
|
|
|
|
const blobsBundleFixedSize = 12
|
|
|
|
func encodeBlobsBundleSSZ(bundle *BlobsBundle) []byte {
|
|
if bundle == nil {
|
|
return nil
|
|
}
|
|
|
|
commitmentsData := encodeFixedSizeList(bundle.Commitments)
|
|
proofsData := encodeFixedSizeList(bundle.Proofs)
|
|
blobsData := encodeFixedSizeList(bundle.Blobs)
|
|
|
|
totalVar := len(commitmentsData) + len(proofsData) + len(blobsData)
|
|
buf := make([]byte, blobsBundleFixedSize+totalVar)
|
|
|
|
commitmentsOffset := blobsBundleFixedSize
|
|
proofsOffset := commitmentsOffset + len(commitmentsData)
|
|
blobsOffset := proofsOffset + len(proofsData)
|
|
|
|
binary.LittleEndian.PutUint32(buf[0:4], uint32(commitmentsOffset))
|
|
binary.LittleEndian.PutUint32(buf[4:8], uint32(proofsOffset))
|
|
binary.LittleEndian.PutUint32(buf[8:12], uint32(blobsOffset))
|
|
|
|
copy(buf[commitmentsOffset:], commitmentsData)
|
|
copy(buf[proofsOffset:], proofsData)
|
|
copy(buf[blobsOffset:], blobsData)
|
|
|
|
return buf
|
|
}
|
|
|
|
func decodeBlobsBundleSSZ(buf []byte) (*BlobsBundle, error) {
|
|
if len(buf) == 0 {
|
|
return nil, nil
|
|
}
|
|
if len(buf) < blobsBundleFixedSize {
|
|
return nil, fmt.Errorf("BlobsBundle SSZ: buffer too short")
|
|
}
|
|
|
|
commitmentsOffset := binary.LittleEndian.Uint32(buf[0:4])
|
|
proofsOffset := binary.LittleEndian.Uint32(buf[4:8])
|
|
blobsOffset := binary.LittleEndian.Uint32(buf[8:12])
|
|
|
|
if commitmentsOffset > uint32(len(buf)) || proofsOffset > uint32(len(buf)) || blobsOffset > uint32(len(buf)) {
|
|
return nil, fmt.Errorf("BlobsBundle SSZ: offsets out of bounds")
|
|
}
|
|
|
|
bundle := &BlobsBundle{}
|
|
|
|
commBuf := buf[commitmentsOffset:proofsOffset]
|
|
if len(commBuf) > 0 {
|
|
if len(commBuf)%48 != 0 {
|
|
return nil, fmt.Errorf("BlobsBundle SSZ: commitments not aligned to 48 bytes")
|
|
}
|
|
bundle.Commitments = make([]hexutil.Bytes, len(commBuf)/48)
|
|
for i := range bundle.Commitments {
|
|
c := make(hexutil.Bytes, 48)
|
|
copy(c, commBuf[i*48:(i+1)*48])
|
|
bundle.Commitments[i] = c
|
|
}
|
|
}
|
|
|
|
proofBuf := buf[proofsOffset:blobsOffset]
|
|
if len(proofBuf) > 0 {
|
|
if len(proofBuf)%48 != 0 {
|
|
return nil, fmt.Errorf("BlobsBundle SSZ: proofs not aligned to 48 bytes")
|
|
}
|
|
bundle.Proofs = make([]hexutil.Bytes, len(proofBuf)/48)
|
|
for i := range bundle.Proofs {
|
|
p := make(hexutil.Bytes, 48)
|
|
copy(p, proofBuf[i*48:(i+1)*48])
|
|
bundle.Proofs[i] = p
|
|
}
|
|
}
|
|
|
|
blobBuf := buf[blobsOffset:]
|
|
if len(blobBuf) > 0 {
|
|
if len(blobBuf)%131072 != 0 {
|
|
return nil, fmt.Errorf("BlobsBundle SSZ: blobs not aligned to 131072 bytes")
|
|
}
|
|
bundle.Blobs = make([]hexutil.Bytes, len(blobBuf)/131072)
|
|
for i := range bundle.Blobs {
|
|
b := make(hexutil.Bytes, 131072)
|
|
copy(b, blobBuf[i*131072:(i+1)*131072])
|
|
bundle.Blobs[i] = b
|
|
}
|
|
}
|
|
|
|
return bundle, nil
|
|
}
|
|
|
|
func encodeFixedSizeList(items []hexutil.Bytes) []byte {
|
|
totalLen := 0
|
|
for _, item := range items {
|
|
totalLen += len(item)
|
|
}
|
|
buf := make([]byte, totalLen)
|
|
pos := 0
|
|
for _, item := range items {
|
|
copy(buf[pos:], item)
|
|
pos += len(item)
|
|
}
|
|
return buf
|
|
}
|
|
|
|
// --- GetBlobs request SSZ ---
|
|
|
|
// EncodeGetBlobsRequestSSZ encodes a list of versioned hashes for the get_blobs SSZ request.
|
|
func EncodeGetBlobsRequestSSZ(hashes []common.Hash) []byte {
|
|
buf := make([]byte, 4+len(hashes)*32)
|
|
binary.LittleEndian.PutUint32(buf[0:4], uint32(len(hashes)))
|
|
for i, h := range hashes {
|
|
copy(buf[4+i*32:4+(i+1)*32], h[:])
|
|
}
|
|
return buf
|
|
}
|
|
|
|
// DecodeGetBlobsRequestSSZ decodes a list of versioned hashes from SSZ bytes.
|
|
func DecodeGetBlobsRequestSSZ(buf []byte) ([]common.Hash, error) {
|
|
if len(buf) < 4 {
|
|
return nil, fmt.Errorf("GetBlobsRequest: buffer too short")
|
|
}
|
|
count := binary.LittleEndian.Uint32(buf[0:4])
|
|
if 4+count*32 > uint32(len(buf)) {
|
|
return nil, fmt.Errorf("GetBlobsRequest: buffer too short for %d hashes", count)
|
|
}
|
|
hashes := make([]common.Hash, count)
|
|
for i := uint32(0); i < count; i++ {
|
|
copy(hashes[i][:], buf[4+i*32:4+(i+1)*32])
|
|
}
|
|
return hashes, nil
|
|
}
|
|
|
|
// --- PayloadAttributes SSZ ---
|
|
|
|
// DecodePayloadAttributesSSZ decodes PayloadAttributes from SSZ bytes.
|
|
func DecodePayloadAttributesSSZ(buf []byte, version int) (*PayloadAttributes, error) {
|
|
if len(buf) < 60 {
|
|
return nil, fmt.Errorf("PayloadAttributes: buffer too short (%d < 60)", len(buf))
|
|
}
|
|
|
|
timestamp := binary.LittleEndian.Uint64(buf[0:8])
|
|
pa := &PayloadAttributes{
|
|
Timestamp: timestamp,
|
|
SuggestedFeeRecipient: common.BytesToAddress(buf[40:60]),
|
|
}
|
|
copy(pa.Random[:], buf[8:40])
|
|
|
|
if version == 1 {
|
|
return pa, nil
|
|
}
|
|
|
|
if len(buf) < 64 {
|
|
return nil, fmt.Errorf("PayloadAttributes V2+: buffer too short (%d < 64)", len(buf))
|
|
}
|
|
withdrawalsOffset := binary.LittleEndian.Uint32(buf[60:64])
|
|
|
|
if version >= 3 {
|
|
if len(buf) < 96 {
|
|
return nil, fmt.Errorf("PayloadAttributes V3: buffer too short (%d < 96)", len(buf))
|
|
}
|
|
root := common.BytesToHash(buf[64:96])
|
|
pa.BeaconRoot = &root
|
|
}
|
|
|
|
if withdrawalsOffset <= uint32(len(buf)) {
|
|
wdBuf := buf[withdrawalsOffset:]
|
|
if len(wdBuf) > 0 {
|
|
if len(wdBuf)%44 != 0 {
|
|
return nil, fmt.Errorf("PayloadAttributes: withdrawals buffer length %d not divisible by 44", len(wdBuf))
|
|
}
|
|
count := len(wdBuf) / 44
|
|
pa.Withdrawals = make([]*types.Withdrawal, count)
|
|
for i := 0; i < count; i++ {
|
|
off := i * 44
|
|
w := &types.Withdrawal{
|
|
Index: binary.LittleEndian.Uint64(wdBuf[off : off+8]),
|
|
Validator: binary.LittleEndian.Uint64(wdBuf[off+8 : off+16]),
|
|
Amount: binary.LittleEndian.Uint64(wdBuf[off+36 : off+44]),
|
|
}
|
|
copy(w.Address[:], wdBuf[off+16:off+36])
|
|
pa.Withdrawals[i] = w
|
|
}
|
|
} else {
|
|
pa.Withdrawals = []*types.Withdrawal{}
|
|
}
|
|
}
|
|
|
|
return pa, nil
|
|
}
|