beacon/engin: add unmarshalJSON for epe and bapl structs

This commit is contained in:
jonny rhea 2026-03-16 11:27:20 -05:00
parent 47b6d473f3
commit 5f909aa1d1
7 changed files with 526 additions and 84 deletions

View file

@ -16,6 +16,8 @@
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 {
@ -87,6 +89,39 @@ func (list BlobAndProofListV1) MarshalJSON() ([]byte, error) {
return buf, nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (list *BlobAndProofListV1) UnmarshalJSON(input []byte) error {
if isJSONNull(input) {
*list = nil
return nil
}
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.
@ -107,3 +142,40 @@ func (list BlobAndProofListV2) MarshalJSON() ([]byte, error) {
buf = append(buf, ']')
return buf, nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (list *BlobAndProofListV2) UnmarshalJSON(input []byte) error {
if isJSONNull(input) {
*list = nil
return nil
}
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

@ -122,6 +122,57 @@ func TestBlobAndProofListV1MarshalJSON(t *testing.T) {
}
}
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
@ -186,6 +237,57 @@ func TestBlobAndProofListV2MarshalJSON(t *testing.T) {
}
}
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

View file

@ -17,10 +17,9 @@
package engine
import (
"encoding/hex"
"encoding/json"
"errors"
"slices"
"math/big"
"github.com/ethereum/go-ethereum/common/hexutil"
)
@ -55,32 +54,26 @@ func marshalBlobsBundle(buf []byte, b *BlobsBundle) []byte {
return buf
}
// 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"...)
func unmarshalBlobsBundle(input []byte) (*BlobsBundle, error) {
if isJSONNull(input) {
return nil, nil
}
buf = append(buf, '[')
for i, item := range items {
if i > 0 {
buf = append(buf, ',')
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)
}
buf = writeHexBytes(buf, item)
return err
}); err != nil {
return nil, err
}
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
return &bundle, nil
}
// MarshalJSON implements json.Marshaler.
@ -175,3 +168,85 @@ func (e ExecutionPayloadEnvelope) MarshalJSON() ([]byte, error) {
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
}
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
}

View file

@ -214,6 +214,100 @@ 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

@ -0,0 +1,158 @@
// 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
}

View file

@ -1,53 +0,0 @@
// 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

@ -165,12 +165,6 @@ type BlobAndProofV2 struct {
// that avoids the overhead of encoding/json for large blob payloads.
type BlobAndProofListV2 []*BlobAndProofV2
// JSON type overrides for ExecutionPayloadEnvelope.
type executionPayloadEnvelopeMarshaling struct {
BlockValue *hexutil.Big
Requests []hexutil.Bytes
}
type PayloadStatusV1 struct {
Status string `json:"status"`
Witness *hexutil.Bytes `json:"witness,omitempty"`