optimistic block gossip validation (#3876)

This commit is contained in:
tersec 2022-07-21 18:39:43 +00:00 committed by GitHub
parent f4208cfb23
commit 2f77f05a1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 60 additions and 51 deletions

View File

@ -255,9 +255,6 @@ type
shuffled_active_validator_indices*: seq[ValidatorIndex] shuffled_active_validator_indices*: seq[ValidatorIndex]
attester_dependent_root*: Eth2Digest attester_dependent_root*: Eth2Digest
# enables more efficient merge block validation
merge_transition_complete*: bool
# balances, as used in fork choice # balances, as used in fork choice
effective_balances_bytes*: seq[byte] effective_balances_bytes*: seq[byte]

View File

@ -179,8 +179,6 @@ func init*(
shuffled_active_validator_indices: shuffled_active_validator_indices:
cache.get_shuffled_active_validator_indices(state, epoch), cache.get_shuffled_active_validator_indices(state, epoch),
attester_dependent_root: attester_dependent_root, attester_dependent_root: attester_dependent_root,
merge_transition_complete: state.is_merge_transition_complete()
) )
epochStart = epoch.start_slot() epochStart = epoch.start_slot()

View File

@ -450,6 +450,16 @@ proc is_optimistic_candidate_block(
self.getBeaconTime().slotOrZero: self.getBeaconTime().slotOrZero:
return true return true
# Once merge is finalized, always true; in principle, should be caught by
# other checks, but sometimes blocks arrive out of order, triggering some
# spurious false negatives because the parent-block-check does not find a
# parent block. This can also occur under conditions where EL client RPCs
# cause processing delays. Either way, bound this risk to post-merge head
# and pre-merge finalization.
if not self.consensusManager.dag.loadExecutionBlockRoot(
self.consensusManager.dag.finalizedHead.blck).isZero:
return true
let let
parentRoot = withBlck(blck): blck.message.parent_root parentRoot = withBlck(blck): blck.message.parent_root
parentBlck = self.consensusManager.dag.getBlockRef(parentRoot).valueOr: parentBlck = self.consensusManager.dag.getBlockRef(parentRoot).valueOr:

View File

@ -175,51 +175,31 @@ template checkedReject(error: ValidationError): untyped =
err(error) err(error)
template validateBeaconBlockBellatrix( template validateBeaconBlockBellatrix(
signed_beacon_block: phase0.SignedBeaconBlock | signed_beacon_block: phase0.SignedBeaconBlock | altair.SignedBeaconBlock,
altair.SignedBeaconBlock, parent: BlockRef): untyped =
parent: BlockRef): untyped =
discard discard
# https://github.com/ethereum/consensus-specs/blob/v1.1.7/specs/merge/p2p-interface.md#beacon_block # https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/bellatrix/p2p-interface.md#beacon_block
template validateBeaconBlockBellatrix( template validateBeaconBlockBellatrix(
signed_beacon_block: bellatrix.SignedBeaconBlock, signed_beacon_block: bellatrix.SignedBeaconBlock,
parent: BlockRef): untyped = parent: BlockRef): untyped =
# If the execution is enabled for the block -- i.e. # If the execution is enabled for the block -- i.e.
# is_execution_enabled(state, block.body) then validate the following: # is_execution_enabled(state, block.body) then validate the following:
let executionEnabled = #
if signed_beacon_block.message.body.execution_payload != # `is_execution_enabled(state, block.body)` is
default(ExecutionPayload): # `is_merge_transition_block(state, block.body) or is_merge_transition_complete(state)` is
true # `(not is_merge_transition_complete(state) and block.body.execution_payload != ExecutionPayload()) or is_merge_transition_complete(state)` is
elif dag.getEpochRef(parent, parent.slot.epoch, true).expect( # `is_merge_transition_complete(state) or block.body.execution_payload != ExecutionPayload()` is
"parent EpochRef doesn't fail").merge_transition_complete: # `is_merge_transition_complete(state) or is_execution_block(block)`
# Should usually be inexpensive, but could require cache refilling - the #
# parent block can be no older than the latest finalized block # `is_merge_transition_complete(state)` tests for
true # `state.latest_execution_payload_header != ExecutionPayloadHeader()`, while
else: # https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/bellatrix/beacon-chain.md#block-processing
# Somewhat more expensive fallback, with database I/O, but should be # shows that `state.latest_execution_payload_header` being default or not is
# mostly relevant around merge transition epochs. It's possible that # exactly equivalent to whether that block's execution payload is default or
# the previous block is phase 0 or Altair, if this is the transition # not, so test cached block information rather than reconstructing a state.
# block itself. if signed_beacon_block.message.is_execution_block or
let blockData = dag.getForkedBlock(parent.bid) not dag.loadExecutionBlockRoot(parent).isZero:
if blockData.isOk():
case blockData.get().kind:
of BeaconBlockFork.Phase0:
false
of BeaconBlockFork.Altair:
false
of BeaconBlockFork.Bellatrix:
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/bellatrix/beacon-chain.md#process_execution_payload
# shows how this gets folded into the state each block; checking this
# is equivalent, without ever requiring state replay or any similarly
# expensive computation.
blockData.get().bellatrixData.message.body.execution_payload !=
default(ExecutionPayload)
else:
warn "Cannot load block parent, assuming execution is disabled",
parent = shortLog(parent)
false
if executionEnabled:
# [REJECT] The block's execution payload timestamp is correct with respect # [REJECT] The block's execution payload timestamp is correct with respect
# to the slot -- i.e. execution_payload.timestamp == # to the slot -- i.e. execution_payload.timestamp ==
# compute_timestamp_at_slot(state, block.slot). # compute_timestamp_at_slot(state, block.slot).
@ -229,10 +209,17 @@ template validateBeaconBlockBellatrix(
if not (signed_beacon_block.message.body.execution_payload.timestamp == if not (signed_beacon_block.message.body.execution_payload.timestamp ==
timestampAtSlot): timestampAtSlot):
quarantine[].addUnviable(signed_beacon_block.root) quarantine[].addUnviable(signed_beacon_block.root)
return errReject("BeaconBlock: Mismatched execution payload timestamp") return errReject("BeaconBlock: mismatched execution payload timestamp")
if signed_beacon_block.message.parent_root in dag.optimisticRoots:
# Definitely don't mark this as unviable.
# [REJECT] The block's parent (defined by `block.parent_root`) passes all
# validation (excluding execution node verification of the
# `block.body.execution_payload`).
return errReject("BeaconBlock: execution payload would build on optimistic parent")
# https://github.com/ethereum/consensus-specs/blob/v1.1.9/specs/phase0/p2p-interface.md#beacon_block # https://github.com/ethereum/consensus-specs/blob/v1.1.9/specs/phase0/p2p-interface.md#beacon_block
# https://github.com/ethereum/consensus-specs/blob/v1.1.8/specs/bellatrix/p2p-interface.md#beacon_block # https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/bellatrix/p2p-interface.md#beacon_block
proc validateBeaconBlock*( proc validateBeaconBlock*(
dag: ChainDAGRef, quarantine: ref Quarantine, dag: ChainDAGRef, quarantine: ref Quarantine,
signed_beacon_block: phase0.SignedBeaconBlock | altair.SignedBeaconBlock | signed_beacon_block: phase0.SignedBeaconBlock | altair.SignedBeaconBlock |
@ -306,12 +293,28 @@ proc validateBeaconBlock*(
# (via both gossip and non-gossip sources) (a client MAY queue blocks for # (via both gossip and non-gossip sources) (a client MAY queue blocks for
# processing once the parent block is retrieved). # processing once the parent block is retrieved).
# #
# And implicitly:
# [REJECT] The block's parent (defined by block.parent_root) passes validation. # [REJECT] The block's parent (defined by block.parent_root) passes validation.
let parent = dag.getBlockRef(signed_beacon_block.message.parent_root).valueOr: let parent = dag.getBlockRef(signed_beacon_block.message.parent_root).valueOr:
if signed_beacon_block.message.parent_root in quarantine[].unviable: if signed_beacon_block.message.parent_root in quarantine[].unviable:
quarantine[].addUnviable(signed_beacon_block.root) quarantine[].addUnviable(signed_beacon_block.root)
return errReject("BeaconBlock: parent from unviable fork")
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/bellatrix/p2p-interface.md#beacon_block
# `is_execution_enabled(state, block.body)` check, but unlike in
# validateBeaconBlockBellatrix() don't have parent BlockRef.
if signed_beacon_block.message.is_execution_block or
not dag.loadExecutionBlockRoot(dag.finalizedHead.blck).isZero:
# Blocks with execution enabled will be permitted to propagate
# regardless of the validity of the execution payload. This prevents
# network segregation between optimistic and non-optimistic nodes.
#
# [IGNORE] The block's parent (defined by `block.parent_root`) passes all
# validation (including execution node verification of the
# `block.body.execution_payload`).
return errIgnore("BeaconBlock: ignored, parent from unviable fork")
else:
# [REJECT] The block's parent (defined by `block.parent_root`) passes
# validation.
return errReject("BeaconBlock: rejected, parent from unviable fork")
# When the parent is missing, we can't validate the block - we'll queue it # When the parent is missing, we can't validate the block - we'll queue it
# in the quarantine for later processing # in the quarantine for later processing
@ -322,6 +325,11 @@ proc validateBeaconBlock*(
return errIgnore("BeaconBlock: Parent not found") return errIgnore("BeaconBlock: Parent not found")
# Continues block parent validity checking in optimistic case, where it does
# appear as a `BlockRef` (and not handled above) but isn't usable for gossip
# validation.
validateBeaconBlockBellatrix(signed_beacon_block, parent)
# [REJECT] The block is from a higher slot than its parent. # [REJECT] The block is from a higher slot than its parent.
if not (signed_beacon_block.message.slot > parent.bid.slot): if not (signed_beacon_block.message.slot > parent.bid.slot):
return errReject("BeaconBlock: block not from higher slot than its parent") return errReject("BeaconBlock: block not from higher slot than its parent")
@ -343,7 +351,6 @@ proc validateBeaconBlock*(
finalized_checkpoint.root == ancestor.root or finalized_checkpoint.root == ancestor.root or
finalized_checkpoint.root.isZero): finalized_checkpoint.root.isZero):
quarantine[].addUnviable(signed_beacon_block.root) quarantine[].addUnviable(signed_beacon_block.root)
return errReject("BeaconBlock: Finalized checkpoint not an ancestor") return errReject("BeaconBlock: Finalized checkpoint not an ancestor")
# [REJECT] The block is proposed by the expected proposer_index for the # [REJECT] The block is proposed by the expected proposer_index for the
@ -361,7 +368,6 @@ proc validateBeaconBlock*(
if uint64(proposer.get()) != signed_beacon_block.message.proposer_index: if uint64(proposer.get()) != signed_beacon_block.message.proposer_index:
quarantine[].addUnviable(signed_beacon_block.root) quarantine[].addUnviable(signed_beacon_block.root)
return errReject("BeaconBlock: Unexpected proposer proposer") return errReject("BeaconBlock: Unexpected proposer proposer")
# [REJECT] The proposer signature, signed_beacon_block.signature, is valid # [REJECT] The proposer signature, signed_beacon_block.signature, is valid
@ -377,8 +383,6 @@ proc validateBeaconBlock*(
return errReject("BeaconBlock: Invalid proposer signature") return errReject("BeaconBlock: Invalid proposer signature")
validateBeaconBlockBellatrix(signed_beacon_block, parent)
ok() ok()
# https://github.com/ethereum/consensus-specs/blob/v1.1.9/specs/phase0/p2p-interface.md#beacon_attestation_subnet_id # https://github.com/ethereum/consensus-specs/blob/v1.1.9/specs/phase0/p2p-interface.md#beacon_attestation_subnet_id