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_hashto the current head of the execution chain (this can be obtained from the beacon state asstate.last_block_hash). - Set
header.parent_block_rootto be the head of the consensus chain (this can be obtained from the beacon state ashash_tree_root(state.latest_block_header). Theparent_block_rootandparent_block_hashmust be compatible, in the sense that they both should come from the samestateby 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_hashto be the block hash of the constructed payload, that ispayload.block_hash. - Set
header.gas_limitto be the gas limit of the constructed payload, that ispayload.gas_limit. - Set
header.builder_indexto be the validator index of the builder performing these actions. - Set
header.slotto be the slot for which this bid is aimed. This slot MUST be either the current slot or the next slot. - Set
header.valueto 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_rootto be thehash_tree_rootof theblobsbundle.commitmentsfield 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 BlobSidecars
[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(
blob_kzg_commitments,
get_generalized_index(List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK], index),
)
proof += compute_merkle_proof(
block.body,
get_generalized_index(
BeaconBlockBody,
"signed_execution_payload_header",
"message",
"blob_kzg_commitments_root",
),
)
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
payloadfield to be theExecutionPayloadconstructed when creating the corresponding bid. This payload MUST have the same block hash asheader.block_hash. - Set the
builder_indexfield to be the validator index of the builder performing these steps. This field MUST beheader.builder_index. - Set
beacon_block_rootto be thehash_tree_rootof the corresponding beacon block. - Set
blob_kzg_commitmentsto be thecommitmentsfield of the blobs bundle constructed when constructing the bid. This field MUST have ahash_tree_rootequal toheader.blob_kzg_commitments_root. - Set
payload_witheldtoFalse.
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_roottohash_tree_root(state). After preparing theenvelopethe 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.