import { BUILDER_INDEX_SELF_BUILD, ForkSeq, GENESIS_SLOT, PAYLOAD_BUILDER_VERSION, SLOTS_PER_EPOCH, } from "@lodestar/params"; import {gloas, heze, ssz} from "@lodestar/types"; import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils"; import {G2_POINT_AT_INFINITY} from "../constants/constants.js"; import {getExecutionPayloadBidSigningRoot} from "../signatureSets/executionPayloadBid.js"; import {CachedBeaconStateGloas, CachedBeaconStateHeze} from "../types.js"; import {canBuilderCoverBid, isActiveBuilder} from "../util/gloas.js"; import { createSingleSignatureSetFromComponents, getBlockRootAtSlot, getCurrentEpoch, getRandaoMix, verifySignatureSet, } from "../util/index.js"; export function processExecutionPayloadBid( state: CachedBeaconStateGloas | CachedBeaconStateHeze, signedBid: gloas.SignedExecutionPayloadBid ): void { const bid = signedBid.message; const {builderIndex, value: amount} = bid; // For self-builds, amount must be zero regardless of withdrawal credential prefix if (builderIndex === BUILDER_INDEX_SELF_BUILD) { if (amount !== 0) { throw Error(`Invalid execution payload bid: self-build with non-zero amount ${amount}`); } if (!byteArrayEquals(signedBid.signature, G2_POINT_AT_INFINITY)) { throw Error("Invalid execution payload bid: self-build with non-zero signature"); } } // Non-self builds require active builder with valid signature else { const builder = state.builders.getReadonly(builderIndex); // Verify that the builder is active if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) { throw Error(`Invalid execution payload bid: builder ${builderIndex} is not active`); } // Verify that the builder is a payload builder if (builder.version !== PAYLOAD_BUILDER_VERSION) { throw Error( `Invalid execution payload bid: builder ${builderIndex} version ${builder.version} != ${PAYLOAD_BUILDER_VERSION}` ); } // Verify that the builder has funds to cover the bid if (!canBuilderCoverBid(state as CachedBeaconStateGloas, builderIndex, amount)) { throw Error(`Invalid execution payload bid: builder ${builderIndex} has insufficient balance`); } // Verify that the bid signature is valid if (!verifyExecutionPayloadBidSignature(state, builder.pubkey, signedBid)) { throw Error(`Invalid execution payload bid: invalid signature for builder ${builderIndex}`); } } if (bid.slot !== state.slot) { throw Error(`Bid slot ${bid.slot} does not match state slot ${state.slot}`); } if (state.slot <= GENESIS_SLOT) { throw Error(`Execution payload bid not allowed at genesis slot ${state.slot}`); } if (!byteArrayEquals(bid.parentBlockHash, state.latestBlockHash)) { throw Error( `Parent block hash ${toRootHex(bid.parentBlockHash)} of bid does not match state's latest block hash ${toRootHex(state.latestBlockHash)}` ); } if (byteArrayEquals(bid.blockHash, bid.parentBlockHash)) { throw Error(`Block hash ${toRootHex(bid.blockHash)} of bid must not equal its parent block hash`); } const parentBlockRoot = getBlockRootAtSlot(state, state.slot - 1); if (!byteArrayEquals(bid.parentBlockRoot, parentBlockRoot)) { throw Error( `Parent block root ${toRootHex(bid.parentBlockRoot)} of bid does not match state's parent block root ${toRootHex(parentBlockRoot)}` ); } const stateRandao = getRandaoMix(state, getCurrentEpoch(state)); if (!byteArrayEquals(bid.prevRandao, stateRandao)) { throw Error(`Prev randao ${toHex(bid.prevRandao)} of bid does not match state's randao mix ${toHex(stateRandao)}`); } // Verify commitments are under limit const maxBlobsPerBlock = state.config.getMaxBlobsPerBlock(state.epochCtx.epoch); if (bid.blobKzgCommitments.length > maxBlobsPerBlock) { throw Error( `Kzg commitments exceed limit commitments.length=${bid.blobKzgCommitments.length} limit=${maxBlobsPerBlock}` ); } if (amount > 0) { const pendingPaymentView = ssz.gloas.BuilderPendingPayment.toViewDU({ weight: 0, withdrawal: ssz.gloas.BuilderPendingWithdrawal.toViewDU({ feeRecipient: bid.feeRecipient, amount, builderIndex, }), proposerIndex: state.epochCtx.getBeaconProposer(state.slot), }); state.builderPendingPayments.set(SLOTS_PER_EPOCH + (bid.slot % SLOTS_PER_EPOCH), pendingPaymentView); } if (state.config.getForkSeq(state.slot) >= ForkSeq.heze) { state.latestExecutionPayloadBid = ssz.heze.ExecutionPayloadBid.toViewDU(bid as heze.ExecutionPayloadBid); } else { state.latestExecutionPayloadBid = ssz.gloas.ExecutionPayloadBid.toViewDU(bid); } } function verifyExecutionPayloadBidSignature( state: CachedBeaconStateGloas | CachedBeaconStateHeze, pubkey: Uint8Array, signedBid: gloas.SignedExecutionPayloadBid ): boolean { const signingRoot = getExecutionPayloadBidSigningRoot(state.config, signedBid.message); try { return verifySignatureSet(createSingleSignatureSetFromComponents(pubkey, signingRoot, signedBid.signature)); } catch (_e) { return false; // Catch all BLS errors: failed key validation, failed signature validation, invalid signature } }