diff --git a/beacon/engine/codec_bapl.go b/beacon/engine/codec_bapl.go index d07882710c..9db390d48a 100644 --- a/beacon/engine/codec_bapl.go +++ b/beacon/engine/codec_bapl.go @@ -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 } diff --git a/beacon/engine/codec_bapl_test.go b/beacon/engine/codec_bapl_test.go deleted file mode 100644 index 6a94c4d530..0000000000 --- a/beacon/engine/codec_bapl_test.go +++ /dev/null @@ -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 . - -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) - } - } - }) - } -} diff --git a/beacon/engine/codec_epe.go b/beacon/engine/codec_epe.go index 985a9485a9..98460e6ef0 100644 --- a/beacon/engine/codec_epe.go +++ b/beacon/engine/codec_epe.go @@ -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) + } + }) } diff --git a/beacon/engine/codec_epe_test.go b/beacon/engine/codec_epe_test.go index aa36d39bce..e4ed5b6578 100644 --- a/beacon/engine/codec_epe_test.go +++ b/beacon/engine/codec_epe_test.go @@ -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, diff --git a/beacon/engine/codec_helper.go b/beacon/engine/codec_helper.go deleted file mode 100644 index 5672cd3f74..0000000000 --- a/beacon/engine/codec_helper.go +++ /dev/null @@ -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 . - -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 -} diff --git a/beacon/engine/gen_epe.go b/beacon/engine/gen_epe.go new file mode 100644 index 0000000000..a125daa030 --- /dev/null +++ b/beacon/engine/gen_epe.go @@ -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 +} diff --git a/beacon/engine/types.go b/beacon/engine/types.go index cfd0dba395..8824e40f5e 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -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. // diff --git a/go.mod b/go.mod index 17897a62c0..2cd1c5bcaf 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index bad8a44cfd..9fa5364940 100644 --- a/go.sum +++ b/go.sum @@ -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=