diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index f5299e261..f2da8d0f0 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -66,6 +66,7 @@ type lightClientPool*: ref LightClientPool exitPool*: ref ExitPool eth1Monitor*: Eth1Monitor + payloadBuilderRestClient*: RestClientRef restServer*: RestServerRef keymanagerServer*: RestServerRef keymanagerToken*: Option[string] diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index 725f44945..d63a92e47 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -541,6 +541,18 @@ type desc: "Suggested fee recipient" name: "suggested-fee-recipient" .}: Option[Address] + payloadBuilderEnable* {. + hidden + desc: "Enable external payload builder" + defaultValue: false + name: "payload-builder-enable" .}: bool + + payloadBuilderUrl* {. + hidden + desc: "Payload builder URL" + defaultValue: "" + name: "payload-builder-url" .}: string + of BNStartUpCmd.createTestnet: testnetDepositsFile* {. desc: "A LaunchPad deposits file for the genesis state validators" diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index a50b1bd5f..2cdb5f615 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -770,6 +770,17 @@ proc init*(T: type BeaconNode, max(Moment.init(bellatrixEpochTime, Second), Moment.now) + let payloadBuilderRestClient = + if config.payloadBuilderEnable: + RestClientRef.new( + config.payloadBuilderUrl, + httpFlags = {HttpClientFlag.NewConnectionAlways}).valueOr: + warn "Payload builder REST client setup failed", + payloadBuilderUrl = config.payloadBuilderUrl + nil + else: + nil + let node = BeaconNode( nickname: nickname, graffitiBytes: if config.graffiti.isSome: config.graffiti.get @@ -780,6 +791,7 @@ proc init*(T: type BeaconNode, config: config, attachedValidators: validatorPool, eth1Monitor: eth1Monitor, + payloadBuilderRestClient: payloadBuilderRestClient, restServer: restServer, keymanagerServer: keymanagerServer, keymanagerToken: keymanagerToken, @@ -801,7 +813,7 @@ proc init*(T: type BeaconNode, node -func strictVerification(node: BeaconNode, slot: Slot) = +func verifyFinalization(node: BeaconNode, slot: Slot) = # Epoch must be >= 4 to check finalization const SETTLING_TIME_OFFSET = 1'u64 let epoch = slot.epoch() @@ -1376,7 +1388,7 @@ proc onSlotStart(node: BeaconNode, wallTime: BeaconTime, wallSlot.epoch.toGaugeValue - finalizedEpoch.toGaugeValue) if node.config.strictVerification: - strictVerification(node, wallSlot) + verifyFinalization(node, wallSlot) node.consensusManager[].updateHead(wallSlot) diff --git a/beacon_chain/nimbus_signing_node.nim b/beacon_chain/nimbus_signing_node.nim index 3b99246a1..56e410f11 100644 --- a/beacon_chain/nimbus_signing_node.nim +++ b/beacon_chain/nimbus_signing_node.nim @@ -320,6 +320,21 @@ proc installApiHandlers*(node: SigningNode) = validator.data.privateKey) signature = cooked.toValidatorSig().toHex() signatureResponse(Http200, signature) + of Web3SignerRequestKind.ValidatorRegistration: + let + forkInfo = request.forkInfo.get() + cooked = get_builder_signature( + forkInfo.fork, ValidatorRegistrationV1( + fee_recipient: + ExecutionAddress(data: distinctBase(Eth1Address.fromHex( + request.validatorRegistration.feeRecipient))), + gas_limit: request.validatorRegistration.gasLimit, + timestamp: request.validatorRegistration.timestamp, + pubkey: request.validatorRegistration.pubkey, + ), + validator.data.privateKey) + signature = cooked.toValidatorSig().toHex() + signatureResponse(Http200, signature) proc validate(key: string, value: string): int = case key diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index 52a520add..61c763d82 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -819,14 +819,10 @@ template unrecognizedFieldWarning = fieldName, typeName = typetraits.name(typeof value) ## ForkedBeaconBlock -proc readValue*[BlockType: Web3SignerForkedBeaconBlock|ForkedBeaconBlock]( - reader: var JsonReader[RestJson], - value: var BlockType) {.raises: [IOError, SerializationError, Defect].} = - var - version: Option[BeaconBlockFork] - data: Option[JsonString] - - for fieldName in readObjectFields(reader): +template prepareForkedBlockReading( + reader: var JsonReader[RestJson], value: untyped, + version: var Option[BeaconBlockFork], data: var Option[JsonString]) = + for fieldName {.inject.} in readObjectFields(reader): case fieldName of "version": if version.isSome(): @@ -855,6 +851,15 @@ proc readValue*[BlockType: Web3SignerForkedBeaconBlock|ForkedBeaconBlock]( if data.isNone(): reader.raiseUnexpectedValue("Field data is missing") +proc readValue*[BlockType: ForkedBeaconBlock]( + reader: var JsonReader[RestJson], + value: var BlockType) {.raises: [IOError, SerializationError, Defect].} = + var + version: Option[BeaconBlockFork] + data: Option[JsonString] + + prepareForkedBlockReading(reader, value, version, data) + case version.get(): of BeaconBlockFork.Phase0: let res = @@ -893,6 +898,58 @@ proc readValue*[BlockType: Web3SignerForkedBeaconBlock|ForkedBeaconBlock]( reader.raiseUnexpectedValue("Incorrect bellatrix block format") value = ForkedBeaconBlock.init(res.get()).BlockType +proc readValue*[BlockType: Web3SignerForkedBeaconBlock]( + reader: var JsonReader[RestJson], + value: var BlockType) {.raises: [IOError, SerializationError, Defect].} = + var + version: Option[BeaconBlockFork] + data: Option[JsonString] + + prepareForkedBlockReading(reader, value, version, data) + + case version.get(): + of BeaconBlockFork.Phase0: + let res = + try: + some(RestJson.decode(string(data.get()), + phase0.BeaconBlock, + requireAllFields = true, + allowUnknownFields = true)) + except SerializationError: + none[phase0.BeaconBlock]() + if res.isNone(): + reader.raiseUnexpectedValue("Incorrect phase0 block format") + value = Web3SignerForkedBeaconBlock( + kind: BeaconBlockFork.Phase0, + phase0Data: res.get()) + of BeaconBlockFork.Altair: + let res = + try: + some(RestJson.decode(string(data.get()), + altair.BeaconBlock, + requireAllFields = true, + allowUnknownFields = true)) + except SerializationError: + none[altair.BeaconBlock]() + if res.isNone(): + reader.raiseUnexpectedValue("Incorrect altair block format") + value = Web3SignerForkedBeaconBlock( + kind: BeaconBlockFork.Altair, + altairData: res.get()) + of BeaconBlockFork.Bellatrix: + let res = + try: + some(RestJson.decode(string(data.get()), + BeaconBlockHeader, + requireAllFields = true, + allowUnknownFields = true)) + except SerializationError: + none[BeaconBlockHeader]() + if res.isNone(): + reader.raiseUnexpectedValue("Incorrect bellatrix block format") + value = Web3SignerForkedBeaconBlock( + kind: BeaconBlockFork.Bellatrix, + bellatrixData: res.get()) proc writeValue*[BlockType: Web3SignerForkedBeaconBlock|ForkedBeaconBlock]( writer: var JsonWriter[RestJson], @@ -1441,6 +1498,9 @@ proc writeValue*(writer: var JsonWriter[RestJson], writer.writeField("fork_info", value.forkInfo.get()) if isSome(value.signingRoot): writer.writeField("signingRoot", value.signingRoot) + + # https://github.com/ConsenSys/web3signer/blob/41834a927088f1bde7a097e17d19e954d0058e54/core/src/main/resources/openapi-specs/eth2/signing/schemas.yaml#L421-L425 (branch v22.7.0) + # It's the "beacon_block" field even when it's not a block, but a header writer.writeField("beacon_block", value.beaconBlock) of Web3SignerRequestKind.Deposit: writer.writeField("type", "DEPOSIT") @@ -1489,6 +1549,15 @@ proc writeValue*(writer: var JsonWriter[RestJson], writer.writeField("signingRoot", value.signingRoot) writer.writeField("contribution_and_proof", value.syncCommitteeContributionAndProof) + of Web3SignerRequestKind.ValidatorRegistration: + # https://consensys.github.io/web3signer/web3signer-eth2.html#operation/ETH2_SIGN + doAssert(value.forkInfo.isSome(), + "forkInfo should be set for this type of request") + writer.writeField("type", "VALIDATOR_REGISTRATION") + writer.writeField("fork_info", value.forkInfo.get()) + if isSome(value.signingRoot): + writer.writeField("signingRoot", value.signingRoot) + writer.writeField("validator_registration", value.validatorRegistration) writer.endRecord() proc readValue*(reader: var JsonReader[RestJson], @@ -1532,6 +1601,8 @@ proc readValue*(reader: var JsonReader[RestJson], Web3SignerRequestKind.SyncCommitteeSelectionProof of "SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF": Web3SignerRequestKind.SyncCommitteeContributionAndProof + of "VALIDATOR_REGISTRATION": + Web3SignerRequestKind.ValidatorRegistration else: reader.raiseUnexpectedValue("Unexpected `type` value") ) @@ -1625,6 +1696,8 @@ proc readValue*(reader: var JsonReader[RestJson], forkInfo: forkInfo, signingRoot: signingRoot, blck: data ) of Web3SignerRequestKind.BlockV2: + # https://github.com/ConsenSys/web3signer/blob/41834a927088f1bde7a097e17d19e954d0058e54/core/src/main/resources/openapi-specs/eth2/signing/schemas.yaml#L421-L425 (branch v22.7.0) + # It's the "beacon_block" field even when it's not a block, but a header if dataName != "beacon_block": reader.raiseUnexpectedValue("Field `beacon_block` is missing") if forkInfo.isNone(): @@ -1740,6 +1813,25 @@ proc readValue*(reader: var JsonReader[RestJson], forkInfo: forkInfo, signingRoot: signingRoot, syncCommitteeContributionAndProof: data ) + of Web3SignerRequestKind.ValidatorRegistration: + if dataName != "validator_registration": + reader.raiseUnexpectedValue( + "Field `validator_registration` is missing") + if forkInfo.isNone(): + reader.raiseUnexpectedValue("Field `fork_info` is missing") + let data = + block: + let res = + decodeJsonString(Web3SignerValidatorRegistration, data.get()) + if res.isErr(): + reader.raiseUnexpectedValue( + "Incorrect field `validator_registration` format") + res.get() + Web3SignerRequest( + kind: Web3SignerRequestKind.ValidatorRegistration, + forkInfo: forkInfo, signingRoot: signingRoot, + validatorRegistration: data + ) ## RemoteKeystoreStatus proc writeValue*(writer: var JsonWriter[RestJson], diff --git a/beacon_chain/spec/eth2_apis/rest_types.nim b/beacon_chain/spec/eth2_apis/rest_types.nim index a98ba9d17..af1dfb4a2 100644 --- a/beacon_chain/spec/eth2_apis/rest_types.nim +++ b/beacon_chain/spec/eth2_apis/rest_types.nim @@ -486,10 +486,20 @@ type serializedFieldName: "beacon_block_root".}: Eth2Digest slot*: Slot + # https://consensys.github.io/web3signer/web3signer-eth2.html#operation/ETH2_SIGN + Web3SignerValidatorRegistration* = object + feeRecipient* {. + serializedFieldName: "fee_recipient".}: string + gasLimit* {. + serializedFieldName: "gas_limit".}: uint64 + timestamp*: uint64 + pubkey*: ValidatorPubKey + Web3SignerRequestKind* {.pure.} = enum AggregationSlot, AggregateAndProof, Attestation, Block, BlockV2, Deposit, RandaoReveal, VoluntaryExit, SyncCommitteeMessage, - SyncCommitteeSelectionProof, SyncCommitteeContributionAndProof + SyncCommitteeSelectionProof, SyncCommitteeContributionAndProof, + ValidatorRegistration Web3SignerRequest* = object signingRoot*: Option[Eth2Digest] @@ -528,6 +538,10 @@ type of Web3SignerRequestKind.SyncCommitteeContributionAndProof: syncCommitteeContributionAndProof* {. serializedFieldName: "contribution_and_proof".}: ContributionAndProof + of Web3SignerRequestKind.ValidatorRegistration: + validatorRegistration* {. + serializedFieldName: "validator_registration".}: + Web3SignerValidatorRegistration GetBlockResponse* = DataEnclosedObject[phase0.SignedBeaconBlock] GetStateResponse* = DataEnclosedObject[phase0.BeaconState] @@ -774,6 +788,26 @@ func init*(t: typedesc[Web3SignerRequest], fork: Fork, syncCommitteeContributionAndProof: data ) +from stew/byteutils import to0xHex + +func init*(t: typedesc[Web3SignerRequest], fork: Fork, + genesis_validators_root: Eth2Digest, + data: ValidatorRegistrationV1, + signingRoot: Option[Eth2Digest] = none[Eth2Digest]() + ): Web3SignerRequest = + Web3SignerRequest( + kind: Web3SignerRequestKind.ValidatorRegistration, + forkInfo: some(Web3SignerForkInfo( + fork: fork, genesis_validators_root: genesis_validators_root + )), + signingRoot: signingRoot, + validatorRegistration: Web3SignerValidatorRegistration( + feeRecipient: data.fee_recipient.data.to0xHex, + gasLimit: data.gas_limit, + timestamp: data.timestamp, + pubkey: data.pubkey) + ) + func init*(t: typedesc[RestSyncCommitteeMessage], slot: Slot, beacon_block_root: Eth2Digest, diff --git a/beacon_chain/spec/forks.nim b/beacon_chain/spec/forks.nim index 045ff49f3..c9eadc69e 100644 --- a/beacon_chain/spec/forks.nim +++ b/beacon_chain/spec/forks.nim @@ -15,7 +15,8 @@ import chronicles, ../extras, "."/[block_id, eth2_merkleization, eth2_ssz_serialization, presets], - ./datatypes/[phase0, altair, bellatrix] + ./datatypes/[phase0, altair, bellatrix], + ./mev/bellatrix_mev export extras, block_id, phase0, altair, bellatrix, eth2_merkleization, @@ -110,7 +111,11 @@ type of BeaconBlockFork.Altair: altairData*: altair.BeaconBlock of BeaconBlockFork.Bellatrix: bellatrixData*: bellatrix.BeaconBlock - Web3SignerForkedBeaconBlock* {.borrow: `.`} = distinct ForkedBeaconBlock + Web3SignerForkedBeaconBlock* = object + case kind*: BeaconBlockFork + of BeaconBlockFork.Phase0: phase0Data*: phase0.BeaconBlock + of BeaconBlockFork.Altair: altairData*: altair.BeaconBlock + of BeaconBlockFork.Bellatrix: bellatrixData*: BeaconBlockHeader ForkedTrustedBeaconBlock* = object case kind*: BeaconBlockFork @@ -452,11 +457,10 @@ template withBlck*( func proposer_index*(x: ForkedBeaconBlock): uint64 = withBlck(x): blck.proposer_index -func hash_tree_root*(x: ForkedBeaconBlock): Eth2Digest = +func hash_tree_root*(x: ForkedBeaconBlock | Web3SignerForkedBeaconBlock): + Eth2Digest = withBlck(x): hash_tree_root(blck) -func hash_tree_root*(x: Web3SignerForkedBeaconBlock): Eth2Digest {.borrow.} - template getForkedBlockField*( x: ForkedSignedBeaconBlock | ForkedMsgTrustedSignedBeaconBlock | @@ -522,7 +526,7 @@ template withStateAndBlck*( body func toBeaconBlockHeader*( - blck: SomeForkyBeaconBlock): BeaconBlockHeader = + blck: SomeForkyBeaconBlock | BlindedBeaconBlock): BeaconBlockHeader = ## Reduce a given `BeaconBlock` to its `BeaconBlockHeader`. BeaconBlockHeader( slot: blck.slot, diff --git a/beacon_chain/spec/mev/bellatrix_mev.nim b/beacon_chain/spec/mev/bellatrix_mev.nim index 290099124..9f9286e98 100644 --- a/beacon_chain/spec/mev/bellatrix_mev.nim +++ b/beacon_chain/spec/mev/bellatrix_mev.nim @@ -26,7 +26,7 @@ type signature*: ValidatorSig # https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/builder.md#builderbid - BuilderBid = object + BuilderBid* = object header*: ExecutionPayloadHeader value*: UInt256 pubkey*: ValidatorPubKey @@ -67,5 +67,5 @@ const DOMAIN_APPLICATION_BUILDER* = DomainType([byte 0x00, 0x00, 0x00, 0x01]) # https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#constants - EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION* = 1.Epoch + EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION* = 1 BUILDER_PROPOSAL_DELAY_TOLERANCE* = 1.seconds diff --git a/beacon_chain/spec/mev/rest_bellatrix_mev_calls.nim b/beacon_chain/spec/mev/rest_bellatrix_mev_calls.nim index 45a1f1774..e582f58f6 100644 --- a/beacon_chain/spec/mev/rest_bellatrix_mev_calls.nim +++ b/beacon_chain/spec/mev/rest_bellatrix_mev_calls.nim @@ -39,5 +39,4 @@ proc submitBlindedBlock*(body: SignedBlindedBeaconBlock proc checkBuilderStatus*(): RestPlainResponse {. rest, endpoint: "/eth/v1/builder/status", meth: MethodGet.} - ## https://github.com/ethereum/builder-specs/blob/v0.1.0/apis/builder/status.yaml ## https://github.com/ethereum/builder-specs/blob/v0.2.0/apis/builder/status.yaml diff --git a/beacon_chain/spec/signatures.nim b/beacon_chain/spec/signatures.nim index 50c0d142a..a3d86f769 100644 --- a/beacon_chain/spec/signatures.nim +++ b/beacon_chain/spec/signatures.nim @@ -320,7 +320,6 @@ proc get_contribution_and_proof_signature*( blsSign(privkey, signing_root.data) - # https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/altair/validator.md#aggregation-selection func is_sync_committee_aggregator*(signature: ValidatorSig): bool = let @@ -336,3 +335,25 @@ proc verify_contribution_and_proof_signature*( fork, genesis_validators_root, msg) blsVerify(pubkey, signing_root.data, signature) + +# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/builder.md#signing +func compute_builder_signing_root*( + fork: Fork, msg: BuilderBid | ValidatorRegistrationV1): Eth2Digest = + # Uses genesis fork version regardless + doAssert fork.current_version == fork.previous_version + + let domain = get_domain( + fork, DOMAIN_APPLICATION_BUILDER, GENESIS_EPOCH, ZERO_HASH) + compute_signing_root(msg, domain) + +proc get_builder_signature*( + fork: Fork, msg: ValidatorRegistrationV1, privkey: ValidatorPrivKey): + CookedSig = + let signing_root = compute_builder_signing_root(fork, msg) + blsSign(privkey, signing_root.data) + +proc verify_builder_signature*( + fork: Fork, msg: 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.nim b/beacon_chain/spec/state_transition.nim index 0fc6d4031..087833417 100644 --- a/beacon_chain/spec/state_transition.nim +++ b/beacon_chain/spec/state_transition.nim @@ -518,7 +518,10 @@ proc makeBeaconBlock*( # TODO: # `verificationFlags` is needed only in tests and can be # removed if we don't use invalid signatures there - verificationFlags: UpdateFlags = {}): Result[ForkedBeaconBlock, cstring] = + verificationFlags: UpdateFlags = {}, + transactions_root: Opt[Eth2Digest] = Opt.none Eth2Digest, + execution_payload_root: Opt[Eth2Digest] = Opt.none Eth2Digest): + Result[ForkedBeaconBlock, cstring] = ## Create a block for the given state. The latest block applied to it will ## be used for the parent_root value, and the slot will be take from ## state.slot meaning process_slots must be called up to the slot for which @@ -540,6 +543,30 @@ proc makeBeaconBlock*( rollback(state) return err(res.error()) + # Override for MEV + if transactions_root.isSome and execution_payload_root.isSome: + withState(state): + static: doAssert high(BeaconStateFork) == BeaconStateFork.Bellatrix + when stateFork == BeaconStateFork.Bellatrix: + state.data.latest_execution_payload_header.transactions_root = + transactions_root.get + + # https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/bellatrix/beacon-chain.md#beaconblockbody + # Effectively hash_tree_root(ExecutionPayload) with the beacon block + # body, with the execution payload replaced by the execution payload + # header. htr(payload) == htr(payload header), so substitute. + state.data.latest_block_header.body_root = hash_tree_root( + [hash_tree_root(randao_reveal), + hash_tree_root(eth1_data), + hash_tree_root(graffiti), + hash_tree_root(exits.proposer_slashings), + hash_tree_root(exits.attester_slashings), + hash_tree_root(List[Attestation, Limit MAX_ATTESTATIONS](attestations)), + hash_tree_root(List[Deposit, Limit MAX_DEPOSITS](deposits)), + hash_tree_root(exits.voluntary_exits), + hash_tree_root(sync_aggregate), + execution_payload_root.get]) + state.`kind Data`.root = hash_tree_root(state.`kind Data`.data) blck.`kind Data`.state_root = state.`kind Data`.root diff --git a/beacon_chain/spec/state_transition_epoch.nim b/beacon_chain/spec/state_transition_epoch.nim index 38701e8d3..f657d34f8 100644 --- a/beacon_chain/spec/state_transition_epoch.nim +++ b/beacon_chain/spec/state_transition_epoch.nim @@ -863,7 +863,7 @@ func process_registry_updates*( # https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/bellatrix/beacon-chain.md#slashings func get_adjusted_total_slashing_balance*( state: ForkyBeaconState, total_balance: Gwei): Gwei = - let multiplier = + const multiplier = # tradeoff here about interleaving phase0/altair, but for these # single-constant changes... when state is phase0.BeaconState: diff --git a/beacon_chain/validators/validator_duties.nim b/beacon_chain/validators/validator_duties.nim index 36b7caa8c..45bb3200a 100644 --- a/beacon_chain/validators/validator_duties.nim +++ b/beacon_chain/validators/validator_duties.nim @@ -44,11 +44,10 @@ import ../sszdump, ../sync/sync_manager, ../gossip_processing/block_processor, ".."/[conf, beacon_clock, beacon_node], - "."/[slashing_protection, validator_pool, keystore_management] + "."/[slashing_protection, validator_pool, keystore_management], + ".."/spec/mev/rest_bellatrix_mev_calls from eth/async_utils import awaitWithTimeout -from web3/engine_api import ForkchoiceUpdatedResponse -from web3/engine_api_types import PayloadExecutionStatus # Metrics for tracking attestation and beacon block loss const delayBuckets = [-Inf, -4.0, -2.0, -1.0, -0.5, -0.1, -0.05, @@ -308,6 +307,8 @@ proc getBlockProposalEth1Data*(node: BeaconNode, state, finalizedEpochRef.eth1_data, finalizedEpochRef.eth1_deposit_index) +from web3/engine_api import ForkchoiceUpdatedResponse + # TODO: This copies the entire BeaconState on each call proc forkchoice_updated(state: bellatrix.BeaconState, head_block_hash: Eth2Digest, @@ -343,6 +344,8 @@ proc get_execution_payload( asConsensusExecutionPayload( await execution_engine.getPayload(payload_id.get)) +from web3/engine_api_types import PayloadExecutionStatus + proc getExecutionPayload( node: BeaconNode, proposalState: auto, epoch: Epoch, @@ -413,12 +416,14 @@ proc getExecutionPayload( msg = err.msg return empty_execution_payload -proc makeBeaconBlockForHeadAndSlot*(node: BeaconNode, - randao_reveal: ValidatorSig, - validator_index: ValidatorIndex, - graffiti: GraffitiBytes, - head: BlockRef, slot: Slot - ): Future[ForkedBlockResult] {.async.} = +proc makeBeaconBlockForHeadAndSlot*( + node: BeaconNode, randao_reveal: ValidatorSig, + validator_index: ValidatorIndex, graffiti: GraffitiBytes, head: BlockRef, + slot: Slot, + execution_payload: Opt[ExecutionPayload] = Opt.none(ExecutionPayload), + transactions_root: Opt[Eth2Digest] = Opt.none(Eth2Digest), + execution_payload_root: Opt[Eth2Digest] = Opt.none(Eth2Digest)): + Future[ForkedBlockResult] {.async.} = # Advance state to the slot that we're proposing for let proposalState = assignClone(node.dag.headState) @@ -461,12 +466,15 @@ proc makeBeaconBlockForHeadAndSlot*(node: BeaconNode, SyncAggregate.init() else: node.syncCommitteeMsgPool[].produceSyncAggregate(head.root), - if slot.epoch < node.dag.cfg.BELLATRIX_FORK_EPOCH or - not ( - is_merge_transition_complete(proposalState.bellatrixData.data) or - ((not node.eth1Monitor.isNil) and - node.eth1Monitor.terminalBlockHash.isSome)): - default(bellatrix.ExecutionPayload) + if executionPayload.isSome: + executionPayload.get + elif slot.epoch < node.dag.cfg.BELLATRIX_FORK_EPOCH or + not ( + is_merge_transition_complete(proposalState.bellatrixData.data) or + ((not node.eth1Monitor.isNil) and + node.eth1Monitor.terminalBlockHash.isSome)): + # https://github.com/nim-lang/Nim/issues/19802 + (static(default(bellatrix.ExecutionPayload))) else: let pubkey = node.dag.validatorKey(validator_index) (await getExecutionPayload( @@ -475,7 +483,17 @@ proc makeBeaconBlockForHeadAndSlot*(node: BeaconNode, # TODO https://github.com/nim-lang/Nim/issues/19802 if pubkey.isSome: pubkey.get.toPubKey else: default(ValidatorPubKey))), noRollback, # Temporary state - no need for rollback - cache) + cache, + transactions_root = + if transactions_root.isSome: + Opt.some transactions_root.get + else: + Opt.none(Eth2Digest), + execution_payload_root = + if execution_payload_root.isSome: + Opt.some execution_payload_root.get + else: + Opt.none Eth2Digest) if res.isErr(): # This is almost certainly a bug, but it's complex enough that there's a # small risk it might happen even when most proposals succeed - thus we @@ -489,6 +507,226 @@ proc makeBeaconBlockForHeadAndSlot*(node: BeaconNode, head = shortLog(head), slot +proc getBlindedExecutionPayload( + node: BeaconNode, slot: Slot, executionBlockRoot: Eth2Digest, + pubkey: ValidatorPubKey): + Future[Result[ExecutionPayloadHeader, cstring]] {.async.} = + if node.payloadBuilderRestClient.isNil: + return err "getBlindedBeaconBlock: nil REST client" + + let blindedHeader = await node.payloadBuilderRestClient.getHeader( + slot, executionBlockRoot, pubkey) + + const httpOk = 200 + if blindedHeader.status != httpOk: + return err "getBlindedExecutionPayload: non-200 HTTP response" + else: + if not verify_builder_signature( + node.dag.cfg.genesisFork, blindedHeader.data.data.message, + blindedHeader.data.data.message.pubkey, + blindedHeader.data.data.signature): + return err "getBlindedExecutionPayload: signature verification failed" + + return ok blindedHeader.data.data.message.header + +import std/macros + +func getFieldNames(x: typedesc[auto]): seq[string] {.compileTime.} = + var res: seq[string] + for name, _ in fieldPairs(default(x)): + res.add name + res + +macro copyFields( + dst: untyped, src: untyped, fieldNames: static[seq[string]]): untyped = + result = newStmtList() + for name in fieldNames: + 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"]: + result.add newAssignment( + newDotExpr(dst, ident(name)), newDotExpr(src, ident(name))) + +proc getBlindedBeaconBlock( + node: BeaconNode, slot: Slot, head: BlockRef, validator: AttachedValidator, + validator_index: ValidatorIndex, forkedBlock: ForkedBeaconBlock, + executionPayloadHeader: ExecutionPayloadHeader): + Future[Result[SignedBlindedBeaconBlock, string]] {.async.} = + const + blckFields = getFieldNames(typeof(forkedBlock.bellatrixData)) + blckBodyFields = getFieldNames(typeof(forkedBlock.bellatrixData.body)) + + # https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#block-proposal + var blindedBlock: SignedBlindedBeaconBlock + + copyFields(blindedBlock.message, forkedBlock.bellatrixData, blckFields) + copyFields( + blindedBlock.message.body, forkedBlock.bellatrixData.body, blckBodyFields) + blindedBlock.message.body.execution_payload_header = executionPayloadHeader + + # Check with slashing protection before submitBlindedBlock + let + fork = node.dag.forkAtEpoch(slot.epoch) + genesis_validators_root = node.dag.genesis_validators_root + blockRoot = hash_tree_root(blindedBlock.message) + signing_root = compute_block_signing_root( + fork, genesis_validators_root, slot, blockRoot) + notSlashable = node.attachedValidators + .slashingProtection + .registerBlock(validator_index, validator.pubkey, slot, signing_root) + + if notSlashable.isErr: + warn "Slashing protection activated for MEV block", + validator = validator.pubkey, + slot = slot, + existingProposal = notSlashable.error + return err("MEV proposal would be slashable: " & $notSlashable.error) + + blindedBlock.signature = + block: + let res = await validator.getBlockSignature( + fork, genesis_validators_root, slot, blockRoot, blindedBlock.message) + if res.isErr(): + return err("Unable to sign block: " & res.error()) + res.get() + + return ok blindedBlock + +proc proposeBlockMEV( + node: BeaconNode, head: BlockRef, validator: AttachedValidator, slot: Slot, + randao: ValidatorSig, validator_index: ValidatorIndex): + Future[Opt[BlockRef]] {.async.} = + let + executionBlockRoot = node.dag.loadExecutionBlockRoot(head) + executionPayloadHeader = awaitWithTimeout( + node.getBlindedExecutionPayload( + slot, executionBlockRoot, validator.pubkey), + BUILDER_PROPOSAL_DELAY_TOLERANCE): + Result[ExecutionPayloadHeader, cstring].err( + "getBlindedExecutionPayload timed out") + + if executionPayloadHeader.isErr: + debug "proposeBlockMEV: getBlindedExecutionPayload failed", + error = executionPayloadHeader.error + # Haven't committed to the MEV block, so allow EL fallback. + return Opt.none BlockRef + + # When creating this block, need to ensure it uses the MEV-provided execution + # payload, both to avoid repeated calls to network services and to ensure the + # consistency of this block (e.g., its state root being correct). Since block + # processing does not work directly using blinded blocks, fix up transactions + # root after running the state transition function on an otherwise equivalent + # non-blinded block without transactions. + var shimExecutionPayload: ExecutionPayload + copyFields( + shimExecutionPayload, executionPayloadHeader.get, + getFieldNames(ExecutionPayloadHeader)) + + let newBlock = await makeBeaconBlockForHeadAndSlot( + node, randao, validator_index, node.graffitiBytes, head, slot, + execution_payload = Opt.some shimExecutionPayload, + transactions_root = Opt.some executionPayloadHeader.get.transactions_root, + execution_payload_root = + Opt.some hash_tree_root(executionPayloadHeader.get)) + + if newBlock.isErr(): + # Haven't committed to the MEV block, so allow EL fallback. + return Opt.none BlockRef # already logged elsewhere! + + let forkedBlck = newBlock.get() + + # This is only substantively asynchronous with a remote key signer + let blindedBlock = awaitWithTimeout( + node.getBlindedBeaconBlock( + slot, head, validator, validator_index, forkedBlck, + executionPayloadHeader.get), + 500.milliseconds): + Result[SignedBlindedBeaconBlock, string].err "getBlindedBlock timed out" + + if blindedBlock.isOk: + # By time submitBlindedBlock is called, must already have done slashing + # protection check + let unblindedPayload = + try: + await node.payloadBuilderRestClient.submitBlindedBlock( + blindedBlock.get) + # From here on, including error paths, disallow local EL production by + # returning Opt.some, regardless of whether on head or newBlock. + except RestDecodingError as exc: + info "proposeBlockMEV: REST recoding error", + slot, head = shortLog(head), validator_index, blindedBlock, + error = exc.msg + return Opt.some head + except CatchableError as exc: + info "proposeBlockMEV: exception in submitBlindedBlock", + slot, head = shortLog(head), validator_index, blindedBlock, + error = exc.msg + return Opt.some head + + const httpOk = 200 + if unblindedPayload.status == httpOk: + if hash_tree_root( + blindedBlock.get.message.body.execution_payload_header) != + hash_tree_root(unblindedPayload.data.data): + debug "proposeBlockMEV: unblinded payload doesn't match blinded payload", + blindedPayload = + blindedBlock.get.message.body.execution_payload_header + else: + # Signature provided is consistent with unblinded execution payload, + # so construct full beacon block + # https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#block-proposal + var signedBlock = bellatrix.SignedBeaconBlock( + signature: blindedBlock.get.signature) + copyFields( + signedBlock.message, blindedBlock.get.message, + getFieldNames(typeof(signedBlock.message))) + copyFields( + signedBlock.message.body, blindedBlock.get.message.body, + getFieldNames(typeof(signedBlock.message.body))) + signedBlock.message.body.execution_payload = unblindedPayload.data.data + + signedBlock.root = hash_tree_root(signedBlock.message) + + doAssert signedBlock.root == hash_tree_root(blindedBlock.get.message) + + debug "proposeBlockMEV: proposing unblinded block", + blck = shortLog(signedBlock) + + let newBlockRef = + (await node.router.routeSignedBeaconBlock(signedBlock)).valueOr: + # submitBlindedBlock has run, so don't allow fallback to run + return Opt.some head # Errors logged in router + + if newBlockRef.isNone(): + return Opt.some head # Validation errors logged in router + + notice "Block proposed (MEV)", + blockRoot = shortLog(signedBlock.root), blck = shortLog(signedBlock), + signature = shortLog(signedBlock.signature), validator = shortLog(validator) + + beacon_blocks_proposed.inc() + + return Opt.some newBlockRef.get() + else: + debug "proposeBlockMEV: submitBlindedBlock failed", + slot, head = shortLog(head), validator_index, blindedBlock, + payloadStatus = unblindedPayload.status + + # https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#proposer-slashing + # This means if a validator publishes a signature for a + # `BlindedBeaconBlock` (via a dissemination of a + # `SignedBlindedBeaconBlock`) then the validator **MUST** not use the + # local build process as a fallback, even in the event of some failure + # with the external buildernetwork. + return Opt.some head + else: + info "proposeBlockMEV: getBlindedBeaconBlock failed", + slot, head = shortLog(head), validator_index, blindedBlock, + error = blindedBlock.error + return Opt.none BlockRef + proc proposeBlock(node: BeaconNode, validator: AttachedValidator, validator_index: ValidatorIndex, @@ -516,8 +754,23 @@ proc proposeBlock(node: BeaconNode, return head res.get() - newBlock = await makeBeaconBlockForHeadAndSlot( - node, randao, validator_index, node.graffitiBytes, head, slot) + # https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#responsibilites-during-the-merge-transition + # "Honest validators will not utilize the external builder network until + # after the transition from the proof-of-work chain to the proof-of-stake + # beacon chain has been finalized by the proof-of-stake validators." + if node.config.payloadBuilderEnable and + not node.dag.loadExecutionBlockRoot(node.dag.finalizedHead.blck).isZero: + let newBlockMEV = await node.proposeBlockMEV( + head, validator, slot, randao, validator_index) + + if newBlockMEV.isSome: + # This might be equivalent to the `head` passed in, but it signals that + # `submitBlindedBlock` ran, so don't do anything else. Otherwise, it is + # fine to try again with the local EL. + return newBlockMEV.get + + let newBlock = await makeBeaconBlockForHeadAndSlot( + node, randao, validator_index, node.graffitiBytes, head, slot) if newBlock.isErr(): return head # already logged elsewhere! @@ -918,6 +1171,80 @@ proc updateValidatorMetrics*(node: BeaconNode) = node.attachedValidatorBalanceTotal = total attached_validator_balance_total.set(total.toGaugeValue) +from std/times import epochTime + +proc getValidatorRegistration( + node: BeaconNode, validator: AttachedValidator): + Future[Result[SignedValidatorRegistrationV1, string]] {.async.} = + # Stand-in, reasonable default + const gasLimit = 30000000 + + let feeRecipient = + node.config.getSuggestedFeeRecipient(validator.pubkey).valueOr: + node.config.defaultFeeRecipient + var validatorRegistration = SignedValidatorRegistrationV1( + message: ValidatorRegistrationV1( + fee_recipient: ExecutionAddress(data: distinctBase(feeRecipient)), + gas_limit: gasLimit, + timestamp: epochTime().uint64, + pubkey: validator.pubkey)) + + let signature = await validator.getBuilderSignature( + node.dag.cfg.genesisFork, validatorRegistration.message) + + debug "getValidatorRegistration: registering", + validatorRegistration + + if signature.isErr: + return err signature.error + + validatorRegistration.signature = signature.get + + return ok validatorRegistration + +proc registerValidators(node: BeaconNode) {.async.} = + try: + if (not node.config.payloadBuilderEnable) or + node.currentSlot.epoch < node.dag.cfg.BELLATRIX_FORK_EPOCH: + return + elif node.config.payloadBuilderEnable and + node.payloadBuilderRestClient.isNil: + warn "registerValidators: node.config.payloadBuilderEnable and node.payloadBuilderRestClient.isNil" + return + + const HttpOk = 200 + + let restBuilderStatus = await node.payloadBuilderRestClient.checkBuilderStatus + if restBuilderStatus.status != HttpOk: + warn "registerValidators: specified builder not available", + builderUrl = node.config.payloadBuilderUrl, + builderStatus = restBuilderStatus + return + + # TODO split this across slots of epoch to support larger numbers of + # validators + # https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#validator-registration + var validatorRegistrations: seq[SignedValidatorRegistrationV1] + for validator in node.attachedValidators[].validators.values: + let validatorRegistration = + await node.getValidatorRegistration(validator) + if validatorRegistration.isErr: + debug "registerValidators: validatorRegistration failed", + validatorRegistration + continue + + validatorRegistrations.add validatorRegistration.get + + let registerValidatorResult = + await node.payloadBuilderRestClient.registerValidator( + validatorRegistrations) + if HttpOk != registerValidatorResult.status: + warn "registerValidators: Couldn't register validator with MEV builder", + registerValidatorResult + except CatchableError as exc: + warn "registerValidators: exception", + error = exc.msg + proc handleValidatorDuties*(node: BeaconNode, lastSlot, slot: Slot) {.async.} = ## Perform validator duties - create blocks, vote and aggregate existing votes if node.attachedValidators[].count == 0: @@ -980,6 +1307,13 @@ proc handleValidatorDuties*(node: BeaconNode, lastSlot, slot: Slot) {.async.} = curSlot += 1 + # https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#registration-dissemination + # This specification suggests validators re-submit to builder software every + # `EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION` epochs. + if slot.is_epoch and + slot.epoch mod EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION == 0: + asyncSpawn node.registerValidators() + let newHead = await handleProposal(node, head, slot) didSubmitBlock = (newHead != head) diff --git a/beacon_chain/validators/validator_pool.nim b/beacon_chain/validators/validator_pool.nim index addf8eae9..5fa9053d1 100644 --- a/beacon_chain/validators/validator_pool.nim +++ b/beacon_chain/validators/validator_pool.nim @@ -210,7 +210,8 @@ proc signData(v: AttachedValidator, # https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/phase0/validator.md#signature proc getBlockSignature*(v: AttachedValidator, fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot, - block_root: Eth2Digest, blck: ForkedBeaconBlock + block_root: Eth2Digest, + blck: ForkedBeaconBlock | BlindedBeaconBlock ): Future[SignatureResult] {.async.} = return case v.kind @@ -220,9 +221,33 @@ proc getBlockSignature*(v: AttachedValidator, fork: Fork, fork, genesis_validators_root, slot, block_root, v.data.privateKey).toValidatorSig()) of ValidatorKind.Remote: - let request = Web3SignerRequest.init( - fork, genesis_validators_root, blck.Web3SignerForkedBeaconBlock) - await v.signData(request) + when blck is BlindedBeaconBlock: + let request = Web3SignerRequest.init( + fork, genesis_validators_root, + Web3SignerForkedBeaconBlock( + kind: BeaconBlockFork.Bellatrix, + bellatrixData: blck.toBeaconBlockHeader)) + await v.signData(request) + else: + let + web3SignerBlock = + case blck.kind + of BeaconBlockFork.Phase0: + Web3SignerForkedBeaconBlock( + kind: BeaconBlockFork.Phase0, + phase0Data: blck.phase0Data) + of BeaconBlockFork.Altair: + Web3SignerForkedBeaconBlock( + kind: BeaconBlockFork.Altair, + altairData: blck.altairData) + of BeaconBlockFork.Bellatrix: + Web3SignerForkedBeaconBlock( + kind: BeaconBlockFork.Bellatrix, + bellatrixData: blck.bellatrixData.toBeaconBlockHeader) + + request = Web3SignerRequest.init( + fork, genesis_validators_root, web3SignerBlock) + await v.signData(request) # https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/phase0/validator.md#aggregate-signature proc getAttestationSignature*(v: AttachedValidator, fork: Fork, @@ -363,3 +388,17 @@ proc getSlotSignature*(v: AttachedValidator, fork: Fork, v.slotSignature = some((slot, signature.get)) return signature + +# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/builder.md#signing +proc getBuilderSignature*(v: AttachedValidator, fork: Fork, + validatorRegistration: ValidatorRegistrationV1): + Future[SignatureResult] {.async.} = + return + case v.kind + of ValidatorKind.Local: + SignatureResult.ok(get_builder_signature( + fork, validatorRegistration, v.data.privateKey).toValidatorSig()) + of ValidatorKind.Remote: + let request = Web3SignerRequest.init( + fork, ZERO_HASH, validatorRegistration) + await v.signData(request)