From 6ce5d5814cdc56838d8e491f6728c090ca6aa352 Mon Sep 17 00:00:00 2001 From: tersec Date: Tue, 9 Apr 2024 10:04:33 +0000 Subject: [PATCH] support electra block proposals for internal BN validators (#6187) --- beacon_chain/beacon_node.nim | 1 - .../consensus_object_pools/spec_cache.nim | 1 - .../eth2_apis/eth2_rest_serialization.nim | 3 + beacon_chain/spec/eth2_apis/rest_types.nim | 1 + .../spec/mev/rest_electra_mev_calls.nim | 8 ++ beacon_chain/spec/signatures.nim | 5 +- beacon_chain/spec/state_transition_block.nim | 1 - beacon_chain/validators/beacon_validators.nim | 118 ++++++++++++------ .../validators/message_router_mev.nim | 4 +- beacon_chain/validators/validator_pool.nim | 16 ++- 10 files changed, 116 insertions(+), 42 deletions(-) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index e48bcfe7c7..a92f72b109 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -105,7 +105,6 @@ type processingDelay*: Opt[Duration] lastValidAttestedBlock*: Opt[BlockSlot] -# TODO stew/sequtils2 template findIt*(s: openArray, predicate: untyped): int = var res = -1 for i, it {.inject.} in s: diff --git a/beacon_chain/consensus_object_pools/spec_cache.nim b/beacon_chain/consensus_object_pools/spec_cache.nim index 0ed6ecd136..3f3411546c 100644 --- a/beacon_chain/consensus_object_pools/spec_cache.nim +++ b/beacon_chain/consensus_object_pools/spec_cache.nim @@ -168,7 +168,6 @@ func get_attesting_indices*(shufflingRef: ShufflingRef, committee_index: CommitteeIndex, bits: CommitteeValidatorsBits): seq[ValidatorIndex] = - # TODO sequtils2 mapIt for idx in get_attesting_indices(shufflingRef, slot, committee_index, bits): result.add(idx) diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index 19af7c3ab2..6f52dcbd52 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -88,6 +88,7 @@ RestJson.useDefaultSerializationFor( GetForkScheduleResponse, GetGenesisResponse, GetHeaderResponseDeneb, + GetHeaderResponseElectra, GetKeystoresResponse, GetNextWithdrawalsResponse, GetPoolAttesterSlashingsResponse, @@ -253,8 +254,10 @@ RestJson.useDefaultSerializationFor( electra.SignedBeaconBlock, electra_mev.BlindedBeaconBlock, electra_mev.BlindedBeaconBlockBody, + electra_mev.BuilderBid, electra_mev.ExecutionPayloadAndBlobsBundle, electra_mev.SignedBlindedBeaconBlock, + electra_mev.SignedBuilderBid, phase0.BeaconBlock, phase0.BeaconBlockBody, phase0.BeaconState, diff --git a/beacon_chain/spec/eth2_apis/rest_types.nim b/beacon_chain/spec/eth2_apis/rest_types.nim index 0a90500163..6b0f1011e1 100644 --- a/beacon_chain/spec/eth2_apis/rest_types.nim +++ b/beacon_chain/spec/eth2_apis/rest_types.nim @@ -548,6 +548,7 @@ type GetForkScheduleResponse* = DataEnclosedObject[seq[Fork]] GetGenesisResponse* = DataEnclosedObject[RestGenesis] GetHeaderResponseDeneb* = DataVersionEnclosedObject[deneb_mev.SignedBuilderBid] + GetHeaderResponseElectra* = DataVersionEnclosedObject[electra_mev.SignedBuilderBid] GetNetworkIdentityResponse* = DataEnclosedObject[RestNetworkIdentity] GetPeerCountResponse* = DataMetaEnclosedObject[RestPeerCount] GetPeerResponse* = DataMetaEnclosedObject[RestNodePeer] diff --git a/beacon_chain/spec/mev/rest_electra_mev_calls.nim b/beacon_chain/spec/mev/rest_electra_mev_calls.nim index fd58ad193d..d6553b7483 100644 --- a/beacon_chain/spec/mev/rest_electra_mev_calls.nim +++ b/beacon_chain/spec/mev/rest_electra_mev_calls.nim @@ -13,6 +13,14 @@ import export chronos, client, rest_types, eth2_rest_serialization +proc getHeaderElectra*(slot: Slot, + parent_hash: Eth2Digest, + pubkey: ValidatorPubKey + ): RestPlainResponse {. + rest, endpoint: "/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}", + meth: MethodGet, connection: {Dedicated, Close}.} + ## https://github.com/ethereum/builder-specs/blob/v0.4.0/apis/builder/header.yaml + proc submitBlindedBlock*(body: electra_mev.SignedBlindedBeaconBlock ): RestPlainResponse {. rest, endpoint: "/eth/v1/builder/blinded_blocks", diff --git a/beacon_chain/spec/signatures.nim b/beacon_chain/spec/signatures.nim index a940922928..aff92afa42 100644 --- a/beacon_chain/spec/signatures.nim +++ b/beacon_chain/spec/signatures.nim @@ -371,7 +371,8 @@ proc verify_contribution_and_proof_signature*( # https://github.com/ethereum/builder-specs/blob/v0.4.0/specs/bellatrix/builder.md#signing func compute_builder_signing_root( fork: Fork, - msg: deneb_mev.BuilderBid | ValidatorRegistrationV1): Eth2Digest = + msg: deneb_mev.BuilderBid | electra_mev.BuilderBid | + ValidatorRegistrationV1): Eth2Digest = # Uses genesis fork version regardless doAssert fork.current_version == fork.previous_version @@ -386,7 +387,7 @@ proc get_builder_signature*( blsSign(privkey, signing_root.data) proc verify_builder_signature*( - fork: Fork, msg: deneb_mev.BuilderBid, + fork: Fork, msg: deneb_mev.BuilderBid | electra_mev.BuilderBid, pubkey: ValidatorPubKey | CookedPubKey, signature: SomeSig): bool = let signing_root = compute_builder_signing_root(fork, msg) blsVerify(pubkey, signing_root.data, signature) diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index 17982be428..d3a33d2b18 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -573,7 +573,6 @@ proc process_sync_aggregate*( # Apply participant and proposer rewards let indices = get_sync_committee_cache(state, cache).current_sync_committee - # TODO could use a sequtils2 zipIt for i in 0 ..< min( state.current_sync_committee.pubkeys.len, sync_aggregate.sync_committee_bits.len): diff --git a/beacon_chain/validators/beacon_validators.nim b/beacon_chain/validators/beacon_validators.nim index d57be0817a..7e70700df9 100644 --- a/beacon_chain/validators/beacon_validators.nim +++ b/beacon_chain/validators/beacon_validators.nim @@ -565,7 +565,8 @@ proc makeBeaconBlockForHeadAndSlot*( kzg_commitments = Opt.none(KzgCommitments)) proc getBlindedExecutionPayload[ - EPH: deneb_mev.BlindedExecutionPayloadAndBlobsBundle]( + EPH: deneb_mev.BlindedExecutionPayloadAndBlobsBundle | + electra_mev.BlindedExecutionPayloadAndBlobsBundle]( node: BeaconNode, payloadBuilderClient: RestClientRef, slot: Slot, executionBlockHash: Eth2Digest, pubkey: ValidatorPubKey): Future[BlindedBlockResult[EPH]] {.async: (raises: [CancelledError, RestError]).} = @@ -587,6 +588,22 @@ proc getBlindedExecutionPayload[ "Unable to decode Deneb blinded header: " & $res.error & " with HTTP status " & $response.status & ", Content-Type " & $response.contentType & " and content " & $response.data) + elif EPH is electra_mev.BlindedExecutionPayloadAndBlobsBundle: + let + response = awaitWithTimeout( + payloadBuilderClient.getHeaderElectra( + slot, executionBlockHash, pubkey), + BUILDER_PROPOSAL_DELAY_TOLERANCE): + return err "Timeout obtaining Electra blinded header from builder" + + res = decodeBytes( + GetHeaderResponseElectra, response.data, response.contentType) + + blindedHeader = res.valueOr: + return err( + "Unable to decode Electra blinded header: " & $res.error & + " with HTTP status " & $response.status & ", Content-Type " & + $response.contentType & " and content " & $response.data) else: static: doAssert false @@ -599,15 +616,12 @@ proc getBlindedExecutionPayload[ blindedHeader.data.message.pubkey, blindedHeader.data.signature): return err "getBlindedExecutionPayload: signature verification failed" - when EPH is deneb_mev.BlindedExecutionPayloadAndBlobsBundle: - template builderBid: untyped = blindedHeader.data.message - return ok(BuilderBid[EPH]( - blindedBlckPart: EPH( - execution_payload_header: builderBid.header, - blob_kzg_commitments: builderBid.blob_kzg_commitments), - executionPayloadValue: builderBid.value)) - else: - static: doAssert false + template builderBid: untyped = blindedHeader.data.message + return ok(BuilderBid[EPH]( + blindedBlckPart: EPH( + execution_payload_header: builderBid.header, + blob_kzg_commitments: builderBid.blob_kzg_commitments), + executionPayloadValue: builderBid.value)) from ./message_router_mev import copyFields, getFieldNames, unblindAndRouteBlockMEV @@ -634,6 +648,28 @@ proc constructSignableBlindedBlock[T: deneb_mev.SignedBlindedBeaconBlock]( blindedBlock +proc constructSignableBlindedBlock[T: electra_mev.SignedBlindedBeaconBlock]( + blck: electra.BeaconBlock, + blindedBundle: electra_mev.BlindedExecutionPayloadAndBlobsBundle): T = + # Leaves signature field default, to be filled in by caller + const + blckFields = getFieldNames(typeof(blck)) + blckBodyFields = getFieldNames(typeof(blck.body)) + + var blindedBlock: T + + # https://github.com/ethereum/builder-specs/blob/v0.4.0/specs/bellatrix/validator.md#block-proposal + copyFields(blindedBlock.message, blck, blckFields) + copyFields(blindedBlock.message.body, blck.body, blckBodyFields) + assign( + blindedBlock.message.body.execution_payload_header, + blindedBundle.execution_payload_header) + assign( + blindedBlock.message.body.blob_kzg_commitments, + blindedBundle.blob_kzg_commitments) + + blindedBlock + func constructPlainBlindedBlock[T: deneb_mev.BlindedBeaconBlock]( blck: ForkyBeaconBlock, blindedBundle: deneb_mev.BlindedExecutionPayloadAndBlobsBundle): T = @@ -658,7 +694,9 @@ func constructPlainBlindedBlock[T: deneb_mev.BlindedBeaconBlock]( blindedBlock -proc blindedBlockCheckSlashingAndSign[T: deneb_mev.SignedBlindedBeaconBlock]( +proc blindedBlockCheckSlashingAndSign[ + T: deneb_mev.SignedBlindedBeaconBlock | + electra_mev.SignedBlindedBeaconBlock]( node: BeaconNode, slot: Slot, validator: AttachedValidator, validator_index: ValidatorIndex, nonsignedBlindedBlock: T): Future[Result[T, string]] {.async: (raises: [CancelledError]).} = @@ -690,17 +728,21 @@ proc blindedBlockCheckSlashingAndSign[T: deneb_mev.SignedBlindedBeaconBlock]( return ok blindedBlock -proc getUnsignedBlindedBeaconBlock[T: deneb_mev.SignedBlindedBeaconBlock]( +proc getUnsignedBlindedBeaconBlock[ + T: deneb_mev.SignedBlindedBeaconBlock | + electra_mev.SignedBlindedBeaconBlock]( node: BeaconNode, slot: Slot, validator_index: ValidatorIndex, forkedBlock: ForkedBeaconBlock, - executionPayloadHeader: capella.ExecutionPayloadHeader | - deneb_mev.BlindedExecutionPayloadAndBlobsBundle): + executionPayloadHeader: deneb_mev.BlindedExecutionPayloadAndBlobsBundle | + electra_mev.BlindedExecutionPayloadAndBlobsBundle): Result[T, string] = withBlck(forkedBlock): when consensusFork >= ConsensusFork.Deneb: when not ( (T is deneb_mev.SignedBlindedBeaconBlock and - consensusFork == ConsensusFork.Deneb)): + consensusFork == ConsensusFork.Deneb) or + (T is electra_mev.SignedBlindedBeaconBlock and + consensusFork == ConsensusFork.Electra)): return err("getUnsignedBlindedBeaconBlock: mismatched block/payload types") else: return ok constructSignableBlindedBlock[T]( @@ -709,8 +751,8 @@ proc getUnsignedBlindedBeaconBlock[T: deneb_mev.SignedBlindedBeaconBlock]( return err("getUnsignedBlindedBeaconBlock: attempt to construct pre-Deneb blinded block") proc getBlindedBlockParts[ - EPH: capella.ExecutionPayloadHeader | - deneb_mev.BlindedExecutionPayloadAndBlobsBundle]( + EPH: deneb_mev.BlindedExecutionPayloadAndBlobsBundle | + electra_mev.BlindedExecutionPayloadAndBlobsBundle]( node: BeaconNode, payloadBuilderClient: RestClientRef, head: BlockRef, pubkey: ValidatorPubKey, slot: Slot, randao: ValidatorSig, validator_index: ValidatorIndex, graffiti: GraffitiBytes): @@ -755,18 +797,7 @@ proc getBlindedBlockParts[ # # This doesn't have withdrawals, which each node has regardless of engine or # builder API. makeBeaconBlockForHeadAndSlot fills it in later. - when EPH is capella.ExecutionPayloadHeader: - type PayloadType = capella.ExecutionPayloadForSigning - template actualEPH: untyped = executionPayloadHeader.get.blindedBlckPart - let withdrawals_root = - Opt.some executionPayloadHeader.get.blindedBlckPart.withdrawals_root - const kzg_commitments = Opt.none KzgCommitments - - var shimExecutionPayload: PayloadType - copyFields( - shimExecutionPayload.executionPayload, - executionPayloadHeader.get.blindedBlckPart, getFieldNames(EPH)) - elif EPH is deneb_mev.BlindedExecutionPayloadAndBlobsBundle: + when EPH is deneb_mev.BlindedExecutionPayloadAndBlobsBundle: type PayloadType = deneb.ExecutionPayloadForSigning template actualEPH: untyped = executionPayloadHeader.get.blindedBlckPart.execution_payload_header @@ -780,6 +811,20 @@ proc getBlindedBlockParts[ deneb_mev.BlindedExecutionPayloadAndBlobsBundle.execution_payload_header copyFields( shimExecutionPayload.executionPayload, actualEPH, getFieldNames(DenebEPH)) + elif EPH is electra_mev.BlindedExecutionPayloadAndBlobsBundle: + type PayloadType = electra.ExecutionPayloadForSigning + template actualEPH: untyped = + executionPayloadHeader.get.blindedBlckPart.execution_payload_header + let + withdrawals_root = Opt.some actualEPH.withdrawals_root + kzg_commitments = Opt.some( + executionPayloadHeader.get.blindedBlckPart.blob_kzg_commitments) + + var shimExecutionPayload: PayloadType + type ElectraEPH = + electra_mev.BlindedExecutionPayloadAndBlobsBundle.execution_payload_header + copyFields( + shimExecutionPayload.executionPayload, actualEPH, getFieldNames(ElectraEPH)) else: static: doAssert false @@ -803,7 +848,9 @@ proc getBlindedBlockParts[ forkedBlck.consensusBlockValue, forkedBlck.blck)) -proc getBuilderBid[SBBB: deneb_mev.SignedBlindedBeaconBlock]( +proc getBuilderBid[ + SBBB: deneb_mev.SignedBlindedBeaconBlock | + electra_mev.SignedBlindedBeaconBlock]( node: BeaconNode, payloadBuilderClient: RestClientRef, head: BlockRef, validator_pubkey: ValidatorPubKey, slot: Slot, randao: ValidatorSig, graffitiBytes: GraffitiBytes, validator_index: ValidatorIndex): @@ -812,6 +859,8 @@ proc getBuilderBid[SBBB: deneb_mev.SignedBlindedBeaconBlock]( ## Used by the BN's own validators, but not the REST server when SBBB is deneb_mev.SignedBlindedBeaconBlock: type EPH = deneb_mev.BlindedExecutionPayloadAndBlobsBundle + elif SBBB is electra_mev.SignedBlindedBeaconBlock: + type EPH = electra_mev.BlindedExecutionPayloadAndBlobsBundle else: static: doAssert false @@ -842,7 +891,9 @@ proc getBuilderBid[SBBB: deneb_mev.SignedBlindedBeaconBlock]( proc proposeBlockMEV( node: BeaconNode, payloadBuilderClient: RestClientRef, - blindedBlock: deneb_mev.SignedBlindedBeaconBlock): + blindedBlock: + deneb_mev.SignedBlindedBeaconBlock | + electra_mev.SignedBlindedBeaconBlock): Future[Result[BlockRef, string]] {.async: (raises: [CancelledError]).} = let unblindedBlockRef = await node.unblindAndRouteBlockMEV( payloadBuilderClient, blindedBlock) @@ -1198,10 +1249,7 @@ proc proposeBlock(node: BeaconNode, genesis_validators_root, node.config.localBlockValueBoost) return withConsensusFork(node.dag.cfg.consensusForkAtEpoch(slot.epoch)): - when consensusFork >= ConsensusFork.Electra: - debugRaiseAssert "can't propose electra block" - return default(BlockRef) - elif consensusFork >= ConsensusFork.Deneb: + when consensusFork >= ConsensusFork.Deneb: proposeBlockContinuation( consensusFork.SignedBlindedBeaconBlock, consensusFork.ExecutionPayloadForSigning) diff --git a/beacon_chain/validators/message_router_mev.nim b/beacon_chain/validators/message_router_mev.nim index 28834f27e6..2927476c89 100644 --- a/beacon_chain/validators/message_router_mev.nim +++ b/beacon_chain/validators/message_router_mev.nim @@ -33,11 +33,13 @@ macro copyFields*( dst: untyped, src: untyped, fieldNames: static[seq[string]]): untyped = result = newStmtList() for name in fieldNames: + debugRaiseAssert "deposit_receipts_root and exits_root are not currently filled in anywhere properly, so blinded electra proposals will fail" if name notin [ # These fields are the ones which vary between the blinded and # unblinded objects, and can't simply be copied. "transactions_root", "execution_payload", - "execution_payload_header", "body", "withdrawals_root"]: + "execution_payload_header", "body", "withdrawals_root", + "deposit_receipts_root", "exits_root"]: # TODO use stew/assign2 result.add newAssignment( newDotExpr(dst, ident(name)), newDotExpr(src, ident(name))) diff --git a/beacon_chain/validators/validator_pool.nim b/beacon_chain/validators/validator_pool.nim index d0f1eb7cab..05046f320e 100644 --- a/beacon_chain/validators/validator_pool.nim +++ b/beacon_chain/validators/validator_pool.nim @@ -502,7 +502,8 @@ proc getBlockSignature*(v: AttachedValidator, fork: Fork, block_root: Eth2Digest, blck: ForkedBeaconBlock | ForkedBlindedBeaconBlock | ForkedMaybeBlindedBeaconBlock | - deneb_mev.BlindedBeaconBlock + deneb_mev.BlindedBeaconBlock | + electra_mev.BlindedBeaconBlock ): Future[SignatureResult] {.async: (raises: [CancelledError]).} = type SomeBlockBody = @@ -578,6 +579,19 @@ proc getBlockSignature*(v: AttachedValidator, fork: Fork, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Deneb, data: blck.toBeaconBlockHeader), proofs) + elif blck is electra_mev.BlindedBeaconBlock: + case v.data.remoteType + of RemoteSignerType.Web3Signer: + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Electra, + data: blck.toBeaconBlockHeader)) + of RemoteSignerType.VerifyingWeb3Signer: + let proofs = blockPropertiesProofs( + blck.body, electraIndex) + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Electra, + data: blck.toBeaconBlockHeader), + proofs) elif blck is ForkedMaybeBlindedBeaconBlock: withForkyMaybeBlindedBlck(blck): # TODO why isn't this a case statement