diff --git a/beacon/engine/premarshal.go b/beacon/engine/premarshal.go new file mode 100644 index 0000000000..d5f62ef35d --- /dev/null +++ b/beacon/engine/premarshal.go @@ -0,0 +1,173 @@ +// 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 ( + "encoding/hex" + "encoding/json" + "slices" + + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// 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 +} + +// marshalHexBytesArray writes an array of hex-encoded byte slices to buf. +func marshalHexBytesArray(buf []byte, items []hexutil.Bytes) []byte { + 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. +// NOTE: This function avoids allocations by pre-allocating the buffer space needed; +// otherwise, we would use hexutil.Encode() and append the result to the buffer. +// hexutil.Encode() uses 64% more memory than writing to buffer directly. +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 +} + +// PremarshaledJSON implements rpc.JSONPremarshaled. It returns pre-serialized +// JSON by delegating small fields to their existing MarshalJSON methods and +// hand-rolling only the BlobsBundle. +func (e ExecutionPayloadEnvelope) PremarshaledJSON() ([]byte, error) { + // 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. + var witness []byte + if e.Witness != nil { + witness, err = json.Marshal(e.Witness) + if err != nil { + return nil, err + } + } + + // Estimate buffer size. + size := len(payload) + len(blockValue) + len(requests) + len(override) + len(witness) + if e.BlobsBundle != nil { + size += estimateBlobsBundleSize(e.BlobsBundle) + } + size += 128 // JSON bloat (keys, braces, commas, etc.) + 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 +} diff --git a/rpc/json.go b/rpc/json.go index fcd801fc95..59d14e8f15 100644 --- a/rpc/json.go +++ b/rpc/json.go @@ -113,8 +113,23 @@ func (msg *jsonrpcMessage) errorResponse(err error) *jsonrpcMessage { return resp } +// JSONPremarshaled may be implemented by RPC return types that provide +// pre-serialized JSON. This bypasses json.Marshal and its re-validation +// pass. Implementors must ensure the returned bytes are valid JSON. +type JSONPremarshaled interface { + PremarshaledJSON() ([]byte, error) +} + func (msg *jsonrpcMessage) response(result interface{}) *jsonrpcMessage { - enc, err := json.Marshal(result) + var ( + enc []byte + err error + ) + if pm, ok := result.(JSONPremarshaled); ok { + enc, err = pm.PremarshaledJSON() + } else { + enc, err = json.Marshal(result) + } if err != nil { return msg.errorResponse(&internalServerError{errcodeMarshalError, err.Error()}) }