Opt-in Slashing protection + interchange (#1643)
* Slashing protection + interchange initial commit * Restrict the when UseSlashingProtection dance in other modules * Integrate slashing tests in other all_tests * Add attestation slashing protection support * Add a message that mention if built with/without slashing protection * no op the initialization proc * test slashing protection in Jenkins (temp) * where to configure NIMFLAGS in Jenkins ... * Jenkins -> ensure Built with slashing protection * Add slashing protection complete import * use Opt.get(otherwise) * Don't use negation in proc name * Turn slashing protection on by default
This commit is contained in:
parent
6e463257f4
commit
52548f079b
|
@ -35,7 +35,9 @@ def runStages() {
|
|||
sh """#!/bin/bash
|
||||
set -e
|
||||
make -j${env.NPROC} V=1
|
||||
make -j${env.NPROC} V=1 LOG_LEVEL=TRACE NIMFLAGS='-d:testnet_servers_image' beacon_node
|
||||
make -j${env.NPROC} V=1 LOG_LEVEL=TRACE NIMFLAGS='-d:UseSlashingProtection=true -d:testnet_servers_image' beacon_node
|
||||
# Miracl fallback
|
||||
# make -j${env.NPROC} V=1 LOG_LEVEL=TRACE NIMFLAGS='-d:BLS_FORCE_BACKEND=miracl -d:UseSlashingProtection=true -d:testnet_servers_image' beacon_node
|
||||
"""
|
||||
}
|
||||
},
|
||||
|
@ -47,18 +49,11 @@ def runStages() {
|
|||
// EXECUTOR_NUMBER will be 0 or 1, since we have 2 executors per Jenkins node
|
||||
sh """#!/bin/bash
|
||||
set -e
|
||||
export NIMFLAGS='-d:UseSlashingProtection=true'
|
||||
./scripts/launch_local_testnet.sh --testnet 0 --nodes 4 --stop-at-epoch 5 --log-level DEBUG --disable-htop --enable-logtrace --data-dir local_testnet0_data --base-port \$(( 9000 + EXECUTOR_NUMBER * 100 )) --base-rpc-port \$(( 7000 + EXECUTOR_NUMBER * 100 )) --base-metrics-port \$(( 8008 + EXECUTOR_NUMBER * 100 )) -- --verify-finalization --discv5:no
|
||||
./scripts/launch_local_testnet.sh --testnet 1 --nodes 4 --stop-at-epoch 5 --log-level DEBUG --disable-htop --enable-logtrace --data-dir local_testnet1_data --base-port \$(( 9000 + EXECUTOR_NUMBER * 100 )) --base-rpc-port \$(( 7000 + EXECUTOR_NUMBER * 100 )) --base-metrics-port \$(( 8008 + EXECUTOR_NUMBER * 100 )) -- --verify-finalization --discv5:no
|
||||
"""
|
||||
}
|
||||
// stage("testnet finalization - Miracl/Milagro fallback") {
|
||||
// // EXECUTOR_NUMBER will be 0 or 1, since we have 2 executors per Jenkins node
|
||||
// sh """#!/bin/bash
|
||||
// set -e
|
||||
// NIMFLAGS="-d:BLS_FORCE_BACKEND=miracl" ./scripts/launch_local_testnet.sh --testnet 0 --nodes 4 --stop-at-epoch 5 --log-level INFO --disable-htop --data-dir local_testnet0_data --base-port \$(( 9000 + EXECUTOR_NUMBER * 100 )) --base-rpc-port \$(( 7000 + EXECUTOR_NUMBER * 100 )) --base-metrics-port \$(( 8008 + EXECUTOR_NUMBER * 100 )) -- --verify-finalization
|
||||
// NIMFLAGS="-d:BLS_FORCE_BACKEND=miracl" ./scripts/launch_local_testnet.sh --testnet 1 --nodes 4 --stop-at-epoch 5 --log-level INFO --disable-htop --data-dir local_testnet1_data --base-port \$(( 9000 + EXECUTOR_NUMBER * 100 )) --base-rpc-port \$(( 7000 + EXECUTOR_NUMBER * 100 )) --base-metrics-port \$(( 8008 + EXECUTOR_NUMBER * 100 )) -- --verify-finalization
|
||||
// """
|
||||
// }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -100,4 +95,3 @@ parallel(
|
|||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -61,10 +61,17 @@ task test, "Run all tests":
|
|||
# Just the part of minimal config which explicitly differs from mainnet
|
||||
buildAndRunBinary "test_fixture_const_sanity_check", "tests/official/", """-d:const_preset=minimal -d:chronicles_sinks="json[file]""""
|
||||
|
||||
# Generic SSZ test, doesn't use consensus objects minimal/mainnet presets
|
||||
buildAndRunBinary "test_fixture_ssz_generic_types", "tests/official/", """-d:chronicles_log_level=TRACE -d:chronicles_sinks="json[file]""""
|
||||
# Consensus object SSZ tests
|
||||
buildAndRunBinary "test_fixture_ssz_consensus_objects", "tests/official/", """-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:chronicles_sinks="json[file]""""
|
||||
# EF tests
|
||||
buildAndRunBinary "all_fixtures_require_ssz", "tests/official/", """-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:chronicles_sinks="json[file]""""
|
||||
|
||||
# Mainnet config
|
||||
buildAndRunBinary "proto_array", "beacon_chain/fork_choice/", """-d:const_preset=mainnet -d:chronicles_sinks="json[file]""""
|
||||
buildAndRunBinary "fork_choice", "beacon_chain/fork_choice/", """-d:const_preset=mainnet -d:chronicles_sinks="json[file]""""
|
||||
buildAndRunBinary "all_tests", "tests/", """-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:chronicles_sinks="json[file]""""
|
||||
buildAndRunBinary "all_tests", "tests/", """-d:UseSlashingProtection=true -d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:chronicles_sinks="json[file]""""
|
||||
|
||||
# Check Miracl/Milagro fallback on select tests
|
||||
buildAndRunBinary "test_interop", "tests/", """-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:BLS_FORCE_BACKEND=miracl -d:chronicles_sinks="json[file]""""
|
||||
|
@ -74,14 +81,6 @@ task test, "Run all tests":
|
|||
buildAndRunBinary "test_attestation_pool", "tests/", """-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:BLS_FORCE_BACKEND=miracl -d:chronicles_sinks="json[file]""""
|
||||
buildAndRunBinary "test_block_pool", "tests/", """-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:BLS_FORCE_BACKEND=miracl -d:chronicles_sinks="json[file]""""
|
||||
|
||||
# Generic SSZ test, doesn't use consensus objects minimal/mainnet presets
|
||||
buildAndRunBinary "test_fixture_ssz_generic_types", "tests/official/", """-d:chronicles_log_level=TRACE -d:chronicles_sinks="json[file]""""
|
||||
|
||||
# Consensus object SSZ tests
|
||||
buildAndRunBinary "test_fixture_ssz_consensus_objects", "tests/official/", """-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:chronicles_sinks="json[file]""""
|
||||
|
||||
buildAndRunBinary "all_fixtures_require_ssz", "tests/official/", """-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:chronicles_sinks="json[file]""""
|
||||
|
||||
# State and block sims; getting to 4th epoch triggers consensus checks
|
||||
buildAndRunBinary "state_sim", "research/", "-d:const_preset=mainnet -d:chronicles_log_level=INFO", "--validators=3000 --slots=128"
|
||||
# buildAndRunBinary "state_sim", "research/", "-d:const_preset=mainnet -d:BLS_FORCE_BACKEND=miracl -d:chronicles_log_level=INFO", "--validators=3000 --slots=128"
|
||||
|
|
|
@ -32,6 +32,7 @@ import
|
|||
mainchain_monitor, version, ssz/[merkleization], merkle_minimal,
|
||||
sync_protocol, request_manager, keystore_management, interop, statusbar,
|
||||
sync_manager, validator_duties, validator_api,
|
||||
validator_slashing_protection,
|
||||
./eth2_processor
|
||||
|
||||
const
|
||||
|
@ -258,7 +259,6 @@ proc init*(T: type BeaconNode,
|
|||
netKeys: netKeys,
|
||||
db: db,
|
||||
config: conf,
|
||||
attachedValidators: ValidatorPool.init(),
|
||||
chainDag: chainDag,
|
||||
quarantine: quarantine,
|
||||
attestationPool: attestationPool,
|
||||
|
@ -271,6 +271,16 @@ proc init*(T: type BeaconNode,
|
|||
topicAggregateAndProofs: topicAggregateAndProofs,
|
||||
)
|
||||
|
||||
res.attachedValidators = ValidatorPool.init(
|
||||
SlashingProtectionDB.init(
|
||||
chainDag.headState.data.data.genesis_validators_root,
|
||||
when UseSlashingProtection:
|
||||
kvStore SqStoreRef.init(conf.validatorsDir(), "slashing_protection").tryGet()
|
||||
else:
|
||||
KvStoreRef()
|
||||
)
|
||||
)
|
||||
|
||||
proc getWallTime(): BeaconTime = res.beaconClock.now()
|
||||
|
||||
res.processor = Eth2Processor.new(
|
||||
|
@ -1312,4 +1322,3 @@ programMain:
|
|||
|
||||
of WalletsCmd.restore:
|
||||
restoreWalletInteractively(rng[], config)
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@ import
|
|||
stew/endians2,
|
||||
spec/[datatypes, crypto],
|
||||
block_pools/block_pools_types,
|
||||
fork_choice/fork_choice_types
|
||||
fork_choice/fork_choice_types,
|
||||
validator_slashing_protection
|
||||
|
||||
export block_pools_types
|
||||
|
||||
|
@ -105,5 +106,6 @@ type
|
|||
|
||||
ValidatorPool* = object
|
||||
validators*: Table[ValidatorPubKey, AttachedValidator]
|
||||
slashingProtection*: SlashingProtectionDB
|
||||
|
||||
func shortLog*(v: AttachedValidator): string = shortLog(v.pubKey)
|
||||
|
|
|
@ -16,14 +16,16 @@ import
|
|||
json_serialization/std/[options, sets, net],
|
||||
|
||||
# Local modules
|
||||
spec/[datatypes, digest, crypto, helpers, network],
|
||||
spec/[datatypes, digest, crypto, helpers, network, signatures],
|
||||
conf, time, version,
|
||||
eth2_network, eth2_discovery, validator_pool, beacon_node_types,
|
||||
nimbus_binary_common,
|
||||
version, ssz/merkleization,
|
||||
sync_manager, keystore_management,
|
||||
spec/eth2_apis/callsigs_types,
|
||||
eth2_json_rpc_serialization
|
||||
eth2_json_rpc_serialization,
|
||||
validator_slashing_protection,
|
||||
eth/db/[kvstore, kvstore_sqlite3]
|
||||
|
||||
logScope: topics = "vc"
|
||||
|
||||
|
@ -132,22 +134,35 @@ proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, a
|
|||
# check if we have a validator which needs to propose on this slot
|
||||
if vc.proposalsForCurrentEpoch.contains slot:
|
||||
let public_key = vc.proposalsForCurrentEpoch[slot]
|
||||
let validator = vc.attachedValidators.validators[public_key]
|
||||
|
||||
info "Proposing block", slot = slot, public_key = public_key
|
||||
let notSlashable = vc.attachedValidators
|
||||
.slashingProtection
|
||||
.checkSlashableBlockProposal(public_key, slot)
|
||||
if notSlashable.isOk:
|
||||
let validator = vc.attachedValidators.validators[public_key]
|
||||
info "Proposing block", slot = slot, public_key = public_key
|
||||
let randao_reveal = await validator.genRandaoReveal(
|
||||
vc.fork, vc.beaconGenesis.genesis_validators_root, slot)
|
||||
var newBlock = SignedBeaconBlock(
|
||||
message: await vc.client.get_v1_validator_block(slot, vc.graffitiBytes, randao_reveal)
|
||||
)
|
||||
newBlock.root = hash_tree_root(newBlock.message)
|
||||
|
||||
let randao_reveal = await validator.genRandaoReveal(
|
||||
vc.fork, vc.beaconGenesis.genesis_validators_root, slot)
|
||||
# TODO: signing_root is recomputed in signBlockProposal just after
|
||||
let signing_root = compute_block_root(vc.fork, vc.beaconGenesis.genesis_validators_root, slot, newBlock.root)
|
||||
vc.attachedValidators
|
||||
.slashingProtection
|
||||
.registerBlock(public_key, slot, signing_root)
|
||||
|
||||
var newBlock = SignedBeaconBlock(
|
||||
message: await vc.client.get_v1_validator_block(slot, vc.graffitiBytes, randao_reveal)
|
||||
)
|
||||
newBlock.signature = await validator.signBlockProposal(
|
||||
vc.fork, vc.beaconGenesis.genesis_validators_root, slot, newBlock.root)
|
||||
|
||||
newBlock.root = hash_tree_root(newBlock.message)
|
||||
newBlock.signature = await validator.signBlockProposal(
|
||||
vc.fork, vc.beaconGenesis.genesis_validators_root, slot, newBlock.root)
|
||||
|
||||
discard await vc.client.post_v1_validator_block(newBlock)
|
||||
discard await vc.client.post_v1_validator_block(newBlock)
|
||||
else:
|
||||
warn "Slashing protection activated for block proposal",
|
||||
validator = public_key,
|
||||
slot = slot,
|
||||
existingProposal = notSlashable.error
|
||||
|
||||
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/validator.md#attesting
|
||||
# A validator should create and broadcast the attestation to the associated
|
||||
|
@ -167,12 +182,31 @@ proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, a
|
|||
let validator = vc.attachedValidators.validators[a.public_key]
|
||||
let ad = await vc.client.get_v1_validator_attestation(slot, a.committee_index)
|
||||
|
||||
# TODO I don't like these (u)int64-to-int conversions...
|
||||
let attestation = await validator.produceAndSignAttestation(
|
||||
ad, a.committee_length.int, a.validator_committee_index.int,
|
||||
vc.fork, vc.beaconGenesis.genesis_validators_root)
|
||||
let notSlashable = vc.attachedValidators
|
||||
.slashingProtection
|
||||
.checkSlashableAttestation(
|
||||
a.public_key,
|
||||
ad.source.epoch,
|
||||
ad.target.epoch)
|
||||
if notSlashable.isOk():
|
||||
# TODO signing_root is recomputed in produceAndSignAttestation/signAttestation just after
|
||||
let signing_root = compute_attestation_root(
|
||||
vc.fork, vc.beaconGenesis.genesis_validators_root, ad)
|
||||
vc.attachedValidators
|
||||
.slashingProtection
|
||||
.registerAttestation(
|
||||
a.public_key, ad.source.epoch, ad.target.epoch, signing_root)
|
||||
|
||||
discard await vc.client.post_v1_beacon_pool_attestations(attestation)
|
||||
# TODO I don't like these (u)int64-to-int conversions...
|
||||
let attestation = await validator.produceAndSignAttestation(
|
||||
ad, a.committee_length.int, a.validator_committee_index.int,
|
||||
vc.fork, vc.beaconGenesis.genesis_validators_root)
|
||||
|
||||
discard await vc.client.post_v1_beacon_pool_attestations(attestation)
|
||||
else:
|
||||
warn "Slashing protection activated for attestation",
|
||||
validator = a.public_key,
|
||||
badVoteDetails = $notSlashable.error
|
||||
|
||||
except CatchableError as err:
|
||||
warn "Caught an unexpected error", err = err.msg, slot = shortLog(slot)
|
||||
|
@ -230,6 +264,13 @@ programMain:
|
|||
vc.beaconGenesis = waitFor vc.client.get_v1_beacon_genesis()
|
||||
vc.beaconClock = BeaconClock.init(vc.beaconGenesis.genesis_time)
|
||||
|
||||
when UseSlashingProtection:
|
||||
vc.attachedValidators.slashingProtection =
|
||||
SlashingProtectionDB.init(
|
||||
vc.beaconGenesis.genesis_validators_root,
|
||||
kvStore SqStoreRef.init(config.validatorsDir(), "slashing_protection").tryGet()
|
||||
)
|
||||
|
||||
let
|
||||
curSlot = vc.beaconClock.now().slotOrZero()
|
||||
nextSlot = curSlot + 1 # No earlier than GENESIS_SLOT + 1
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import
|
||||
# Standard library
|
||||
std/[os, tables, strutils, sequtils, osproc, streams],
|
||||
std/[os, tables, sequtils, osproc, streams],
|
||||
|
||||
# Nimble packages
|
||||
stew/[objects], stew/shims/macros,
|
||||
|
@ -18,13 +18,14 @@ import
|
|||
eth/[keys, async_utils], eth/p2p/discoveryv5/[protocol, enr],
|
||||
|
||||
# Local modules
|
||||
spec/[datatypes, digest, crypto, helpers, validator, network],
|
||||
spec/[datatypes, digest, crypto, helpers, validator, network, signatures],
|
||||
spec/state_transition,
|
||||
conf, time, validator_pool,
|
||||
attestation_pool, block_pools/[spec_cache, chain_dag, clearance],
|
||||
eth2_network, keystore_management, beacon_node_common, beacon_node_types,
|
||||
nimbus_binary_common, mainchain_monitor, version, ssz/merkleization, interop,
|
||||
attestation_aggregation, sync_manager, sszdump
|
||||
attestation_aggregation, sync_manager, sszdump,
|
||||
validator_slashing_protection
|
||||
|
||||
# Metrics for tracking attestation and beacon block loss
|
||||
declareCounter beacon_attestations_sent,
|
||||
|
@ -120,6 +121,8 @@ proc isSynced*(node: BeaconNode, head: BlockRef): bool =
|
|||
beaconTime = node.beaconClock.now()
|
||||
wallSlot = beaconTime.toSlot()
|
||||
|
||||
# TODO: MaxEmptySlotCount should likely involve the weak subjectivity period.
|
||||
|
||||
# TODO if everyone follows this logic, the network will not recover from a
|
||||
# halt: nobody will be producing blocks because everone expects someone
|
||||
# else to do it
|
||||
|
@ -293,6 +296,16 @@ proc proposeBlock(node: BeaconNode,
|
|||
slot = shortLog(slot)
|
||||
return head
|
||||
|
||||
let notSlashable = node.attachedValidators
|
||||
.slashingProtection
|
||||
.checkSlashableBlockProposal(validator.pubkey, slot)
|
||||
if notSlashable.isErr:
|
||||
warn "Slashing protection activated",
|
||||
validator = validator.pubkey,
|
||||
slot = slot,
|
||||
existingProposal = notSlashable.error
|
||||
return head
|
||||
|
||||
let valInfo = ValidatorInfoForMakeBeaconBlock(kind: viValidator, validator: validator)
|
||||
let beaconBlockTuple = await makeBeaconBlockForHeadAndSlot(
|
||||
node, valInfo, validator_index, node.graffitiBytes, head, slot)
|
||||
|
@ -304,6 +317,14 @@ proc proposeBlock(node: BeaconNode,
|
|||
)
|
||||
|
||||
newBlock.root = hash_tree_root(newBlock.message)
|
||||
|
||||
# TODO: recomputed in block proposal
|
||||
let signing_root = compute_block_root(
|
||||
beaconBlockTuple.fork, beaconBlockTuple.genesis_validators_root, slot, newBlock.root)
|
||||
node.attachedValidators
|
||||
.slashingProtection
|
||||
.registerBlock(validator.pubkey, slot, signing_root)
|
||||
|
||||
newBlock.signature = await validator.signBlockProposal(
|
||||
beaconBlockTuple.fork, beaconBlockTuple.genesis_validators_root, slot, newBlock.root)
|
||||
|
||||
|
@ -368,9 +389,21 @@ proc handleAttestations(node: BeaconNode, head: BlockRef, slot: Slot) =
|
|||
attestations.add((ad, committee.len, index_in_committee, validator))
|
||||
|
||||
for a in attestations:
|
||||
traceAsyncErrors createAndSendAttestation(
|
||||
node, fork, genesis_validators_root, a.validator, a.data,
|
||||
a.committeeLen, a.indexInCommittee, num_active_validators)
|
||||
let notSlashable = node.attachedValidators
|
||||
.slashingProtection
|
||||
.checkSlashableAttestation(
|
||||
a.validator.pubkey,
|
||||
a.data.source.epoch,
|
||||
a.data.target.epoch)
|
||||
|
||||
if notSlashable.isOk():
|
||||
traceAsyncErrors createAndSendAttestation(
|
||||
node, fork, genesis_validators_root, a.validator, a.data,
|
||||
a.committeeLen, a.indexInCommittee, num_active_validators)
|
||||
else:
|
||||
warn "Slashing protection activated for attestation",
|
||||
validator = a.validator.pubkey,
|
||||
badVoteDetails = $notSlashable.error
|
||||
|
||||
proc handleProposal(node: BeaconNode, head: BlockRef, slot: Slot):
|
||||
Future[BlockRef] {.async.} =
|
||||
|
|
|
@ -3,10 +3,18 @@ import
|
|||
chronos, chronicles,
|
||||
spec/[datatypes, crypto, digest, signatures, helpers],
|
||||
beacon_node_types,
|
||||
json_serialization/std/[sets, net]
|
||||
json_serialization/std/[sets, net],
|
||||
validator_slashing_protection,
|
||||
eth/db/[kvstore, kvstore_sqlite3]
|
||||
|
||||
func init*(T: type ValidatorPool): T =
|
||||
func init*(T: type ValidatorPool,
|
||||
slashingProtectionDB: SlashingProtectionDB): T =
|
||||
## Initialize the validator pool and the slashing protection service
|
||||
## `genesis_validator_root` is used as an unique ID for the
|
||||
## blockchain
|
||||
## `backend` is the KeyValue Store backend
|
||||
result.validators = initTable[ValidatorPubKey, AttachedValidator]()
|
||||
result.slashingProtection = slashingProtectionDB
|
||||
|
||||
template count*(pool: ValidatorPool): int =
|
||||
pool.validators.len
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -30,7 +30,9 @@ import # Unit test
|
|||
./test_sync_manager,
|
||||
./test_honest_validator,
|
||||
./test_interop,
|
||||
./fork_choice/tests_fork_choice
|
||||
./fork_choice/tests_fork_choice,
|
||||
./slashing_protection/test_slashing_interchange,
|
||||
./slashing_protection/test_slashing_protection_db
|
||||
|
||||
import # Refactor state transition unit tests
|
||||
# In mainnet these take 2 minutes and are empty TODOs
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
*.json
|
|
@ -0,0 +1,97 @@
|
|||
# Nimbus
|
||||
# Copyright (c) 2018 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0)
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT)
|
||||
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
import
|
||||
# Standard library
|
||||
std/[unittest, os],
|
||||
# Status lib
|
||||
eth/db/kvstore,
|
||||
stew/results,
|
||||
nimcrypto/utils,
|
||||
# Internal
|
||||
../../beacon_chain/validator_slashing_protection,
|
||||
../../beacon_chain/spec/[datatypes, digest, crypto, presets],
|
||||
# Test utilies
|
||||
../testutil
|
||||
|
||||
static: doAssert UseSlashingProtection, "The test was compiled without slashing protection, pass -d:UseSlashingProtection=true"
|
||||
|
||||
template wrappedTimedTest(name: string, body: untyped) =
|
||||
# `check` macro takes a copy of whatever it's checking, on the stack!
|
||||
block: # Symbol namespacing
|
||||
proc wrappedTest() =
|
||||
timedTest name:
|
||||
body
|
||||
wrappedTest()
|
||||
|
||||
func fakeRoot(index: SomeInteger): Eth2Digest =
|
||||
## Create fake roots
|
||||
## Those are just the value serialized in big-endian
|
||||
## We prevent zero hash special case via a power of 2 prefix
|
||||
result.data[0 ..< 8] = (1'u64 shl 32 + index.uint64).toBytesBE()
|
||||
|
||||
func fakeValidator(index: SomeInteger): ValidatorPubKey =
|
||||
## Create fake validator public key
|
||||
result = ValidatorPubKey(kind: OpaqueBlob)
|
||||
result.blob[0 ..< 8] = (1'u64 shl 48 + index.uint64).toBytesBE()
|
||||
|
||||
func hexToDigest(hex: string): Eth2Digest =
|
||||
result = Eth2Digest.fromHex(hex)
|
||||
|
||||
suiteReport "Slashing Protection DB - Interchange" & preset():
|
||||
# https://hackmd.io/@sproul/Bk0Y0qdGD#Format-1-Complete
|
||||
wrappedTimedTest "Smoke test - Complete format" & preset():
|
||||
let genesis_validators_root = hexToDigest"0x04700007fabc8282644aed6d1c7c9e21d38a03a0c4ba193f3afe428824b3a673"
|
||||
block: # export
|
||||
let db = SlashingProtectionDB.init(genesis_validators_root, kvStore MemStoreRef.init())
|
||||
|
||||
let pubkey = ValidatorPubKey
|
||||
.fromHex"0xb845089a1457f811bfc000588fbb4e713669be8ce060ea6be3c6ece09afc3794106c91ca73acda5e5457122d58723bed"
|
||||
.get()
|
||||
db.registerBlock(
|
||||
pubkey,
|
||||
Slot 81952,
|
||||
hexToDigest"0x4ff6f743a43f3b4f95350831aeaf0a122a1a392922c45d804280284a69eb850b"
|
||||
)
|
||||
# db.registerBlock(
|
||||
# pubkey,
|
||||
# Slot 81951,
|
||||
# fakeRoot(65535)
|
||||
# )
|
||||
|
||||
db.registerAttestation(
|
||||
pubkey,
|
||||
source = Epoch 2290,
|
||||
target = Epoch 3007,
|
||||
hexToDigest"0x587d6a4f59a58fe24f406e0502413e77fe1babddee641fda30034ed37ecc884d"
|
||||
)
|
||||
db.registerAttestation(
|
||||
pubkey,
|
||||
source = Epoch 2290,
|
||||
target = Epoch 3008,
|
||||
fakeRoot(65535)
|
||||
)
|
||||
|
||||
db.toSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")
|
||||
|
||||
block: # import - zero root db
|
||||
let db2 = SlashingProtectionDB.init(Eth2Digest(), kvStore MemStoreRef.init())
|
||||
|
||||
doAssert db2.fromSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")
|
||||
db2.toSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection_roundtrip1.json")
|
||||
|
||||
block: # import - same root db
|
||||
let db3 = SlashingProtectionDB.init(genesis_validators_root, kvStore MemStoreRef.init())
|
||||
|
||||
doAssert db3.fromSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")
|
||||
db3.toSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection_roundtrip2.json")
|
||||
|
||||
block: # import - invalid root db
|
||||
let invalid_genvalroot = hexToDigest"0x1234"
|
||||
let db3 = SlashingProtectionDB.init(invalid_genvalroot, kvStore MemStoreRef.init())
|
||||
|
||||
doAssert not db3.fromSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")
|
|
@ -0,0 +1,566 @@
|
|||
# Nimbus
|
||||
# Copyright (c) 2018 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0)
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT)
|
||||
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
import
|
||||
# Standard library
|
||||
std/unittest,
|
||||
# Status lib
|
||||
eth/db/kvstore,
|
||||
stew/results,
|
||||
# Internal
|
||||
../../beacon_chain/validator_slashing_protection,
|
||||
../../beacon_chain/spec/[datatypes, digest, crypto, presets],
|
||||
# Test utilies
|
||||
../testutil
|
||||
|
||||
static: doAssert UseSlashingProtection, "The test was compiled without slashing protection, pass -d:UseSlashingProtection=true"
|
||||
|
||||
template wrappedTimedTest(name: string, body: untyped) =
|
||||
# `check` macro takes a copy of whatever it's checking, on the stack!
|
||||
block: # Symbol namespacing
|
||||
proc wrappedTest() =
|
||||
timedTest name:
|
||||
body
|
||||
wrappedTest()
|
||||
|
||||
func fakeRoot(index: SomeInteger): Eth2Digest =
|
||||
## Create fake roots
|
||||
## Those are just the value serialized in big-endian
|
||||
## We prevent zero hash special case via a power of 2 prefix
|
||||
result.data[0 ..< 8] = (1'u64 shl 32 + index.uint64).toBytesBE()
|
||||
|
||||
func fakeValidator(index: SomeInteger): ValidatorPubKey =
|
||||
## Create fake validator public key
|
||||
result = ValidatorPubKey(kind: OpaqueBlob)
|
||||
result.blob[0 ..< 8] = (1'u64 shl 48 + index.uint64).toBytesBE()
|
||||
|
||||
suiteReport "Slashing Protection DB" & preset():
|
||||
wrappedTimedTest "Empty database" & preset():
|
||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
||||
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(1234),
|
||||
slot = Slot 1
|
||||
).isOk()
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(1234),
|
||||
source = Epoch 1,
|
||||
target = Epoch 2
|
||||
).isOk()
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(1234),
|
||||
source = Epoch 2,
|
||||
target = Epoch 1
|
||||
).error.kind == TargetPrecedesSource
|
||||
|
||||
db.close()
|
||||
|
||||
wrappedTimedTest "SP for block proposal - linear append":
|
||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
||||
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 10,
|
||||
fakeRoot(100)
|
||||
)
|
||||
db.registerBlock(
|
||||
fakeValidator(111),
|
||||
Slot 15,
|
||||
fakeRoot(111)
|
||||
)
|
||||
check:
|
||||
# Slot occupied by same validator
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
slot = Slot 10
|
||||
).isErr()
|
||||
# Slot occupied by another validator
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(111),
|
||||
slot = Slot 10
|
||||
).isOk()
|
||||
# Slot occupied by another validator
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
slot = Slot 15
|
||||
).isOk()
|
||||
# Slot occupied by same validator
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(111),
|
||||
slot = Slot 15
|
||||
).isErr()
|
||||
|
||||
# Slot inoccupied
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(255),
|
||||
slot = Slot 20
|
||||
).isOk()
|
||||
|
||||
db.registerBlock(
|
||||
fakeValidator(255),
|
||||
slot = Slot 20,
|
||||
fakeRoot(4321)
|
||||
)
|
||||
|
||||
check:
|
||||
# Slot now occupied
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(255),
|
||||
slot = Slot 20
|
||||
).isErr()
|
||||
|
||||
wrappedTimedTest "SP for block proposal - backtracking append":
|
||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
||||
|
||||
# last finalized block
|
||||
db.registerBlock(
|
||||
fakeValidator(0),
|
||||
Slot 0,
|
||||
fakeRoot(0)
|
||||
)
|
||||
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 10,
|
||||
fakeRoot(10)
|
||||
)
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 20,
|
||||
fakeRoot(20)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {10, 20}:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isErr()
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 15,
|
||||
fakeRoot(15)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {10, 15, 20}:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isErr()
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Slot i
|
||||
).isOk()
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 12,
|
||||
fakeRoot(12)
|
||||
)
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 17,
|
||||
fakeRoot(17)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {10, 12, 15, 17, 20}:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isErr()
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Slot i
|
||||
).isOk()
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 9,
|
||||
fakeRoot(9)
|
||||
)
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 1,
|
||||
fakeRoot(1)
|
||||
)
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 3,
|
||||
fakeRoot(3)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {1, 3, 9, 10, 12, 15, 17, 20}:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isErr()
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Slot i
|
||||
).isOk()
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 29,
|
||||
fakeRoot(29)
|
||||
)
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 2,
|
||||
fakeRoot(2)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {1, 2, 3, 9, 10, 12, 15, 17, 20, 29}:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isErr()
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Slot i
|
||||
).isOk()
|
||||
|
||||
wrappedTimedTest "SP for same epoch attestation target - linear append":
|
||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 10,
|
||||
fakeRoot(100)
|
||||
)
|
||||
db.registerAttestation(
|
||||
fakeValidator(111),
|
||||
Epoch 0, Epoch 15,
|
||||
fakeRoot(111)
|
||||
)
|
||||
check:
|
||||
# Epoch occupied by same validator
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 10,
|
||||
).error.kind == DoubleVote
|
||||
# Epoch occupied by another validator
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(111),
|
||||
Epoch 0, Epoch 10
|
||||
).isOk()
|
||||
# Epoch occupied by another validator
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 15
|
||||
).isOk()
|
||||
# Epoch occupied by same validator
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(111),
|
||||
Epoch 0, Epoch 15
|
||||
).error.kind == DoubleVote
|
||||
|
||||
# Epoch inoccupied
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(255),
|
||||
Epoch 0, Epoch 20
|
||||
).isOk()
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(255),
|
||||
Epoch 0, Epoch 20,
|
||||
fakeRoot(4321)
|
||||
)
|
||||
|
||||
check:
|
||||
# Epoch now occupied
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(255),
|
||||
Epoch 0, Epoch 20
|
||||
).error.kind == DoubleVote
|
||||
|
||||
wrappedTimedTest "SP for same epoch attestation target - backtracking append":
|
||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
||||
|
||||
# last finalized block
|
||||
db.registerAttestation(
|
||||
fakeValidator(0),
|
||||
Epoch 0, Epoch 0,
|
||||
fakeRoot(0)
|
||||
)
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 10,
|
||||
fakeRoot(10)
|
||||
)
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 20,
|
||||
fakeRoot(20)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {10, 20}:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).error.kind == DoubleVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 15,
|
||||
fakeRoot(15)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {10, 15, 20}:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).error.kind == DoubleVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 12,
|
||||
fakeRoot(12)
|
||||
)
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 17,
|
||||
fakeRoot(17)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {10, 12, 15, 17, 20}:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).error.kind == DoubleVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 9,
|
||||
fakeRoot(9)
|
||||
)
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 1,
|
||||
fakeRoot(1)
|
||||
)
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 3,
|
||||
fakeRoot(3)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {1, 3, 9, 10, 12, 15, 17, 20}:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).error.kind == DoubleVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 29,
|
||||
fakeRoot(29)
|
||||
)
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 2,
|
||||
fakeRoot(2)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {1, 2, 3, 9, 10, 12, 15, 17, 20, 29}:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).error.kind == DoubleVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
|
||||
wrappedTimedTest "SP for surrounded attestations":
|
||||
block:
|
||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 10, Epoch 20,
|
||||
fakeRoot(20)
|
||||
)
|
||||
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 11, Epoch 19
|
||||
).error.kind == SurroundedVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(200),
|
||||
Epoch 11, Epoch 19
|
||||
).isOk
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 11, Epoch 21
|
||||
).isOk
|
||||
# TODO: is that possible?
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 9, Epoch 19
|
||||
).isOk
|
||||
|
||||
block:
|
||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 1,
|
||||
fakeRoot(0)
|
||||
)
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 10, Epoch 20,
|
||||
fakeRoot(20)
|
||||
)
|
||||
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 11, Epoch 19
|
||||
).error.kind == SurroundedVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(200),
|
||||
Epoch 11, Epoch 19
|
||||
).isOk
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 11, Epoch 21
|
||||
).isOk
|
||||
# TODO: is that possible?
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 9, Epoch 19
|
||||
).isOk
|
||||
|
||||
|
||||
wrappedTimedTest "SP for surrounding attestations":
|
||||
block:
|
||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 10, Epoch 20,
|
||||
fakeRoot(20)
|
||||
)
|
||||
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 9, Epoch 21
|
||||
).error.kind == SurroundingVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 21
|
||||
).error.kind == SurroundingVote
|
||||
|
||||
block:
|
||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 1,
|
||||
fakeRoot(20)
|
||||
)
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 10, Epoch 20,
|
||||
fakeRoot(20)
|
||||
)
|
||||
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 9, Epoch 21
|
||||
).error.kind == SurroundingVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 21
|
||||
).error.kind == SurroundingVote
|
Loading…
Reference in New Issue