Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attest immediately if block from proposer - fix #966 #1014

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions beacon_chain/beacon_node_common.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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
5 changes: 5 additions & 0 deletions beacon_chain/beacon_node_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ type

slot*: Slot # TODO could calculate this by walking to root, but..

# Cache for ProtoArray fork-choice
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something I forgot until we talked about it in the other PR: BlockRef is not the right granularity for these checkpoints: epoch - that means you need to attach them to a (BlockRef, Slot) tuple, or BlockSlot, more simply, the issue being that epoch processing happens regardless if there's a block or not on the epoch boundary..

justified_checkpoint*: Checkpoint
finalized_checkpoint*: Checkpoint

BlockData* = object
## Body and graph in one

Expand All @@ -195,6 +199,7 @@ type
## has advanced without blocks

Head* = object
# TODO: delete - all BlockRef tracks the justified checkpoint
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, head still needs to stay..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field, but it can be a regular BlockRef

blck*: BlockRef
justified*: BlockSlot

Expand Down
17 changes: 11 additions & 6 deletions beacon_chain/block_pool.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions beacon_chain/spec/datatypes.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 0 additions & 8 deletions beacon_chain/spec/state_transition_helpers.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
# --------------------------------------------------------

Expand Down
101 changes: 84 additions & 17 deletions beacon_chain/validator_duties.nim
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import
# Standard library
os, tables, strutils, times,
os, tables, strutils, times, atomics,

# Nimble packages
stew/[objects, bitseqs], stew/shims/macros,
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't check the proposer actually - ie if someone from an alternate history posts a block, this check will misfire

Copy link
Contributor Author

@mratsim mratsim May 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process_block_header requires that the correct proposer was used:

https://github.com/status-im/nim-beacon-chain/blob/96ba039648e030f9e3725e71ac9dda34a02df657/beacon_chain/spec/state_transition_block.nim#L49-L70

So what can happen is:

  • (a) You found a block at that exact slot in the blockpool so it was run through process_block_header and you can attest to it as it was chacked to be from the expected proposer
  • (b) You didn't find a block at that exact slot in the blockpool so you vote for one of the head you have but the blck.slot is different from the current slot.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mm.. ok - so it's only because it's used on the head block that the function lives up to its name - I wonder if it wouldn't be more simple to just have the slot comparison with a comment that we're waiting for the the proposed block of that particular 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
Expand Down Expand Up @@ -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.} =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AsyncEvent that fires on updateHead is likely what you're looking for here - ping @cheatfate - no need for this polling

# 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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what cancels the poll loop on timeout?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not clear on how to implement timeout with Chronos:

It contains a poll() so Chronos can do maintenance there, like stopping to yield the closure iterator, but in my testing the node gets stuck. Pointer appreciated:

https://github.com/status-im/nim-beacon-chain/blob/96ba039648e030f9e3725e71ac9dda34a02df657/beacon_chain/validator_duties.nim#L426-L443

Otherwise I can use an AsyncChannel but I would be surprised if there wasn't a simple solution, cc @cheatfate

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes but who the proposer is depends on which history you're following - isn't this "shortcut" only for when the proposer is from the same history as you?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this supposed to be here? #1014 (comment), is this unclear or did I miss something?

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))
Expand All @@ -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),
Expand Down