Skip to content

Commit b647574

Browse files
JkLondonJkLondon
andauthored
[rpc] [kimiko] support for testing_buildBlockV1 (#19478)
closes #19054 tried to oneshot random issue with my new reasoning bot Kimiko) Seems good wdyt @canepat --------- Co-authored-by: JkLondon <me@ilyamikheev.com>
1 parent 3b668d3 commit b647574

File tree

6 files changed

+845
-1
lines changed

6 files changed

+845
-1
lines changed

cmd/rpcdaemon/cli/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ func RootCommand() (*cobra.Command, *httpcfg.HttpCfg) {
131131
rootCmd.PersistentFlags().IntVar(&cfg.DBReadConcurrency, utils.DBReadConcurrencyFlag.Name, utils.DBReadConcurrencyFlag.Value, utils.DBReadConcurrencyFlag.Usage)
132132
rootCmd.PersistentFlags().BoolVar(&cfg.TraceCompatibility, "trace.compat", false, "Bug for bug compatibility with OE for trace_ routines")
133133
rootCmd.PersistentFlags().BoolVar(&cfg.GethCompatibility, "rpc.gethcompat", false, "Enables Geth-compatible storage iteration order for debug_storageRangeAt (sorted by keccak256 hash). Disabled by default for performance.")
134+
rootCmd.PersistentFlags().BoolVar(&cfg.TestingEnabled, "rpc.testing", false, "Enables the testing_ RPC namespace (testing_buildBlockV1). WARNING: do not enable on production networks.")
134135
rootCmd.PersistentFlags().StringVar(&cfg.TxPoolApiAddr, "txpool.api.addr", "", "txpool api network address, for example: 127.0.0.1:9090 (default: use value of --private.api.addr)")
135136

136137
rootCmd.PersistentFlags().StringVar(&stateCacheStr, "state.cache", "0MB", "Amount of data to store in StateCache (enabled if no --datadir set). Set 0 to disable StateCache. Defaults to 0MB RAM")

cmd/rpcdaemon/cli/httpcfg/http_cfg.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,7 @@ type HttpCfg struct {
111111

112112
RpcTxSyncDefaultTimeout time.Duration // Default timeout for eth_sendRawTransactionSync
113113
RpcTxSyncMaxTimeout time.Duration // Maximum timeout for eth_sendRawTransactionSync
114+
115+
// TestingEnabled enables the testing_ RPC namespace. Should only be used in test/dev environments.
116+
TestingEnabled bool
114117
}

execution/engineapi/engine_server.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,16 @@ func (e *EngineServer) Start(
156156
Version: "1.0",
157157
}}
158158

159+
if httpConfig.TestingEnabled {
160+
e.logger.Warn("[EngineServer] testing_ RPC namespace is ENABLED — do not use on production networks")
161+
apiList = append(apiList, rpc.API{
162+
Namespace: "testing",
163+
Public: false,
164+
Service: TestingAPI(NewTestingImpl(e, true)),
165+
Version: "1.0",
166+
})
167+
}
168+
159169
eg.Go(func() error {
160170
defer e.logger.Debug("[EngineServer] engine rpc server goroutine terminated")
161171
err := cli.StartRpcServerWithJwtAuthentication(ctx, httpConfig, apiList, e.logger)

execution/engineapi/testing_api.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// Copyright 2024 The Erigon Authors
2+
// This file is part of Erigon.
3+
//
4+
// Erigon is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Erigon is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package engineapi
18+
19+
// testing_api.go implements the testing_ RPC namespace, specifically testing_buildBlockV1.
20+
// This namespace MUST NOT be enabled on production networks. Gate it with --rpc.testing.
21+
22+
import (
23+
"context"
24+
"errors"
25+
"time"
26+
27+
"github.com/erigontech/erigon/cl/clparams"
28+
"github.com/erigontech/erigon/common"
29+
"github.com/erigontech/erigon/common/hexutil"
30+
"github.com/erigontech/erigon/execution/engineapi/engine_types"
31+
"github.com/erigontech/erigon/node/gointerfaces"
32+
"github.com/erigontech/erigon/node/gointerfaces/executionproto"
33+
"github.com/erigontech/erigon/rpc"
34+
)
35+
36+
// TestingAPI is the interface for the testing_ RPC namespace.
37+
type TestingAPI interface {
38+
// BuildBlockV1 synchronously builds and returns an execution payload given a parent hash,
39+
// payload attributes, an optional transaction list, and optional extra data.
40+
// Unlike the two-phase forkchoiceUpdated+getPayload flow this call blocks until the block
41+
// is fully assembled and returns the result in a single response.
42+
//
43+
// transactions: nil → draw from mempool (normal builder behaviour)
44+
// [] → build an empty block (mempool bypassed, no txs)
45+
// [...] → TODO: explicit tx list not yet supported; returns error
46+
//
47+
// NOTE: overriding extraData post-assembly means the BlockHash in the returned payload
48+
// will NOT match a block header that includes that extraData. Callers should treat the
49+
// result as a template when extraData is overridden.
50+
BuildBlockV1(ctx context.Context, parentHash common.Hash, payloadAttributes *engine_types.PayloadAttributes, transactions *[]hexutil.Bytes, extraData *hexutil.Bytes) (*engine_types.GetPayloadResponse, error)
51+
}
52+
53+
// testingImpl is the concrete implementation of TestingAPI.
54+
type testingImpl struct {
55+
server *EngineServer
56+
enabled bool
57+
}
58+
59+
// NewTestingImpl returns a new TestingAPI implementation wrapping the given EngineServer.
60+
func NewTestingImpl(server *EngineServer, enabled bool) TestingAPI {
61+
return &testingImpl{server: server, enabled: enabled}
62+
}
63+
64+
// BuildBlockV1 implements TestingAPI.
65+
func (t *testingImpl) BuildBlockV1(
66+
ctx context.Context,
67+
parentHash common.Hash,
68+
payloadAttributes *engine_types.PayloadAttributes,
69+
transactions *[]hexutil.Bytes,
70+
extraData *hexutil.Bytes,
71+
) (*engine_types.GetPayloadResponse, error) {
72+
if !t.enabled {
73+
return nil, &rpc.InvalidParamsError{Message: "testing namespace is disabled; start erigon with --rpc.testing (WARNING: not for production use)"}
74+
}
75+
76+
if payloadAttributes == nil {
77+
return nil, &rpc.InvalidParamsError{Message: "payloadAttributes must not be null"}
78+
}
79+
80+
// Explicit transaction list is not yet supported (requires proto extension).
81+
// TODO: implement forced_transactions via AssembleBlockRequest proto extension.
82+
if transactions != nil && len(*transactions) > 0 {
83+
return nil, &rpc.InvalidParamsError{Message: "explicit transaction list not yet supported in testing_buildBlockV1; use null for mempool or [] for empty block"}
84+
}
85+
86+
// Validate parent block exists.
87+
parentHeader := t.server.chainRW.GetHeaderByHash(ctx, parentHash)
88+
if parentHeader == nil {
89+
return nil, &rpc.InvalidParamsError{Message: "unknown parent hash"}
90+
}
91+
92+
timestamp := uint64(payloadAttributes.Timestamp)
93+
94+
// Timestamp must be strictly greater than parent.
95+
if parentHeader.Time >= timestamp {
96+
return nil, &rpc.InvalidParamsError{Message: "payload timestamp must be greater than parent block timestamp"}
97+
}
98+
99+
// Validate withdrawals presence.
100+
if err := t.server.checkWithdrawalsPresence(timestamp, payloadAttributes.Withdrawals); err != nil {
101+
return nil, err
102+
}
103+
104+
// Determine version from timestamp for proper fork handling.
105+
version := clparams.BellatrixVersion
106+
switch {
107+
case t.server.config.IsAmsterdam(timestamp):
108+
version = clparams.GloasVersion
109+
case t.server.config.IsOsaka(timestamp):
110+
version = clparams.FuluVersion
111+
case t.server.config.IsPrague(timestamp):
112+
version = clparams.ElectraVersion
113+
case t.server.config.IsCancun(timestamp):
114+
version = clparams.DenebVersion
115+
case t.server.config.IsShanghai(timestamp):
116+
version = clparams.CapellaVersion
117+
}
118+
119+
// Validate parentBeaconBlockRoot presence for Cancun+.
120+
if version >= clparams.DenebVersion && payloadAttributes.ParentBeaconBlockRoot == nil {
121+
return nil, &rpc.InvalidParamsError{Message: "parentBeaconBlockRoot required for Cancun and later forks"}
122+
}
123+
if version < clparams.DenebVersion && payloadAttributes.ParentBeaconBlockRoot != nil {
124+
return nil, &rpc.InvalidParamsError{Message: "parentBeaconBlockRoot not supported before Cancun"}
125+
}
126+
127+
// Build the AssembleBlock request (mirrors forkchoiceUpdated logic).
128+
req := &executionproto.AssembleBlockRequest{
129+
ParentHash: gointerfaces.ConvertHashToH256(parentHash),
130+
Timestamp: timestamp,
131+
PrevRandao: gointerfaces.ConvertHashToH256(payloadAttributes.PrevRandao),
132+
SuggestedFeeRecipient: gointerfaces.ConvertAddressToH160(payloadAttributes.SuggestedFeeRecipient),
133+
SlotNumber: (*uint64)(payloadAttributes.SlotNumber),
134+
}
135+
if version >= clparams.CapellaVersion {
136+
req.Withdrawals = engine_types.ConvertWithdrawalsToRpc(payloadAttributes.Withdrawals)
137+
}
138+
if version >= clparams.DenebVersion {
139+
req.ParentBeaconBlockRoot = gointerfaces.ConvertHashToH256(*payloadAttributes.ParentBeaconBlockRoot)
140+
}
141+
142+
// Both steps share a single slot-duration budget so the total wall-clock
143+
// time of BuildBlockV1 is bounded to one slot (e.g. 12 s), not two.
144+
// Each step acquires the lock independently, matching production behaviour
145+
// where ForkChoiceUpdated and GetPayload are separate RPC calls.
146+
deadline := time.Now().Add(time.Duration(t.server.config.SecondsPerSlot()) * time.Second)
147+
148+
// Step 1: AssembleBlock (locked scope).
149+
assembleResp, execBusy, err := func() (*executionproto.AssembleBlockResponse, bool, error) {
150+
t.server.lock.Lock()
151+
defer t.server.lock.Unlock()
152+
153+
var resp *executionproto.AssembleBlockResponse
154+
var err error
155+
busy, err := waitForResponse(time.Until(deadline), func() (bool, error) {
156+
resp, err = t.server.executionService.AssembleBlock(ctx, req)
157+
if err != nil {
158+
return false, err
159+
}
160+
return resp.Busy, nil
161+
})
162+
return resp, busy, err
163+
}()
164+
if err != nil {
165+
return nil, err
166+
}
167+
if execBusy {
168+
return nil, errors.New("execution service is busy, cannot build block")
169+
}
170+
171+
payloadID := assembleResp.Id
172+
173+
// Step 2: GetAssembledBlock (separate locked scope).
174+
getResp, execBusy, err := func() (*executionproto.GetAssembledBlockResponse, bool, error) {
175+
t.server.lock.Lock()
176+
defer t.server.lock.Unlock()
177+
178+
var resp *executionproto.GetAssembledBlockResponse
179+
var err error
180+
busy, err := waitForResponse(time.Until(deadline), func() (bool, error) {
181+
resp, err = t.server.executionService.GetAssembledBlock(ctx, &executionproto.GetAssembledBlockRequest{
182+
Id: payloadID,
183+
})
184+
if err != nil {
185+
return false, err
186+
}
187+
return resp.Busy, nil
188+
})
189+
return resp, busy, err
190+
}()
191+
if err != nil {
192+
return nil, err
193+
}
194+
if execBusy {
195+
return nil, errors.New("execution service is busy retrieving assembled block")
196+
}
197+
if getResp.Data == nil {
198+
return nil, errors.New("no assembled block data available for payload ID")
199+
}
200+
201+
data := getResp.Data
202+
203+
// Build execution requests for Prague+.
204+
var executionRequests []hexutil.Bytes
205+
if version >= clparams.ElectraVersion {
206+
executionRequests = make([]hexutil.Bytes, 0)
207+
if data.Requests != nil {
208+
for _, r := range data.Requests.Requests {
209+
executionRequests = append(executionRequests, r)
210+
}
211+
}
212+
}
213+
214+
response := &engine_types.GetPayloadResponse{
215+
ExecutionPayload: engine_types.ConvertPayloadFromRpc(data.ExecutionPayload),
216+
BlockValue: (*hexutil.Big)(gointerfaces.ConvertH256ToUint256Int(data.BlockValue).ToBig()),
217+
BlobsBundle: engine_types.ConvertBlobsFromRpc(data.BlobsBundle),
218+
ExecutionRequests: executionRequests,
219+
// ShouldOverrideBuilder is always false for the testing namespace (no MEV-boost context).
220+
ShouldOverrideBuilder: false,
221+
}
222+
223+
// Override extra data if provided. Note: the BlockHash in ExecutionPayload reflects the
224+
// originally built block; overriding ExtraData here means BlockHash will NOT match.
225+
if extraData != nil {
226+
response.ExecutionPayload.ExtraData = *extraData
227+
}
228+
229+
return response, nil
230+
}

0 commit comments

Comments
 (0)