8.4 KiB
EIP-7732 -- Honest Builder
This is an accompanying document which describes the expected actions of a "builder" participating in the Ethereum proof-of-stake protocol.
Introduction
With the EIP-7732 Fork, the protocol includes new staked participants of the protocol called Builders. While Builders are a subset of the validator set, they have extra attributions that are optional. Validators may opt to not be builders and as such we collect the set of guidelines for those validators that want to act as builders in this document.
Builders attributions
Builders can submit bids to produce execution payloads. They can broadcast these bids in the form of SignedExecutionPayloadHeader
objects, these objects encode a commitment to reveal an execution payload in exchange for a payment. When their bids are chosen by the corresponding proposer, builders are expected to broadcast an accompanying SignedExecutionPayloadEnvelope
object honoring the commitment.
Thus, builders tasks are divided in two, submitting bids, and submitting payloads.
Constructing the payload bid
Builders can broadcast a payload bid for the current or the next slot's proposer to include. They produce a SignedExecutionPayloadHeader
as follows.
- Set
header.parent_block_hash
to the current head of the execution chain (this can be obtained from the beacon state asstate.last_block_hash
). - Set
header.parent_block_root
to be the head of the consensus chain (this can be obtained from the beacon state ashash_tree_root(state.latest_block_header)
. Theparent_block_root
andparent_block_hash
must be compatible, in the sense that they both should come from the samestate
by the method described in this and the previous point. - Construct an execution payload. This can be performed with an external execution engine with a call to
engine_getPayloadV4
. - Set
header.block_hash
to be the block hash of the constructed payload, that ispayload.block_hash
. - Set
header.gas_limit
to be the gas limit of the constructed payload, that ispayload.gas_limit
. - Set
header.builder_index
to be the validator index of the builder performing these actions. - Set
header.slot
to be the slot for which this bid is aimed. This slot MUST be either the current slot or the next slot. - Set
header.value
to be the value that the builder will pay the proposer if the bid is accepted. The builder MUST have balance enough to fulfill this bid. - Set
header.kzg_commitments_root
to be thehash_tree_root
of theblobsbundle.commitments
field returned byengine_getPayloadV4
.
After building the header
, the builder obtains a signature
of the header by using
def get_execution_payload_header_signature(
state: BeaconState, header: ExecutionPayloadHeader, privkey: int) -> BLSSignature:
domain = get_domain(state, DOMAIN_BEACON_BUILDER, compute_epoch_at_slot(header.slot))
signing_root = compute_signing_root(header, domain)
return bls.Sign(privkey, signing_root)
The builder assembles then signed_execution_payload_header = SignedExecutionPayloadHeader(message=header, signature=signature)
and broadcasts it on the execution_payload_header
global gossip topic.
Constructing the BlobSidecar
s
[Modified in EIP-7732]
The BlobSidecar
container is modified indirectly because the constant KZG_COMMITMENT_INCLUSION_PROOF_DEPTH
is modified. The function get_blob_sidecars
is modified because the KZG commitments are no longer included in the beacon block but rather in the ExecutionPayloadEnvelope
, the builder has to send the commitments as parameters to this function.
def get_blob_sidecars(signed_block: SignedBeaconBlock,
blobs: Sequence[Blob],
blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK],
blob_kzg_proofs: Sequence[KZGProof]) -> Sequence[BlobSidecar]:
block = signed_block.message
block_header = BeaconBlockHeader(
slot=block.slot,
proposer_index=block.proposer_index,
parent_root=block.parent_root,
state_root=block.state_root,
body_root=hash_tree_root(block.body),
)
signed_block_header = SignedBeaconBlockHeader(message=block_header, signature=signed_block.signature)
sidecars: List[BlobSidecar] = []
for index, blob in enumerate(blobs):
proof = compute_merkle_proof(
block.body,
get_generalized_index(
BeaconBlockBody,
"signed_execution_payload_header",
"message",
"blob_kzg_commitments_root",
),
)
proof += compute_merkle_proof(
blob_kzg_commitments,
get_generalized_index(List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK], index),
)
sidecars.append(
BlobSidecar(
index=index,
blob=blob,
kzg_commitment=blob_kzg_commitments[index],
kzg_proof=blob_kzg_proofs[index],
signed_block_header=signed_block_header,
kzg_commitment_inclusion_proof=proof
)
)
return sidecars
Constructing the execution payload envelope
When the proposer publishes a valid SignedBeaconBlock
containing a signed commitment by the builder, the builder is later expected to broadcast the corresponding SignedExecutionPayloadEnvelope
that fulfills this commitment. See below for a special case of an honestly withheld payload.
To construct the execution_payload_envelope
the builder must perform the following steps, we alias header
to be the committed ExecutionPayloadHeader
in the beacon block.
- Set the
payload
field to be theExecutionPayload
constructed when creating the corresponding bid. This payload MUST have the same block hash asheader.block_hash
. - Set the
builder_index
field to be the validator index of the builder performing these steps. This field MUST beheader.builder_index
. - Set
beacon_block_root
to be thehash_tree_root
of the corresponding beacon block. - Set
blob_kzg_commitments
to be thecommitments
field of the blobs bundle constructed when constructing the bid. This field MUST have ahash_tree_root
equal toheader.blob_kzg_commitments_root
. - Set
payload_witheld
toFalse
.
After setting these parameters, the builder should run process_execution_payload(state, signed_envelope, verify=False)
and this function should not trigger an exception.
- Set
state_root
tohash_tree_root(state)
. After preparing theenvelope
the builder should sign the envelope using:
def get_execution_payload_envelope_signature(
state: BeaconState, envelope: ExecutionPayloadEnvelope, privkey: int) -> BLSSignature:
domain = get_domain(state, DOMAIN_BEACON_BUILDER, compute_epoch_at_slot(state.slot))
signing_root = compute_signing_root(envelope, domain)
return bls.Sign(privkey, signing_root)
The builder assembles then signed_execution_payload_envelope = SignedExecutionPayloadEnvelope(message=envelope, signature=signature)
and broadcasts it on the execution_payload
global gossip topic.
Honest payload withheld messages
An honest builder that has seen a SignedBeaconBlock
referencing his signed bid, but that block was not timely and thus it is not the head of the builder's chain, may choose to withhold their execution payload. For this the builder should simply act as if it were building an empty payload, without any transactions, withdrawals, etc. The payload.block_hash
may not be equal to header.block_hash
. The builder may then sets payload_withheld
to True
. If the PTC sees this message and votes for it, validators will attribute a withholding boost to the builder, which would increase the forkchoice weight of the parent block, favoring it and preventing the builder from being charged for the bid by not revealing.