mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-24 08:49:29 +00:00
separate JSON encoding from I/O, add fast-path for GetBlobs
This commit is contained in:
parent
3688824db3
commit
0ce0151d89
5 changed files with 172 additions and 9 deletions
109
beacon/engine/marshal_bap_list.go
Normal file
109
beacon/engine/marshal_bap_list.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// 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
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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, ',')
|
||||
}
|
||||
buf = marshalBlobAndProofV1(buf, item)
|
||||
}
|
||||
buf = append(buf, ']')
|
||||
return buf, 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, ',')
|
||||
}
|
||||
buf = marshalBlobAndProofV2(buf, item)
|
||||
}
|
||||
buf = append(buf, ']')
|
||||
return buf, nil
|
||||
}
|
||||
|
|
@ -152,11 +152,19 @@ type BlobAndProofV1 struct {
|
|||
Proof hexutil.Bytes `json:"proof"`
|
||||
}
|
||||
|
||||
// BlobAndProofListV1 is a list of BlobAndProofV1 with a hand-rolled JSON marshaler
|
||||
// that avoids the overhead of encoding/json for large blob payloads.
|
||||
type BlobAndProofListV1 []*BlobAndProofV1
|
||||
|
||||
type BlobAndProofV2 struct {
|
||||
Blob hexutil.Bytes `json:"blob"`
|
||||
CellProofs []hexutil.Bytes `json:"proofs"` // proofs MUST contain exactly CELLS_PER_EXT_BLOB cell proofs.
|
||||
}
|
||||
|
||||
// BlobAndProofListV2 is a list of BlobAndProofV2 with a hand-rolled JSON marshaler
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -535,7 +535,7 @@ func (api *ConsensusAPI) getPayload(payloadID engine.PayloadID, full bool, versi
|
|||
//
|
||||
// Client software MAY return an array of all null entries if syncing or otherwise
|
||||
// unable to serve blob pool data.
|
||||
func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) ([]*engine.BlobAndProofV1, error) {
|
||||
func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) (engine.BlobAndProofListV1, error) {
|
||||
// Reject the request if Osaka has been activated.
|
||||
// follow https://github.com/ethereum/execution-apis/blob/main/src/engine/osaka.md#cancun-api
|
||||
head := api.eth.BlockChain().CurrentHeader()
|
||||
|
|
@ -549,7 +549,7 @@ func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) ([]*engine.BlobAndProo
|
|||
if err != nil {
|
||||
return nil, engine.InvalidParams.With(err)
|
||||
}
|
||||
res := make([]*engine.BlobAndProofV1, len(hashes))
|
||||
res := make(engine.BlobAndProofListV1, len(hashes))
|
||||
for i := 0; i < len(blobs); i++ {
|
||||
// Skip the non-existing blob
|
||||
if blobs[i] == nil {
|
||||
|
|
@ -588,7 +588,7 @@ func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) ([]*engine.BlobAndProo
|
|||
//
|
||||
// Client software MUST return null if syncing or otherwise unable to serve
|
||||
// blob pool data.
|
||||
func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) ([]*engine.BlobAndProofV2, error) {
|
||||
func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) (engine.BlobAndProofListV2, error) {
|
||||
head := api.eth.BlockChain().CurrentHeader()
|
||||
if api.config().LatestFork(head.Time) < forks.Osaka {
|
||||
return nil, nil
|
||||
|
|
@ -599,7 +599,7 @@ func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) ([]*engine.BlobAndProo
|
|||
// GetBlobsV3 returns a set of blobs from the transaction pool. Same as
|
||||
// GetBlobsV2, except will return partial responses in case there is a missing
|
||||
// blob.
|
||||
func (api *ConsensusAPI) GetBlobsV3(hashes []common.Hash) ([]*engine.BlobAndProofV2, error) {
|
||||
func (api *ConsensusAPI) GetBlobsV3(hashes []common.Hash) (engine.BlobAndProofListV2, error) {
|
||||
head := api.eth.BlockChain().CurrentHeader()
|
||||
if api.config().LatestFork(head.Time) < forks.Osaka {
|
||||
return nil, nil
|
||||
|
|
@ -609,7 +609,7 @@ func (api *ConsensusAPI) GetBlobsV3(hashes []common.Hash) ([]*engine.BlobAndProo
|
|||
|
||||
// getBlobs returns all available blobs. In v2, partial responses are not allowed,
|
||||
// while v3 supports partial responses.
|
||||
func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) ([]*engine.BlobAndProofV2, error) {
|
||||
func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) (engine.BlobAndProofListV2, error) {
|
||||
if len(hashes) > 128 {
|
||||
return nil, engine.TooLargeRequest.With(fmt.Errorf("requested blob count too large: %v", len(hashes)))
|
||||
}
|
||||
|
|
@ -629,7 +629,7 @@ func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) ([]*engine.Blob
|
|||
return nil, engine.InvalidParams.With(err)
|
||||
}
|
||||
// Validate the blobs from the pool and assemble the response
|
||||
res := make([]*engine.BlobAndProofV2, len(hashes))
|
||||
res := make(engine.BlobAndProofListV2, len(hashes))
|
||||
for i := range blobs {
|
||||
// The blob has been evicted since the last AvailableBlobs call.
|
||||
// Return null if partial response is not allowed.
|
||||
|
|
|
|||
|
|
@ -691,3 +691,49 @@ func BenchmarkGetPayloadV5RPCServerOnly(b *testing.B) {
|
|||
b.StopTimer()
|
||||
b.ReportMetric(float64(b.Elapsed().Milliseconds())/float64(b.N), "ms/op")
|
||||
}
|
||||
|
||||
// BenchmarkGetBlobsV3RPCServerOnly benchmarks only the EL server-side cost of
|
||||
// engine_getBlobsV3: method dispatch, JSON serialization, and wire encoding.
|
||||
// Client-side decoding is excluded by writing to io.Discard.
|
||||
func BenchmarkGetBlobsV3RPCServerOnly(b *testing.B) {
|
||||
blobCount := 72
|
||||
env := newBenchmarkBlobEnv(b, blobCount, 1, forkOsaka)
|
||||
defer env.Close()
|
||||
|
||||
// Register the engine API on the running node's in-process RPC server.
|
||||
rpcServer, err := env.node.RPCHandler()
|
||||
if err != nil {
|
||||
b.Fatalf("RPCHandler failed: %v", err)
|
||||
}
|
||||
rpcServer.RegisterName("engine", env.api)
|
||||
|
||||
// Verify the blobs are available via the direct API first.
|
||||
result, err := env.api.GetBlobsV3(env.vhashes)
|
||||
if err != nil {
|
||||
b.Fatalf("GetBlobsV3 failed: %v", err)
|
||||
}
|
||||
if len(result) != blobCount {
|
||||
b.Fatalf("expected %d blobs, got %d", blobCount, len(result))
|
||||
}
|
||||
b.Logf("blob count: %d", blobCount)
|
||||
|
||||
// Build the JSON-RPC request bytes once.
|
||||
// Format the versioned hashes as a JSON array of hex strings.
|
||||
var hashStrs []string
|
||||
for _, h := range env.vhashes {
|
||||
hashStrs = append(hashStrs, fmt.Sprintf(`"%s"`, h.Hex()))
|
||||
}
|
||||
reqJSON := fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"engine_getBlobsV3","params":[[%s]]}`, strings.Join(hashStrs, ","))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
conn := discardConn{
|
||||
Reader: strings.NewReader(reqJSON),
|
||||
Writer: io.Discard,
|
||||
}
|
||||
codec := rpc.NewCodec(conn)
|
||||
rpcServer.ServeCodec(codec, 0)
|
||||
}
|
||||
b.StopTimer()
|
||||
b.ReportMetric(float64(b.Elapsed().Milliseconds())/float64(b.N), "ms/op")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1961,7 +1961,7 @@ func TestGetBlobsV1(t *testing.T) {
|
|||
// Fill the request for retrieving blobs
|
||||
var (
|
||||
vhashes []common.Hash
|
||||
expect []*engine.BlobAndProofV1
|
||||
expect engine.BlobAndProofListV1
|
||||
)
|
||||
// fill missing blob at the beginning
|
||||
if suite.fillRandom {
|
||||
|
|
@ -2072,13 +2072,13 @@ func BenchmarkGetBlobsV2(b *testing.B) {
|
|||
}
|
||||
}
|
||||
|
||||
type getBlobsFn func(hashes []common.Hash) ([]*engine.BlobAndProofV2, error)
|
||||
type getBlobsFn func(hashes []common.Hash) (engine.BlobAndProofListV2, error)
|
||||
|
||||
func runGetBlobs(t testing.TB, getBlobs getBlobsFn, start, limit int, fillRandom bool, expectPartialResponse bool, name string) {
|
||||
// Fill the request for retrieving blobs
|
||||
var (
|
||||
vhashes []common.Hash
|
||||
expect []*engine.BlobAndProofV2
|
||||
expect engine.BlobAndProofListV2
|
||||
)
|
||||
for j := start; j < limit; j++ {
|
||||
vhashes = append(vhashes, testBlobVHashes[j])
|
||||
|
|
|
|||
Loading…
Reference in a new issue