refactor exit pool and gossipsub-subscribe to exit pool message types (#1733)

* refactor exit pool and gossipsub-subscribe to all exit pool message types

* remove unused loop counter
This commit is contained in:
tersec 2020-09-24 17:05:49 +00:00 committed by GitHub
parent 6398a43cc1
commit 6cf7e837ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 218 additions and 111 deletions

View File

@ -70,6 +70,13 @@ OK: 3/3 Fail: 0/3 Skip: 0/3
+ parent sanity [Preset: mainnet] OK
```
OK: 2/2 Fail: 0/2 Skip: 0/2
## Exit pool testing suite
```diff
+ addExitMessage/getAttesterSlashingMessage OK
+ addExitMessage/getProposerSlashingMessage OK
+ addExitMessage/getVoluntaryExitMessage OK
```
OK: 3/3 Fail: 0/3 Skip: 0/3
## Fork Choice + Finality [Preset: mainnet]
```diff
+ fork_choice - testing finality #01 OK
@ -179,11 +186,12 @@ OK: 52/60 Fail: 0/60 Skip: 8/60
+ Peer lifetime test OK
+ Safe/Clear test OK
+ Score check test OK
+ Space tests OK
+ addPeer() test OK
+ addPeerNoWait() test OK
+ deletePeer() test OK
```
OK: 11/11 Fail: 0/11 Skip: 0/11
OK: 12/12 Fail: 0/12 Skip: 0/12
## SSZ dynamic navigator
```diff
+ navigating fields OK
@ -271,4 +279,4 @@ OK: 1/1 Fail: 0/1 Skip: 0/1
OK: 1/1 Fail: 0/1 Skip: 0/1
---TOTAL---
OK: 145/153 Fail: 0/153 Skip: 8/153
OK: 149/157 Fail: 0/157 Skip: 8/157

View File

@ -415,6 +415,8 @@ proc addMessageHandlers(node: BeaconNode): Future[void] =
# As a side-effect, this gets the attestation subnets too.
node.network.subscribe(node.topicBeaconBlocks),
node.network.subscribe(getAttesterSlashingsTopic(node.forkDigest)),
node.network.subscribe(getProposerSlashingsTopic(node.forkDigest)),
node.network.subscribe(getVoluntaryExitsTopic(node.forkDigest)),
node.getAttestationHandlers()
)
@ -838,8 +840,8 @@ proc installMessageValidators(node: BeaconNode) =
node.network.addValidator(
getVoluntaryExitsTopic(node.forkDigest),
proc (voluntaryExit: VoluntaryExit): ValidationResult =
node.processor[].voluntaryExitValidator(voluntaryExit))
proc (signedVoluntaryExit: SignedVoluntaryExit): ValidationResult =
node.processor[].voluntaryExitValidator(signedVoluntaryExit))
proc stop*(node: BeaconNode) =
status = BeaconNodeStatus.Stopping

View File

@ -1,7 +1,7 @@
{.push raises: [Defect].}
import
std/[deques, tables, streams],
std/[deques, sets, streams, tables],
stew/endians2,
spec/[datatypes, digest, crypto],
block_pools/block_pools_types,
@ -79,9 +79,18 @@ type
proposer_slashings*: Deque[ProposerSlashing] ## \
## Not a function of chain DAG branch; just used as a FIFO queue for blocks
voluntary_exits*: Deque[VoluntaryExit] ## \
voluntary_exits*: Deque[SignedVoluntaryExit] ## \
## Not a function of chain DAG branch; just used as a FIFO queue for blocks
prior_seen_attester_slashed_indices*: HashSet[ValidatorIndex] ##\
## Records attester-slashed indices seen.
prior_seen_proposer_slashed_indices*: HashSet[ValidatorIndex] ##\
## Records proposer-slashed indices seen.
prior_seen_voluntary_exit_indices*: HashSet[ValidatorIndex] ##\
## Records voluntary exit indices seen.
chainDag*: ChainDAGRef
quarantine*: QuarantineRef

View File

@ -398,11 +398,12 @@ proc proposerSlashingValidator*(
EVRESULT_ACCEPT
proc voluntaryExitValidator*(
self: var Eth2Processor, voluntaryExit: VoluntaryExit): ValidationResult =
self: var Eth2Processor, signedVoluntaryExit: SignedVoluntaryExit):
ValidationResult =
logScope:
voluntaryExit = shortLog(voluntaryExit)
signedVoluntaryExit = shortLog(signedVoluntaryExit)
let v = self.exitPool[].validateVoluntaryExit(voluntaryExit)
let v = self.exitPool[].validateVoluntaryExit(signedVoluntaryExit)
if v.isErr:
debug "Dropping voluntary exit", err = v.error
return v.error[0]

View File

@ -13,8 +13,8 @@ import
# Status libraries
chronicles, json_serialization/std/sets as jsonSets,
# Internal
./spec/[datatypes, crypto, state_transition_block],
./block_pools/[chain_dag, clearance, quarantine, spec_cache],
./spec/[crypto, datatypes, helpers, state_transition_block],
./block_pools/[chain_dag, clearance, quarantine],
./beacon_node_types
export beacon_node_types, sets
@ -30,12 +30,12 @@ proc init*(
proposer_slashings:
initDeque[ProposerSlashing](initialSize = MAX_PROPOSER_SLASHINGS.int),
voluntary_exits:
initDeque[VoluntaryExit](initialSize = MAX_VOLUNTARY_EXITS.int),
initDeque[SignedVoluntaryExit](initialSize = MAX_VOLUNTARY_EXITS.int),
chainDag: chainDag,
quarantine: quarantine
)
func addExitMessage(subpool: var auto, exitMessage, bound: auto) =
func addExitMessage*(subpool: var auto, exitMessage, bound: auto) =
# Prefer newer to older exit message
while subpool.lenu64 >= bound:
discard subpool.popFirst()
@ -64,120 +64,111 @@ func getProposerSlashingsForBlock*(pool: var ExitPool):
pool.proposer_slashings, MAX_PROPOSER_SLASHINGS)
func getVoluntaryExitsForBlock*(pool: var ExitPool):
seq[VoluntaryExit] =
seq[SignedVoluntaryExit] =
## Retrieve voluntary exits that may be added to a new block
getExitMessagesForBlock[VoluntaryExit](
getExitMessagesForBlock[SignedVoluntaryExit](
pool.voluntary_exits, MAX_VOLUNTARY_EXITS)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/p2p-interface.md#attester_slashing
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.3/specs/phase0/p2p-interface.md#attester_slashing
proc validateAttesterSlashing*(
pool: var ExitPool, attesterSlashing: AttesterSlashing):
pool: var ExitPool, attester_slashing: AttesterSlashing):
Result[bool, (ValidationResult, cstring)] =
# [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))).
#
# This is what the spec states, but even when a validators was slashed using
# proposer slashing it's still pointless relaying an attester slashing for a
# validator; process_attester_slashing() will note not that validator as not
# slashable. Therefore, check whether it's slashed for any reason.
# TODO check for upstream spec disposition on this
let
attestation_1 = attester_slashing.attestation_1
attestation_2 = attester_slashing.attestation_2
# TODO sequtils2 should be able to make this more reasonable, from asSeq on
# down
attesting_indices_1 =
toHashSet(mapIt(attestation_1.attesting_indices.asSeq, it.ValidatorIndex))
attesting_indices_2 =
toHashSet(mapIt(attestation_1.attesting_indices.asSeq, it.ValidatorIndex))
attester_slashed_indices = attesting_indices_1 * attesting_indices_2
# TODO
# TODO sequtils2 should be able to make this more reasonable, from asSeq on
# down
let attester_slashed_indices =
toHashSet(mapIt(
attester_slashing.attestation_1.attesting_indices.asSeq,
it.ValidatorIndex)) *
toHashSet(mapIt(
attester_slashing.attestation_2.attesting_indices.asSeq,
it.ValidatorIndex))
if not disjoint(
attester_slashed_indices, pool.prior_seen_attester_slashed_indices):
const err_str: cstring =
"validateAttesterSlashing: attester-slashed index already attester-slashed"
return err((EVRESULT_IGNORE, err_str))
# [REJECT] All of the conditions within process_attester_slashing pass
# validation.
# This is similar to process_attester_slashing, but both cut-down (it doesn't
# have the loop over attesting indices) and using EpochRef caches, so there's
# no real overlap in code terms with process_proposer_slashing().
block:
let tgtBlck_1 = pool.chainDag.getRef(attestation_1.data.target.root)
if tgtBlck_1.isNil:
pool.quarantine.addMissing(attestation_1.data.target.root)
const err_str: cstring = "Attestation 1 target block unknown"
return err((EVRESULT_IGNORE, err_str))
let tgtBlck_2 = pool.chainDag.getRef(attestation_2.data.target.root)
if tgtBlck_2.isNil:
pool.quarantine.addMissing(attestation_2.data.target.root)
const err_str: cstring = "Attestation 2 target block unknown"
return err((EVRESULT_IGNORE, err_str))
let
epochRef_1 = pool.chainDag.getEpochRef(
tgtBlck_1, attestation_1.data.target.epoch)
epochRef_2 = pool.chainDag.getEpochRef(
tgtBlck_2, attestation_2.data.target.epoch)
fork = pool.chainDag.headState.data.data.fork
genesis_validators_root =
pool.chainDag.headState.data.data.genesis_validators_root
if not is_slashable_attestation_data(
attestation_1.data, attestation_2.data):
const err_str: cstring = "Attestation data not slashable"
return err((EVRESULT_REJECT, err_str))
block:
let v = is_valid_indexed_attestation(
fork, genesis_validators_root, epochRef_1, attestation_1, {})
if v.isErr():
return err((EVRESULT_REJECT, v.error))
block:
let v = is_valid_indexed_attestation(
fork, genesis_validators_root, epochRef_2, attestation_2, {})
if v.isErr():
return err((EVRESULT_REJECT, v.error))
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:
return err((EVRESULT_REJECT, attester_slashing_validity.error))
pool.prior_seen_attester_slashed_indices.incl attester_slashed_indices
pool.attester_slashings.addExitMessage(
attesterSlashing, MAX_ATTESTER_SLASHINGS)
attester_slashing, MAX_ATTESTER_SLASHINGS)
ok(true)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/p2p-interface.md#proposer_slashing
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.3/specs/phase0/p2p-interface.md#proposer_slashing
proc validateProposerSlashing*(
pool: var ExitPool, proposerSlashing: ProposerSlashing):
pool: var ExitPool, proposer_slashing: ProposerSlashing):
Result[bool, (ValidationResult, cstring)] =
# [IGNORE] The proposer slashing is the first valid proposer slashing
# received for the proposer with index
# proposer_slashing.signed_header_1.message.proposer_index.
#
# This is what the spec states, but even when the validator was slashed from
# attester slashing, it's still pointless to relay a proposer slashing for a
# validator; process_proposer_slashing() will mark not that validator as not
# slashable. Therefore, check whether it's slashed for any reason.
# TODO check for upstream spec disposition on this
if proposer_slashing.signed_header_1.message.proposer_index.ValidatorIndex in
pool.prior_seen_proposer_slashed_indices:
const err_str: cstring =
"validateProposerSlashing: proposer-slashed index already proposer-slashed"
return err((EVRESULT_IGNORE, err_str))
# [REJECT] All of the conditions within process_proposer_slashing pass validation.
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:
return err((EVRESULT_REJECT, proposer_slashing_validity.error))
# TODO not called yet, so vacuousness is fine
pool.prior_seen_proposer_slashed_indices.incl(
proposer_slashing.signed_header_1.message.proposer_index.ValidatorIndex)
pool.proposer_slashings.addExitMessage(
proposerSlashing, MAX_PROPOSER_SLASHINGS)
proposer_slashing, MAX_PROPOSER_SLASHINGS)
ok(true)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/p2p-interface.md#voluntary_exit
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.3/specs/phase0/p2p-interface.md#voluntary_exit
proc validateVoluntaryExit*(
pool: var ExitPool, voluntaryExit: VoluntaryExit):
pool: var ExitPool, signed_voluntary_exit: SignedVoluntaryExit):
Result[bool, (ValidationResult, cstring)] =
# [IGNORE] The voluntary exit is the first valid voluntary exit received for
# the validator with index signed_voluntary_exit.message.validator_index.
if signed_voluntary_exit.message.validator_index >=
pool.chainDag.headState.data.data.validators.lenu64:
const err_str: cstring = "validateVoluntaryExit: validator index too high"
return err((EVRESULT_IGNORE, err_str))
if signed_voluntary_exit.message.validator_index.ValidatorIndex in
pool.prior_seen_voluntary_exit_indices:
const err_str: cstring = "validateVoluntaryExit: validator index already voluntarily exited"
return err((EVRESULT_IGNORE, err_str))
# [REJECT] All of the conditions within process_voluntary_exit pass
# validation.
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:
return err((EVRESULT_REJECT, voluntary_exit_validity.error))
# TODO not called yet, so vacuousness is fine
pool.prior_seen_voluntary_exit_indices.incl(
signed_voluntary_exit.message.validator_index.ValidatorIndex)
pool.voluntary_exits.addExitMessage(
voluntaryExit, MAX_VOLUNTARY_EXITS)
signed_voluntary_exit, MAX_VOLUNTARY_EXITS)
ok(true)

View File

@ -739,6 +739,12 @@ func shortLog*(v: VoluntaryExit): auto =
validator_index: v.validator_index
)
func shortLog*(v: SignedVoluntaryExit): auto =
(
message: shortLog(v.message),
signature: shortLog(v.signature)
)
chronicles.formatIt Slot: it.shortLog
chronicles.formatIt Epoch: it.shortLog
chronicles.formatIt BeaconBlock: it.shortLog

View File

@ -134,9 +134,9 @@ func is_slashable_validator(validator: Validator, epoch: Epoch): bool =
(epoch < validator.withdrawable_epoch)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/beacon-chain.md#proposer-slashings
proc process_proposer_slashing*(
proc check_proposer_slashing*(
state: var BeaconState, proposer_slashing: ProposerSlashing,
flags: UpdateFlags, stateCache: var StateCache):
flags: UpdateFlags, cache: var StateCache):
Result[void, cstring] {.nbench.} =
let
@ -145,36 +145,46 @@ proc process_proposer_slashing*(
# Not from spec
if header_1.proposer_index >= state.validators.lenu64:
return err("process_proposer_slashing: invalid proposer index")
return err("check_proposer_slashing: invalid proposer index")
# Verify header slots match
if not (header_1.slot == header_2.slot):
return err("process_proposer_slashing: slot mismatch")
return err("check_proposer_slashing: slot mismatch")
# Verify header proposer indices match
if not (header_1.proposer_index == header_2.proposer_index):
return err("process_proposer_slashing: proposer indices mismatch")
return err("check_proposer_slashing: proposer indices mismatch")
# Verify the headers are different
if not (header_1 != header_2):
return err("process_proposer_slashing: headers not different")
return err("check_proposer_slashing: headers not different")
# Verify the proposer is slashable
let proposer = state.validators[header_1.proposer_index]
if not is_slashable_validator(proposer, get_current_epoch(state)):
return err("process_proposer_slashing: slashed proposer")
return err("check_proposer_slashing: slashed proposer")
# Verify signatures
if skipBlsValidation notin flags:
for i, signed_header in [proposer_slashing.signed_header_1,
for signed_header in [proposer_slashing.signed_header_1,
proposer_slashing.signed_header_2]:
if not verify_block_signature(
state.fork, state.genesis_validators_root, signed_header.message.slot,
signed_header.message, proposer.pubkey, signed_header.signature):
return err("process_proposer_slashing: invalid signature")
return err("check_proposer_slashing: invalid signature")
slash_validator(state, header_1.proposer_index.ValidatorIndex, stateCache)
ok()
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/beacon-chain.md#proposer-slashings
proc process_proposer_slashing*(
state: var BeaconState, proposer_slashing: ProposerSlashing,
flags: UpdateFlags, cache: var StateCache):
Result[void, cstring] {.nbench.} =
? check_proposer_slashing(state, proposer_slashing, flags, cache)
slash_validator(
state,
proposer_slashing.signed_header_1.message.proposer_index.ValidatorIndex,
cache)
ok()
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/beacon-chain.md#is_slashable_attestation_data
@ -190,12 +200,12 @@ func is_slashable_attestation_data*(
data_2.target.epoch < data_1.target.epoch)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/beacon-chain.md#attester-slashings
proc process_attester_slashing*(
proc check_attester_slashing*(
state: var BeaconState,
attester_slashing: AttesterSlashing,
flags: UpdateFlags,
stateCache: var StateCache
): Result[void, cstring] {.nbench.}=
cache: var StateCache
): Result[seq[ValidatorIndex], cstring] {.nbench.} =
let
attestation_1 = attester_slashing.attestation_1
attestation_2 = attester_slashing.attestation_2
@ -210,21 +220,39 @@ proc process_attester_slashing*(
if not is_valid_indexed_attestation(state, attestation_2, flags).isOk():
return err("Attester slashing: invalid attestation 2")
var slashed_any = false
var slashed_indices: seq[ValidatorIndex]
for index in sorted(toSeq(intersection(
toHashSet(attestation_1.attesting_indices.asSeq),
toHashSet(attestation_2.attesting_indices.asSeq)).items), system.cmp):
if is_slashable_validator(
state.validators[index], get_current_epoch(state)):
slash_validator(state, index.ValidatorIndex, stateCache)
slashed_any = true
if not slashed_any:
slashed_indices.add index.ValidatorIndex
if slashed_indices.len == 0:
return err("Attester slashing: Trying to slash participant(s) twice")
ok slashed_indices
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/beacon-chain.md#attester-slashings
proc process_attester_slashing*(
state: var BeaconState,
attester_slashing: AttesterSlashing,
flags: UpdateFlags,
cache: var StateCache
): Result[void, cstring] {.nbench.} =
let attester_slashing_validity =
check_attester_slashing(state, attester_slashing, flags, cache)
if attester_slashing_validity.isErr:
return err(attester_slashing_validity.error)
for index in attester_slashing_validity.value:
slash_validator(state, index, cache)
ok()
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/beacon-chain.md#voluntary-exits
proc process_voluntary_exit*(
proc check_voluntary_exit*(
state: var BeaconState,
signed_voluntary_exit: SignedVoluntaryExit,
flags: UpdateFlags,
@ -264,7 +292,7 @@ proc process_voluntary_exit*(
return err("Exit: invalid signature")
# Initiate exit
debug "Exit: processing voluntary exit (validator_leaving)",
debug "Exit: checking voluntary exit (validator_leaving)",
index = voluntary_exit.validator_index,
num_validators = state.validators.len,
epoch = voluntary_exit.epoch,
@ -273,11 +301,20 @@ proc process_voluntary_exit*(
validator_withdrawable_epoch = validator.withdrawable_epoch,
validator_exit_epoch = validator.exit_epoch,
validator_effective_balance = validator.effective_balance
initiate_validator_exit(
state, voluntary_exit.validator_index.ValidatorIndex, cache)
ok()
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.3/specs/phase0/beacon-chain.md#voluntary-exits
proc process_voluntary_exit*(
state: var BeaconState,
signed_voluntary_exit: SignedVoluntaryExit,
flags: UpdateFlags,
cache: var StateCache): Result[void, cstring] {.nbench.} =
? check_voluntary_exit(state, signed_voluntary_exit, flags, cache)
initiate_validator_exit(
state, signed_voluntary_exit.message.validator_index.ValidatorIndex, cache)
ok()
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/beacon-chain.md#operations
proc process_operations(preset: RuntimePreset,
state: var BeaconState,

View File

@ -26,6 +26,7 @@ import # Unit test
./test_state_transition,
./test_sync_protocol,
./test_zero_signature,
./test_exit_pool,
./test_peer_pool,
./test_sync_manager,
./test_honest_validator,

52
tests/test_exit_pool.nim Normal file
View File

@ -0,0 +1,52 @@
# 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.
{.used.}
import std/unittest
import chronicles, chronos, testutil
import ../beacon_chain/spec/[datatypes, presets]
import ../beacon_chain/exit_pool
import ../beacon_chain/block_pools/chain_dag
proc getExitPool(): auto =
let chainDag =
init(ChainDAGRef, defaultRuntimePreset, makeTestDB(SLOTS_PER_EPOCH * 3))
newClone(ExitPool.init(chainDag, QuarantineRef()))
suiteReport "Exit pool testing suite":
setup:
let pool = getExitPool()
timedTest "addExitMessage/getProposerSlashingMessage":
for i in 0'u64 .. MAX_PROPOSER_SLASHINGS + 5:
for j in 0'u64 .. i:
pool.proposer_slashings.addExitMessage(
ProposerSlashing(), MAX_PROPOSER_SLASHINGS)
check:
pool[].getProposerSlashingsForBlock().lenu64 ==
min(i + 1, MAX_PROPOSER_SLASHINGS)
pool[].getProposerSlashingsForBlock().len == 0
timedTest "addExitMessage/getAttesterSlashingMessage":
for i in 0'u64 .. MAX_ATTESTER_SLASHINGS + 5:
for j in 0'u64 .. i:
pool.attester_slashings.addExitMessage(
AttesterSlashing(), MAX_ATTESTER_SLASHINGS)
check:
pool[].getAttesterSlashingsForBlock().lenu64 ==
min(i + 1, MAX_ATTESTER_SLASHINGS)
pool[].getAttesterSlashingsForBlock().len == 0
timedTest "addExitMessage/getVoluntaryExitMessage":
for i in 0'u64 .. MAX_VOLUNTARY_EXITS + 5:
for j in 0'u64 .. i:
pool.voluntary_exits.addExitMessage(
SignedVoluntaryExit(), MAX_VOLUNTARY_EXITS)
check:
pool[].getVoluntaryExitsForBlock().lenu64 ==
min(i + 1, MAX_VOLUNTARY_EXITS)
pool[].getProposerSlashingsForBlock().len == 0