# beacon_chain # Copyright (c) 2021-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. {.push raises: [].} import chronicles, ".."/validators/activity_metrics, ".."/spec/forks, "."/[common, api, fallback_service] const ServiceName = "block_service" BlockPollInterval = attestationSlotOffset.nanoseconds div 4 BlockPollOffset1 = TimeDiff(nanoseconds: BlockPollInterval) BlockPollOffset2 = TimeDiff(nanoseconds: BlockPollInterval * 2) BlockPollOffset3 = TimeDiff(nanoseconds: BlockPollInterval * 3) logScope: service = ServiceName func shortLog(v: Opt[UInt256]): auto = if v.isNone(): "" else: toString(v.get, 10) func shortLog(v: ForkedMaybeBlindedBeaconBlock): auto = withForkyMaybeBlindedBlck(v): when consensusFork < ConsensusFork.Deneb: shortLog(forkyMaybeBlindedBlck) else: when isBlinded: shortLog(forkyMaybeBlindedBlck) else: shortLog(forkyMaybeBlindedBlck.`block`) proc proposeBlock( vc: ValidatorClientRef, slot: Slot, proposerKey: ValidatorPubKey ) {.async: (raises: [CancelledError]).} proc prepareRandao( vc: ValidatorClientRef, slot: Slot, proposerKey: ValidatorPubKey ) {.async: (raises: [CancelledError]).} = if slot == vc.beaconClock.now().slotOrZero(): # Its impossible to prepare RANDAO in the beginning of the epoch. Epoch # signature will be requested by block proposer. return let destSlot = slot - 1'u64 destOffset = TimeDiff(nanoseconds: NANOSECONDS_PER_SLOT.int64 div 2) deadline = destSlot.start_beacon_time() + destOffset epoch = slot.epoch() # We going to wait to T - (T / 4 * 2), where T is proposer's # duty slot. currentSlot = (await vc.checkedWaitForSlot(destSlot, destOffset, false)).valueOr: debug "Unable to perform RANDAO signature preparation because of " & "system time failure" return validator = vc.getValidatorForDuties(proposerKey, slot, true).valueOr: return if currentSlot <= destSlot: # We do not need result, because we want it to be cached. let start = Moment.now() genesisRoot = vc.beaconGenesis.genesis_validators_root fork = vc.forkAtEpoch(epoch) rsig = await validator.getEpochSignature(fork, genesisRoot, epoch) timeElapsed = Moment.now() - start if rsig.isErr(): debug "Unable to prepare RANDAO signature", epoch = epoch, validator = validatorLog(validator), elapsed_time = timeElapsed, current_slot = currentSlot, destination_slot = destSlot, delay = vc.getDelay(deadline) else: debug "RANDAO signature has been prepared", epoch = epoch, validator = validatorLog(validator), elapsed_time = timeElapsed, current_slot = currentSlot, destination_slot = destSlot, delay = vc.getDelay(deadline) else: debug "RANDAO signature preparation timed out", epoch = epoch, validator = validatorLog(validator), current_slot = currentSlot, destination_slot = destSlot, delay = vc.getDelay(deadline) proc spawnProposalTask(vc: ValidatorClientRef, duty: RestProposerDuty): ProposerTask = ProposerTask( randaoFut: prepareRandao(vc, duty.slot, duty.pubkey), proposeFut: proposeBlock(vc, duty.slot, duty.pubkey), duty: duty ) proc publishBlockV3( vc: ValidatorClientRef, currentSlot, slot: Slot, fork: Fork, randaoReveal: ValidatorSig, validator: AttachedValidator ) {.async: (raises: [CancelledError]).} = let genesisRoot = vc.beaconGenesis.genesis_validators_root graffiti = if vc.config.graffiti.isSome(): vc.config.graffiti.get() else: defaultGraffitiBytes() vindex = validator.index.get() logScope: validator = validatorLog(validator) validator_index = vindex slot = slot wall_slot = currentSlot let maybeBlock = try: await vc.produceBlockV3(slot, randaoReveal, graffiti, vc.config.builderBoostFactor, ApiStrategyKind.Best) except ValidatorApiError as exc: warn "Unable to retrieve block data", reason = exc.getFailureReason() return except CancelledError as exc: debug "Block data production has been interrupted" raise exc withForkyMaybeBlindedBlck(maybeBlock): when isBlinded: let blockRoot = hash_tree_root(forkyMaybeBlindedBlck) debug "Block produced", block_type = "blinded", block_root = shortLog(blockRoot), blck = shortLog(maybeBlock), execution_value = shortLog(maybeBlock.executionValue), consensus_value = shortLog(maybeBlock.consensusValue) let signingRoot = compute_block_signing_root(fork, genesisRoot, slot, blockRoot) notSlashable = vc.attachedValidators[] .slashingProtection .registerBlock(vindex, validator.pubkey, slot, signingRoot) logScope: blck = shortLog(forkyMaybeBlindedBlck) block_root = shortLog(blockRoot) signing_root = shortLog(signingRoot) if notSlashable.isErr(): warn "Slashing protection activated for blinded block proposal" return let signature = try: let res = await validator.getBlockSignature(fork, genesisRoot, slot, blockRoot, maybeBlock) if res.isErr(): warn "Unable to sign blinded block proposal using remote signer", reason = res.error() return res.get() except CancelledError as exc: debug "Blinded block signature process has been interrupted" raise exc let signedBlock = ForkedSignedBlindedBeaconBlock.init(forkyMaybeBlindedBlck, blockRoot, signature) res = try: debug "Sending blinded block" if vc.isPastElectraFork(slot.epoch()): await vc.publishBlindedBlockV2( signedBlock, BroadcastValidationType.Gossip, ApiStrategyKind.First) else: await vc.publishBlindedBlock( signedBlock, ApiStrategyKind.First) except ValidatorApiError as exc: warn "Unable to publish blinded block", reason = exc.getFailureReason() return except CancelledError as exc: debug "Blinded block publication has been interrupted" raise exc if res: let delay = vc.getDelay(slot.block_deadline()) beacon_blocks_sent.inc() beacon_blocks_sent_delay.observe(delay.toFloatSeconds()) notice "Blinded block published", delay = delay else: warn "Blinded block was not accepted by beacon node" else: let blockRoot = hash_tree_root( when consensusFork < ConsensusFork.Deneb: forkyMaybeBlindedBlck else: forkyMaybeBlindedBlck.`block` ) debug "Block produced", block_type = "non-blinded", block_root = shortLog(blockRoot), blck = shortLog(maybeBlock), execution_value = shortLog(maybeBlock.executionValue), consensus_value = shortLog(maybeBlock.consensusValue) let signingRoot = compute_block_signing_root(fork, genesisRoot, slot, blockRoot) notSlashable = vc.attachedValidators[] .slashingProtection .registerBlock(vindex, validator.pubkey, slot, signingRoot) logScope: blck = shortLog( when consensusFork < ConsensusFork.Deneb: forkyMaybeBlindedBlck else: forkyMaybeBlindedBlck.`block` ) block_root = shortLog(blockRoot) signing_root = shortLog(signingRoot) if notSlashable.isErr(): warn "Slashing protection activated for block proposal" return let signature = try: let res = await validator.getBlockSignature( fork, genesisRoot, slot, blockRoot, maybeBlock) if res.isErr(): warn "Unable to sign block proposal using remote signer", reason = res.error() return res.get() except CancelledError as exc: debug "Block signature process has been interrupted" raise exc signedBlockContents = RestPublishedSignedBlockContents.init( forkyMaybeBlindedBlck, blockRoot, signature) res = try: debug "Sending block" if vc.isPastElectraFork(slot.epoch()): await vc.publishBlockV2( signedBlockContents, BroadcastValidationType.Gossip, ApiStrategyKind.First) else: await vc.publishBlock( signedBlockContents, ApiStrategyKind.First) except ValidatorApiError as exc: warn "Unable to publish block", reason = exc.getFailureReason() return except CancelledError as exc: debug "Block publication has been interrupted" raise exc if res: let delay = vc.getDelay(slot.block_deadline()) beacon_blocks_sent.inc() beacon_blocks_sent_delay.observe(delay.toFloatSeconds()) notice "Block published", delay = delay else: warn "Block was not accepted by beacon node" proc publishBlock( vc: ValidatorClientRef, currentSlot, slot: Slot, validator: AttachedValidator ) {.async: (raises: [CancelledError]).} = let genesisRoot = vc.beaconGenesis.genesis_validators_root graffiti = vc.getGraffitiBytes(validator) fork = vc.forkAtEpoch(slot.epoch) vindex = validator.index.get() logScope: validator = validatorLog(validator) validator_index = vindex slot = slot wall_slot = currentSlot debug "Publishing block", delay = vc.getDelay(slot.block_deadline()), genesis_root = genesisRoot, graffiti = graffiti, fork = fork let randaoReveal = try: (await validator.getEpochSignature(fork, genesisRoot, slot.epoch())).valueOr: warn "Unable to generate RANDAO reveal using remote signer", reason = error return except CancelledError as exc: debug "RANDAO reveal production has been interrupted" raise exc await vc.publishBlockV3(currentSlot, slot, fork, randaoReveal, validator) proc proposeBlock( vc: ValidatorClientRef, slot: Slot, proposerKey: ValidatorPubKey ) {.async: (raises: [CancelledError]).} = let currentSlot = (await vc.checkedWaitForSlot(slot, ZeroTimeDiff, false)).valueOr: error "Unable to perform block production because of system time" return if currentSlot > slot: warn "Skip block production for expired slot", current_slot = currentSlot, duties_slot = slot return let validator = vc.getValidatorForDuties(proposerKey, slot).valueOr: return try: await vc.publishBlock(currentSlot, slot, validator) except CancelledError as exc: debug "Block proposing process was interrupted", slot = slot, validator = validatorLog(validator) raise exc proc contains(data: openArray[RestProposerDuty], task: ProposerTask): bool = for item in data: if (item.pubkey == task.duty.pubkey) and (item.slot == task.duty.slot): return true false proc contains(data: openArray[ProposerTask], duty: RestProposerDuty): bool = for item in data: if (item.duty.pubkey == duty.pubkey) and (item.duty.slot == duty.slot): return true false proc checkDuty(duty: RestProposerDuty, epoch: Epoch, slot: Slot): bool = let lastSlot = start_slot(epoch + 1'u64) if duty.slot >= slot: if duty.slot < lastSlot: true else: warn "Block proposal duty is in the far future, ignoring", duty_slot = duty.slot, pubkey = shortLog(duty.pubkey), wall_slot = slot, last_slot_in_epoch = (lastSlot - 1'u64) false else: warn "Block proposal duty is in the past, ignoring", duty_slot = duty.slot, pubkey = shortLog(duty.pubkey), wall_slot = slot false proc addOrReplaceProposers*(vc: ValidatorClientRef, epoch: Epoch, dependentRoot: Eth2Digest, duties: openArray[RestProposerDuty]) = let default = ProposedData(epoch: FAR_FUTURE_EPOCH) currentSlot = vc.getCurrentSlot().get(Slot(0)) epochDuties = vc.proposers.getOrDefault(epoch, default) if not(epochDuties.isDefault()): if epochDuties.dependentRoot != dependentRoot: warn "Proposer duties re-organization", duties_count = len(duties), wall_slot = currentSlot, epoch = epoch, prior_dependent_root = epochDuties.dependentRoot, dependent_root = dependentRoot let tasks = block: var res: seq[ProposerTask] var hashset = initHashSet[Slot]() for task in epochDuties.duties: if task notin duties: # Task is no more relevant, so cancel it. debug "Cancelling running proposal duty tasks", slot = task.duty.slot, pubkey = shortLog(task.duty.pubkey) task.proposeFut.cancelSoon() task.randaoFut.cancelSoon() else: # If task is already running for proper slot, we keep it alive. debug "Keep running previous proposal duty tasks", slot = task.duty.slot, pubkey = shortLog(task.duty.pubkey) res.add(task) for duty in duties: if duty notin res: info "Received new proposer duty", slot = duty.slot, pubkey = shortLog(duty.pubkey) if checkDuty(duty, epoch, currentSlot): let task = vc.spawnProposalTask(duty) if duty.slot in hashset: error "Multiple block proposers for this slot, " & "producing blocks for all proposers", slot = duty.slot else: hashset.incl(duty.slot) res.add(task) res vc.proposers[epoch] = ProposedData.init(epoch, dependentRoot, tasks) else: debug "New block proposal duties received", dependent_root = dependentRoot, duties_count = len(duties), wall_slot = currentSlot, epoch = epoch # Spawn new proposer tasks and modify proposers map. let tasks = block: var hashset = initHashSet[Slot]() var res: seq[ProposerTask] for duty in duties: info "Received new proposer duty", slot = duty.slot, pubkey = shortLog(duty.pubkey) if checkDuty(duty, epoch, currentSlot): let task = vc.spawnProposalTask(duty) if duty.slot in hashset: error "Multiple block proposers for this slot, " & "producing blocks for all proposers", slot = duty.slot else: hashset.incl(duty.slot) res.add(task) res vc.proposers[epoch] = ProposedData.init(epoch, dependentRoot, tasks) proc pollForEvents(service: BlockServiceRef, node: BeaconNodeServerRef, response: RestHttpResponseRef) {. async: (raises: [CancelledError]).} = let vc = service.client logScope: node = node while true: let events = try: await response.getServerSentEvents() except HttpError as exc: debug "Unable to receive server-sent event", reason = $exc.msg return except RestError as exc: debug "Unable to receive server-sent event", reason = $exc.msg return except CancelledError as exc: raise exc for event in events: case event.name of "data": let blck = EventBeaconBlockObject.decodeString(event.data).valueOr: debug "Got invalid block event format", reason = error return vc.registerBlock(blck, node) of "event": if event.data != "block": debug "Got unexpected event name field", event_name = event.name, event_data = event.data else: debug "Got some unexpected event field", event_name = event.name if len(events) == 0: break proc runBlockEventMonitor(service: BlockServiceRef, node: BeaconNodeServerRef) {. async: (raises: [CancelledError]).} = let vc = service.client roles = {BeaconNodeRole.BlockProposalData} statuses = {RestBeaconNodeStatus.Synced} logScope: node = node while true: while node.status notin statuses: await vc.waitNodes(nil, statuses, roles, true) let response = block: var resp: HttpClientResponseRef try: resp = await node.client.subscribeEventStream({EventTopic.Block}) if resp.status == 200: Opt.some(resp) else: let body = await resp.getBodyBytes() await resp.closeWait() let plain = RestPlainResponse(status: resp.status, contentType: resp.contentType, data: body) reason = plain.getErrorMessage() debug "Unable to obtain events stream", code = resp.status, reason = reason Opt.none(HttpClientResponseRef) except HttpError as exc: debug "Unable to obtain events stream", reason = $exc.msg Opt.none(HttpClientResponseRef) except RestError as exc: if not(isNil(resp)): await resp.closeWait() debug "Unable to obtain events stream", reason = $exc.msg Opt.none(HttpClientResponseRef) except CancelledError as exc: if not(isNil(resp)): await resp.closeWait() debug "Block monitoring loop has been interrupted" raise exc if response.isSome(): debug "Block monitoring connection has been established" try: await service.pollForEvents(node, response.get()) except CancelledError as exc: raise exc finally: debug "Block monitoring connection has been lost" await response.get().closeWait() proc pollForBlockHeaders(service: BlockServiceRef, node: BeaconNodeServerRef, slot: Slot, waitTime: Duration, index: int): Future[bool] {. async: (raises: [CancelledError]).} = let vc = service.client logScope: node = node slot = slot wait_time = waitTime schedule_index = index trace "Polling for block header" let bres = try: await sleepAsync(waitTime) await node.client.getBlockHeader(BlockIdent.init(slot)) except RestError as exc: debug "Unable to obtain block header", reason = $exc.msg, error = $exc.name return false except RestResponseError as exc: debug "Got an error while trying to obtain block header", reason = exc.message, status = exc.status return false except CancelledError as exc: raise exc if bres.isNone(): trace "Beacon node does not yet have block" return false let blockHeader = bres.get() let eventBlock = EventBeaconBlockObject( slot: blockHeader.data.header.message.slot, block_root: blockHeader.data.root, optimistic: blockHeader.execution_optimistic ) vc.registerBlock(eventBlock, node) true proc runBlockPollMonitor(service: BlockServiceRef, node: BeaconNodeServerRef) {. async: (raises: [CancelledError]).} = let vc = service.client roles = {BeaconNodeRole.BlockProposalData} statuses = {RestBeaconNodeStatus.Synced} logScope: node = node while true: let currentSlot {.used.} = (await vc.checkedWaitForNextSlot(ZeroTimeDiff, false)).valueOr: continue while node.status notin statuses: await vc.waitNodes(nil, statuses, roles, true) let currentTime = vc.beaconClock.now() afterSlot = currentTime.slotOrZero() if currentTime > afterSlot.attestation_deadline(): # Attestation time already, lets wait for next slot. continue let pollTime1 = afterSlot.start_beacon_time() + BlockPollOffset1 pollTime2 = afterSlot.start_beacon_time() + BlockPollOffset2 pollTime3 = afterSlot.start_beacon_time() + BlockPollOffset3 var pendingTasks = block: var res: seq[FutureBase] if currentTime <= pollTime1: let stime = nanoseconds((pollTime1 - currentTime).nanoseconds) res.add(FutureBase( service.pollForBlockHeaders(node, afterSlot, stime, 0))) if currentTime <= pollTime2: let stime = nanoseconds((pollTime2 - currentTime).nanoseconds) res.add(FutureBase( service.pollForBlockHeaders(node, afterSlot, stime, 1))) if currentTime <= pollTime3: let stime = nanoseconds((pollTime3 - currentTime).nanoseconds) res.add(FutureBase( service.pollForBlockHeaders(node, afterSlot, stime, 2))) res try: while true: let completedFuture = try: await race(pendingTasks) except ValueError: raiseAssert "Number of pending tasks should not be zero" let blockReceived = block: var res = false for future in pendingTasks: if not(future.completed()): continue if not(cast[Future[bool]](future).value): continue res = true break res if blockReceived: let pending = pendingTasks.filterIt(not(it.finished())).mapIt(it.cancelAndWait()) # We use `noCancel` here because its cleanup and we have `break` # after it. await noCancel allFutures(pending) break pendingTasks.keepItIf(it != completedFuture) if len(pendingTasks) == 0: break except CancelledError as exc: let pending = pendingTasks.filterIt(not(it.finished())).mapIt(it.cancelAndWait()) await noCancel allFutures(pending) raise exc proc runBlockMonitor(service: BlockServiceRef) {. async: (raises: [CancelledError]).} = let vc = service.client blockNodes = vc.filterNodes(ResolvedBeaconNodeStatuses, {BeaconNodeRole.BlockProposalData}) let pendingTasks = case vc.config.monitoringType of BlockMonitoringType.Disabled: debug "Block monitoring disabled" @[Future[void].Raising([CancelledError]).init("block.monitor.disabled")] of BlockMonitoringType.Poll: blockNodes.mapIt(service.runBlockPollMonitor(it)) of BlockMonitoringType.Event: blockNodes.mapIt(service.runBlockEventMonitor(it)) try: await allFutures(pendingTasks) except CancelledError as exc: let pending = pendingTasks.filterIt(not(it.finished())).mapIt(it.cancelAndWait()) await noCancel allFutures(pending) raise exc proc mainLoop(service: BlockServiceRef) {.async: (raises: []).} = let vc = service.client service.state = ServiceState.Running debug "Service started" let future = service.runBlockMonitor() try: # Future is not going to be completed, so the only way to exit, is to # cancel it. await future except CancelledError: debug "Service interrupted" # We going to cleanup all the pending proposer tasks. var res: seq[FutureBase] for epoch, data in vc.proposers.pairs(): for duty in data.duties.items(): if not(duty.proposeFut.finished()): res.add(duty.proposeFut.cancelAndWait()) if not(duty.randaoFut.finished()): res.add(duty.randaoFut.cancelAndWait()) await noCancel allFutures(res) proc init*( t: typedesc[BlockServiceRef], vc: ValidatorClientRef ): Future[BlockServiceRef] {.async: (raises: []).} = logScope: service = ServiceName let res = BlockServiceRef(name: ServiceName, client: vc, state: ServiceState.Initialized) debug "Initializing service" res proc start*(service: BlockServiceRef) = service.lifeFut = mainLoop(service)