2020-05-19 14:18:07 +00:00
|
|
|
# beacon_chain
|
2021-02-25 13:37:22 +00:00
|
|
|
# Copyright (c) 2018-2021 Status Research & Development GmbH
|
2020-05-19 14:18:07 +00:00
|
|
|
# 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.
|
|
|
|
|
2020-06-16 05:45:04 +00:00
|
|
|
{.push raises: [Defect].}
|
|
|
|
|
2020-05-19 14:18:07 +00:00
|
|
|
import
|
2020-10-20 12:31:20 +00:00
|
|
|
std/tables,
|
2020-07-28 13:54:32 +00:00
|
|
|
chronicles,
|
2020-12-16 08:37:22 +00:00
|
|
|
stew/[assign2, results],
|
2021-01-25 18:45:48 +00:00
|
|
|
eth/keys,
|
2021-03-05 13:12:00 +00:00
|
|
|
../extras, ../beacon_clock,
|
2021-01-25 18:45:48 +00:00
|
|
|
../spec/[crypto, datatypes, digest, helpers, signatures, signatures_batch, state_transition],
|
2021-03-04 09:13:44 +00:00
|
|
|
./block_pools_types, ./blockchain_dag, ./block_quarantine
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2020-05-21 17:08:31 +00:00
|
|
|
export results
|
|
|
|
|
2020-05-19 14:18:07 +00:00
|
|
|
# Clearance
|
|
|
|
# ---------------------------------------------
|
|
|
|
#
|
|
|
|
# This module is in charge of making the
|
|
|
|
# "quarantined" network blocks
|
2020-07-30 19:18:17 +00:00
|
|
|
# pass the firewall and be stored in the chain DAG
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2020-06-16 05:45:04 +00:00
|
|
|
logScope:
|
|
|
|
topics = "clearance"
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2021-01-25 18:45:48 +00:00
|
|
|
template asSigVerified(x: SignedBeaconBlock): SigVerifiedSignedBeaconBlock =
|
|
|
|
## This converts a signed beacon block to a sig verified beacon clock.
|
|
|
|
## This assumes that their bytes representation is the same.
|
|
|
|
##
|
|
|
|
## At the GC-level, the GC is type-agnostic it's all type erased so
|
|
|
|
## casting between seq[Attestation] and seq[TrustedAttestation]
|
|
|
|
## will not disrupt GC operations.
|
|
|
|
##
|
|
|
|
## This SHOULD be used in function calls to avoid expensive temporary.
|
|
|
|
## see https://github.com/status-im/nimbus-eth2/pull/2250#discussion_r562010679
|
|
|
|
cast[ptr SigVerifiedSignedBeaconBlock](signedBlock.unsafeAddr)[]
|
|
|
|
|
|
|
|
template asTrusted(x: SignedBeaconBlock or SigVerifiedBeaconBlock): TrustedSignedBeaconBlock =
|
|
|
|
## This converts a sigverified beacon block to a trusted beacon clock.
|
|
|
|
## This assumes that their bytes representation is the same.
|
|
|
|
##
|
|
|
|
## At the GC-level, the GC is type-agnostic it's all type erased so
|
|
|
|
## casting between seq[Attestation] and seq[TrustedAttestation]
|
|
|
|
## will not disrupt GC operations.
|
|
|
|
##
|
|
|
|
## This SHOULD be used in function calls to avoid expensive temporary.
|
|
|
|
## see https://github.com/status-im/nimbus-eth2/pull/2250#discussion_r562010679
|
|
|
|
cast[ptr TrustedSignedBeaconBlock](signedBlock.unsafeAddr)[]
|
|
|
|
|
2020-07-31 14:49:06 +00:00
|
|
|
func getOrResolve*(dag: ChainDAGRef, quarantine: var QuarantineRef, root: Eth2Digest): BlockRef =
|
2020-05-19 14:18:07 +00:00
|
|
|
## Fetch a block ref, or nil if not found (will be added to list of
|
|
|
|
## blocks-to-resolve)
|
|
|
|
result = dag.getRef(root)
|
|
|
|
|
|
|
|
if result.isNil:
|
2020-11-16 19:15:43 +00:00
|
|
|
quarantine.addMissing(root)
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2021-01-25 18:45:48 +00:00
|
|
|
proc batchVerify(quarantine: var QuarantineRef, sigs: openArray[SignatureSet]): bool =
|
|
|
|
var secureRandomBytes: array[32, byte]
|
|
|
|
quarantine.rng[].brHmacDrbgGenerate(secureRandomBytes)
|
|
|
|
|
|
|
|
# TODO: For now only enable serial batch verification
|
|
|
|
return batchVerifySerial(quarantine.sigVerifCache, sigs, secureRandomBytes)
|
|
|
|
|
2020-07-09 09:29:32 +00:00
|
|
|
proc addRawBlock*(
|
2020-07-31 14:49:06 +00:00
|
|
|
dag: var ChainDAGRef, quarantine: var QuarantineRef,
|
2020-07-22 09:42:55 +00:00
|
|
|
signedBlock: SignedBeaconBlock, onBlockAdded: OnBlockAdded
|
2020-09-18 11:53:09 +00:00
|
|
|
): Result[BlockRef, (ValidationResult, BlockError)] {.gcsafe.}
|
2020-05-19 14:18:07 +00:00
|
|
|
|
|
|
|
proc addResolvedBlock(
|
2020-07-31 14:49:06 +00:00
|
|
|
dag: var ChainDAGRef, quarantine: var QuarantineRef,
|
2021-01-25 18:45:48 +00:00
|
|
|
state: var StateData, trustedBlock: TrustedSignedBeaconBlock,
|
2020-08-05 06:28:43 +00:00
|
|
|
parent: BlockRef, cache: var StateCache,
|
2020-07-22 09:42:55 +00:00
|
|
|
onBlockAdded: OnBlockAdded
|
2020-08-18 20:29:33 +00:00
|
|
|
) =
|
2020-07-30 15:48:25 +00:00
|
|
|
# TODO move quarantine processing out of here
|
2021-01-25 18:45:48 +00:00
|
|
|
doAssert state.data.data.slot == trustedBlock.message.slot,
|
2020-08-18 20:29:33 +00:00
|
|
|
"state must match block"
|
2021-01-25 18:45:48 +00:00
|
|
|
doAssert state.blck.root == trustedBlock.message.parent_root,
|
2020-08-18 20:29:33 +00:00
|
|
|
"the StateData passed into the addResolved function not yet updated!"
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2020-07-16 13:16:51 +00:00
|
|
|
let
|
2021-01-25 18:45:48 +00:00
|
|
|
blockRoot = trustedBlock.root
|
|
|
|
blockRef = BlockRef.init(blockRoot, trustedBlock.message)
|
2020-07-30 15:48:25 +00:00
|
|
|
blockEpoch = blockRef.slot.compute_epoch_at_slot()
|
2020-08-11 19:39:53 +00:00
|
|
|
|
|
|
|
link(parent, blockRef)
|
|
|
|
|
2021-03-17 10:17:15 +00:00
|
|
|
var epochRef = dag.findEpochRef(parent, blockEpoch)
|
2020-08-18 20:29:33 +00:00
|
|
|
if epochRef == nil:
|
2021-03-17 10:17:15 +00:00
|
|
|
let prevEpochRef =
|
|
|
|
if blockEpoch < 1: nil else: dag.findEpochRef(parent, blockEpoch - 1)
|
2020-08-18 20:29:33 +00:00
|
|
|
|
|
|
|
epochRef = EpochRef.init(state.data.data, cache, prevEpochRef)
|
2021-03-17 10:17:15 +00:00
|
|
|
dag.addEpochRef(blockRef, epochRef)
|
2020-08-26 15:06:40 +00:00
|
|
|
|
2021-03-17 10:17:15 +00:00
|
|
|
dag.blocks.incl(KeyedBlockRef.init(blockRef))
|
2020-05-19 14:18:07 +00:00
|
|
|
trace "Populating block dag", key = blockRoot, val = blockRef
|
|
|
|
|
|
|
|
# Resolved blocks should be stored in database
|
2021-01-25 18:45:48 +00:00
|
|
|
dag.putBlock(trustedBlock)
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2020-07-28 13:54:32 +00:00
|
|
|
var foundHead: BlockRef
|
2020-05-19 14:18:07 +00:00
|
|
|
for head in dag.heads.mitems():
|
2020-07-28 13:54:32 +00:00
|
|
|
if head.isAncestorOf(blockRef):
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2020-07-28 13:54:32 +00:00
|
|
|
head = blockRef
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2020-07-28 13:54:32 +00:00
|
|
|
foundHead = head
|
2020-05-19 14:18:07 +00:00
|
|
|
break
|
|
|
|
|
2020-07-28 13:54:32 +00:00
|
|
|
if foundHead.isNil:
|
|
|
|
foundHead = blockRef
|
|
|
|
dag.heads.add(foundHead)
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2020-10-01 18:56:42 +00:00
|
|
|
debug "Block resolved",
|
2021-01-25 18:45:48 +00:00
|
|
|
blck = shortLog(trustedBlock.message),
|
2020-05-19 14:18:07 +00:00
|
|
|
blockRoot = shortLog(blockRoot),
|
2020-07-16 13:16:51 +00:00
|
|
|
heads = dag.heads.len()
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2020-08-18 20:29:33 +00:00
|
|
|
state.blck = blockRef
|
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
# Notify others of the new block before processing the quarantine, such that
|
|
|
|
# notifications for parents happens before those of the children
|
2020-07-22 09:42:55 +00:00
|
|
|
if onBlockAdded != nil:
|
2021-01-25 18:45:48 +00:00
|
|
|
onBlockAdded(blockRef, trustedBlock, epochRef, state.data)
|
2020-07-09 09:29:32 +00:00
|
|
|
|
2020-05-19 14:18:07 +00:00
|
|
|
# Now that we have the new block, we should see if any of the previously
|
|
|
|
# unresolved blocks magically become resolved
|
|
|
|
# TODO This code is convoluted because when there are more than ~1.5k
|
|
|
|
# blocks being synced, there's a stack overflow as `add` gets called
|
|
|
|
# for the whole chain of blocks. Instead we use this ugly field in `dag`
|
|
|
|
# which could be avoided by refactoring the code
|
2020-07-09 09:29:32 +00:00
|
|
|
# TODO unit test the logic, in particular interaction with fork choice block parents
|
2020-05-19 14:18:07 +00:00
|
|
|
if not quarantine.inAdd:
|
|
|
|
quarantine.inAdd = true
|
|
|
|
defer: quarantine.inAdd = false
|
2020-07-28 13:54:32 +00:00
|
|
|
var entries = 0
|
|
|
|
while entries != quarantine.orphans.len:
|
|
|
|
entries = quarantine.orphans.len # keep going while quarantine is shrinking
|
|
|
|
var resolved: seq[SignedBeaconBlock]
|
|
|
|
for _, v in quarantine.orphans:
|
2021-03-17 10:17:15 +00:00
|
|
|
if v.message.parent_root in dag:
|
|
|
|
resolved.add(v)
|
2020-07-28 13:54:32 +00:00
|
|
|
|
|
|
|
for v in resolved:
|
2020-07-22 09:42:55 +00:00
|
|
|
discard addRawBlock(dag, quarantine, v, onBlockAdded)
|
2020-07-09 09:29:32 +00:00
|
|
|
|
2021-01-25 18:45:48 +00:00
|
|
|
proc addRawBlockCheckStateTransition(
|
|
|
|
dag: var ChainDAGRef, quarantine: var QuarantineRef,
|
|
|
|
signedBlock: SomeSignedBeaconBlock, cache: var StateCache
|
|
|
|
): (ValidationResult, BlockError) =
|
|
|
|
## addRawBlock - Ensure block can be applied on a state
|
|
|
|
let
|
|
|
|
poolPtr = unsafeAddr dag # safe because restore is short-lived
|
|
|
|
func restore(v: var HashedBeaconState) =
|
|
|
|
# TODO address this ugly workaround - there should probably be a
|
|
|
|
# `state_transition` that takes a `StateData` instead and updates
|
|
|
|
# the block as well
|
|
|
|
doAssert v.addr == addr poolPtr.clearanceState.data
|
|
|
|
assign(poolPtr.clearanceState, poolPtr.headState)
|
|
|
|
|
|
|
|
if not state_transition(dag.runtimePreset, dag.clearanceState.data, signedBlock,
|
|
|
|
cache, dag.updateFlags + {slotProcessed}, restore):
|
|
|
|
info "Invalid block"
|
|
|
|
|
|
|
|
return (ValidationResult.Reject, Invalid)
|
|
|
|
return (ValidationResult.Accept, default(BlockError))
|
|
|
|
|
|
|
|
proc addRawBlockKnownParent(
|
2020-07-31 14:49:06 +00:00
|
|
|
dag: var ChainDAGRef, quarantine: var QuarantineRef,
|
2020-07-09 09:29:32 +00:00
|
|
|
signedBlock: SignedBeaconBlock,
|
2021-01-25 18:45:48 +00:00
|
|
|
parent: BlockRef,
|
2020-07-22 09:42:55 +00:00
|
|
|
onBlockAdded: OnBlockAdded
|
2020-09-18 11:53:09 +00:00
|
|
|
): Result[BlockRef, (ValidationResult, BlockError)] =
|
2021-01-25 18:45:48 +00:00
|
|
|
## addRawBlock - Block has a parent
|
|
|
|
|
|
|
|
if parent.slot >= signedBlock.message.slot:
|
|
|
|
# A block whose parent is newer than the block itself is clearly invalid -
|
|
|
|
# discard it immediately
|
|
|
|
debug "Invalid block slot",
|
|
|
|
parentBlock = shortLog(parent)
|
|
|
|
|
|
|
|
return err((ValidationResult.Reject, Invalid))
|
|
|
|
|
|
|
|
if (parent.slot < dag.finalizedHead.slot) or
|
|
|
|
(parent.slot == dag.finalizedHead.slot and
|
|
|
|
parent != dag.finalizedHead.blck):
|
|
|
|
# We finalized a block that's newer than the parent of this block - this
|
|
|
|
# block, although recent, is thus building on a history we're no longer
|
|
|
|
# interested in pursuing. This can happen if a client produces a block
|
|
|
|
# while syncing - ie it's own head block will be old, but it'll create
|
|
|
|
# a block according to the wall clock, in its own little world - this is
|
|
|
|
# correct - from their point of view, the head block they have is the
|
|
|
|
# latest thing that happened on the chain and they're performing their
|
|
|
|
# duty correctly.
|
|
|
|
debug "Unviable block, dropping",
|
2020-05-19 14:18:07 +00:00
|
|
|
finalizedHead = shortLog(dag.finalizedHead),
|
2020-07-16 13:16:51 +00:00
|
|
|
tail = shortLog(dag.tail)
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2020-10-20 12:31:20 +00:00
|
|
|
return err((ValidationResult.Ignore, Unviable))
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2021-01-25 18:45:48 +00:00
|
|
|
# The block might have been in either of `orphans` or `missing` - we don't
|
|
|
|
# want any more work done on its behalf
|
|
|
|
quarantine.removeOrphan(signedBlock)
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2021-01-25 18:45:48 +00:00
|
|
|
# The block is resolved, now it's time to validate it to ensure that the
|
|
|
|
# blocks we add to the database are clean for the given state
|
2020-05-19 14:18:07 +00:00
|
|
|
|
|
|
|
|
2021-01-25 18:45:48 +00:00
|
|
|
# TODO if the block is from the future, we should not be resolving it (yet),
|
|
|
|
# but maybe we should use it as a hint that our clock is wrong?
|
|
|
|
var cache = StateCache()
|
|
|
|
updateStateData(
|
|
|
|
dag, dag.clearanceState, parent.atSlot(signedBlock.message.slot), true, cache)
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2021-01-25 18:45:48 +00:00
|
|
|
# First batch verify crypto
|
|
|
|
if skipBLSValidation notin dag.updateFlags:
|
|
|
|
# TODO: remove skipBLSValidation
|
2020-07-22 09:42:55 +00:00
|
|
|
|
2021-01-25 18:45:48 +00:00
|
|
|
var sigs: seq[SignatureSet]
|
|
|
|
if not sigs.collectSignatureSets(signedBlock, dag.clearanceState.data.data, cache):
|
|
|
|
# A PublicKey or Signature isn't on the BLS12-381 curve
|
|
|
|
return err((ValidationResult.Reject, Invalid))
|
|
|
|
if not quarantine.batchVerify(sigs):
|
|
|
|
return err((ValidationResult.Reject, Invalid))
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2021-01-25 18:45:48 +00:00
|
|
|
static: doAssert sizeof(SignedBeaconBlock) == sizeof(SigVerifiedSignedBeaconBlock)
|
|
|
|
let (valRes, blockErr) = addRawBlockCheckStateTransition(
|
|
|
|
dag, quarantine, signedBlock.asSigVerified(), cache)
|
|
|
|
if valRes != ValidationResult.Accept:
|
|
|
|
return err((valRes, blockErr))
|
|
|
|
|
|
|
|
# Careful, clearanceState.data has been updated but not blck - we need to
|
|
|
|
# create the BlockRef first!
|
|
|
|
addResolvedBlock(
|
|
|
|
dag, quarantine, dag.clearanceState,
|
|
|
|
signedBlock.asTrusted(),
|
|
|
|
parent, cache,
|
|
|
|
onBlockAdded)
|
|
|
|
|
|
|
|
return ok dag.clearanceState.blck
|
|
|
|
|
|
|
|
proc addRawBlockUnresolved(
|
|
|
|
dag: var ChainDAGRef,
|
|
|
|
quarantine: var QuarantineRef,
|
|
|
|
signedBlock: SignedBeaconBlock
|
|
|
|
): Result[BlockRef, (ValidationResult, BlockError)] =
|
|
|
|
## addRawBlock - Block is unresolved / has no parent
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2020-11-16 19:15:43 +00:00
|
|
|
# This is an unresolved block - add it to the quarantine, which will cause its
|
|
|
|
# parent to be scheduled for downloading
|
2020-08-31 09:00:38 +00:00
|
|
|
if not quarantine.add(dag, signedBlock):
|
|
|
|
debug "Block quarantine full"
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2021-01-25 18:45:48 +00:00
|
|
|
if signedBlock.message.parent_root in quarantine.missing or
|
2020-09-04 06:39:46 +00:00
|
|
|
containsOrphan(quarantine, signedBlock):
|
2020-08-04 19:28:40 +00:00
|
|
|
debug "Unresolved block (parent missing or orphaned)",
|
|
|
|
orphans = quarantine.orphans.len,
|
|
|
|
missing = quarantine.missing.len
|
|
|
|
|
2020-10-20 12:31:20 +00:00
|
|
|
return err((ValidationResult.Ignore, MissingParent))
|
2020-05-19 14:18:07 +00:00
|
|
|
|
|
|
|
# TODO if we receive spam blocks, one heurestic to implement might be to wait
|
|
|
|
# for a couple of attestations to appear before fetching parents - this
|
|
|
|
# would help prevent using up network resources for spam - this serves
|
|
|
|
# two purposes: one is that attestations are likely to appear for the
|
|
|
|
# block only if it's valid / not spam - the other is that malicious
|
|
|
|
# validators that are not proposers can sign invalid blocks and send
|
|
|
|
# them out without penalty - but signing invalid attestations carries
|
|
|
|
# a risk of being slashed, making attestations a more valuable spam
|
|
|
|
# filter.
|
|
|
|
debug "Unresolved block (parent missing)",
|
2020-06-18 10:03:36 +00:00
|
|
|
orphans = quarantine.orphans.len,
|
2020-07-16 13:16:51 +00:00
|
|
|
missing = quarantine.missing.len
|
2020-05-19 14:18:07 +00:00
|
|
|
|
2020-10-20 12:31:20 +00:00
|
|
|
return err((ValidationResult.Ignore, MissingParent))
|
2020-05-21 17:08:31 +00:00
|
|
|
|
2021-01-25 18:45:48 +00:00
|
|
|
proc addRawBlock*(
|
|
|
|
dag: var ChainDAGRef, quarantine: var QuarantineRef,
|
|
|
|
signedBlock: SignedBeaconBlock,
|
|
|
|
onBlockAdded: OnBlockAdded
|
|
|
|
): Result[BlockRef, (ValidationResult, BlockError)] =
|
|
|
|
## Try adding a block to the chain, verifying first that it passes the state
|
|
|
|
## transition function and contains correct cryptographic signature.
|
|
|
|
##
|
|
|
|
## Cryptographic checks can be skipped by adding skipBLSValidation to dag.updateFlags
|
|
|
|
|
|
|
|
logScope:
|
|
|
|
blck = shortLog(signedBlock.message)
|
|
|
|
blockRoot = shortLog(signedBlock.root)
|
|
|
|
|
|
|
|
template blck(): untyped = signedBlock.message # shortcuts without copy
|
|
|
|
template blockRoot(): untyped = signedBlock.root
|
|
|
|
|
2021-03-17 10:17:15 +00:00
|
|
|
if blockRoot in dag:
|
2021-01-25 18:45:48 +00:00
|
|
|
debug "Block already exists"
|
|
|
|
|
|
|
|
# We should not call the block added callback for blocks that already
|
|
|
|
# existed in the pool, as that may confuse consumers such as the fork
|
|
|
|
# choice. While the validation result won't be accessed, it's IGNORE,
|
|
|
|
# according to the spec.
|
|
|
|
return err((ValidationResult.Ignore, Duplicate))
|
|
|
|
|
|
|
|
quarantine.missing.del(blockRoot)
|
|
|
|
|
|
|
|
# If the block we get is older than what we finalized already, we drop it.
|
|
|
|
# One way this can happen is that we start resolving a block and finalization
|
|
|
|
# happens in the meantime - the block we requested will then be stale
|
|
|
|
# by the time it gets here.
|
|
|
|
if blck.slot <= dag.finalizedHead.slot:
|
|
|
|
debug "Old block, dropping",
|
|
|
|
finalizedHead = shortLog(dag.finalizedHead),
|
|
|
|
tail = shortLog(dag.tail)
|
|
|
|
|
|
|
|
# Doesn't correspond to any specific validation condition, and still won't
|
|
|
|
# be used, but certainly would be IGNORE.
|
|
|
|
return err((ValidationResult.Ignore, Unviable))
|
|
|
|
|
2021-03-17 10:17:15 +00:00
|
|
|
let parent = dag.getRef(blck.parent_root)
|
2021-01-25 18:45:48 +00:00
|
|
|
|
|
|
|
if parent != nil:
|
|
|
|
return addRawBlockKnownParent(dag, quarantine, signedBlock, parent, onBlockAdded)
|
|
|
|
return addRawBlockUnresolved(dag, quarantine, signedBlock)
|