Skip to content
115 changes: 115 additions & 0 deletions src/testing/testing_buildBlockV1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# `testing_buildBlockV1`

This document specifies the `testing_buildBlockV1` RPC method. This method is a debugging and testing tool that simplifies the block production process into a single call. It is intended to replace the multi-step workflow of sending transactions, calling `engine_forkchoiceUpdated` with `payloadAttributes`, and then calling `engine_getPayload`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the current spec is tied to specific types like PayloadAttributesV3and engine_getPayloadV5does this implicitly mean that for each fork which would change these types (PayloadAttributesV4) we thus also need a new testing_buildBlockVx instance? (Which is fine).

Another main motivation of this method is bypassing the txpool. We could start a client and only inject specific txs into txpool, but if we then ask it to build a block via normal route (engine API) then it is not guaranteed all txs are in it, and the order of the txs is also not guaranteed.


This method is considered sensitive and is intended for testing environments only. See [**Security Considerations**](#security-considerations) for more details.

### Request

* method: `testing_buildBlockV1`
* params:
1. `parentBlockHash`: `DATA`, 32 Bytes - block hash of the parent of the requested block.
2. `payloadAttributes`: `Object` - instance of [`PayloadAttributesV3`](../engine/cancun.md#payloadattributesv3)
3. `transactions`: `Array of DATA` - an array of raw, signed transactions (hex-encoded `0x...` strings) to forcibly include in the generated block.
4. `extraData`: `DATA|null`, 0 to 32 Bytes - data to be set as the `extraData` field of the built block.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another feature I'd like here is a gas limit. Besides the obvious reason to create bigger blocks for benchmarks, there might also be other reasons to control the gas limit, for instance instead of going up, bringing it down (this has influence on the fee market).
The suggestion here is to add an optional parameter "gas target" or something similar, and the client will attempt to move to that gas target (within the bounds of increasing/decreasing the gas limit of a block)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can obviously be done via client config, but with EELS as driver for the test/benchmark generator it is handy to be able to control this from EELS directly, without having to edit a config to set a specific gas target.


### Response

* result: `OBJECT` - instance of [`engine_getPayloadV5`](../engine/osaka.md#response)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not really a good description, are we returning the method? or the same response as getPayloadV5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read this as defining the schema of the response object to the the same as engine_getPayloadV5 response object?

* error: code and message set in case an exception happens while building the requested block.

### Specification

* The client MUST build a new execution payload using the block specified by `parentBlockHash` as its parent.

* The client MUST use the provided `payloadAttributes` to define the context of the new block.

* The client MUST include all transactions from the `transactions` array on the block's transaction list, in the order they were provided.

* The client MUST NOT include any transactions from its local transaction pool. The resulting block MUST only contain the transactions specified in the `transactions` array.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* The client MUST NOT include any transactions from its local transaction pool. The resulting block MUST only contain the transactions specified in the `transactions` array.
* If the `transactions` array is non-empty, the client MUST NOT include any transactions from its local transaction pool. The resulting block MUST only contain the transactions specified in the `transactions` array.

It might be a useful (optional?) feature to allow for selectiung txpool transactions in the case where the transaction array is empty.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep it as simple as possible in V1 to make it as easy as possible to implement for all EL teams. The use case is simple here - to build a block from provided txs. Your proposition is interesting, but:

  1. It can break some test cases e.g. empty payload tests
  2. It will add complexity - in current form we can totally ignore mempool and it's transactions

Also here, if majority of people will opt to have such feature, I'm fine with adding it.

Copy link
Copy Markdown
Contributor

@siladu siladu Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my wording keeps the option open without specifying what the client should do in the case of empty transaction array.

In Besu's case, we actually have a similar RPC method we use for internal testing, engine_preparePayload_debug see besu-eth/besu#4427
There are enough similarities that we would probably modify it to fit this spec, but the main use case for ours is to test txpool selection.

Copy link
Copy Markdown
Contributor

@MysticRyuujin MysticRyuujin Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if an empty array is passed then an empty transaction list should be used, otherwise it's non-deterministic, in the case of test chains and devnets, you may very well be trying to generate empty blocks. And to that effect, only the txs in the array should be included.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we make an empty array be "build an empty block" and null be "use your mempool if you want to"? Or is that too much?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be fine with that, as long as the option is possible in some form that is spec-compliant, don't mind about the syntax.

Copy link
Copy Markdown
Member

@raxhvl raxhvl Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transactions parameter MUST accept an array, not null.

Accepting mempool transactions contradicts the intent:

"It is intended to replace the multi-step workflow of sending transactions"

With null + mempool, we are back to multi-step: eth_sendRawTransaction + testing_buildBlockV1.[1]

This endpoint will be heavily used for benchmarking. We should keep the underlying logic straightforward and slim.

As always, we can extend if there is pressing need for the mempool path for testing later. Lets keep v1 simple for now.

[1]: mempool test case

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then don't send null for your use case

Comment thread
marcindsobczak marked this conversation as resolved.

* If `extraData` is provided, the client MUST set the `extraData` field of the resulting payload to this value.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extraData field is a bit weird, since we didn't use it before, it needs to be piped through to the miner

Copy link
Copy Markdown
Contributor

@MysticRyuujin MysticRyuujin Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extraData is part of the blockhash though, is it not? I think it's important that all the fields that would contribute to the blockHash of a real block are properly represented otherwise the returned blockhash wouldn't match the actual/expected blockhash (which is very useful for testing).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With extraData edits you can generate the same block, but with a different hash, which is a nice property. All other fields will edit something in the block which is observable from EVM.


* This method MUST NOT modify the client's canonical chain or head block. It is a read-only method for payload generation.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if invalid txs get added?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a test case for that, it's a defined error in my PR


### Security Considerations

* **HIGHLY SENSITIVE**: This method is a powerful debugging tool intended for testing environments ONLY.

* It allows for the creation of arbitrary blocks. It can be in used to forcibly include any valid transaction.

* This method MUST NOT be exposed on public-facing RPC APIs.

* It is strongly recommended that this method, and its `testing_` namespace, be disabled by default. Enabling it should require an explicit command-line flag or configuration setting by the node operator.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this fit better under debug_ namespace?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea of creating a new namespace is to prevent enabling this method by mistake, together with other debug ones.
It was specified here: ethpandaops/gas-lighting-tracker#5
This new endpoint is sensitive and should exist under a new, non-exposed namespace (i.e., not debug_ or admin_ which are sometimes exposed by RPC providers).

@spencer-tb has another idea - to make it _debug so it will be different than current debug, but will not introduce quite exotic testing namespace.
I'm open for every option.

Copy link
Copy Markdown
Contributor

@siladu siladu Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besu actually already has a similar endpoint called engine_preparePayload_debug so would support the _debug suffix 👍

I think debug_ prefix is also fine because there are already debug_ methods that can do quite a lot of damage so it would be a misconfiguration to leave them publicly accessible IMO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with the debug namespace is that it contains the tracing methods, and basically everyone needs/wants the tracing methods, so debug is very often forced to be on, even for people who don't want the expose "dangerous methods"

I have no problem with a new namespace called testing being created, but I also have no problem with it going under debug since debug has been treated like a catchall and one more dangerous method isn't going to break the mess that is the debug namespace any more than it already is.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's probably the trace ones that are most dangerous, in terms of resource consumption at least 😆
But yeh I take your point, I imagine there's ppl who want trace but not this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also hide it behind the jwt?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not opposed to hiding behind jwt but there are a few things to consider:

  1. Not all clients allow enabling/disabling methods specifically on the auth rpc port
  2. It makes tooling an automation lot more difficult

Since this is a new namespace, I think it's overkill to require it to be hidden behind auth


### Example

#### Request

```json
{
"id": 1,
"jsonrpc": "2.0",
"method": "testing_buildBlockV1",
"params": [
{
"parentBlockHash": "0x3b8fb240d288781d4f1e1d32f4c1550BEEFDEADBEEFDEADBEEFDEADBEEFDEAD",
"payloadAttributes": {
"timestamp": "0x6705D918",
"prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
"suggestedFeeRecipient": "0x0000000000000000000000000000000000000000",
"withdrawals": [],
"parentBeaconBlockRoot": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884365149a42212e8822"
},
"transactions": [
"0xf86c0a8504a817c80082520894...cb61163c917540a0c64c12b8f50015",
"0xf86b0f8504a817c80082520894...5443c01e69d3b0c5112101564914a2"
],
"extraData": "0x746573745F6E616D65"
}
]
}
```

#### Response (Success)

```json
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"executionPayload": {
"blockHash": "0x8980a3a7f8b16053c4dec86e1050e6378e176272517852f8c5b56f34e9a0f9b6",
"parentHash": "0x3b8fb240d288781d4f1e1d32f4c15509a2b7538b8e0e719541a50a31a2631a01",
"feeRecipient": "0x0000000000000000000000000000000000000000",
"stateRoot": "0xca3699d05d3369680315af1d03c6f8f73188945f3c83756bde2575ddc25c6040",
"receiptsRoot": "0x2e0617b0c3545d1668e8d8cb530a6c71b0a8f82875b1d0b38c352718016feff6",
"logsBloom": "0x00...00",
"prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
"blockNumber": "0x1b4",
"gasLimit": "0x1c9c380",
"gasUsed": "0xa410",
"timestamp": "0x6705D918",
"extraData": "0x746573745F6E616D65",
"baseFeePerGas": "0x7",
"excessBlobGas": "0x0",
"blobGasUsed": "0x0",
"transactions": [
"0xf86c0a8504a817c80082520894...cb61163c917540a0c64c12b8f50015",
"0xf86b0f8504a817c80082520894...5443c01e69d3b0c5112101564914a2"
],
"parentBeaconBlockRoot": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884365149a42212e8822",
"depositRequests": [],
"consolidationRequests": []
},
"blockValue": "0x1b681a965684000",
"blobsBundle": {
"commitments": [],
"blobs": [],
"kzgProofs": []
},
"shouldOverrideBuilder": false,
"executionRequests": []
}
}
```