2020-09-14 14:26:31 +00:00
|
|
|
# beacon_chain
|
|
|
|
# Copyright (c) 2020 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: [Defect].}
|
|
|
|
|
|
|
|
import
|
|
|
|
# Standard libraries
|
2020-10-07 16:57:21 +00:00
|
|
|
std/[deques, options, sequtils, sets, tables],
|
2020-09-14 14:26:31 +00:00
|
|
|
# Status libraries
|
|
|
|
chronicles, json_serialization/std/sets as jsonSets,
|
|
|
|
# Internal
|
2020-09-24 17:05:49 +00:00
|
|
|
./spec/[crypto, datatypes, helpers, state_transition_block],
|
|
|
|
./block_pools/[chain_dag, clearance, quarantine],
|
2020-09-14 14:26:31 +00:00
|
|
|
./beacon_node_types
|
|
|
|
|
|
|
|
export beacon_node_types, sets
|
|
|
|
|
2020-10-07 16:57:21 +00:00
|
|
|
logScope: topics = "exitpool"
|
|
|
|
|
|
|
|
const
|
|
|
|
ATTESTER_SLASHINGS_BOUND = MAX_ATTESTER_SLASHINGS * 2
|
|
|
|
PROPOSER_SLASHINGS_BOUND = MAX_PROPOSER_SLASHINGS * 2
|
|
|
|
VOLUNTARY_EXITS_BOUND = MAX_VOLUNTARY_EXITS * 2
|
2020-09-14 14:26:31 +00:00
|
|
|
|
|
|
|
proc init*(
|
|
|
|
T: type ExitPool, chainDag: ChainDAGRef, quarantine: QuarantineRef): T =
|
|
|
|
## Initialize an ExitPool from the chainDag `headState`
|
|
|
|
T(
|
2020-10-07 16:57:21 +00:00
|
|
|
# Allow for filtering out some exit messages during block production
|
2020-09-14 14:26:31 +00:00
|
|
|
attester_slashings:
|
2020-10-07 16:57:21 +00:00
|
|
|
initDeque[AttesterSlashing](initialSize = ATTESTER_SLASHINGS_BOUND.int),
|
2020-09-14 14:26:31 +00:00
|
|
|
proposer_slashings:
|
2020-10-07 16:57:21 +00:00
|
|
|
initDeque[ProposerSlashing](initialSize = PROPOSER_SLASHINGS_BOUND.int),
|
2020-09-14 14:26:31 +00:00
|
|
|
voluntary_exits:
|
2020-10-07 16:57:21 +00:00
|
|
|
initDeque[SignedVoluntaryExit](initialSize = VOLUNTARY_EXITS_BOUND.int),
|
2020-09-14 14:26:31 +00:00
|
|
|
chainDag: chainDag,
|
|
|
|
quarantine: quarantine
|
|
|
|
)
|
|
|
|
|
2020-09-24 17:05:49 +00:00
|
|
|
func addExitMessage*(subpool: var auto, exitMessage, bound: auto) =
|
2020-10-07 16:57:21 +00:00
|
|
|
# Prefer newer to older exit messages
|
2020-09-14 14:26:31 +00:00
|
|
|
while subpool.lenu64 >= bound:
|
|
|
|
discard subpool.popFirst()
|
|
|
|
|
|
|
|
subpool.addLast(exitMessage)
|
|
|
|
doAssert subpool.lenu64 <= bound
|
|
|
|
|
2020-10-07 16:57:21 +00:00
|
|
|
iterator getValidatorIndices(attester_slashing: AttesterSlashing): uint64 =
|
|
|
|
# TODO rely on sortedness and do this sans memory allocations, but it's only
|
|
|
|
# when producing a beacon block, which is rare bottlenecked elsewhere.
|
|
|
|
let
|
|
|
|
attestation_1_indices =
|
|
|
|
attester_slashing.attestation_1.attesting_indices.asSeq
|
|
|
|
attestation_2_indices =
|
|
|
|
attester_slashing.attestation_2.attesting_indices.asSeq
|
|
|
|
attester_slashed_indices =
|
|
|
|
toHashSet(attestation_1_indices) * toHashSet(attestation_2_indices)
|
|
|
|
|
|
|
|
for validator_index in attester_slashed_indices:
|
|
|
|
yield validator_index
|
|
|
|
|
|
|
|
iterator getValidatorIndices(proposer_slashing: ProposerSlashing): uint64 =
|
|
|
|
yield proposer_slashing.signed_header_1.message.proposer_index
|
|
|
|
|
|
|
|
iterator getValidatorIndices(voluntary_exit: SignedVoluntaryExit): uint64 =
|
|
|
|
yield voluntary_exit.message.validator_index
|
|
|
|
|
|
|
|
# TODO stew/sequtils2
|
|
|
|
template allIt(s, pred: untyped): bool =
|
|
|
|
# https://github.com/nim-lang/Nim/blob/version-1-2/lib/pure/collections/sequtils.nim#L640-L662
|
|
|
|
# without the items(...)
|
|
|
|
var result = true
|
|
|
|
for it {.inject.} in s:
|
|
|
|
if not pred:
|
|
|
|
result = false
|
|
|
|
break
|
|
|
|
result
|
|
|
|
|
|
|
|
func getExitMessagesForBlock[T](
|
|
|
|
subpool: var Deque[T], pool: var ExitPool, bound: uint64): seq[T] =
|
|
|
|
# Approach taken here is to simply collect messages, effectively, a circular
|
|
|
|
# buffer and only re-validate that they haven't already found themselves out
|
|
|
|
# of the network eventually via some exit message at block construction time
|
|
|
|
# at which point we use exit_epoch. It doesn't matter which of these message
|
|
|
|
# types has triggered that exit, as the validation on incoming messages will
|
|
|
|
# find it to either be IGNORE (if it's the same type of exit message) or, if
|
|
|
|
# it's a different type, REJECT. Neither is worth packaging into BeaconBlock
|
|
|
|
# messages we broadcast.
|
|
|
|
#
|
|
|
|
# Beyond that, no other criterion of the exit messages' validity changes from
|
|
|
|
# when they were created, so given that we validated them to start with, they
|
|
|
|
# otherwise remain as valid as when we received them. There's no need to thus
|
|
|
|
# re-validate them on their way out.
|
|
|
|
#
|
|
|
|
# This overall approach handles a scenario wherein we receive an exit message
|
|
|
|
# over gossip and put it in the pool; receive a block X, with that message in
|
|
|
|
# it, and select it as head; then orphan block X and build instead on X-1. If
|
|
|
|
# this occurs, only validating after the fact ensures that we still broadcast
|
|
|
|
# out those exit messages that were in orphaned block X by not having eagerly
|
|
|
|
# removed them, if we have the chance.
|
|
|
|
while true:
|
|
|
|
if subpool.len == 0 or result.lenu64 >= bound:
|
2020-09-14 14:26:31 +00:00
|
|
|
break
|
|
|
|
|
2020-10-07 16:57:21 +00:00
|
|
|
# Prefer recent messages
|
|
|
|
let exit_message = subpool.popLast()
|
|
|
|
|
|
|
|
if allIt(
|
|
|
|
getValidatorIndices(exit_message),
|
|
|
|
pool.chainDag.headState.data.data.validators[it].exit_epoch !=
|
|
|
|
FAR_FUTURE_EPOCH):
|
|
|
|
# A beacon block exit message already targeted all these validators
|
|
|
|
continue
|
|
|
|
|
|
|
|
result.add exit_message
|
|
|
|
|
|
|
|
subpool.clear()
|
2020-09-16 07:16:23 +00:00
|
|
|
doAssert result.lenu64 <= bound
|
2020-09-14 14:26:31 +00:00
|
|
|
|
2020-09-16 07:16:23 +00:00
|
|
|
func getAttesterSlashingsForBlock*(pool: var ExitPool):
|
|
|
|
seq[AttesterSlashing] =
|
|
|
|
## Retrieve attester slashings that may be added to a new block
|
|
|
|
getExitMessagesForBlock[AttesterSlashing](
|
2020-10-07 16:57:21 +00:00
|
|
|
pool.attester_slashings, pool, MAX_ATTESTER_SLASHINGS)
|
2020-09-14 14:26:31 +00:00
|
|
|
|
2020-09-16 07:16:23 +00:00
|
|
|
func getProposerSlashingsForBlock*(pool: var ExitPool):
|
|
|
|
seq[ProposerSlashing] =
|
|
|
|
## Retrieve proposer slashings that may be added to a new block
|
|
|
|
getExitMessagesForBlock[ProposerSlashing](
|
2020-10-07 16:57:21 +00:00
|
|
|
pool.proposer_slashings, pool, MAX_PROPOSER_SLASHINGS)
|
2020-09-14 14:26:31 +00:00
|
|
|
|
2020-09-16 07:16:23 +00:00
|
|
|
func getVoluntaryExitsForBlock*(pool: var ExitPool):
|
2020-09-24 17:05:49 +00:00
|
|
|
seq[SignedVoluntaryExit] =
|
2020-09-16 07:16:23 +00:00
|
|
|
## Retrieve voluntary exits that may be added to a new block
|
2020-09-24 17:05:49 +00:00
|
|
|
getExitMessagesForBlock[SignedVoluntaryExit](
|
2020-10-07 16:57:21 +00:00
|
|
|
pool.voluntary_exits, pool, MAX_VOLUNTARY_EXITS)
|
2020-09-14 14:26:31 +00:00
|
|
|
|
2020-10-12 14:37:14 +00:00
|
|
|
# https://github.com/ethereum/eth2.0-specs/blob/v1.0.0-rc.0/specs/phase0/p2p-interface.md#attester_slashing
|
2020-09-14 14:26:31 +00:00
|
|
|
proc validateAttesterSlashing*(
|
2020-09-24 17:05:49 +00:00
|
|
|
pool: var ExitPool, attester_slashing: AttesterSlashing):
|
2020-09-18 11:53:09 +00:00
|
|
|
Result[bool, (ValidationResult, cstring)] =
|
2020-09-14 14:26:31 +00:00
|
|
|
# [IGNORE] At least one index in the intersection of the attesting indices of
|
|
|
|
# each attestation has not yet been seen in any prior attester_slashing (i.e.
|
|
|
|
# attester_slashed_indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices),
|
|
|
|
# verify if any(attester_slashed_indices.difference(prior_seen_attester_slashed_indices))).
|
2020-09-24 17:05:49 +00:00
|
|
|
# TODO sequtils2 should be able to make this more reasonable, from asSeq on
|
2020-10-07 16:57:21 +00:00
|
|
|
# down, and can sort and just find intersection that way
|
|
|
|
let
|
|
|
|
attestation_1_indices =
|
|
|
|
attester_slashing.attestation_1.attesting_indices.asSeq
|
|
|
|
attestation_2_indices =
|
|
|
|
attester_slashing.attestation_2.attesting_indices.asSeq
|
|
|
|
attester_slashed_indices =
|
|
|
|
toHashSet(attestation_1_indices) * toHashSet(attestation_2_indices)
|
2020-09-24 17:05:49 +00:00
|
|
|
|
|
|
|
if not disjoint(
|
|
|
|
attester_slashed_indices, pool.prior_seen_attester_slashed_indices):
|
|
|
|
const err_str: cstring =
|
|
|
|
"validateAttesterSlashing: attester-slashed index already attester-slashed"
|
2020-10-20 12:31:20 +00:00
|
|
|
return err((ValidationResult.Ignore, err_str))
|
2020-09-14 14:26:31 +00:00
|
|
|
|
|
|
|
# [REJECT] All of the conditions within process_attester_slashing pass
|
|
|
|
# validation.
|
2020-09-24 17:05:49 +00:00
|
|
|
var cache =
|
|
|
|
getStateCache(pool.chainDag.head,
|
|
|
|
pool.chainDag.headState.data.data.slot.compute_epoch_at_slot)
|
|
|
|
let attester_slashing_validity =
|
|
|
|
check_attester_slashing(
|
|
|
|
pool.chainDag.headState.data.data, attester_slashing, {}, cache)
|
|
|
|
if attester_slashing_validity.isErr:
|
2020-10-20 12:31:20 +00:00
|
|
|
return err((ValidationResult.Reject, attester_slashing_validity.error))
|
2020-09-24 17:05:49 +00:00
|
|
|
|
|
|
|
pool.prior_seen_attester_slashed_indices.incl attester_slashed_indices
|
2020-09-14 14:26:31 +00:00
|
|
|
pool.attester_slashings.addExitMessage(
|
2020-10-07 16:57:21 +00:00
|
|
|
attester_slashing, ATTESTER_SLASHINGS_BOUND)
|
2020-09-14 14:26:31 +00:00
|
|
|
|
|
|
|
ok(true)
|
|
|
|
|
2020-10-12 14:37:14 +00:00
|
|
|
# https://github.com/ethereum/eth2.0-specs/blob/v1.0.0-rc.0/specs/phase0/p2p-interface.md#proposer_slashing
|
2020-09-14 14:26:31 +00:00
|
|
|
proc validateProposerSlashing*(
|
2020-09-24 17:05:49 +00:00
|
|
|
pool: var ExitPool, proposer_slashing: ProposerSlashing):
|
2020-09-18 11:53:09 +00:00
|
|
|
Result[bool, (ValidationResult, cstring)] =
|
2020-09-14 14:26:31 +00:00
|
|
|
# [IGNORE] The proposer slashing is the first valid proposer slashing
|
|
|
|
# received for the proposer with index
|
|
|
|
# proposer_slashing.signed_header_1.message.proposer_index.
|
2020-10-07 16:57:21 +00:00
|
|
|
if proposer_slashing.signed_header_1.message.proposer_index in
|
2020-09-24 17:05:49 +00:00
|
|
|
pool.prior_seen_proposer_slashed_indices:
|
|
|
|
const err_str: cstring =
|
|
|
|
"validateProposerSlashing: proposer-slashed index already proposer-slashed"
|
2020-10-20 12:31:20 +00:00
|
|
|
return err((ValidationResult.Ignore, err_str))
|
2020-09-14 14:26:31 +00:00
|
|
|
|
|
|
|
# [REJECT] All of the conditions within process_proposer_slashing pass validation.
|
2020-09-24 17:05:49 +00:00
|
|
|
var cache =
|
|
|
|
getStateCache(pool.chainDag.head,
|
|
|
|
pool.chainDag.headState.data.data.slot.compute_epoch_at_slot)
|
|
|
|
let proposer_slashing_validity =
|
|
|
|
check_proposer_slashing(
|
|
|
|
pool.chainDag.headState.data.data, proposer_slashing, {}, cache)
|
|
|
|
if proposer_slashing_validity.isErr:
|
2020-10-20 12:31:20 +00:00
|
|
|
return err((ValidationResult.Reject, proposer_slashing_validity.error))
|
2020-09-24 17:05:49 +00:00
|
|
|
|
|
|
|
pool.prior_seen_proposer_slashed_indices.incl(
|
2020-10-07 16:57:21 +00:00
|
|
|
proposer_slashing.signed_header_1.message.proposer_index)
|
2020-09-16 07:16:23 +00:00
|
|
|
pool.proposer_slashings.addExitMessage(
|
2020-10-07 16:57:21 +00:00
|
|
|
proposer_slashing, PROPOSER_SLASHINGS_BOUND)
|
2020-09-16 07:16:23 +00:00
|
|
|
|
2020-09-14 14:26:31 +00:00
|
|
|
ok(true)
|
|
|
|
|
2020-10-12 14:37:14 +00:00
|
|
|
# https://github.com/ethereum/eth2.0-specs/blob/v1.0.0-rc.0/specs/phase0/p2p-interface.md#voluntary_exit
|
2020-09-14 14:26:31 +00:00
|
|
|
proc validateVoluntaryExit*(
|
2020-09-24 17:05:49 +00:00
|
|
|
pool: var ExitPool, signed_voluntary_exit: SignedVoluntaryExit):
|
2020-09-18 11:53:09 +00:00
|
|
|
Result[bool, (ValidationResult, cstring)] =
|
2020-09-14 14:26:31 +00:00
|
|
|
# [IGNORE] The voluntary exit is the first valid voluntary exit received for
|
|
|
|
# the validator with index signed_voluntary_exit.message.validator_index.
|
2020-09-24 17:05:49 +00:00
|
|
|
if signed_voluntary_exit.message.validator_index >=
|
|
|
|
pool.chainDag.headState.data.data.validators.lenu64:
|
|
|
|
const err_str: cstring = "validateVoluntaryExit: validator index too high"
|
2020-10-20 12:31:20 +00:00
|
|
|
return err((ValidationResult.Ignore, err_str))
|
2020-10-07 16:57:21 +00:00
|
|
|
if signed_voluntary_exit.message.validator_index in
|
2020-09-24 17:05:49 +00:00
|
|
|
pool.prior_seen_voluntary_exit_indices:
|
|
|
|
const err_str: cstring = "validateVoluntaryExit: validator index already voluntarily exited"
|
2020-10-20 12:31:20 +00:00
|
|
|
return err((ValidationResult.Ignore, err_str))
|
2020-09-14 14:26:31 +00:00
|
|
|
|
|
|
|
# [REJECT] All of the conditions within process_voluntary_exit pass
|
|
|
|
# validation.
|
2020-09-24 17:05:49 +00:00
|
|
|
var cache =
|
|
|
|
getStateCache(pool.chainDag.head,
|
|
|
|
pool.chainDag.headState.data.data.slot.compute_epoch_at_slot)
|
|
|
|
let voluntary_exit_validity =
|
|
|
|
check_voluntary_exit(
|
|
|
|
pool.chainDag.headState.data.data, signed_voluntary_exit, {}, cache)
|
|
|
|
if voluntary_exit_validity.isErr:
|
2020-10-20 12:31:20 +00:00
|
|
|
return err((ValidationResult.Reject, voluntary_exit_validity.error))
|
2020-09-24 17:05:49 +00:00
|
|
|
|
|
|
|
pool.prior_seen_voluntary_exit_indices.incl(
|
2020-10-07 16:57:21 +00:00
|
|
|
signed_voluntary_exit.message.validator_index)
|
2020-09-16 07:16:23 +00:00
|
|
|
pool.voluntary_exits.addExitMessage(
|
2020-10-07 16:57:21 +00:00
|
|
|
signed_voluntary_exit, VOLUNTARY_EXITS_BOUND)
|
2020-09-16 07:16:23 +00:00
|
|
|
|
2020-09-14 14:26:31 +00:00
|
|
|
ok(true)
|