diff --git a/beacon_chain/beacon_node_common.nim b/beacon_chain/beacon_node_common.nim index a19dace26d..b62a551c59 100644 --- a/beacon_chain/beacon_node_common.nim +++ b/beacon_chain/beacon_node_common.nim @@ -44,7 +44,7 @@ const MaxEmptySlotCount* = uint64(10*60) div SECONDS_PER_SLOT declareGauge beacon_head_root, "Root of the head block of the beacon chain" -proc updateHead*(node: BeaconNode): BlockRef = +proc updateHead*(node: BeaconNode, logNoUpdate = true): BlockRef = # Check pending attestations - maybe we found some blocks for them node.attestationPool.resolve() @@ -53,7 +53,7 @@ proc updateHead*(node: BeaconNode): BlockRef = # Store the new head in the block pool - this may cause epochs to be # justified and finalized - node.blockPool.updateHead(newHead) + node.blockPool.updateHead(newHead, logNoUpdate) beacon_head_root.set newHead.root.toGaugeValue newHead diff --git a/beacon_chain/beacon_node_types.nim b/beacon_chain/beacon_node_types.nim index 12e70dd07d..a1882c15ae 100644 --- a/beacon_chain/beacon_node_types.nim +++ b/beacon_chain/beacon_node_types.nim @@ -170,6 +170,10 @@ type slot*: Slot # TODO could calculate this by walking to root, but.. + # Cache for ProtoArray fork-choice + justified_checkpoint*: Checkpoint + finalized_checkpoint*: Checkpoint + BlockData* = object ## Body and graph in one @@ -195,6 +199,7 @@ type ## has advanced without blocks Head* = object + # TODO: delete - all BlockRef tracks the justified checkpoint blck*: BlockRef justified*: BlockSlot diff --git a/beacon_chain/block_pool.nim b/beacon_chain/block_pool.nim index 8ff372f5d6..c4e3f277af 100644 --- a/beacon_chain/block_pool.nim +++ b/beacon_chain/block_pool.nim @@ -266,7 +266,12 @@ proc addResolvedBlock( logScope: pcs = "block_resolution" doAssert state.slot == signedBlock.message.slot, "state must match block" - let blockRef = BlockRef.init(blockRoot, signedBlock.message) + let blockRef = BlockRef( + root: blockRoot, + slot: signedBlock.message.slot, + justified_checkpoint: state.current_justified_checkpoint, + finalized_checkpoint: state.finalized_checkpoint + ) link(parent, blockRef) pool.blocks[blockRoot] = blockRef @@ -867,7 +872,7 @@ proc delState(pool: BlockPool, bs: BlockSlot) = if (let root = pool.db.getStateRoot(bs.blck.root, bs.slot); root.isSome()): pool.db.delState(root.get()) -proc updateHead*(pool: BlockPool, newHead: BlockRef) = +proc updateHead*(pool: BlockPool, newHead: BlockRef, logNoUpdate = false) = ## Update what we consider to be the current head, as given by the fork ## choice. ## The choice of head affects the choice of finalization point - the order @@ -878,10 +883,10 @@ proc updateHead*(pool: BlockPool, newHead: BlockRef) = logScope: pcs = "fork_choice" if pool.head.blck == newHead: - info "No head block update", - head = shortLog(newHead), - cat = "fork_choice" - + if logNoUpdate: + info "No head block update", + head = shortLog(newHead), + cat = "fork_choice" return let diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index a9301a7983..760081012b 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -645,6 +645,12 @@ func shortLog*(v: Attestation): auto = signature: shortLog(v.signature) ) +func shortLog*(cp: Checkpoint): auto = + ( + epoch: cp.epoch, + root: shortLog(cp.root) + ) + chronicles.formatIt Slot: it.shortLog chronicles.formatIt Epoch: it.shortLog chronicles.formatIt BeaconBlock: it.shortLog diff --git a/beacon_chain/spec/state_transition_helpers.nim b/beacon_chain/spec/state_transition_helpers.nim index 3e6aee73bf..6edac0b44d 100644 --- a/beacon_chain/spec/state_transition_helpers.nim +++ b/beacon_chain/spec/state_transition_helpers.nim @@ -13,14 +13,6 @@ import # Internals ./datatypes, ./digest, ./beaconstate -# Logging utilities -# -------------------------------------------------------- - -# TODO: gather all logging utilities -# from crypto, digest, etc in a single file -func shortLog*(x: Checkpoint): string = - "(epoch: " & $x.epoch & ", root: \"" & shortLog(x.root) & "\")" - # Helpers used in epoch transition and trace-level block transition # -------------------------------------------------------- diff --git a/beacon_chain/validator_duties.nim b/beacon_chain/validator_duties.nim index f806c075af..f5f6efb2bb 100644 --- a/beacon_chain/validator_duties.nim +++ b/beacon_chain/validator_duties.nim @@ -7,7 +7,7 @@ import # Standard library - os, tables, strutils, times, + os, tables, strutils, times, atomics, # Nimble packages stew/[objects, bitseqs], stew/shims/macros, @@ -352,9 +352,21 @@ proc broadcastAggregatedAttestations( state.genesis_validators_root)) node.network.broadcast(node.topicAggregateAndProofs, signedAP) +func isBlockFromExpectedProposerAtSlot(blck: BlockRef, targetSlot: Slot): bool = + ## Returns true if the block is from the expected proposer + ## + ## This is true if-and-only-if the block slot matches the target slot + blck.slot == targetSlot + +template displayParent(blck: BlockRef): string = + if blck.parent.isNil: + "orphan" + else: + shortLog(blck.parent.root) + proc handleValidatorDuties*( node: BeaconNode, head: BlockRef, lastSlot, slot: Slot): Future[BlockRef] {.async.} = - ## Perform validator duties - create blocks, vote and aggreagte existing votes + ## Perform validator duties - create blocks, vote and aggregate existing votes if node.attachedValidators.count == 0: # Nothing to do because we have no validator attached return head @@ -406,10 +418,75 @@ proc handleValidatorDuties*( # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#attesting # A validator should create and broadcast the attestation to the associated - # attestation subnet when either (a) the validator has received a valid - # block from the expected block proposer for the assigned slot or + # attestation subnet when either + # (a) the validator has received a valid + # block from the expected block proposer for the assigned slot or # (b) one-third of the slot has transpired (`SECONDS_PER_SLOT / 3` seconds - # after the start of slot) -- whichever comes first. + # after the start of slot) -- whichever comes first. + + # Poor man's AsyncChannel to notify timeout + # TODO: refactor with a Producer->Consumer mode of operation + var timedOut: Atomic[bool] + timedOut.store(false, moRelaxed) + let timeOutNotifChannel = timedOut.addr + + proc waitForProposedBlock(node: BeaconNode, slot: Slot, timeOutNotifChannel: ptr Atomic[bool]): Future[void] {.async.} = + # Wait until the head is from the expected proposer + # TODO: refactor with a Producer->Consumer mode of operation + var head = node.updateHead(logNoUpdate = true) + while not head.isBlockFromExpectedProposerAtSlot(slot) and not timeOutNotifChannel[].load(moRelaxed): + await sleepAsync(chronos.milliseconds(50)) # Timers are expensive and it takes 300~600ms to process a block at the moment + head = node.updateHead(logNoUpdate = false) + + const attCutoff = chronos.seconds(SECONDS_PER_SLOT.int64 div 3) + let cutoffFromNow = node.beaconClock.fromNow(slot.toBeaconTime(attCutoff)) + + if cutoffFromNow.inFuture: + let foundBlockInTime = await withTimeout( + waitForProposedBlock(node, slot, timeOutNotifChannel), + cutoffFromNow.offset + ) + + # Reload the head - this should be the same as in `waitForProposedBlock` + head = node.updateHead() + + if foundBlockInTime: + info "Found a block from expected proposer, attesting immediately", + block_root = shortlog(head.root), + parent = head.displayParent(), + slot = head.slot, + justified = shortLog(head.justified_checkpoint), + finalized = shortLog(head.finalized_checkpoint) + else: + timeOutNotifChannel[].store(true, moRelaxed) + + info "Timeout when waiting 2s from an expected block, attesting", + block_root = shortlog(head.root), + parent = head.displayParent(), + slot = head.slot, + justified = shortLog(head.justified_checkpoint), + finalized = shortLog(head.finalized_checkpoint) + else: + # Reload the head + head = node.updateHead() + + info "Late for attesting", + block_root = shortlog(head.root), + parent = head.displayParent(), + slot = head.slot, + justified = shortLog(head.justified_checkpoint), + finalized = shortLog(head.finalized_checkpoint), + blockFromExpectedProposer = head.isBlockFromExpectedProposerAtSlot(slot) + + # Attest to the head we found + handleAttestations(node, head, slot) + + # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#broadcast-aggregate + # If the validator is selected to aggregate (is_aggregator), then they + # broadcast their best aggregate as a SignedAggregateAndProof to the global + # aggregate channel (beacon_aggregate_and_proof) two-thirds of the way + # through the slot-that is, SECONDS_PER_SLOT * 2 / 3 seconds after the start + # of slot. template sleepToSlotOffset(extra: chronos.Duration, msg: static string) = let fromNow = node.beaconClock.fromNow(slot.toBeaconTime(extra)) @@ -423,19 +500,9 @@ proc handleValidatorDuties*( await sleepAsync(fromNow.offset) # Time passed - we might need to select a new head in that case - head = node.updateHead() - - sleepToSlotOffset( - seconds(int64(SECONDS_PER_SLOT)) div 3, "Waiting to send attestations") + head = node.updateHead(logNoUpdate = false) - handleAttestations(node, head, slot) - - # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#broadcast-aggregate - # If the validator is selected to aggregate (is_aggregator), then they - # broadcast their best aggregate as a SignedAggregateAndProof to the global - # aggregate channel (beacon_aggregate_and_proof) two-thirds of the way - # through the slot-that is, SECONDS_PER_SLOT * 2 / 3 seconds after the start - # of slot. + # TODO: this should probably be independent from block-proposal and signing if slot > 2: sleepToSlotOffset( seconds(int64(SECONDS_PER_SLOT * 2) div 3),