beacon/engine: improve hand-written JSON marshaling

- remove unmarshal methods (for now)
- use jsonw library instead of string concatenation
This commit is contained in:
Felix Lange 2026-05-19 16:58:15 +02:00
parent bf3464a34c
commit 469cd8d781
9 changed files with 153 additions and 1056 deletions

View file

@ -16,166 +16,54 @@
package engine
import "encoding/json"
// estimateBlobAndProofV1Size returns a rough estimate of the JSON size for a BlobAndProofV1.
func estimateBlobAndProofV1Size(item *BlobAndProofV1) int {
if item == nil {
return 4
}
return len(item.Blob)*2 + len(item.Proof)*2 + 30
}
// marshalBlobAndProofV1 writes a BlobAndProofV1 as JSON and appends it to buf.
func marshalBlobAndProofV1(buf []byte, item *BlobAndProofV1) []byte {
if item == nil {
return append(buf, "null"...)
}
buf = append(buf, `{"blob":`...)
buf = writeHexBytes(buf, item.Blob)
buf = append(buf, `,"proof":`...)
buf = writeHexBytes(buf, item.Proof)
buf = append(buf, '}')
return buf
}
// estimateBlobAndProofV2Size returns a rough estimate of the JSON size for a BlobAndProofV2.
func estimateBlobAndProofV2Size(item *BlobAndProofV2) int {
if item == nil {
return 4
}
size := len(item.Blob)*2 + 30
for _, proof := range item.CellProofs {
size += len(proof)*2 + 6
}
return size
}
// marshalBlobAndProofV2 writes a BlobAndProofV2 as JSON and appends it to buf.
func marshalBlobAndProofV2(buf []byte, item *BlobAndProofV2) []byte {
if item == nil {
return append(buf, "null"...)
}
buf = append(buf, `{"blob":`...)
buf = writeHexBytes(buf, item.Blob)
buf = append(buf, `,"proofs":`...)
buf = marshalHexBytesArray(buf, item.CellProofs)
buf = append(buf, '}')
return buf
}
import (
jsonw "github.com/fjl/jsonw"
)
// MarshalJSON implements json.Marshaler.
func (list BlobAndProofListV1) MarshalJSON() ([]byte, error) {
// Estimate buffer size.
size := 2
for _, item := range list {
size += estimateBlobAndProofV1Size(item) + 1
}
buf := make([]byte, 0, size)
// Write the array elements to the buffer.
buf = append(buf, '[')
for i, item := range list {
if i > 0 {
buf = append(buf, ',')
var b jsonw.Buffer
b.Array(func() {
for _, item := range list {
marshalBlobAndProofV1(&b, item)
}
buf = marshalBlobAndProofV1(buf, item)
}
buf = append(buf, ']')
return buf, nil
})
return b.Output(), nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (list *BlobAndProofListV1) UnmarshalJSON(input []byte) error {
if isJSONNull(input) {
*list = nil
return nil
func marshalBlobAndProofV1(b *jsonw.Buffer, item *BlobAndProofV1) {
if item == nil {
b.Null()
} else {
b.Object(func() {
b.Key("blob")
b.HexBytes(item.Blob)
b.Key("proof")
b.HexBytes(item.Proof)
})
}
items := make(BlobAndProofListV1, 0)
if err := decodeJSONArray(input, func(value json.RawMessage) error {
if isJSONNull(value) {
items = append(items, nil)
return nil
}
item := new(BlobAndProofV1)
if err := decodeJSONObject(value, func(key string, value json.RawMessage) error {
switch key {
case "blob":
return item.Blob.UnmarshalJSON(value)
case "proof":
return item.Proof.UnmarshalJSON(value)
}
return nil
}); err != nil {
return err
}
items = append(items, item)
return nil
}); err != nil {
return err
}
*list = items
return nil
}
// MarshalJSON implements json.Marshaler.
func (list BlobAndProofListV2) MarshalJSON() ([]byte, error) {
// Estimate buffer size.
size := 2
for _, item := range list {
size += estimateBlobAndProofV2Size(item) + 1
}
buf := make([]byte, 0, size)
// Write the array elements to the buffer.
buf = append(buf, '[')
for i, item := range list {
if i > 0 {
buf = append(buf, ',')
var b jsonw.Buffer
b.Array(func() {
for _, item := range list {
marshalBlobAndProofV2(&b, item)
}
buf = marshalBlobAndProofV2(buf, item)
}
buf = append(buf, ']')
return buf, nil
})
return b.Output(), nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (list *BlobAndProofListV2) UnmarshalJSON(input []byte) error {
if isJSONNull(input) {
*list = nil
return nil
func marshalBlobAndProofV2(b *jsonw.Buffer, item *BlobAndProofV2) {
if item == nil {
b.Null()
} else {
b.Object(func() {
b.Key("blob")
b.HexBytes(item.Blob)
b.Key("proofs")
appendHexBytesArray(b, item.CellProofs)
})
}
items := make(BlobAndProofListV2, 0)
if err := decodeJSONArray(input, func(value json.RawMessage) error {
if isJSONNull(value) {
items = append(items, nil)
return nil
}
item := new(BlobAndProofV2)
if err := decodeJSONObject(value, func(key string, value json.RawMessage) error {
switch key {
case "blob":
return item.Blob.UnmarshalJSON(value)
case "proofs":
proofs, err := unmarshalHexBytesArray(value)
if err != nil {
return err
}
item.CellProofs = proofs
}
return nil
}); err != nil {
return err
}
items = append(items, item)
return nil
}); err != nil {
return err
}
*list = items
return nil
}

View file

@ -1,329 +0,0 @@
// 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 engine
import (
"bytes"
"encoding/json"
"reflect"
"testing"
"github.com/ethereum/go-ethereum/common/hexutil"
)
// canonicalBlobAndProofV1 is a reference type for BlobAndProofV1 that uses
// standard json.Marshal (no custom MarshalJSON).
type canonicalBlobAndProofV1 struct {
Blob hexutil.Bytes `json:"blob"`
Proof hexutil.Bytes `json:"proof"`
}
// canonicalBlobAndProofV2 is a reference type for BlobAndProofV2 that uses
// standard json.Marshal (no custom MarshalJSON).
type canonicalBlobAndProofV2 struct {
Blob hexutil.Bytes `json:"blob"`
CellProofs []hexutil.Bytes `json:"proofs"`
}
func toCanonicalBlobAndProofListV1(list BlobAndProofListV1) []*canonicalBlobAndProofV1 {
canonical := make([]*canonicalBlobAndProofV1, len(list))
for i, item := range list {
if item == nil {
continue
}
canonical[i] = &canonicalBlobAndProofV1{
Blob: item.Blob,
Proof: item.Proof,
}
}
return canonical
}
func toCanonicalBlobAndProofListV2(list BlobAndProofListV2) []*canonicalBlobAndProofV2 {
canonical := make([]*canonicalBlobAndProofV2, len(list))
for i, item := range list {
if item == nil {
continue
}
canonical[i] = &canonicalBlobAndProofV2{
Blob: item.Blob,
CellProofs: item.CellProofs,
}
}
return canonical
}
func TestBlobAndProofListV1MarshalJSON(t *testing.T) {
tests := []struct {
name string
list BlobAndProofListV1
}{
{
name: "multiple items",
list: BlobAndProofListV1{
{
Blob: hexutil.Bytes{0x01, 0x02},
Proof: hexutil.Bytes{0x03, 0x04},
},
{
Blob: hexutil.Bytes{},
Proof: hexutil.Bytes{0x05},
},
},
},
{
name: "nil item",
list: BlobAndProofListV1{
nil,
{
Blob: hexutil.Bytes{0xaa},
Proof: hexutil.Bytes{0xbb},
},
},
},
{
name: "empty list",
list: BlobAndProofListV1{},
},
{
name: "nil list",
list: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.list.MarshalJSON()
if err != nil {
t.Fatalf("MarshalJSON error: %v", err)
}
want, err := json.Marshal(toCanonicalBlobAndProofListV1(tt.list))
if err != nil {
t.Fatalf("canonical marshal error: %v", err)
}
if !bytes.Equal(compactJSON(got), compactJSON(want)) {
t.Errorf("JSON mismatch\ngot: %s\nwant: %s", got, want)
}
})
}
}
func TestBlobAndProofListV1UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
json string
want BlobAndProofListV1
}{
{
name: "multiple items",
json: `[{"blob":"0x0102","proof":"0x0304"},{"blob":"0x","proof":"0x05"}]`,
want: BlobAndProofListV1{
{
Blob: hexutil.Bytes{0x01, 0x02},
Proof: hexutil.Bytes{0x03, 0x04},
},
{
Blob: hexutil.Bytes{},
Proof: hexutil.Bytes{0x05},
},
},
},
{
name: "nil item",
json: `[null,{"blob":"0xaa","proof":"0xbb"}]`,
want: BlobAndProofListV1{
nil,
{
Blob: hexutil.Bytes{0xaa},
Proof: hexutil.Bytes{0xbb},
},
},
},
{
name: "null list",
json: `null`,
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got BlobAndProofListV1
if err := got.UnmarshalJSON([]byte(tt.json)); err != nil {
t.Fatalf("UnmarshalJSON error: %v", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("decoded mismatch\ngot: %#v\nwant: %#v", got, tt.want)
}
})
}
}
func TestBlobAndProofListV2MarshalJSON(t *testing.T) {
tests := []struct {
name string
list BlobAndProofListV2
}{
{
name: "multiple items",
list: BlobAndProofListV2{
{
Blob: hexutil.Bytes{0x01, 0x02},
CellProofs: []hexutil.Bytes{{0x03, 0x04}, {0x05}},
},
{
Blob: hexutil.Bytes{},
CellProofs: []hexutil.Bytes{},
},
},
},
{
name: "nil item",
list: BlobAndProofListV2{
nil,
{
Blob: hexutil.Bytes{0xaa},
CellProofs: []hexutil.Bytes{{0xbb}},
},
},
},
{
name: "nil proofs slice",
list: BlobAndProofListV2{
{
Blob: hexutil.Bytes{0xcc},
CellProofs: nil,
},
},
},
{
name: "empty list",
list: BlobAndProofListV2{},
},
{
name: "nil list",
list: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.list.MarshalJSON()
if err != nil {
t.Fatalf("MarshalJSON error: %v", err)
}
want, err := json.Marshal(toCanonicalBlobAndProofListV2(tt.list))
if err != nil {
t.Fatalf("canonical marshal error: %v", err)
}
if !bytes.Equal(compactJSON(got), compactJSON(want)) {
t.Errorf("JSON mismatch\ngot: %s\nwant: %s", got, want)
}
})
}
}
func TestBlobAndProofListV2UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
json string
want BlobAndProofListV2
}{
{
name: "multiple items",
json: `[{"blob":"0x0102","proofs":["0x0304","0x05"]},{"blob":"0x","proofs":[]}]`,
want: BlobAndProofListV2{
{
Blob: hexutil.Bytes{0x01, 0x02},
CellProofs: []hexutil.Bytes{{0x03, 0x04}, {0x05}},
},
{
Blob: hexutil.Bytes{},
CellProofs: []hexutil.Bytes{},
},
},
},
{
name: "nil item and nil proofs",
json: `[null,{"blob":"0xcc","proofs":null}]`,
want: BlobAndProofListV2{
nil,
{
Blob: hexutil.Bytes{0xcc},
CellProofs: nil,
},
},
},
{
name: "null list",
json: `null`,
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got BlobAndProofListV2
if err := got.UnmarshalJSON([]byte(tt.json)); err != nil {
t.Fatalf("UnmarshalJSON error: %v", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("decoded mismatch\ngot: %#v\nwant: %#v", got, tt.want)
}
})
}
}
func TestBlobAndProofFieldCoverage(t *testing.T) {
tests := []struct {
name string
typ reflect.Type
expected []string
}{
{
name: "BlobAndProofV1",
typ: reflect.TypeOf(BlobAndProofV1{}),
expected: []string{
"Blob",
"Proof",
},
},
{
name: "BlobAndProofV2",
typ: reflect.TypeOf(BlobAndProofV2{}),
expected: []string{
"Blob",
"CellProofs",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.typ.NumField() != len(tt.expected) {
t.Fatalf("%s has %d fields, expected %d; update marshal_bap_list.go",
tt.name, tt.typ.NumField(), len(tt.expected))
}
for i, name := range tt.expected {
if tt.typ.Field(i).Name != name {
t.Errorf("field %d: got %q, want %q; update marshal_bap_list.go",
i, tt.typ.Field(i).Name, name)
}
}
})
}
}

View file

@ -19,61 +19,25 @@ package engine
import (
"encoding/json"
"errors"
"math/big"
"github.com/ethereum/go-ethereum/common/hexutil"
jsonw "github.com/fjl/jsonw"
)
// estimateBlobsBundleSize returns a rough estimate of the JSON size for a BlobsBundle.
func estimateBlobsBundleSize(b *BlobsBundle) int {
size := 80 // JSON structure overhead
for _, blob := range b.Blobs {
size += len(blob)*2 + 6
}
for _, c := range b.Commitments {
size += len(c)*2 + 6
}
for _, p := range b.Proofs {
size += len(p)*2 + 6
}
return size
}
// marshalBlobsBundle writes BlobsBundle as JSON and appends it to buf.
func marshalBlobsBundle(buf []byte, b *BlobsBundle) []byte {
buf = append(buf, `{"commitments":`...)
buf = marshalHexBytesArray(buf, b.Commitments)
buf = append(buf, `,"proofs":`...)
buf = marshalHexBytesArray(buf, b.Proofs)
buf = append(buf, `,"blobs":`...)
buf = marshalHexBytesArray(buf, b.Blobs)
buf = append(buf, '}')
return buf
}
func unmarshalBlobsBundle(input []byte) (*BlobsBundle, error) {
if isJSONNull(input) {
return nil, nil
func marshalBlobsBundle(b *jsonw.Buffer, bundle *BlobsBundle) {
if bundle == nil {
b.Null()
return
}
var bundle BlobsBundle
if err := decodeJSONObject(input, func(key string, value json.RawMessage) error {
var err error
switch key {
case "commitments":
bundle.Commitments, err = unmarshalHexBytesArray(value)
case "proofs":
bundle.Proofs, err = unmarshalHexBytesArray(value)
case "blobs":
bundle.Blobs, err = unmarshalHexBytesArray(value)
}
return err
}); err != nil {
return nil, err
}
return &bundle, nil
b.Object(func() {
b.Key("commitments")
appendHexBytesArray(b, bundle.Commitments)
b.Key("proofs")
appendHexBytesArray(b, bundle.Proofs)
b.Key("blobs")
appendHexBytesArray(b, bundle.Blobs)
})
}
// MarshalJSON implements json.Marshaler.
@ -82,38 +46,12 @@ func (e ExecutionPayloadEnvelope) MarshalJSON() ([]byte, error) {
return nil, errors.New("missing required field 'executionPayload' for ExecutionPayloadEnvelope")
}
// Marshal the execution payload using its gencodec MarshalJSON.
// Pre-marshal the execution payload using its gencodec MarshalJSON.
payload, err := e.ExecutionPayload.MarshalJSON()
if err != nil {
return nil, err
}
// Marshal the block value.
blockValue, err := json.Marshal((*hexutil.Big)(e.BlockValue))
if err != nil {
return nil, err
}
// Marshal the execution requests.
var requests []byte
if e.Requests != nil {
hexRequests := make([]hexutil.Bytes, len(e.Requests))
for i, req := range e.Requests {
hexRequests[i] = req
}
requests, err = json.Marshal(hexRequests)
if err != nil {
return nil, err
}
}
// Marshal the override.
override, err := json.Marshal(e.Override)
if err != nil {
return nil, err
}
// Marshal the witness.
// Pre-marshal the witness.
var witness []byte
if e.Witness != nil {
witness, err = json.Marshal(e.Witness)
@ -122,131 +60,40 @@ func (e ExecutionPayloadEnvelope) MarshalJSON() ([]byte, error) {
}
}
// Estimate buffer size.
size := len(payload) + len(blockValue) + len(requests) + len(override) + len(witness)
if e.BlobsBundle != nil {
size += estimateBlobsBundleSize(e.BlobsBundle)
}
size += 256 // JSON bloat (keys, braces, commas, etc. and room for growth)
buf := make([]byte, 0, size)
// Write the execution payload to the buffer
buf = append(buf, `{"executionPayload":`...)
buf = append(buf, payload...)
// Write the block value to the buffer
buf = append(buf, `,"blockValue":`...)
buf = append(buf, blockValue...)
// Write the blobs bundle to the buffer
buf = append(buf, `,"blobsBundle":`...)
if e.BlobsBundle != nil {
buf = marshalBlobsBundle(buf, e.BlobsBundle)
} else {
buf = append(buf, "null"...)
}
// Write the execution requests to the buffer
buf = append(buf, `,"executionRequests":`...)
if requests != nil {
buf = append(buf, requests...)
} else {
buf = append(buf, "null"...)
}
// Write the override to the buffer
buf = append(buf, `,"shouldOverrideBuilder":`...)
buf = append(buf, override...)
// Write the witness to the buffer if present
if witness != nil {
buf = append(buf, `,"witness":`...)
buf = append(buf, witness...)
}
// Close the envelope
buf = append(buf, '}')
return buf, nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (e *ExecutionPayloadEnvelope) UnmarshalJSON(input []byte) error {
var (
payloadSeen bool
blockValueSeen bool
)
*e = ExecutionPayloadEnvelope{}
if err := decodeJSONObject(input, func(key string, value json.RawMessage) error {
switch key {
case "executionPayload":
payloadSeen = true
if isJSONNull(value) {
e.ExecutionPayload = nil
return nil
}
var payload ExecutableData
if err := payload.UnmarshalJSON(value); err != nil {
return err
}
e.ExecutionPayload = &payload
case "blockValue":
blockValueSeen = true
if isJSONNull(value) {
e.BlockValue = nil
return nil
}
var blockValue hexutil.Big
if err := blockValue.UnmarshalJSON(value); err != nil {
return err
}
e.BlockValue = (*big.Int)(&blockValue)
case "blobsBundle":
bundle, err := unmarshalBlobsBundle(value)
if err != nil {
return err
}
e.BlobsBundle = bundle
case "executionRequests":
requests, err := unmarshalHexBytesArray(value)
if err != nil {
return err
}
if requests == nil {
e.Requests = nil
return nil
}
e.Requests = make([][]byte, len(requests))
for i, req := range requests {
e.Requests[i] = req
}
case "shouldOverrideBuilder":
if isJSONNull(value) {
e.Override = false
return nil
}
if err := json.Unmarshal(value, &e.Override); err != nil {
return err
}
case "witness":
if isJSONNull(value) {
e.Witness = nil
return nil
}
var witness hexutil.Bytes
if err := witness.UnmarshalJSON(value); err != nil {
return err
}
e.Witness = &witness
var b jsonw.Buffer
b.Object(func() {
b.Key("executionPayload")
b.RawValue(payload)
b.Key("blockValue")
b.MustValue((*hexutil.Big)(e.BlockValue))
b.Key("blobsBundle")
marshalBlobsBundle(&b, e.BlobsBundle)
b.Key("executionRequests")
if e.Requests == nil {
b.Null()
} else {
b.Array(func() {
for _, r := range e.Requests {
b.HexBytes(r)
}
})
}
return nil
}); err != nil {
return err
}
if !payloadSeen || e.ExecutionPayload == nil {
return errors.New("missing required field 'executionPayload' for ExecutionPayloadEnvelope")
}
if !blockValueSeen || e.BlockValue == nil {
return errors.New("missing required field 'blockValue' for ExecutionPayloadEnvelope")
}
return nil
b.Key("shouldOverrideBuilder")
b.Bool(e.Override)
if e.Witness != nil {
b.Key("witness")
b.RawValue(witness)
}
})
return b.Output(), nil
}
func appendHexBytesArray[T ~[]byte](b *jsonw.Buffer, slice []T) {
b.Array(func() {
for _, elem := range slice {
b.HexBytes(elem)
}
})
}

View file

@ -27,42 +27,6 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
)
// canonicalEnvelope is a reference type for ExecutionPayloadEnvelope that uses
// standard json.Marshal (no custom MarshalJSON). It mirrors the gencodec type
// overrides so its output matches what the generated code would produce.
type canonicalEnvelope struct {
ExecutionPayload *ExecutableData `json:"executionPayload"`
BlockValue *hexutil.Big `json:"blockValue"`
BlobsBundle *BlobsBundle `json:"blobsBundle"`
Requests []hexutil.Bytes `json:"executionRequests"`
Override bool `json:"shouldOverrideBuilder"`
Witness *hexutil.Bytes `json:"witness,omitempty"`
}
func toCanonical(e *ExecutionPayloadEnvelope) *canonicalEnvelope {
c := &canonicalEnvelope{
ExecutionPayload: e.ExecutionPayload,
BlockValue: (*hexutil.Big)(e.BlockValue),
BlobsBundle: e.BlobsBundle,
Override: e.Override,
Witness: e.Witness,
}
if e.Requests != nil {
c.Requests = make([]hexutil.Bytes, len(e.Requests))
for i, r := range e.Requests {
c.Requests[i] = r
}
}
return c
}
// compactJSON returns the compacted form of a JSON byte slice.
func compactJSON(data []byte) []byte {
var buf bytes.Buffer
json.Compact(&buf, data)
return buf.Bytes()
}
func makeTestPayload() *ExecutableData {
return &ExecutableData{
ParentHash: common.HexToHash("0x01"),
@ -82,93 +46,6 @@ func makeTestPayload() *ExecutableData {
}
}
func TestMarshalJSON(t *testing.T) {
witness := hexutil.Bytes{0xde, 0xad}
tests := []struct {
name string
env ExecutionPayloadEnvelope
}{
{
name: "full envelope with blobs",
env: ExecutionPayloadEnvelope{
ExecutionPayload: makeTestPayload(),
BlockValue: big.NewInt(12345),
BlobsBundle: &BlobsBundle{
Commitments: []hexutil.Bytes{{0x01, 0x02}},
Proofs: []hexutil.Bytes{{0x03, 0x04}},
Blobs: []hexutil.Bytes{{0x05, 0x06}},
},
Requests: [][]byte{{0xaa}, {0xbb, 0xcc}},
Override: true,
Witness: &witness,
},
},
{
name: "nil BlobsBundle",
env: ExecutionPayloadEnvelope{
ExecutionPayload: makeTestPayload(),
BlockValue: big.NewInt(0),
},
},
{
name: "nil Requests",
env: ExecutionPayloadEnvelope{
ExecutionPayload: makeTestPayload(),
BlockValue: big.NewInt(1),
Requests: nil,
},
},
{
name: "empty Requests",
env: ExecutionPayloadEnvelope{
ExecutionPayload: makeTestPayload(),
BlockValue: big.NewInt(1),
Requests: [][]byte{},
},
},
{
name: "nil Witness",
env: ExecutionPayloadEnvelope{
ExecutionPayload: makeTestPayload(),
BlockValue: big.NewInt(1),
Witness: nil,
},
},
{
name: "empty blobs bundle arrays",
env: ExecutionPayloadEnvelope{
ExecutionPayload: makeTestPayload(),
BlockValue: big.NewInt(1),
BlobsBundle: &BlobsBundle{
Commitments: []hexutil.Bytes{},
Proofs: []hexutil.Bytes{},
Blobs: []hexutil.Bytes{},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Hand-rolled marshal.
got, err := tt.env.MarshalJSON()
if err != nil {
t.Fatalf("MarshalJSON error: %v", err)
}
// Canonical marshal via reference struct.
want, err := json.Marshal(toCanonical(&tt.env))
if err != nil {
t.Fatalf("canonical marshal error: %v", err)
}
if !bytes.Equal(compactJSON(got), compactJSON(want)) {
t.Errorf("JSON mismatch\ngot: %s\nwant: %s", got, want)
}
})
}
}
func TestMarshalJSONRoundtrip(t *testing.T) {
witness := hexutil.Bytes{0xde, 0xad}
original := ExecutionPayloadEnvelope{
@ -190,7 +67,7 @@ func TestMarshalJSONRoundtrip(t *testing.T) {
}
var decoded ExecutionPayloadEnvelope
if err := decoded.UnmarshalJSON(data); err != nil {
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("UnmarshalJSON error: %v", err)
}
@ -214,100 +91,6 @@ func TestMarshalJSONRoundtrip(t *testing.T) {
}
}
func TestUnmarshalJSON(t *testing.T) {
witness := hexutil.Bytes{0xde, 0xad}
tests := []struct {
name string
env ExecutionPayloadEnvelope
}{
{
name: "full envelope with blobs",
env: ExecutionPayloadEnvelope{
ExecutionPayload: makeTestPayload(),
BlockValue: big.NewInt(12345),
BlobsBundle: &BlobsBundle{
Commitments: []hexutil.Bytes{{0x01, 0x02}},
Proofs: []hexutil.Bytes{{0x03, 0x04}},
Blobs: []hexutil.Bytes{{0x05, 0x06}},
},
Requests: [][]byte{{0xaa}, {0xbb, 0xcc}},
Override: true,
Witness: &witness,
},
},
{
name: "null optional fields",
env: ExecutionPayloadEnvelope{
ExecutionPayload: makeTestPayload(),
BlockValue: big.NewInt(1),
BlobsBundle: nil,
Requests: nil,
Override: false,
Witness: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
input, err := json.Marshal(toCanonical(&tt.env))
if err != nil {
t.Fatalf("canonical marshal error: %v", err)
}
var got ExecutionPayloadEnvelope
if err := got.UnmarshalJSON(input); err != nil {
t.Fatalf("UnmarshalJSON error: %v", err)
}
gotJSON, err := json.Marshal(toCanonical(&got))
if err != nil {
t.Fatalf("canonical marshal after unmarshal error: %v", err)
}
if !bytes.Equal(compactJSON(gotJSON), compactJSON(input)) {
t.Errorf("JSON mismatch after unmarshal\ngot: %s\nwant: %s", gotJSON, input)
}
})
}
}
func TestUnmarshalJSONMissingRequiredFields(t *testing.T) {
tests := []struct {
name string
json func(t *testing.T) []byte
}{
{
name: "missing executionPayload",
json: func(t *testing.T) []byte {
return []byte(`{"blockValue":"0x1"}`)
},
},
{
name: "missing blockValue",
json: func(t *testing.T) []byte {
input, err := json.Marshal(struct {
ExecutionPayload *ExecutableData `json:"executionPayload"`
}{
ExecutionPayload: makeTestPayload(),
})
if err != nil {
t.Fatalf("marshal input: %v", err)
}
return input
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var env ExecutionPayloadEnvelope
if err := env.UnmarshalJSON(tt.json(t)); err == nil {
t.Fatal("expected error")
}
})
}
}
func TestMarshalJSONNilPayload(t *testing.T) {
env := ExecutionPayloadEnvelope{
ExecutionPayload: nil,

View file

@ -1,158 +0,0 @@
// 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 engine
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"slices"
"github.com/ethereum/go-ethereum/common/hexutil"
)
// marshalHexBytesArray writes an array of hex-encoded byte slices to buf.
// A nil slice is written as "null" to match encoding/json semantics.
func marshalHexBytesArray(buf []byte, items []hexutil.Bytes) []byte {
if items == nil {
return append(buf, "null"...)
}
buf = append(buf, '[')
for i, item := range items {
if i > 0 {
buf = append(buf, ',')
}
buf = writeHexBytes(buf, item)
}
buf = append(buf, ']')
return buf
}
// writeHexBytes writes a hex-encoded byte slice as a JSON string ("0x...") to buf.
func writeHexBytes(buf []byte, data []byte) []byte {
buf = append(buf, '"', '0', 'x')
buf = slices.Grow(buf, len(data)*2+1)
cur := len(buf)
buf = buf[:cur+len(data)*2]
hex.Encode(buf[cur:], data)
buf = append(buf, '"')
return buf
}
func decodeJSONObject(input []byte, fn func(key string, value json.RawMessage) error) error {
dec := json.NewDecoder(bytes.NewReader(input))
tok, err := dec.Token()
if err != nil {
return err
}
delim, ok := tok.(json.Delim)
if !ok || delim != '{' {
return fmt.Errorf("expected JSON object")
}
for dec.More() {
tok, err := dec.Token()
if err != nil {
return err
}
key, ok := tok.(string)
if !ok {
return fmt.Errorf("expected JSON object key")
}
var value json.RawMessage
if err := dec.Decode(&value); err != nil {
return err
}
if err := fn(key, value); err != nil {
return err
}
}
tok, err = dec.Token()
if err != nil {
return err
}
delim, ok = tok.(json.Delim)
if !ok || delim != '}' {
return fmt.Errorf("expected end of JSON object")
}
if _, err := dec.Token(); err != io.EOF {
if err == nil {
return fmt.Errorf("unexpected trailing data")
}
return err
}
return nil
}
func decodeJSONArray(input []byte, fn func(value json.RawMessage) error) error {
dec := json.NewDecoder(bytes.NewReader(input))
tok, err := dec.Token()
if err != nil {
return err
}
delim, ok := tok.(json.Delim)
if !ok || delim != '[' {
return fmt.Errorf("expected JSON array")
}
for dec.More() {
var value json.RawMessage
if err := dec.Decode(&value); err != nil {
return err
}
if err := fn(value); err != nil {
return err
}
}
tok, err = dec.Token()
if err != nil {
return err
}
delim, ok = tok.(json.Delim)
if !ok || delim != ']' {
return fmt.Errorf("expected end of JSON array")
}
if _, err := dec.Token(); err != io.EOF {
if err == nil {
return fmt.Errorf("unexpected trailing data")
}
return err
}
return nil
}
func isJSONNull(input []byte) bool {
return bytes.Equal(bytes.TrimSpace(input), []byte("null"))
}
func unmarshalHexBytesArray(input []byte) ([]hexutil.Bytes, error) {
if isJSONNull(input) {
return nil, nil
}
items := make([]hexutil.Bytes, 0)
if err := decodeJSONArray(input, func(value json.RawMessage) error {
var item hexutil.Bytes
if err := item.UnmarshalJSON(value); err != nil {
return err
}
items = append(items, item)
return nil
}); err != nil {
return nil, err
}
return items, nil
}

53
beacon/engine/gen_epe.go Normal file
View file

@ -0,0 +1,53 @@
// Code generated by github.com/fjl/gencodec. DO NOT EDIT.
package engine
import (
"encoding/json"
"errors"
"math/big"
"github.com/ethereum/go-ethereum/common/hexutil"
)
var _ = (*executionPayloadEnvelopeMarshaling)(nil)
// UnmarshalJSON unmarshals from JSON.
func (e *ExecutionPayloadEnvelope) UnmarshalJSON(input []byte) error {
type ExecutionPayloadEnvelope struct {
ExecutionPayload *ExecutableData `json:"executionPayload" gencodec:"required"`
BlockValue *hexutil.Big `json:"blockValue" gencodec:"required"`
BlobsBundle *BlobsBundle `json:"blobsBundle"`
Requests []hexutil.Bytes `json:"executionRequests"`
Override *bool `json:"shouldOverrideBuilder"`
Witness *hexutil.Bytes `json:"witness,omitempty"`
}
var dec ExecutionPayloadEnvelope
if err := json.Unmarshal(input, &dec); err != nil {
return err
}
if dec.ExecutionPayload == nil {
return errors.New("missing required field 'executionPayload' for ExecutionPayloadEnvelope")
}
e.ExecutionPayload = dec.ExecutionPayload
if dec.BlockValue == nil {
return errors.New("missing required field 'blockValue' for ExecutionPayloadEnvelope")
}
e.BlockValue = (*big.Int)(dec.BlockValue)
if dec.BlobsBundle != nil {
e.BlobsBundle = dec.BlobsBundle
}
if dec.Requests != nil {
e.Requests = make([][]byte, len(dec.Requests))
for k, v := range dec.Requests {
e.Requests[k] = v
}
}
if dec.Override != nil {
e.Override = *dec.Override
}
if dec.Witness != nil {
e.Witness = dec.Witness
}
return nil
}

View file

@ -125,7 +125,7 @@ type StatelessPayloadStatusV1 struct {
ValidationError *string `json:"validationError"`
}
//go:generate go run github.com/fjl/gencodec -type ExecutionPayloadEnvelope -field-override executionPayloadEnvelopeMarshaling -out gen_epe.go
//go:generate go run github.com/fjl/gencodec -enc=false -type ExecutionPayloadEnvelope -field-override executionPayloadEnvelopeMarshaling -out gen_epe.go
type ExecutionPayloadEnvelope struct {
ExecutionPayload *ExecutableData `json:"executionPayload" gencodec:"required"`
@ -136,6 +136,12 @@ type ExecutionPayloadEnvelope struct {
Witness *hexutil.Bytes `json:"witness,omitempty"`
}
// JSON type overrides for ExecutionPayloadEnvelope.
type executionPayloadEnvelopeMarshaling struct {
BlockValue *hexutil.Big
Requests []hexutil.Bytes
}
// BlobsBundle includes the marshalled sidecar data. Note this structure is
// shared by BlobsBundleV1 and BlobsBundleV2 for the sake of simplicity.
//

3
go.mod
View file

@ -83,6 +83,7 @@ require (
require (
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/fjl/jsonw v0.0.0-20260519133611-b234a3a62a01 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
@ -121,7 +122,7 @@ require (
github.com/deepmap/oapi-codegen v1.6.0 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/emicklei/dot v1.6.2 // indirect
github.com/fjl/gencodec v0.1.0 // indirect
github.com/fjl/gencodec v0.1.2 // indirect
github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect
github.com/getsentry/sentry-go v0.27.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect

6
go.sum
View file

@ -125,6 +125,12 @@ github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeD
github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
github.com/fjl/gencodec v0.1.0 h1:B3K0xPfc52cw52BBgUbSPxYo+HlLfAgWMVKRWXUXBcs=
github.com/fjl/gencodec v0.1.0/go.mod h1:Um1dFHPONZGTHog1qD1NaWjXJW/SPB38wPv0O8uZ2fI=
github.com/fjl/gencodec v0.1.2 h1:nf+MMsmuii5ZQMbS6/xjZoe5LRkN0415FOJOSwmnuW8=
github.com/fjl/gencodec v0.1.2/go.mod h1:chDHL3wKXuBgauP8x3XNZkl5EIAR5SoCTmmmDTZRzmw=
github.com/fjl/jsonw v0.0.0-20260518201611-f2cd7df7ef66 h1:B+iPMRxXE3dbWXwQX6Un0MLOGGXXjj04gvWYD3pDrQE=
github.com/fjl/jsonw v0.0.0-20260518201611-f2cd7df7ef66/go.mod h1:2KMLevM6FXEJnfhtk7naXu9vZdVfOma1GlnGdPRlumU=
github.com/fjl/jsonw v0.0.0-20260519133611-b234a3a62a01 h1:2fvSvrrQMWs3l3MY1Ot4cPzv3Iww17ha1KJwjymX+Ks=
github.com/fjl/jsonw v0.0.0-20260519133611-b234a3a62a01/go.mod h1:2KMLevM6FXEJnfhtk7naXu9vZdVfOma1GlnGdPRlumU=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=