Slashing protection refactor - EIP 3076 (#2094)
* Create CLI tool for slashing export * Use SQLite as a DB instead of a KV-store * Keeps v1 and v2 DBs around * Uses the same schema as Lighthouse v1.1.0 * Passes all interchange tests + skeleton of finalization pruning * Removes tests that would violate v5 / minimal slashing DB and MinSlot rules * Migration tool added using low-watermark scheme for faster migration of large number of validators
This commit is contained in:
parent
9968944329
commit
03f47c8f2f
|
@ -194,8 +194,14 @@ OK: 1/1 Fail: 0/1 Skip: 0/1
|
||||||
OK: 3/3 Fail: 0/3 Skip: 0/3
|
OK: 3/3 Fail: 0/3 Skip: 0/3
|
||||||
## Slashing Protection DB - Interchange [Preset: mainnet]
|
## Slashing Protection DB - Interchange [Preset: mainnet]
|
||||||
```diff
|
```diff
|
||||||
|
+ Smoke test - Complete format - Invalid database is refused [Preset: mainnet] OK
|
||||||
+ Smoke test - Complete format [Preset: mainnet] OK
|
+ Smoke test - Complete format [Preset: mainnet] OK
|
||||||
```
|
```
|
||||||
|
OK: 2/2 Fail: 0/2 Skip: 0/2
|
||||||
|
## Slashing Protection DB - v1 and v2 migration [Preset: mainnet]
|
||||||
|
```diff
|
||||||
|
+ Minimal format migration [Preset: mainnet] OK
|
||||||
|
```
|
||||||
OK: 1/1 Fail: 0/1 Skip: 0/1
|
OK: 1/1 Fail: 0/1 Skip: 0/1
|
||||||
## Slashing Protection DB [Preset: mainnet]
|
## Slashing Protection DB [Preset: mainnet]
|
||||||
```diff
|
```diff
|
||||||
|
@ -203,13 +209,12 @@ OK: 1/1 Fail: 0/1 Skip: 0/1
|
||||||
+ Empty database [Preset: mainnet] OK
|
+ Empty database [Preset: mainnet] OK
|
||||||
+ SP for block proposal - backtracking append OK
|
+ SP for block proposal - backtracking append OK
|
||||||
+ SP for block proposal - linear append OK
|
+ SP for block proposal - linear append OK
|
||||||
+ SP for same epoch attestation target - backtracking append OK
|
|
||||||
+ SP for same epoch attestation target - linear append OK
|
+ SP for same epoch attestation target - linear append OK
|
||||||
+ SP for surrounded attestations OK
|
+ SP for surrounded attestations OK
|
||||||
+ SP for surrounding attestations OK
|
+ SP for surrounding attestations OK
|
||||||
+ Test valid attestation #1699 OK
|
+ Test valid attestation #1699 OK
|
||||||
```
|
```
|
||||||
OK: 9/9 Fail: 0/9 Skip: 0/9
|
OK: 8/8 Fail: 0/8 Skip: 0/8
|
||||||
## Spec datatypes
|
## Spec datatypes
|
||||||
```diff
|
```diff
|
||||||
+ Graffiti bytes OK
|
+ Graffiti bytes OK
|
||||||
|
@ -275,4 +280,4 @@ OK: 1/1 Fail: 0/1 Skip: 0/1
|
||||||
OK: 2/2 Fail: 0/2 Skip: 0/2
|
OK: 2/2 Fail: 0/2 Skip: 0/2
|
||||||
|
|
||||||
---TOTAL---
|
---TOTAL---
|
||||||
OK: 148/157 Fail: 0/157 Skip: 9/157
|
OK: 149/158 Fail: 0/158 Skip: 9/158
|
||||||
|
|
|
@ -73,6 +73,9 @@ task test, "Run all tests":
|
||||||
# EF tests
|
# EF tests
|
||||||
buildAndRunBinary "all_fixtures_require_ssz", "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]""""
|
||||||
|
|
||||||
|
# EIP-3076 - Slashing interchange
|
||||||
|
buildAndRunBinary "test_official_interchange_vectors", "tests/slashing_protection/", """-d:chronicles_log_level=TRACE -d:const_preset=mainnet -d:chronicles_sinks="json[file]""""
|
||||||
|
|
||||||
# Mainnet config
|
# Mainnet config
|
||||||
buildAndRunBinary "proto_array", "beacon_chain/fork_choice/", """-d:const_preset=mainnet -d:chronicles_sinks="json[file]""""
|
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 "fork_choice", "beacon_chain/fork_choice/", """-d:const_preset=mainnet -d:chronicles_sinks="json[file]""""
|
||||||
|
|
|
@ -6,7 +6,7 @@ import
|
||||||
spec/[datatypes, digest, crypto],
|
spec/[datatypes, digest, crypto],
|
||||||
block_pools/block_pools_types,
|
block_pools/block_pools_types,
|
||||||
fork_choice/fork_choice_types,
|
fork_choice/fork_choice_types,
|
||||||
validator_slashing_protection
|
validator_protection/slashing_protection
|
||||||
|
|
||||||
from libp2p/protocols/pubsub/pubsub import ValidationResult
|
from libp2p/protocols/pubsub/pubsub import ValidationResult
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ import
|
||||||
eth1_monitor, version, ssz/merkleization,
|
eth1_monitor, version, ssz/merkleization,
|
||||||
sync_protocol, request_manager, keystore_management, interop, statusbar,
|
sync_protocol, request_manager, keystore_management, interop, statusbar,
|
||||||
sync_manager, validator_duties, filepath,
|
sync_manager, validator_duties, filepath,
|
||||||
validator_slashing_protection, ./eth2_processor
|
validator_protection/slashing_protection, ./eth2_processor
|
||||||
|
|
||||||
from eth/common/eth_types import BlockHashOrNumber
|
from eth/common/eth_types import BlockHashOrNumber
|
||||||
|
|
||||||
|
@ -324,7 +324,7 @@ proc init*(T: type BeaconNode,
|
||||||
res.attachedValidators = ValidatorPool.init(
|
res.attachedValidators = ValidatorPool.init(
|
||||||
SlashingProtectionDB.init(
|
SlashingProtectionDB.init(
|
||||||
chainDag.headState.data.data.genesis_validators_root,
|
chainDag.headState.data.data.genesis_validators_root,
|
||||||
kvStore SqStoreRef.init(conf.validatorsDir(), "slashing_protection").tryGet()
|
conf.validatorsDir(), "slashing_protection"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ import
|
||||||
sync_manager, keystore_management,
|
sync_manager, keystore_management,
|
||||||
spec/eth2_apis/callsigs_types,
|
spec/eth2_apis/callsigs_types,
|
||||||
eth2_json_rpc_serialization,
|
eth2_json_rpc_serialization,
|
||||||
validator_slashing_protection,
|
validator_protection/slashing_protection,
|
||||||
eth/db/[kvstore, kvstore_sqlite3]
|
eth/db/[kvstore, kvstore_sqlite3]
|
||||||
|
|
||||||
logScope: topics = "vc"
|
logScope: topics = "vc"
|
||||||
|
@ -314,7 +314,7 @@ programMain:
|
||||||
vc.attachedValidators.slashingProtection =
|
vc.attachedValidators.slashingProtection =
|
||||||
SlashingProtectionDB.init(
|
SlashingProtectionDB.init(
|
||||||
vc.beaconGenesis.genesis_validators_root,
|
vc.beaconGenesis.genesis_validators_root,
|
||||||
kvStore SqStoreRef.init(config.validatorsDir(), "slashing_protection").tryGet()
|
config.validatorsDir(), "slashing_protection"
|
||||||
)
|
)
|
||||||
|
|
||||||
let
|
let
|
||||||
|
|
|
@ -27,7 +27,7 @@ import
|
||||||
./eth2_network, ./keystore_management, ./beacon_node_common,
|
./eth2_network, ./keystore_management, ./beacon_node_common,
|
||||||
./beacon_node_types, ./nimbus_binary_common, ./eth1_monitor, ./version,
|
./beacon_node_types, ./nimbus_binary_common, ./eth1_monitor, ./version,
|
||||||
./ssz/merkleization, ./attestation_aggregation, ./sync_manager, ./sszdump,
|
./ssz/merkleization, ./attestation_aggregation, ./sync_manager, ./sszdump,
|
||||||
./validator_slashing_protection
|
./validator_protection/slashing_protection
|
||||||
|
|
||||||
# Metrics for tracking attestation and beacon block loss
|
# Metrics for tracking attestation and beacon block loss
|
||||||
const delayBuckets = [-Inf, -4.0, -2.0, -1.0, -0.5, -0.1, -0.05,
|
const delayBuckets = [-Inf, -4.0, -2.0, -1.0, -0.5, -0.1, -0.05,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import
|
||||||
json_serialization/std/[sets, net],
|
json_serialization/std/[sets, net],
|
||||||
eth/db/[kvstore, kvstore_sqlite3],
|
eth/db/[kvstore, kvstore_sqlite3],
|
||||||
./spec/[datatypes, crypto, digest, signatures, helpers],
|
./spec/[datatypes, crypto, digest, signatures, helpers],
|
||||||
./beacon_node_types, validator_slashing_protection
|
./beacon_node_types, validator_protection/slashing_protection
|
||||||
|
|
||||||
declareGauge validators,
|
declareGauge validators,
|
||||||
"Number of validators attached to the beacon node"
|
"Number of validators attached to the beacon node"
|
||||||
|
@ -12,7 +12,7 @@ declareGauge validators,
|
||||||
func init*(T: type ValidatorPool,
|
func init*(T: type ValidatorPool,
|
||||||
slashingProtectionDB: SlashingProtectionDB): T =
|
slashingProtectionDB: SlashingProtectionDB): T =
|
||||||
## Initialize the validator pool and the slashing protection service
|
## Initialize the validator pool and the slashing protection service
|
||||||
## `genesis_validator_root` is used as an unique ID for the
|
## `genesis_validators_root` is used as an unique ID for the
|
||||||
## blockchain
|
## blockchain
|
||||||
## `backend` is the KeyValue Store backend
|
## `backend` is the KeyValue Store backend
|
||||||
result.validators = initTable[ValidatorPubKey, AttachedValidator]()
|
result.validators = initTable[ValidatorPubKey, AttachedValidator]()
|
||||||
|
|
|
@ -0,0 +1,442 @@
|
||||||
|
# beacon_chain
|
||||||
|
# Copyright (c) 2018-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.
|
||||||
|
|
||||||
|
import
|
||||||
|
# stdlib
|
||||||
|
std/os,
|
||||||
|
# Status
|
||||||
|
eth/db/[kvstore, kvstore_sqlite3],
|
||||||
|
stew/results, chronicles,
|
||||||
|
# Internal
|
||||||
|
../spec/[datatypes, digest, crypto],
|
||||||
|
./slashing_protection_common,
|
||||||
|
./slashing_protection_v1,
|
||||||
|
./slashing_protection_v2
|
||||||
|
|
||||||
|
export slashing_protection_common
|
||||||
|
# Generic sandwich
|
||||||
|
export chronicles
|
||||||
|
|
||||||
|
# The high-level slashing protection DB
|
||||||
|
# -------------------------------------
|
||||||
|
# This file abstracts differences and
|
||||||
|
# migration between slashing protection implementations
|
||||||
|
# and DB schemas
|
||||||
|
#
|
||||||
|
# This is done by instantiating
|
||||||
|
# multiple slashing DB versions using the same handle.
|
||||||
|
#
|
||||||
|
# We assume that in case of backward compatible changes
|
||||||
|
# The new version will use different tables.
|
||||||
|
#
|
||||||
|
# During transition period, we allow using multiple
|
||||||
|
# slashing protection implementations to validate
|
||||||
|
# the behavior of the new implementation.
|
||||||
|
#
|
||||||
|
# Note: this will increase disk IO.
|
||||||
|
|
||||||
|
type
|
||||||
|
SlashProtDBMode* = enum
|
||||||
|
kCompleteArchiveV1 # Complete Format V1 backend (saves all attestations)
|
||||||
|
kCompleteArchiveV2 # Complete Format V2 backend (saves all attestations)
|
||||||
|
kLowWatermarkV2 # Low-Watermark Format V2 backend (prunes attestations)
|
||||||
|
|
||||||
|
SlashingProtectionDB* = ref object
|
||||||
|
## Database storing the blocks attested
|
||||||
|
## by validators attached to a beacon node
|
||||||
|
## or validator client.
|
||||||
|
db_v1: SlashingProtectionDB_v1
|
||||||
|
db_v2: SlashingProtectionDB_v2
|
||||||
|
modes: set[SlashProtDBMode]
|
||||||
|
disagreementBehavior: DisagreementBehavior
|
||||||
|
|
||||||
|
DisagreementBehavior* = enum
|
||||||
|
## How to handle disagreement between DB versions
|
||||||
|
kCrash
|
||||||
|
kChooseV1
|
||||||
|
kChooseV2
|
||||||
|
|
||||||
|
# DB Multiversioning
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
|
||||||
|
func version*(_: type SlashingProtectionDB): static int =
|
||||||
|
# The highest DB version supported
|
||||||
|
2
|
||||||
|
|
||||||
|
# DB Migration
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
|
||||||
|
proc requiresMigrationFromDB_v1(db: SlashingProtectionDB_v2): bool =
|
||||||
|
## Migrate a v1 DB to v2.
|
||||||
|
# Check if we have v2 data:
|
||||||
|
let rawdb = kvstore db.getRawDBHandle()
|
||||||
|
|
||||||
|
let v1Root = rawdb.getMetadataTable_DbV1()
|
||||||
|
if v1Root.isNone():
|
||||||
|
return false
|
||||||
|
|
||||||
|
let v2Root = db.getMetadataTable_DbV2()
|
||||||
|
if v2Root.isNone():
|
||||||
|
return true
|
||||||
|
|
||||||
|
if v1Root != v2Root:
|
||||||
|
fatal "Trying to merge-migrate slashing databases from different chains",
|
||||||
|
v1Root = shortLog(v1Root.get()),
|
||||||
|
v2Root = shortLog(v2Root.get())
|
||||||
|
quit 1
|
||||||
|
return true
|
||||||
|
|
||||||
|
# Resource Management
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
|
||||||
|
proc init*(
|
||||||
|
T: type SlashingProtectionDB,
|
||||||
|
genesis_validators_root: Eth2Digest,
|
||||||
|
basePath, dbname: string,
|
||||||
|
modes: set[SlashProtDBMode],
|
||||||
|
disagreementBehavior: DisagreementBehavior
|
||||||
|
): T =
|
||||||
|
## Initialize or load a slashing protection DB
|
||||||
|
## This is for Beacon Node usage
|
||||||
|
## Handles DB version migration
|
||||||
|
|
||||||
|
doAssert modes.card >= 1, "No slashing protection mode chosen. Choose a v1, a v2 or v1 and v2 slashing DB mode."
|
||||||
|
doAssert not(
|
||||||
|
kCompleteArchiveV2 in modes and
|
||||||
|
kLowWatermarkV2 in modes), "Mode(s): " & $modes & ". Choose only one of V2 DB modes."
|
||||||
|
|
||||||
|
new result
|
||||||
|
result.modes = modes
|
||||||
|
result.disagreementBehavior = disagreementBehavior
|
||||||
|
|
||||||
|
result.db_v2 = SlashingProtectionDB_v2.initCompatV1(
|
||||||
|
genesis_validators_root,
|
||||||
|
basePath, dbname
|
||||||
|
)
|
||||||
|
|
||||||
|
let requiresMigration = result.db_v2.requiresMigrationFromDB_v1()
|
||||||
|
|
||||||
|
let rawdb = kvstore result.db_v2.getRawDBHandle()
|
||||||
|
if not rawdb.checkOrPutGenesis_DbV1(genesis_validators_root):
|
||||||
|
fatal "The slashing database refers to another chain/mainnet/testnet",
|
||||||
|
path = basePath/dbname,
|
||||||
|
genesis_validators_root = genesis_validators_root
|
||||||
|
result.db_v1.fromRawDB(rawdb)
|
||||||
|
|
||||||
|
if requiresMigration:
|
||||||
|
info "Migrating local validators slashing DB from v1 to v2"
|
||||||
|
let spdir = result.db_v1.toSPDIR_lowWatermark()
|
||||||
|
let status = result.db_v2.inclSPDIR(spdir)
|
||||||
|
case status
|
||||||
|
of siSuccess:
|
||||||
|
info "Slashing DB migration successful."
|
||||||
|
of siPartial:
|
||||||
|
warn "Slashing DB migration is a partial success."
|
||||||
|
of siFailure:
|
||||||
|
fatal "Slashing DB migration failure. Aborting to protect validators."
|
||||||
|
quit 1
|
||||||
|
|
||||||
|
proc init*(
|
||||||
|
T: type SlashingProtectionDB,
|
||||||
|
genesis_validators_root: Eth2Digest,
|
||||||
|
basePath, dbname: string
|
||||||
|
): T =
|
||||||
|
## Initialize or load a slashing protection DB
|
||||||
|
## With defaults
|
||||||
|
## - v2 DB only, low watermark (regular pruning)
|
||||||
|
##
|
||||||
|
## Does not handle migration
|
||||||
|
init(
|
||||||
|
T, genesis_validators_root, basePath, dbname,
|
||||||
|
modes = {kLowWatermarkV2},
|
||||||
|
disagreementBehavior = kChooseV2
|
||||||
|
)
|
||||||
|
|
||||||
|
proc loadUnchecked*(
|
||||||
|
T: type SlashingProtectionDB,
|
||||||
|
basePath, dbname: string, readOnly: bool
|
||||||
|
): SlashingProtectionDB {.raises:[Defect, IOError].}=
|
||||||
|
## Load a slashing protection DB
|
||||||
|
## Note: This is for CLI tool usage
|
||||||
|
## this doesn't check the genesis validator root
|
||||||
|
##
|
||||||
|
## Does not handle migration
|
||||||
|
|
||||||
|
result.modes = {kCompleteArchiveV1, kCompleteArchiveV2}
|
||||||
|
result.disagreementBehavior = kCrash
|
||||||
|
|
||||||
|
result.db_v2 = SlashingProtectionDB_v2.loadUnchecked(
|
||||||
|
basePath, dbname, readOnly
|
||||||
|
)
|
||||||
|
|
||||||
|
result.db_v1.fromRawDB(kvstore result.db_v2.getRawDBHandle())
|
||||||
|
|
||||||
|
proc close*(db: SlashingProtectionDB) =
|
||||||
|
## Close a slashing protection database
|
||||||
|
db.db_v2.close()
|
||||||
|
# v1 and v2 are ref objects and use the same DB handle
|
||||||
|
# so closing one closes both
|
||||||
|
|
||||||
|
# DB Queries
|
||||||
|
# --------------------------------------------
|
||||||
|
|
||||||
|
proc useV1(db: SlashingProtectionDB): bool =
|
||||||
|
kCompleteArchiveV1 in db.modes
|
||||||
|
|
||||||
|
proc useV2(db: SlashingProtectionDB): bool =
|
||||||
|
kCompleteArchiveV2 in db.modes or
|
||||||
|
kLowWatermarkV2 in db.modes
|
||||||
|
|
||||||
|
template queryVersions(
|
||||||
|
db: SlashingProtectionDB,
|
||||||
|
queryExpr: untyped
|
||||||
|
): auto =
|
||||||
|
## Query multiple DB versions
|
||||||
|
## Query should be in the form
|
||||||
|
## myQuery(db_version, args...)
|
||||||
|
##
|
||||||
|
## Resolve conflicts according to
|
||||||
|
## `db.disagreementBehavior`
|
||||||
|
##
|
||||||
|
## For example
|
||||||
|
## checkSlashableBlockProposal(db_version, validator, slot)
|
||||||
|
##
|
||||||
|
## db_version will be replaced by db_v1 and db_v2 accordingly
|
||||||
|
type T = typeof(block:
|
||||||
|
template db_version: untyped = db.db_v1
|
||||||
|
queryExpr
|
||||||
|
)
|
||||||
|
|
||||||
|
var res1, res2: T
|
||||||
|
let useV1 = db.useV1()
|
||||||
|
let useV2 = db.useV2()
|
||||||
|
|
||||||
|
if useV1:
|
||||||
|
template db_version: untyped = db.db_v1
|
||||||
|
res1 = queryExpr
|
||||||
|
if useV2:
|
||||||
|
template db_version: untyped = db.db_v2
|
||||||
|
res2 = queryExpr
|
||||||
|
|
||||||
|
if useV1 and useV2:
|
||||||
|
if res1 == res2:
|
||||||
|
res1
|
||||||
|
else:
|
||||||
|
# TODO: Chronicles doesn't work with astToStr.
|
||||||
|
const queryStr = astToStr(queryExpr)
|
||||||
|
case db.disagreementBehavior
|
||||||
|
of kCrash:
|
||||||
|
fatal "Slashing protection DB has an internal error",
|
||||||
|
query = queryStr,
|
||||||
|
dbV1_result = res1,
|
||||||
|
dbV2_result = res2
|
||||||
|
doAssert false, "Slashing DB internal error"
|
||||||
|
res1 # For proper type deduction
|
||||||
|
of kChooseV1:
|
||||||
|
error "Slashing protection DB has an internal error, using v1 result",
|
||||||
|
query = queryStr,
|
||||||
|
dbV1_result = res1,
|
||||||
|
dbV2_result = res2
|
||||||
|
res1
|
||||||
|
of kChooseV2:
|
||||||
|
error "Slashing protection DB has an internal error, using v2 result",
|
||||||
|
query = queryStr,
|
||||||
|
dbV1_result = res1,
|
||||||
|
dbV2_result = res2
|
||||||
|
res2
|
||||||
|
elif useV1:
|
||||||
|
res1
|
||||||
|
else:
|
||||||
|
res2
|
||||||
|
|
||||||
|
proc checkSlashableBlockProposal*(
|
||||||
|
db: SlashingProtectionDB,
|
||||||
|
validator: ValidatorPubKey,
|
||||||
|
slot: Slot
|
||||||
|
): Result[void, BadProposal] =
|
||||||
|
## Returns an error if the specified validator
|
||||||
|
## already proposed a block for the specified slot.
|
||||||
|
## This would lead to slashing.
|
||||||
|
## The error contains the blockroot that was already proposed
|
||||||
|
##
|
||||||
|
## Returns success otherwise
|
||||||
|
db.queryVersions(
|
||||||
|
checkSlashableBlockProposal(db_version, validator, slot)
|
||||||
|
)
|
||||||
|
|
||||||
|
proc checkSlashableAttestation*(
|
||||||
|
db: SlashingProtectionDB,
|
||||||
|
validator: ValidatorPubKey,
|
||||||
|
source: Epoch,
|
||||||
|
target: Epoch
|
||||||
|
): Result[void, BadVote] =
|
||||||
|
## Returns an error if the specified validator
|
||||||
|
## already voted for the specified slot
|
||||||
|
## or would vote in a contradiction to previous votes
|
||||||
|
## (surrounding vote or surrounded vote).
|
||||||
|
##
|
||||||
|
## Returns success otherwise
|
||||||
|
db.queryVersions(
|
||||||
|
checkSlashableAttestation(db_version, validator, source, target)
|
||||||
|
)
|
||||||
|
|
||||||
|
# DB Updates
|
||||||
|
# --------------------------------------------
|
||||||
|
|
||||||
|
template updateVersions(
|
||||||
|
db: SlashingProtectionDB,
|
||||||
|
query: untyped
|
||||||
|
) {.dirty.} =
|
||||||
|
## Update multiple DB versions
|
||||||
|
## Query should be in the form
|
||||||
|
## myQuery(db_version, args...)
|
||||||
|
##
|
||||||
|
## Resolve conflicts according to
|
||||||
|
## `db.disagreementBehavior`
|
||||||
|
##
|
||||||
|
## For example
|
||||||
|
## registerBlock(db_version, validator, slot, block_root)
|
||||||
|
##
|
||||||
|
## db_version will be replaced by db_v1 and db_v2 accordingly
|
||||||
|
|
||||||
|
if db.useV1():
|
||||||
|
template db_version: untyped = db.db_v1
|
||||||
|
query
|
||||||
|
if db.useV2():
|
||||||
|
template db_version: untyped = db.db_v2
|
||||||
|
query
|
||||||
|
|
||||||
|
proc registerBlock*(
|
||||||
|
db: SlashingProtectionDB,
|
||||||
|
validator: ValidatorPubKey,
|
||||||
|
slot: Slot, block_signing_root: Eth2Digest) =
|
||||||
|
## Add a block to the slashing protection DB
|
||||||
|
## `checkSlashableBlockProposal` MUST be run
|
||||||
|
## before to ensure no overwrite.
|
||||||
|
##
|
||||||
|
## block_signing_root is the output of
|
||||||
|
## compute_signing_root(block, domain)
|
||||||
|
db.updateVersions(
|
||||||
|
registerBlock(db_version, validator, slot, block_signing_root)
|
||||||
|
)
|
||||||
|
|
||||||
|
proc registerAttestation*(
|
||||||
|
db: SlashingProtectionDB,
|
||||||
|
validator: ValidatorPubKey,
|
||||||
|
source, target: Epoch,
|
||||||
|
attestation_signing_root: Eth2Digest) =
|
||||||
|
## Add an attestation to the slashing protection DB
|
||||||
|
## `checkSlashableAttestation` MUST be run
|
||||||
|
## before to ensure no overwrite.
|
||||||
|
##
|
||||||
|
## attestation_signing_root is the output of
|
||||||
|
## compute_signing_root(attestation, domain)
|
||||||
|
db.updateVersions(
|
||||||
|
registerAttestation(db_version, validator,
|
||||||
|
source, target, attestation_signing_root)
|
||||||
|
)
|
||||||
|
|
||||||
|
# DB maintenance
|
||||||
|
# --------------------------------------------
|
||||||
|
# private for now
|
||||||
|
|
||||||
|
proc pruneBlocks*(
|
||||||
|
db: SlashingProtectionDB,
|
||||||
|
validator: ValidatorPubkey,
|
||||||
|
newMinSlot: Slot) =
|
||||||
|
## Prune all blocks from a validator before the specified newMinSlot
|
||||||
|
## This is intended for interchange import to ensure
|
||||||
|
## that in case of a gap, we don't allow signing in that gap.
|
||||||
|
##
|
||||||
|
## Note: DB v1 does not support pruning
|
||||||
|
|
||||||
|
# {.error: "This is a backend specific proc".}
|
||||||
|
fatal "This is a backend specific proc"
|
||||||
|
quit 1
|
||||||
|
|
||||||
|
proc pruneAttestations*(
|
||||||
|
db: SlashingProtectionDB,
|
||||||
|
validator: ValidatorPubkey,
|
||||||
|
newMinSourceEpoch: Epoch,
|
||||||
|
newMinTargetEpoch: Epoch) =
|
||||||
|
## Prune all blocks from a validator before the specified newMinSlot
|
||||||
|
## This is intended for interchange import to ensure
|
||||||
|
## that in case of a gap, we don't allow signing in that gap.
|
||||||
|
##
|
||||||
|
## Note: DB v1 does not support pruning
|
||||||
|
|
||||||
|
# {.error: "This is a backend specific proc".}
|
||||||
|
fatal "This is a backend specific proc"
|
||||||
|
quit 1
|
||||||
|
|
||||||
|
proc pruneAfterFinalization*(
|
||||||
|
db: SlashingProtectionDB,
|
||||||
|
finalizedEpoch: Epoch
|
||||||
|
) =
|
||||||
|
# TODO
|
||||||
|
# call sqlPruneAfterFinalizationBlocks
|
||||||
|
# and sqlPruneAfterFinalizationAttestations
|
||||||
|
# and test that wherever pruning happens, tests still pass
|
||||||
|
# and/or devise new tests
|
||||||
|
|
||||||
|
# {.error: "NotImplementedError".}
|
||||||
|
fatal "Pruning is not implemented"
|
||||||
|
quit 1
|
||||||
|
|
||||||
|
# Interchange
|
||||||
|
# --------------------------------------------
|
||||||
|
|
||||||
|
proc toSPDIR*(db: SlashingProtectionDB): SPDIR
|
||||||
|
{.raises: [IOError, Defect].} =
|
||||||
|
## Assumes that if the db uses both v1 and v2
|
||||||
|
## the v2 has the latest information and includes the v1 DB
|
||||||
|
if db.useV2():
|
||||||
|
return db.db_v2.toSPDIR()
|
||||||
|
else:
|
||||||
|
doAssert db.useV1()
|
||||||
|
return db.db_v1.toSPDIR()
|
||||||
|
|
||||||
|
proc inclSPDIR*(db: SlashingProtectionDB, spdir: SPDIR): SlashingImportStatus
|
||||||
|
{.raises: [SerializationError, IOError, Defect].} =
|
||||||
|
let useV1 = db.useV1()
|
||||||
|
let useV2 = db.useV2()
|
||||||
|
|
||||||
|
if useV2 and useV1:
|
||||||
|
let resultV2 = db.db_v2.inclSPDIR(spdir)
|
||||||
|
let resultV1 = db.db_v1.inclSPDIR(spdir)
|
||||||
|
if resultV1 == resultV2:
|
||||||
|
return resultV2
|
||||||
|
else:
|
||||||
|
error "The legacy and new slashing protection DB have imported the file with different level of success",
|
||||||
|
resultV1 = resultV1,
|
||||||
|
resultV2 = resultV2
|
||||||
|
return resultV2
|
||||||
|
|
||||||
|
if useV2 and not useV1:
|
||||||
|
return db.db_v2.inclSPDIR(spdir)
|
||||||
|
else:
|
||||||
|
doAssert useV1
|
||||||
|
return db.db_v1.inclSPDIR(spdir)
|
||||||
|
|
||||||
|
# The high-level import/export functions are
|
||||||
|
# - importSlashingInterchange
|
||||||
|
# - exportSlashingInterchange
|
||||||
|
# in slashing_protection_types.nim
|
||||||
|
#
|
||||||
|
# That builds on a DB backend inclSPDIR and toSPDIR
|
||||||
|
# SPDIR being a common Intermediate Representation
|
||||||
|
|
||||||
|
# Sanity check
|
||||||
|
# --------------------------------------------------------------
|
||||||
|
|
||||||
|
proc foo(db: SlashingProtectionDB_Concept) =
|
||||||
|
discard
|
||||||
|
|
||||||
|
var x: SlashingProtectionDB
|
||||||
|
foo(x) {.explain.}
|
||||||
|
|
||||||
|
static: doAssert SlashingProtectionDB is SlashingProtectionDB_Concept
|
|
@ -0,0 +1,449 @@
|
||||||
|
# beacon_chain
|
||||||
|
# Copyright (c) 2018-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.
|
||||||
|
|
||||||
|
import
|
||||||
|
# Stdlib
|
||||||
|
std/[typetraits, strutils, algorithm],
|
||||||
|
# Status
|
||||||
|
eth/db/[kvstore, kvstore_sqlite3],
|
||||||
|
stew/results,
|
||||||
|
stew/byteutils,
|
||||||
|
serialization,
|
||||||
|
json_serialization,
|
||||||
|
chronicles,
|
||||||
|
# Internal
|
||||||
|
../spec/[datatypes, digest, crypto]
|
||||||
|
|
||||||
|
export serialization, json_serialization # Generic sandwich https://github.com/nim-lang/Nim/issues/11225
|
||||||
|
|
||||||
|
# Slashing Protection Interop
|
||||||
|
# --------------------------------------------
|
||||||
|
# We use the SPDIR type as an intermediate representation
|
||||||
|
# between database versions and to generate
|
||||||
|
# the serialized interchanged format.
|
||||||
|
#
|
||||||
|
# References: https://eips.ethereum.org/EIPS/eip-3076
|
||||||
|
#
|
||||||
|
# SPDIR: Nimbus-specific, Slashing Protection Database Intermediate Representation
|
||||||
|
# SPDIF: Cross-client, json, Slashing Protection Database Interchange Format
|
||||||
|
|
||||||
|
type
|
||||||
|
SPDIR* = object
|
||||||
|
## Slashing Protection Database Interchange Format
|
||||||
|
metadata*: SPDIR_Meta
|
||||||
|
data*: seq[SPDIR_Validator]
|
||||||
|
|
||||||
|
Eth2Digest0x* = distinct Eth2Digest
|
||||||
|
## The spec mandates "0x" prefix on serialization
|
||||||
|
## So we need to set custom read/write
|
||||||
|
|
||||||
|
PubKeyBytes* = array[RawPubKeySize, byte]
|
||||||
|
## This is the serialized byte representation
|
||||||
|
## of a Validator Public Key.
|
||||||
|
## Portable between Miracl/BLST
|
||||||
|
## and limits serialization/deserialization call
|
||||||
|
|
||||||
|
PubKey0x* = distinct PubKeyBytes
|
||||||
|
## The spec mandates "0x" prefix on serialization
|
||||||
|
## So we need to set custom read/write
|
||||||
|
## We also assume that pubkeys in the database
|
||||||
|
## are valid points on the BLS12-381 G1 curve
|
||||||
|
## (so we skip fromRaw/serialization checks)
|
||||||
|
|
||||||
|
SlotString* = distinct Slot
|
||||||
|
## The spec mandates string serialization for wide compatibility (javascript)
|
||||||
|
EpochString* = distinct Epoch
|
||||||
|
## The spec mandates string serialization for wide compatibility (javascript)
|
||||||
|
|
||||||
|
SPDIR_Meta* = object
|
||||||
|
interchange_format_version*: string
|
||||||
|
genesis_validators_root*: Eth2Digest0x
|
||||||
|
|
||||||
|
SPDIR_Validator* = object
|
||||||
|
pubkey*: PubKey0x
|
||||||
|
signed_blocks*: seq[SPDIR_SignedBlock]
|
||||||
|
signed_attestations*: seq[SPDIR_SignedAttestation]
|
||||||
|
|
||||||
|
SPDIR_SignedBlock* = object
|
||||||
|
slot*: SlotString
|
||||||
|
signing_root*: Eth2Digest0x # compute_signing_root(block, domain)
|
||||||
|
|
||||||
|
SPDIR_SignedAttestation* = object
|
||||||
|
source_epoch*: EpochString
|
||||||
|
target_epoch*: EpochString
|
||||||
|
signing_root*: Eth2Digest0x # compute_signing_root(attestation, domain)
|
||||||
|
|
||||||
|
# Slashing Protection types
|
||||||
|
# --------------------------------------------
|
||||||
|
|
||||||
|
type
|
||||||
|
SlashingProtectionDB_Concept* = concept db, type DB
|
||||||
|
## Database storing the blocks attested
|
||||||
|
## by validators attached to a beacon node
|
||||||
|
## or validator client.
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
# --------------------------------------------
|
||||||
|
DB.version is int
|
||||||
|
|
||||||
|
# Resource Management
|
||||||
|
# --------------------------------------------
|
||||||
|
DB is ref
|
||||||
|
|
||||||
|
DB.init(Eth2Digest, string, string) is DB
|
||||||
|
# DB.init(genesis_root, dir, filename)
|
||||||
|
DB.loadUnchecked(string, string, bool) is DB
|
||||||
|
# DB.load(dir, filename, readOnly)
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
# Queries
|
||||||
|
# --------------------------------------------
|
||||||
|
db.checkSlashableBlockProposal(ValidatorPubKey, Slot) is Result[void, BadProposal]
|
||||||
|
# db.checkSlashableBlockProposal(validator, slot)
|
||||||
|
db.checkSlashableAttestation(ValidatorPubKey, Epoch, Epoch) is Result[void, BadVote]
|
||||||
|
# db.checkSlashableAttestation(validator, source, target)
|
||||||
|
|
||||||
|
# Updates
|
||||||
|
# --------------------------------------------
|
||||||
|
db.registerBlock(ValidatorPubKey, Slot, Eth2Digest)
|
||||||
|
# db.checkSlashableAttestation(validator, slot, block_root)
|
||||||
|
db.registerAttestation(ValidatorPubKey, Epoch, Epoch, Eth2Digest)
|
||||||
|
# db.checkSlashableAttestation(validator, source, target, block_root)
|
||||||
|
|
||||||
|
# Pruning
|
||||||
|
# --------------------------------------------
|
||||||
|
db.pruneBlocks(ValidatorPubKey, Slot)
|
||||||
|
db.pruneAttestations(ValidatorPubKey, Epoch, Epoch)
|
||||||
|
db.pruneAfterFinalization(Epoch)
|
||||||
|
|
||||||
|
# Interchange
|
||||||
|
# --------------------------------------------
|
||||||
|
db.toSPDIR() is SPDIR
|
||||||
|
# to Slashing Protection Data Intermediate Representation
|
||||||
|
# db.toSPDIR()
|
||||||
|
db.inclSPDIR(SPDIR) is SlashingImportStatus
|
||||||
|
# include the content of Slashing Protection Data Intermediate Representation
|
||||||
|
# in the database
|
||||||
|
# db.inclSPDIR(path)
|
||||||
|
|
||||||
|
SlashingImportStatus* = enum
|
||||||
|
siSuccess
|
||||||
|
siFailure
|
||||||
|
siPartial
|
||||||
|
|
||||||
|
BadVoteKind* = enum
|
||||||
|
## Attestation bad vote kind
|
||||||
|
# h: height (i.e. epoch for attestation, slot for blocks)
|
||||||
|
# t: target
|
||||||
|
# s: source
|
||||||
|
# 1: existing attestations
|
||||||
|
# 2: candidate attestation
|
||||||
|
|
||||||
|
# Spec slashing condition
|
||||||
|
DoubleVote # h(t1) == h(t2)
|
||||||
|
SurroundedVote # h(s1) < h(s2) < h(t2) < h(t1)
|
||||||
|
SurroundingVote # h(s2) < h(s1) < h(t1) < h(t2)
|
||||||
|
|
||||||
|
# Non-spec, should never happen in a well functioning client
|
||||||
|
TargetPrecedesSource # h(t1) < h(s1) - current epoch precedes last justified epoch
|
||||||
|
|
||||||
|
# EIP-3067 (https://eips.ethereum.org/EIPS/eip-3076)
|
||||||
|
MinSourceViolation # h(s2) < h(s1) - EIP3067 condition 4 (strict inequality)
|
||||||
|
MinTargetViolation # h(t2) <= h(t1) - EIP3067 condition 5
|
||||||
|
|
||||||
|
BadVote* = object
|
||||||
|
case kind*: BadVoteKind
|
||||||
|
of DoubleVote:
|
||||||
|
existingAttestation*: Eth2Digest
|
||||||
|
of SurroundedVote, SurroundingVote:
|
||||||
|
existingAttestationRoot*: Eth2Digest # Many roots might be in conflict
|
||||||
|
sourceExisting*, targetExisting*: Epoch
|
||||||
|
sourceSlashable*, targetSlashable*: Epoch
|
||||||
|
of TargetPrecedesSource:
|
||||||
|
discard
|
||||||
|
of MinSourceViolation:
|
||||||
|
minSource*: Epoch
|
||||||
|
candidateSource*: Epoch
|
||||||
|
of MinTargetViolation:
|
||||||
|
minTarget*: Epoch
|
||||||
|
candidateTarget*: Epoch
|
||||||
|
|
||||||
|
BadProposalKind* = enum
|
||||||
|
# Spec slashing condition
|
||||||
|
DoubleProposal # h(t1) == h(t2)
|
||||||
|
# EIP-3067 (https://eips.ethereum.org/EIPS/eip-3076)
|
||||||
|
MinSlotViolation # h(t2) <= h(t1)
|
||||||
|
|
||||||
|
BadProposal* = object
|
||||||
|
case kind*: BadProposalKind
|
||||||
|
of DoubleProposal:
|
||||||
|
existingBlock*: Eth2Digest
|
||||||
|
of MinSlotViolation:
|
||||||
|
minSlot*: Slot
|
||||||
|
candidateSlot*: Slot
|
||||||
|
|
||||||
|
func `==`*(a, b: BadVote): bool =
|
||||||
|
## Comparison operator.
|
||||||
|
## Used implictily by Result when comparing the
|
||||||
|
## result of multiple DB versions
|
||||||
|
if a.kind != b.kind:
|
||||||
|
false
|
||||||
|
elif a.kind == DoubleVote:
|
||||||
|
a.existingAttestation == b.existingAttestation
|
||||||
|
elif a.kind in {SurroundedVote, SurroundingVote}:
|
||||||
|
(a.existingAttestationRoot == b.existingAttestationRoot) and
|
||||||
|
(a.sourceExisting == b.sourceExisting) and
|
||||||
|
(a.targetExisting == b.targetExisting) and
|
||||||
|
(a.sourceSlashable == b.sourceSlashable) and
|
||||||
|
(a.targetSlashable == b.targetSlashable)
|
||||||
|
elif a.kind == TargetPrecedesSource:
|
||||||
|
true
|
||||||
|
elif a.kind == MinSourceViolation:
|
||||||
|
(a.minSource == b.minSource) and
|
||||||
|
(a.candidateSource == b.candidateSource)
|
||||||
|
elif a.kind == MinTargetViolation:
|
||||||
|
(a.minTarget == b.minTarget) and
|
||||||
|
(a.candidateTarget == b.candidateTarget)
|
||||||
|
else: # Unreachable
|
||||||
|
false
|
||||||
|
|
||||||
|
func `==`*(a, b: BadProposal): bool =
|
||||||
|
## Comparison operator.
|
||||||
|
## Used implictily by Result when comparing the
|
||||||
|
## result of multiple DB versions
|
||||||
|
##
|
||||||
|
## Except that V1 doesn't support low-watermark...
|
||||||
|
if a.kind != b.kind:
|
||||||
|
false
|
||||||
|
elif a.kind == DoubleProposal:
|
||||||
|
a.existingBlock == b.existingBlock
|
||||||
|
elif a.kind == MinSlotViolation:
|
||||||
|
a.minSlot == b.minSlot and
|
||||||
|
a.candidateSlot == b.candidateSlot
|
||||||
|
else: # Unreachable
|
||||||
|
false
|
||||||
|
|
||||||
|
# Serialization
|
||||||
|
# --------------------------------------------
|
||||||
|
|
||||||
|
proc writeValue*(writer: var JsonWriter, value: PubKey0x)
|
||||||
|
{.inline, raises: [IOError, Defect].} =
|
||||||
|
writer.writeValue("0x" & value.PubKeyBytes.toHex())
|
||||||
|
|
||||||
|
proc readValue*(reader: var JsonReader, value: var PubKey0x)
|
||||||
|
{.raises: [SerializationError, IOError, Defect].} =
|
||||||
|
try:
|
||||||
|
value = PubKey0x reader.readValue(string).hexToByteArray[:RawPubKeySize]()
|
||||||
|
except ValueError:
|
||||||
|
raiseUnexpectedValue(reader, "Hex string expected")
|
||||||
|
|
||||||
|
proc writeValue*(w: var JsonWriter, a: Eth2Digest0x)
|
||||||
|
{.inline, raises: [IOError, Defect].} =
|
||||||
|
w.writeValue "0x" & a.Eth2Digest.data.toHex()
|
||||||
|
|
||||||
|
proc readValue*(r: var JsonReader, a: var Eth2Digest0x)
|
||||||
|
{.raises: [SerializationError, IOError, Defect].} =
|
||||||
|
try:
|
||||||
|
a = Eth2Digest0x fromHex(Eth2Digest, r.readValue(string))
|
||||||
|
except ValueError:
|
||||||
|
raiseUnexpectedValue(r, "Hex string expected")
|
||||||
|
|
||||||
|
proc writeValue*(w: var JsonWriter, a: SlotString or EpochString)
|
||||||
|
{.inline, raises: [IOError, Defect].} =
|
||||||
|
w.writeValue $distinctBase(a)
|
||||||
|
|
||||||
|
proc readValue*(r: var JsonReader, a: var (SlotString or EpochString))
|
||||||
|
{.raises: [SerializationError, IOError, Defect].} =
|
||||||
|
try:
|
||||||
|
a = (typeof a)(r.readValue(string).parseBiggestUint())
|
||||||
|
except ValueError:
|
||||||
|
raiseUnexpectedValue(r, "Integer in a string expected")
|
||||||
|
|
||||||
|
proc exportSlashingInterchange*(
|
||||||
|
db: SlashingProtectionDB_Concept,
|
||||||
|
path: string, prettify = true) =
|
||||||
|
## Export a database to the Slashing Protection Database Interchange Format
|
||||||
|
let spdir = db.toSPDIR()
|
||||||
|
Json.saveFile(path, spdir, prettify)
|
||||||
|
echo "Exported slashing protection DB to '", path, "'"
|
||||||
|
|
||||||
|
proc importSlashingInterchange*(
|
||||||
|
db: SlashingProtectionDB_Concept,
|
||||||
|
path: string): SlashingImportStatus =
|
||||||
|
## Import a Slashing Protection Database Interchange Format
|
||||||
|
## into a Nimbus DB.
|
||||||
|
## This adds data to already existing data.
|
||||||
|
let spdir = Json.loadFile(path, SPDIR)
|
||||||
|
return db.inclSPDIR(spdir)
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
# --------------------------------------------
|
||||||
|
|
||||||
|
func shortLog*(v: SPDIR_SignedBlock): auto =
|
||||||
|
(
|
||||||
|
slot: shortLog(v.slot.Slot),
|
||||||
|
signing_root: shortLog(v.signing_root.Eth2Digest)
|
||||||
|
)
|
||||||
|
func shortLog*(v: SPDIR_SignedAttestation): auto =
|
||||||
|
(
|
||||||
|
source_epoch: shortLog(v.source_epoch.Epoch),
|
||||||
|
target_epoch: shortLog(v.target_epoch.Epoch),
|
||||||
|
signing_root: shortLog(v.signing_root.Eth2Digest)
|
||||||
|
)
|
||||||
|
|
||||||
|
chronicles.formatIt SlotString: it.Slot.shortLog
|
||||||
|
chronicles.formatIt EpochString: it.Slot.shortLog
|
||||||
|
chronicles.formatIt Eth2Digest0x: it.Eth2Digest.shortLog
|
||||||
|
chronicles.formatIt SPDIR_SignedBlock: it.shortLog
|
||||||
|
chronicles.formatIt SPDIR_SignedAttestation: it.shortLog
|
||||||
|
|
||||||
|
# Interchange import
|
||||||
|
# --------------------------------------------
|
||||||
|
|
||||||
|
proc importInterchangeV5Impl*(
|
||||||
|
db: SlashingProtectionDB_Concept,
|
||||||
|
spdir: var SPDIR
|
||||||
|
): SlashingImportStatus
|
||||||
|
{.raises: [SerializationError, IOError, Defect].} =
|
||||||
|
## Common implementation of interchange import
|
||||||
|
## according to https://eips.ethereum.org/EIPS/eip-3076
|
||||||
|
## spdir needs to be `var` as it will be sorted in-place
|
||||||
|
|
||||||
|
result = siSuccess
|
||||||
|
|
||||||
|
for v in 0 ..< spdir.data.len:
|
||||||
|
let parsedKey = block:
|
||||||
|
let key = ValidatorPubKey.fromRaw(spdir.data[v].pubkey.PubKeyBytes)
|
||||||
|
if key.isErr:
|
||||||
|
# The bytes does not describe a valid encoding (length error)
|
||||||
|
error "Invalid public key.",
|
||||||
|
pubkey = "0x" & spdir.data[v].pubkey.PubKeyBytes.toHex()
|
||||||
|
|
||||||
|
result = siPartial
|
||||||
|
continue
|
||||||
|
if key.get().loadWithCache().isNone():
|
||||||
|
# The bytes don't deserialize to a valid BLS G1 elliptic curve point.
|
||||||
|
# Deserialization is costly but done only once per validator.
|
||||||
|
# and SlashingDB import is a very rare event.
|
||||||
|
error "Invalid public key.",
|
||||||
|
pubkey = "0x" & spdir.data[v].pubkey.PubKeyBytes.toHex()
|
||||||
|
|
||||||
|
result = siPartial
|
||||||
|
continue
|
||||||
|
key.get()
|
||||||
|
|
||||||
|
# Sort by ascending minimum slot so that we don't trigger MinSlotViolation
|
||||||
|
spdir.data[v].signed_blocks.sort do (a, b: SPDIR_SignedBlock) -> int:
|
||||||
|
result = cmp(a.slot.int, b.slot.int)
|
||||||
|
|
||||||
|
spdir.data[v].signed_attestations.sort do (a, b: SPDIR_SignedAttestation) -> int:
|
||||||
|
result = cmp(a.source_epoch.int, b.source_epoch.int)
|
||||||
|
if result == 0: # Same epoch
|
||||||
|
result = cmp(a.target_epoch.int, b.target_epoch.int)
|
||||||
|
|
||||||
|
const ZeroDigest = Eth2Digest()
|
||||||
|
|
||||||
|
# Blocks
|
||||||
|
# ---------------------------------------------------
|
||||||
|
# After import we need to prune the DB from everything
|
||||||
|
# besides the last imported block slot.
|
||||||
|
# This ensures that even if 2 slashing DB are imported in the wrong order
|
||||||
|
# (the last before the earliest) the minSlotViolation check stays consistent.
|
||||||
|
var maxValidSlotSeen = -1
|
||||||
|
|
||||||
|
for b in 0 ..< spdir.data[v].signed_blocks.len:
|
||||||
|
template B: untyped = spdir.data[v].signed_blocks[b]
|
||||||
|
let status = db.checkSlashableBlockProposal(
|
||||||
|
parsedKey, B.slot.Slot
|
||||||
|
)
|
||||||
|
if status.isErr():
|
||||||
|
# We might be importing a duplicate which EIP-3076 allows
|
||||||
|
# there is no reason during normal operation to integrate
|
||||||
|
# a duplicate so checkSlashableBlockProposal would have rejected it.
|
||||||
|
# We special-case that for imports.
|
||||||
|
# Note: rule 2 mentions repeat signing in the MinSlotViolation case
|
||||||
|
# having 2 blocks with the same signing root and different slots
|
||||||
|
# would break the blockchain so we only check for exact slot.
|
||||||
|
if status.error.kind == DoubleProposal and
|
||||||
|
B.signing_root.Eth2Digest != ZeroDigest and
|
||||||
|
status.error.existingBlock == B.signing_root.Eth2Digest:
|
||||||
|
warn "Block already exists in the DB",
|
||||||
|
pubkey = spdir.data[v].pubkey.PubKeyBytes.toHex(),
|
||||||
|
candidateBlock = B
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
error "Slashable block. Skipping its import.",
|
||||||
|
pubkey = spdir.data[v].pubkey.PubKeyBytes.toHex(),
|
||||||
|
candidateBlock = B,
|
||||||
|
conflict = status.error()
|
||||||
|
result = siPartial
|
||||||
|
continue
|
||||||
|
|
||||||
|
if B.slot.int > maxValidSlotSeen:
|
||||||
|
maxValidSlotSeen = B.slot.int
|
||||||
|
|
||||||
|
db.registerBlock(
|
||||||
|
parsedKey,
|
||||||
|
B.slot.Slot,
|
||||||
|
B.signing_root.Eth2Digest
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now prune everything that predates
|
||||||
|
# this interchange file max slot
|
||||||
|
db.pruneBlocks(parsedKey, Slot maxValidSlotSeen)
|
||||||
|
|
||||||
|
# Attestations
|
||||||
|
# ---------------------------------------------------
|
||||||
|
# After import we need to prune the DB from everything
|
||||||
|
# besides the last imported attestation source and target epochs.
|
||||||
|
# This ensures that even if 2 slashing DB are imported in the wrong order
|
||||||
|
# (the last before the earliest) the minEpochViolation check stays consistent.
|
||||||
|
var maxValidSourceEpochSeen = -1
|
||||||
|
var maxValidTargetEpochSeen = -1
|
||||||
|
|
||||||
|
for a in 0 ..< spdir.data[v].signed_attestations.len:
|
||||||
|
template A: untyped = spdir.data[v].signed_attestations[a]
|
||||||
|
let status = db.checkSlashableAttestation(
|
||||||
|
parsedKey,
|
||||||
|
A.source_epoch.Epoch,
|
||||||
|
A.target_epoch.Epoch
|
||||||
|
)
|
||||||
|
if status.isErr():
|
||||||
|
# We might be importing a duplicate which EIP-3076 allows
|
||||||
|
# there is no reason during normal operation to integrate
|
||||||
|
# a duplicate so checkSlashableAttestation would have rejected it.
|
||||||
|
# We special-case that for imports.
|
||||||
|
if status.error.kind == DoubleVote and
|
||||||
|
A.signing_root.Eth2Digest != ZeroDigest and
|
||||||
|
status.error.existingAttestation == A.signing_root.Eth2Digest:
|
||||||
|
warn "Attestation already exists in the DB",
|
||||||
|
pubkey = spdir.data[v].pubkey.PubKeyBytes.toHex(),
|
||||||
|
candidateAttestation = A
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
error "Slashable vote. Skipping its import.",
|
||||||
|
pubkey = spdir.data[v].pubkey.PubKeyBytes.toHex(),
|
||||||
|
candidateAttestation = A,
|
||||||
|
conflict = status.error()
|
||||||
|
result = siPartial
|
||||||
|
continue
|
||||||
|
|
||||||
|
if A.source_epoch.int > maxValidSourceEpochSeen:
|
||||||
|
maxValidSourceEpochSeen = A.source_epoch.int
|
||||||
|
if A.target_epoch.int > maxValidTargetEpochSeen:
|
||||||
|
maxValidTargetEpochSeen = A.target_epoch.int
|
||||||
|
|
||||||
|
db.registerAttestation(
|
||||||
|
parsedKey,
|
||||||
|
A.source_epoch.Epoch,
|
||||||
|
A.target_epoch.Epoch,
|
||||||
|
A.signing_root.Eth2Digest
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now prune everything that predates
|
||||||
|
# this interchange file max slot
|
||||||
|
db.pruneAttestations(parsedKey, Epoch maxValidSourceEpochSeen, Epoch maxValidTargetEpochSeen)
|
|
@ -7,16 +7,17 @@
|
||||||
|
|
||||||
import
|
import
|
||||||
# Standard library
|
# Standard library
|
||||||
std/tables,
|
std/[tables, os],
|
||||||
# Status
|
# Status
|
||||||
eth/db/kvstore,
|
eth/db/[kvstore, kvstore_sqlite3],
|
||||||
chronicles,
|
chronicles,
|
||||||
nimcrypto/[hash, utils],
|
nimcrypto/[hash, utils],
|
||||||
serialization,
|
serialization,
|
||||||
json_serialization,
|
json_serialization,
|
||||||
# Internal
|
# Internal
|
||||||
./spec/[datatypes, digest, crypto],
|
../spec/[datatypes, digest, crypto],
|
||||||
./ssz
|
../ssz,
|
||||||
|
./slashing_protection_common
|
||||||
|
|
||||||
# Requirements
|
# Requirements
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
@ -136,38 +137,12 @@ import
|
||||||
# as per-validator linked lists
|
# as per-validator linked lists
|
||||||
|
|
||||||
type
|
type
|
||||||
SlashingProtectionDB* = ref object
|
SlashingProtectionDB_v1* = ref object
|
||||||
## Database storing the blocks attested
|
## Database storing the blocks attested
|
||||||
## by validators attached to a beacon node
|
## by validators attached to a beacon node
|
||||||
## or validator client.
|
## or validator client.
|
||||||
backend: KvStoreRef
|
backend: KvStoreRef
|
||||||
|
|
||||||
BadVoteKind* = enum
|
|
||||||
## Attestation bad vote kind
|
|
||||||
# h: height (i.e. epoch for attestation, slot for blocks)
|
|
||||||
# t: target
|
|
||||||
# s: source
|
|
||||||
# 1: existing attestations
|
|
||||||
# 2: candidate attestation
|
|
||||||
|
|
||||||
# Spec slashing condition
|
|
||||||
DoubleVote # h(t1) = h(t2)
|
|
||||||
SurroundedVote # h(s1) < h(s2) < h(t2) < h(t1)
|
|
||||||
SurroundingVote # h(s2) < h(s1) < h(t1) < h(t2)
|
|
||||||
# Non-spec, should never happen in a well functioning client
|
|
||||||
TargetPrecedesSource # h(t1) < h(s1) - current epoch precedes last justified epoch
|
|
||||||
|
|
||||||
BadVote* = object
|
|
||||||
case kind*: BadVoteKind
|
|
||||||
of DoubleVote:
|
|
||||||
existingAttestation*: Eth2Digest
|
|
||||||
of SurroundedVote, SurroundingVote:
|
|
||||||
existingAttestationRoot*: Eth2Digest # Many roots might be in conflict
|
|
||||||
sourceExisting*, targetExisting*: Epoch
|
|
||||||
sourceSlashable*, targetSlashable*: Epoch
|
|
||||||
of TargetPrecedesSource:
|
|
||||||
discard
|
|
||||||
|
|
||||||
SlotDesc = object
|
SlotDesc = object
|
||||||
# Using tuple instead of objects, crashes the Nim compiler
|
# Using tuple instead of objects, crashes the Nim compiler
|
||||||
# with SSZ serialization
|
# with SSZ serialization
|
||||||
|
@ -191,7 +166,7 @@ type
|
||||||
kTargetEpoch
|
kTargetEpoch
|
||||||
kLinkedListMeta
|
kLinkedListMeta
|
||||||
# Interchange format
|
# Interchange format
|
||||||
kGenesisValidatorRoot
|
kGenesisValidatorsRoot
|
||||||
kNumValidators
|
kNumValidators
|
||||||
kValidator
|
kValidator
|
||||||
|
|
||||||
|
@ -212,6 +187,9 @@ type
|
||||||
## Portable between Miracl/BLST
|
## Portable between Miracl/BLST
|
||||||
## and limits serialization/deserialization call
|
## and limits serialization/deserialization call
|
||||||
|
|
||||||
|
# Internal
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
{.push raises: [Defect].}
|
||||||
logScope:
|
logScope:
|
||||||
topics = "antislash"
|
topics = "antislash"
|
||||||
|
@ -254,7 +232,7 @@ func subkey(
|
||||||
result[1 .. ^1] = validator
|
result[1 .. ^1] = validator
|
||||||
|
|
||||||
func subkey(kind: static SlashingKeyKind): array[1, byte] =
|
func subkey(kind: static SlashingKeyKind): array[1, byte] =
|
||||||
static: doAssert kind in {kNumValidators, kGenesisValidatorRoot}
|
static: doAssert kind in {kNumValidators, kGenesisValidatorsRoot}
|
||||||
result[0] = byte ord(kind)
|
result[0] = byte ord(kind)
|
||||||
|
|
||||||
func subkey(kind: static SlashingKeyKind, valIndex: uint32): array[5, byte] =
|
func subkey(kind: static SlashingKeyKind, valIndex: uint32): array[5, byte] =
|
||||||
|
@ -263,13 +241,13 @@ func subkey(kind: static SlashingKeyKind, valIndex: uint32): array[5, byte] =
|
||||||
result[1..<5] = toBytesBE(valIndex)
|
result[1..<5] = toBytesBE(valIndex)
|
||||||
result[0] = byte ord(kind)
|
result[0] = byte ord(kind)
|
||||||
|
|
||||||
proc put(db: SlashingProtectionDB, key: openArray[byte], v: auto) =
|
proc put(db: SlashingProtectionDB_v1, key: openArray[byte], v: auto) =
|
||||||
db.backend.put(
|
db.backend.put(
|
||||||
key,
|
key,
|
||||||
SSZ.encode(v)
|
SSZ.encode(v)
|
||||||
).expect("working database")
|
).expect("working database")
|
||||||
|
|
||||||
proc get(db: SlashingProtectionDB,
|
proc rawGet(rawdb: KvStoreRef,
|
||||||
key: openArray[byte],
|
key: openArray[byte],
|
||||||
T: typedesc): Opt[T] =
|
T: typedesc): Opt[T] =
|
||||||
|
|
||||||
|
@ -286,6 +264,8 @@ proc get(db: SlashingProtectionDB,
|
||||||
sizeof(uint32)
|
sizeof(uint32)
|
||||||
elif T is ValidatorPubKey:
|
elif T is ValidatorPubKey:
|
||||||
RawPubKeySize
|
RawPubKeySize
|
||||||
|
elif T is PubKeyBytes:
|
||||||
|
RawPubKeySize
|
||||||
else:
|
else:
|
||||||
{.error: "Invalid database node type: " & $T.}
|
{.error: "Invalid database node type: " & $T.}
|
||||||
## SSZ serialization is packed
|
## SSZ serialization is packed
|
||||||
|
@ -322,37 +302,113 @@ proc get(db: SlashingProtectionDB,
|
||||||
expectedSize = ExpectedNodeSszSize
|
expectedSize = ExpectedNodeSszSize
|
||||||
discard
|
discard
|
||||||
|
|
||||||
discard db.backend.get(key, decode).expect("working database")
|
discard rawdb.get(key, decode).expect("working database")
|
||||||
|
|
||||||
res
|
res
|
||||||
|
|
||||||
proc setGenesis(db: SlashingProtectionDB, genesis_validator_root: Eth2Digest) =
|
proc get(db: SlashingProtectionDB_v1,
|
||||||
|
key: openArray[byte],
|
||||||
|
T: typedesc): Opt[T] =
|
||||||
|
db.backend.rawGet(key, T)
|
||||||
|
|
||||||
|
proc setGenesis(db: SlashingProtectionDB_v1, genesis_validators_root: Eth2Digest) =
|
||||||
# Workaround SSZ / nim-serialization visibility issue
|
# Workaround SSZ / nim-serialization visibility issue
|
||||||
# "template WriterType(T: type SSZ): type"
|
# "template WriterType(T: type SSZ): type"
|
||||||
# by having a non-generic proc
|
# by having a non-generic proc
|
||||||
db.put(
|
db.put(
|
||||||
subkey(kGenesisValidatorRoot),
|
subkey(kGenesisValidatorsRoot),
|
||||||
genesis_validator_root
|
genesis_validators_root
|
||||||
)
|
)
|
||||||
|
|
||||||
proc init*(
|
# DB Multiversioning
|
||||||
T: type SlashingProtectionDB,
|
# -------------------------------------------------------------
|
||||||
genesis_validator_root: Eth2Digest,
|
|
||||||
backend: KVStoreRef): SlashingProtectionDB =
|
|
||||||
result = T(backend: backend)
|
|
||||||
result.setGenesis(genesis_validator_root)
|
|
||||||
|
|
||||||
proc close*(db: SlashingProtectionDB) =
|
func version*(_: type SlashingProtectionDB_v1): static int =
|
||||||
|
1
|
||||||
|
|
||||||
|
proc getMetadataTable_DbV1*(rawdb: KvStoreRef): Option[Eth2Digest] =
|
||||||
|
## Check if the DB has v2 metadata
|
||||||
|
## and get its genesis root
|
||||||
|
|
||||||
|
if rawdb.contains(
|
||||||
|
subkey(kGenesisValidatorsRoot)
|
||||||
|
).get():
|
||||||
|
return some(
|
||||||
|
rawdb.rawGet(
|
||||||
|
subkey(kGenesisValidatorsRoot),
|
||||||
|
Eth2Digest
|
||||||
|
).get())
|
||||||
|
else:
|
||||||
|
return none(Eth2Digest)
|
||||||
|
|
||||||
|
proc checkOrPutGenesis_DbV1*(rawdb: KvStoreRef, genesis_validators_root: Eth2Digest): bool =
|
||||||
|
if rawdb.contains(
|
||||||
|
subkey(kGenesisValidatorsRoot)
|
||||||
|
).get():
|
||||||
|
return genesis_validators_root == rawdb.rawGet(
|
||||||
|
subkey(kGenesisValidatorsRoot),
|
||||||
|
Eth2Digest
|
||||||
|
).get()
|
||||||
|
else:
|
||||||
|
rawdb.put(
|
||||||
|
subkey(kGenesisValidatorsRoot),
|
||||||
|
genesis_validators_root.data
|
||||||
|
).expect("working database")
|
||||||
|
return true
|
||||||
|
|
||||||
|
proc fromRawDB*(dst: var SlashingProtectionDB_v1, rawdb: KvStoreRef) =
|
||||||
|
## Initialize a SlashingProtectionDB_v1 from a raw DB
|
||||||
|
## For first instantiation, do not forget to call setGenesis
|
||||||
|
doAssert rawdb.contains(
|
||||||
|
subkey(kGenesisValidatorsRoot)
|
||||||
|
).get(), "The Slashing DB is missing genesis information"
|
||||||
|
|
||||||
|
dst = SlashingProtectionDB_v1(backend: rawdb)
|
||||||
|
|
||||||
|
# Resource Management
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
|
||||||
|
proc init*(
|
||||||
|
T: type SlashingProtectionDB_v1,
|
||||||
|
genesis_validators_root: Eth2Digest,
|
||||||
|
basePath, dbname: string): T =
|
||||||
|
result = T(backend: kvStore SqStoreRef.init(basePath, dbname).get())
|
||||||
|
if not result.backend.checkOrPutGenesis_DbV1(genesis_validators_root):
|
||||||
|
fatal "The slashing database refers to another chain/mainnet/testnet",
|
||||||
|
path = basePath/dbname,
|
||||||
|
genesis_validators_root = genesis_validators_root
|
||||||
|
|
||||||
|
proc loadUnchecked*(
|
||||||
|
T: type SlashingProtectionDB_v1,
|
||||||
|
basePath, dbname: string, readOnly: bool
|
||||||
|
): SlashingProtectionDB_v1 {.raises:[Defect, IOError].}=
|
||||||
|
## Load a slashing protection DB
|
||||||
|
## Note: This is for conversion usage
|
||||||
|
## this doesn't check the genesis validator root
|
||||||
|
let path = basepath/dbname&".sqlite3"
|
||||||
|
let alreadyExists = fileExists(path)
|
||||||
|
if not alreadyExists:
|
||||||
|
raise newException(IOError, "DB '" & path & "' does not exist.")
|
||||||
|
|
||||||
|
let backend = kvStore SqStoreRef.init(basePath, dbname, readOnly = false).get()
|
||||||
|
|
||||||
|
doAssert backend.contains(
|
||||||
|
subkey(kGenesisValidatorsRoot)
|
||||||
|
).get(), "The Slashing DB is missing genesis information"
|
||||||
|
|
||||||
|
result = T(backend: backend)
|
||||||
|
|
||||||
|
proc close*(db: SlashingProtectionDB_v1) =
|
||||||
discard db.backend.close()
|
discard db.backend.close()
|
||||||
|
|
||||||
# DB Queries
|
# DB Queries
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|
||||||
proc checkSlashableBlockProposal*(
|
proc checkSlashableBlockProposal*(
|
||||||
db: SlashingProtectionDB,
|
db: SlashingProtectionDB_v1,
|
||||||
validator: ValidatorPubKey,
|
validator: ValidatorPubKey,
|
||||||
slot: Slot
|
slot: Slot
|
||||||
): Result[void, Eth2Digest] =
|
): Result[void, BadProposal] =
|
||||||
## Returns an error if the specified validator
|
## Returns an error if the specified validator
|
||||||
## already proposed a block for the specified slot.
|
## already proposed a block for the specified slot.
|
||||||
## This would lead to slashing.
|
## This would lead to slashing.
|
||||||
|
@ -367,18 +423,21 @@ proc checkSlashableBlockProposal*(
|
||||||
)
|
)
|
||||||
if foundBlock.isNone():
|
if foundBlock.isNone():
|
||||||
return ok()
|
return ok()
|
||||||
return err(foundBlock.unsafeGet().block_root)
|
return err(BadProposal(
|
||||||
|
kind: DoubleProposal,
|
||||||
|
existing_block: foundBlock.unsafeGet().block_root
|
||||||
|
))
|
||||||
|
|
||||||
proc checkSlashableAttestation*(
|
proc checkSlashableAttestation*(
|
||||||
db: SlashingProtectionDB,
|
db: SlashingProtectionDB_v1,
|
||||||
validator: ValidatorPubKey,
|
validator: ValidatorPubKey,
|
||||||
source: Epoch,
|
source: Epoch,
|
||||||
target: Epoch
|
target: Epoch
|
||||||
): Result[void, BadVote] =
|
): Result[void, BadVote] =
|
||||||
## Returns an error if the specified validator
|
## Returns an error if the specified validator
|
||||||
## already proposed a block for the specified slot.
|
## already voted for the specified slot
|
||||||
## This would lead to slashing.
|
## or would vote in a contradiction to previous votes
|
||||||
## The error contains the blockroot that was already proposed
|
## (surrounding vote or surrounded vote).
|
||||||
##
|
##
|
||||||
## Returns success otherwise
|
## Returns success otherwise
|
||||||
# TODO distinct type for the result attestation root
|
# TODO distinct type for the result attestation root
|
||||||
|
@ -428,7 +487,7 @@ proc checkSlashableAttestation*(
|
||||||
# Chain reorg
|
# Chain reorg
|
||||||
# Detect h(s2) < h(s1)
|
# Detect h(s2) < h(s1)
|
||||||
# If the candidate attestation source precedes
|
# If the candidate attestation source precedes
|
||||||
# source(s) we have in the SlashingProtectionDB
|
# source(s) we have in the SlashingProtectionDB_v1
|
||||||
# we have a chain reorg
|
# we have a chain reorg
|
||||||
# ---------------------------------
|
# ---------------------------------
|
||||||
if source < ll.sourceEpochs.stop:
|
if source < ll.sourceEpochs.stop:
|
||||||
|
@ -507,7 +566,7 @@ proc checkSlashableAttestation*(
|
||||||
# DB update
|
# DB update
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|
||||||
proc registerValidator(db: SlashingProtectionDB, validator: ValidatorPubKey) =
|
proc registerValidator(db: SlashingProtectionDB_v1, validator: ValidatorPubKey) =
|
||||||
## Add a new validator to the database
|
## Add a new validator to the database
|
||||||
## Assumes the validator does not exist
|
## Assumes the validator does not exist
|
||||||
let maybeNumVals = db.get(
|
let maybeNumVals = db.get(
|
||||||
|
@ -524,7 +583,7 @@ proc registerValidator(db: SlashingProtectionDB, validator: ValidatorPubKey) =
|
||||||
db.put(subkey(kValidator, valIndex), validator)
|
db.put(subkey(kValidator, valIndex), validator)
|
||||||
|
|
||||||
proc registerBlock*(
|
proc registerBlock*(
|
||||||
db: SlashingProtectionDB,
|
db: SlashingProtectionDB_v1,
|
||||||
validator: ValidatorPubKey,
|
validator: ValidatorPubKey,
|
||||||
slot: Slot, block_root: Eth2Digest) =
|
slot: Slot, block_root: Eth2Digest) =
|
||||||
## Add a block to the slashing protection DB
|
## Add a block to the slashing protection DB
|
||||||
|
@ -660,7 +719,7 @@ proc registerBlock*(
|
||||||
).unsafeGet()
|
).unsafeGet()
|
||||||
|
|
||||||
proc registerAttestation*(
|
proc registerAttestation*(
|
||||||
db: SlashingProtectionDB,
|
db: SlashingProtectionDB_v1,
|
||||||
validator: ValidatorPubKey,
|
validator: ValidatorPubKey,
|
||||||
source, target: Epoch,
|
source, target: Epoch,
|
||||||
attestation_root: Eth2Digest) =
|
attestation_root: Eth2Digest) =
|
||||||
|
@ -812,7 +871,7 @@ proc registerAttestation*(
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|
||||||
proc dumpBlocks*(
|
proc dumpBlocks*(
|
||||||
db: SlashingProtectionDB,
|
db: SlashingProtectionDB_v1,
|
||||||
validator: ValidatorPubKey
|
validator: ValidatorPubKey
|
||||||
): string =
|
): string =
|
||||||
## Dump the linked list of blocks proposd by a validator in a string
|
## Dump the linked list of blocks proposd by a validator in a string
|
||||||
|
@ -847,7 +906,7 @@ proc dumpBlocks*(
|
||||||
return $blocks
|
return $blocks
|
||||||
|
|
||||||
proc dumpAttestations*(
|
proc dumpAttestations*(
|
||||||
db: SlashingProtectionDB,
|
db: SlashingProtectionDB_v1,
|
||||||
validator: ValidatorPubKey
|
validator: ValidatorPubKey
|
||||||
): string =
|
): string =
|
||||||
## Dump the linked list of blocks proposd by a validator in a string
|
## Dump the linked list of blocks proposd by a validator in a string
|
||||||
|
@ -883,80 +942,52 @@ proc dumpAttestations*(
|
||||||
|
|
||||||
# DB maintenance
|
# DB maintenance
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
# TODO: pruning
|
proc pruneBlocks*(db: SlashingProtectionDB_v1, validator: ValidatorPubkey, newMinSlot: Slot) =
|
||||||
# Note that the complete interchange format
|
## Prune all blocks from a validator before the specified newMinSlot
|
||||||
# requires all proposals/attestations ever and so prevent pruning.
|
## This is intended for interchange import to ensure
|
||||||
|
## that in case of a gap, we don't allow signing in that gap.
|
||||||
|
##
|
||||||
|
## Note: the Database v1 does not support pruning.
|
||||||
|
warn "Slashing DB pruning is not supported on the v1 of our database. Request ignored.",
|
||||||
|
validator = shortLog(validator),
|
||||||
|
newMinSlot = shortLog(newMinSlot)
|
||||||
|
|
||||||
|
proc pruneAttestations*(
|
||||||
|
db: SlashingProtectionDB_v1,
|
||||||
|
validator: ValidatorPubkey,
|
||||||
|
newMinSourceEpoch: Epoch,
|
||||||
|
newMinTargetEpoch: Epoch) =
|
||||||
|
## Prune all blocks from a validator before the specified newMinSlot
|
||||||
|
## This is intended for interchange import.
|
||||||
|
##
|
||||||
|
## Note: the Database v1 does not support pruning.
|
||||||
|
warn "Slashing DB pruning is not supported on the v1 of our database. Request ignored.",
|
||||||
|
validator = shortLog(validator),
|
||||||
|
newMinSourceEpoch = shortLog(newMinSourceEpoch),
|
||||||
|
newMinTargetEpoch = shortLog(newMinTargetEpoch)
|
||||||
|
|
||||||
|
proc pruneAfterFinalization*(
|
||||||
|
db: SlashingProtectionDB_v1,
|
||||||
|
finalizedEpoch: Epoch
|
||||||
|
) =
|
||||||
|
warn "Slashing DB pruning is not supported on the v1 of our database. Request ignored.",
|
||||||
|
finalizedEpoch = shortLog(finalizedEpoch)
|
||||||
|
|
||||||
# Interchange
|
# Interchange
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|
||||||
type
|
proc toSPDIR_lowWatermark*(db: SlashingProtectionDB_v1): SPDIR
|
||||||
SPDIF = object
|
|
||||||
## Slashing Protection Database Interchange Format
|
|
||||||
metadata: SPDIF_Meta
|
|
||||||
data: seq[SPDIF_Validator]
|
|
||||||
|
|
||||||
Eth2Digest0x = distinct Eth2Digest
|
|
||||||
## The spec mandates "0x" prefix on serialization
|
|
||||||
## So we need to set custom read/write
|
|
||||||
PubKey0x = distinct ValidatorPubKey
|
|
||||||
## The spec mandates "0x" prefix on serialization
|
|
||||||
## So we need to set custom read/write
|
|
||||||
|
|
||||||
SPDIF_Meta = object
|
|
||||||
interchange_format: string
|
|
||||||
interchange_format_version: string
|
|
||||||
genesis_validator_root: Eth2Digest0x
|
|
||||||
|
|
||||||
SPDIF_Validator = object
|
|
||||||
pubkey: PubKey0x
|
|
||||||
signed_blocks: seq[SPDIF_SignedBlock]
|
|
||||||
signed_attestations: seq[SPDIF_SignedAttestation]
|
|
||||||
|
|
||||||
SPDIF_SignedBlock = object
|
|
||||||
slot: Slot
|
|
||||||
signing_root: Eth2Digest0x # compute_signing_root(block, domain)
|
|
||||||
|
|
||||||
SPDIF_SignedAttestation = object
|
|
||||||
source_epoch: Epoch
|
|
||||||
target_epoch: Epoch
|
|
||||||
signing_root: Eth2Digest0x # compute_signing_root(attestation, domain)
|
|
||||||
|
|
||||||
proc writeValue*(writer: var JsonWriter, value: PubKey0x)
|
|
||||||
{.inline, raises: [IOError, Defect].} =
|
|
||||||
writer.writeValue("0x" & value.ValidatorPubKey.toHex())
|
|
||||||
|
|
||||||
proc readValue*(reader: var JsonReader, value: var PubKey0x)
|
|
||||||
{.raises: [SerializationError, IOError, Defect].} =
|
|
||||||
let key = ValidatorPubKey.fromHex(reader.readValue(string))
|
|
||||||
if key.isOk:
|
|
||||||
value = PubKey0x key.get
|
|
||||||
else:
|
|
||||||
# TODO: Can we provide better diagnostic?
|
|
||||||
raiseUnexpectedValue(reader, "Valid hex-encoded public key expected")
|
|
||||||
|
|
||||||
proc writeValue*(w: var JsonWriter, a: Eth2Digest0x)
|
|
||||||
{.inline, raises: [IOError, Defect].} =
|
|
||||||
w.writeValue "0x" & a.Eth2Digest.data.toHex(lowercase = true)
|
|
||||||
|
|
||||||
proc readValue*(r: var JsonReader, a: var Eth2Digest0x)
|
|
||||||
{.raises: [SerializationError, IOError, Defect].} =
|
|
||||||
try:
|
|
||||||
a = Eth2Digest0x fromHex(Eth2Digest, r.readValue(string))
|
|
||||||
except ValueError:
|
|
||||||
raiseUnexpectedValue(r, "Hex string expected")
|
|
||||||
|
|
||||||
proc toSPDIF*(db: SlashingProtectionDB, path: string)
|
|
||||||
{.raises: [IOError, Defect].} =
|
{.raises: [IOError, Defect].} =
|
||||||
## Export the full slashing protection database
|
## Export only the low watermark metadata
|
||||||
## to a json the Slashing Protection Database Interchange (Complete) Format
|
## to the Nimbus Slashing Protection Database Intermediate Representation
|
||||||
var extract: SPDIF
|
##
|
||||||
extract.metadata.interchange_format = "complete"
|
## The full history is lost.
|
||||||
extract.metadata.interchange_format_version = "3"
|
result.metadata.interchange_format_version = "5"
|
||||||
extract.metadata.genesis_validator_root = Eth2Digest0x db.get(
|
|
||||||
subkey(kGenesisValidatorRoot), ETH2Digest
|
result.metadata.genesis_validators_root = Eth2Digest0x db.get(
|
||||||
|
subkey(kGenesisValidatorsRoot), ETH2Digest
|
||||||
# Bug in results.nim
|
# Bug in results.nim
|
||||||
# ).expect("Slashing Protection requires genesis_validator_root at init")
|
# ).expect("Slashing Protection requires genesis_validators_root at init")
|
||||||
).unsafeGet()
|
).unsafeGet()
|
||||||
|
|
||||||
let numValidators = db.get(
|
let numValidators = db.get(
|
||||||
|
@ -965,13 +996,68 @@ proc toSPDIF*(db: SlashingProtectionDB, path: string)
|
||||||
).get(otherwise = 0'u32)
|
).get(otherwise = 0'u32)
|
||||||
|
|
||||||
for i in 0'u32 ..< numValidators:
|
for i in 0'u32 ..< numValidators:
|
||||||
var validator: SPDIF_Validator
|
var validator: SPDIR_Validator
|
||||||
validator.pubkey = PubKey0x db.get(
|
validator.pubkey = PubKey0x db.get(
|
||||||
subkey(kValidator, i),
|
subkey(kValidator, i),
|
||||||
ValidatorPubKey
|
PubKeyBytes
|
||||||
).unsafeGet()
|
).unsafeGet()
|
||||||
|
|
||||||
let valID = validator.pubkey.ValidatorPubKey.toRaw()
|
template valID: untyped = PubKeyBytes validator.pubkey
|
||||||
|
let ll = db.get(
|
||||||
|
subkey(kLinkedListMeta, valID),
|
||||||
|
KeysEpochs
|
||||||
|
).unsafeGet()
|
||||||
|
|
||||||
|
# Create a fake block with the highest slot seen
|
||||||
|
# to prevent all signing from lower slots
|
||||||
|
if ll.blockSlots.isInit:
|
||||||
|
validator.signed_blocks.add SPDIR_SignedBlock(
|
||||||
|
slot: SlotString ll.blockSlots.stop
|
||||||
|
# signing_root - empty
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a fake attestation with the highest epochs seen
|
||||||
|
# to prevent all signing from lower epochs.
|
||||||
|
# In reality, the max source epoch and max target epochs
|
||||||
|
# may be from different attestations.
|
||||||
|
if ll.targetEpochs.isInit:
|
||||||
|
validator.signed_attestations.add SPDIR_SignedAttestation(
|
||||||
|
source_epoch: EpochString ll.sourceEpochs.stop,
|
||||||
|
target_epoch: EpochString ll.targetEpochs.stop,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update extract without reallocating seqs
|
||||||
|
# by manually transferring ownership
|
||||||
|
result.data.setLen(result.data.len + 1)
|
||||||
|
shallowCopy(result.data[^1], validator)
|
||||||
|
|
||||||
|
proc toSPDIR*(db: SlashingProtectionDB_v1): SPDIR
|
||||||
|
{.raises: [IOError, Defect].} =
|
||||||
|
## Export the full slashing protection database
|
||||||
|
## to the Nimbus Slashing Protection Database Intermediate Representation
|
||||||
|
##
|
||||||
|
## Note: this is slow due to how we implement range queries in a KV-store
|
||||||
|
result.metadata.interchange_format_version = "5"
|
||||||
|
|
||||||
|
result.metadata.genesis_validators_root = Eth2Digest0x db.get(
|
||||||
|
subkey(kGenesisValidatorsRoot), ETH2Digest
|
||||||
|
# Bug in results.nim
|
||||||
|
# ).expect("Slashing Protection requires genesis_validators_root at init")
|
||||||
|
).unsafeGet()
|
||||||
|
|
||||||
|
let numValidators = db.get(
|
||||||
|
subkey(kNumValidators),
|
||||||
|
uint32
|
||||||
|
).get(otherwise = 0'u32)
|
||||||
|
|
||||||
|
for i in 0'u32 ..< numValidators:
|
||||||
|
var validator: SPDIR_Validator
|
||||||
|
validator.pubkey = PubKey0x db.get(
|
||||||
|
subkey(kValidator, i),
|
||||||
|
PubKeyBytes
|
||||||
|
).unsafeGet()
|
||||||
|
|
||||||
|
template valID: untyped = PubKeyBytes validator.pubkey
|
||||||
let ll = db.get(
|
let ll = db.get(
|
||||||
subkey(kLinkedListMeta, valID),
|
subkey(kLinkedListMeta, valID),
|
||||||
KeysEpochs
|
KeysEpochs
|
||||||
|
@ -985,8 +1071,8 @@ proc toSPDIF*(db: SlashingProtectionDB, path: string)
|
||||||
BlockNode
|
BlockNode
|
||||||
).unsafeGet()
|
).unsafeGet()
|
||||||
|
|
||||||
validator.signed_blocks.add SPDIF_SignedBlock(
|
validator.signed_blocks.add SPDIR_SignedBlock(
|
||||||
slot: curSlot,
|
slot: SlotString curSlot,
|
||||||
signing_root: Eth2Digest0x node.block_root
|
signing_root: Eth2Digest0x node.block_root
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -997,15 +1083,15 @@ proc toSPDIF*(db: SlashingProtectionDB, path: string)
|
||||||
|
|
||||||
if ll.targetEpochs.isInit:
|
if ll.targetEpochs.isInit:
|
||||||
var curEpoch = ll.targetEpochs.start
|
var curEpoch = ll.targetEpochs.start
|
||||||
var count = 0
|
|
||||||
while true:
|
while true:
|
||||||
let node = db.get(
|
let node = db.get(
|
||||||
subkey(kTargetEpoch, valID, curEpoch),
|
subkey(kTargetEpoch, valID, curEpoch),
|
||||||
TargetEpochNode
|
TargetEpochNode
|
||||||
).unsafeGet()
|
).unsafeGet()
|
||||||
|
|
||||||
validator.signed_attestations.add SPDIF_SignedAttestation(
|
validator.signed_attestations.add SPDIR_SignedAttestation(
|
||||||
source_epoch: node.source, target_epoch: curEpoch,
|
source_epoch: EpochString node.source,
|
||||||
|
target_epoch: EpochString curEpoch,
|
||||||
signing_root: Eth2Digest0x node.attestation_root
|
signing_root: Eth2Digest0x node.attestation_root
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1014,59 +1100,44 @@ proc toSPDIF*(db: SlashingProtectionDB, path: string)
|
||||||
else:
|
else:
|
||||||
curEpoch = node.next
|
curEpoch = node.next
|
||||||
|
|
||||||
inc count
|
|
||||||
doAssert count < 5
|
|
||||||
|
|
||||||
# Update extract without reallocating seqs
|
# Update extract without reallocating seqs
|
||||||
# by manually transferring ownership
|
# by manually transferring ownership
|
||||||
extract.data.setLen(extract.data.len + 1)
|
result.data.setLen(result.data.len + 1)
|
||||||
shallowCopy(extract.data[^1], validator)
|
shallowCopy(result.data[^1], validator)
|
||||||
|
|
||||||
Json.saveFile(path, extract, pretty = true)
|
proc inclSPDIR*(db: SlashingProtectionDB_v1, spdir: SPDIR): SlashingImportStatus
|
||||||
echo "Exported slashing protection DB to '", path, "'"
|
|
||||||
|
|
||||||
proc fromSPDIF*(db: SlashingProtectionDB, path: string): bool
|
|
||||||
{.raises: [SerializationError, IOError, Defect].} =
|
{.raises: [SerializationError, IOError, Defect].} =
|
||||||
## Import a (Complete) Slashing Protection Database Interchange Format
|
## Import a Slashing Protection Database Intermediate Representation
|
||||||
## file into the specified slahsing protection DB
|
## file into the specified slashing protection DB
|
||||||
##
|
##
|
||||||
## The database must be initialized.
|
## The database must be initialized.
|
||||||
## The genesis_validator_root must match or
|
## The genesis_validators_root must match or
|
||||||
## the DB must have a zero root
|
## the DB must have a zero root
|
||||||
|
|
||||||
let extract = Json.loadFile(path, SPDIF)
|
|
||||||
|
|
||||||
doAssert not db.isNil, "The Slashing Protection DB must be initialized."
|
doAssert not db.isNil, "The Slashing Protection DB must be initialized."
|
||||||
doAssert not db.backend.isNil, "The Slashing Protection DB must be initialized."
|
doAssert not db.backend.isNil, "The Slashing Protection DB must be initialized."
|
||||||
|
|
||||||
let dbGenValRoot = db.get(
|
let dbGenValRoot = db.get(
|
||||||
subkey(kGenesisValidatorRoot), ETH2Digest
|
subkey(kGenesisValidatorsRoot), ETH2Digest
|
||||||
).unsafeGet()
|
).unsafeGet()
|
||||||
|
|
||||||
if dbGenValRoot != default(Eth2Digest) and
|
if dbGenValRoot != default(Eth2Digest) and
|
||||||
dbGenValRoot != extract.metadata.genesis_validator_root.Eth2Digest:
|
dbGenValRoot != spdir.metadata.genesis_validators_root.Eth2Digest:
|
||||||
echo "The slashing protection database and imported file refer to different blockchains."
|
error "The slashing protection database and imported file refer to different blockchains.",
|
||||||
return false
|
DB_genesis_validators_root = dbGenValRoot,
|
||||||
|
Imported_genesis_validators_root = spdir.metadata.genesis_validators_root.Eth2Digest
|
||||||
|
return siFailure
|
||||||
|
|
||||||
if dbGenValRoot == default(Eth2Digest):
|
if dbGenValRoot == default(Eth2Digest):
|
||||||
db.put(
|
db.put(
|
||||||
subkey(kGenesisValidatorRoot),
|
subkey(kGenesisValidatorsRoot),
|
||||||
extract.metadata.genesis_validator_root.Eth2Digest
|
spdir.metadata.genesis_validators_root.Eth2Digest
|
||||||
)
|
)
|
||||||
|
|
||||||
for v in 0 ..< extract.data.len:
|
# Create a mutable copy for sorting
|
||||||
for b in 0 ..< extract.data[v].signed_blocks.len:
|
var spdir = spdir
|
||||||
db.registerBlock(
|
return db.importInterchangeV5Impl(spdir)
|
||||||
extract.data[v].pubkey.ValidatorPubKey,
|
|
||||||
extract.data[v].signed_blocks[b].slot,
|
|
||||||
extract.data[v].signed_blocks[b].signing_root.Eth2Digest
|
|
||||||
)
|
|
||||||
for a in 0 ..< extract.data[v].signed_attestations.len:
|
|
||||||
db.registerAttestation(
|
|
||||||
extract.data[v].pubkey.ValidatorPubKey,
|
|
||||||
extract.data[v].signed_attestations[a].source_epoch,
|
|
||||||
extract.data[v].signed_attestations[a].target_epoch,
|
|
||||||
extract.data[v].signed_attestations[a].signing_root.Eth2Digest
|
|
||||||
)
|
|
||||||
|
|
||||||
return true
|
# Sanity check
|
||||||
|
# --------------------------------------------------------------
|
||||||
|
|
||||||
|
static: doAssert SlashingProtectionDB_v1 is SlashingProtectionDB_Concept
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,44 @@
|
||||||
|
# beacon_chain
|
||||||
|
# Copyright (c) 2018-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.
|
||||||
|
|
||||||
|
# Import/export the validator slashing protection database
|
||||||
|
|
||||||
|
import
|
||||||
|
std/[os, strutils],
|
||||||
|
confutils,
|
||||||
|
eth/db/[kvstore, kvstore_sqlite3],
|
||||||
|
../beacon_chain/validator_protection/slashing_protection,
|
||||||
|
../beacon_chain/spec/digest
|
||||||
|
|
||||||
|
type
|
||||||
|
SlashProtCmd = enum
|
||||||
|
dump = "Dump the validator slashing protection DB to json"
|
||||||
|
restore = "Restore the validator slashing protection DB from json"
|
||||||
|
|
||||||
|
SlashProtConf = object
|
||||||
|
|
||||||
|
case cmd {.
|
||||||
|
command,
|
||||||
|
desc: "Dump database or restore" .}: SlashProtCmd
|
||||||
|
of dump, restore:
|
||||||
|
infile {.argument.}: string
|
||||||
|
outfile {.argument.}: string
|
||||||
|
|
||||||
|
proc doDump(conf: SlashProtConf) =
|
||||||
|
let (dir, file) = splitPath(conf.infile)
|
||||||
|
# TODO: Make it read-only https://github.com/status-im/nim-eth/issues/312
|
||||||
|
# TODO: why is sqlite3 always appending .sqlite3 ?
|
||||||
|
let filetrunc = file.changeFileExt("")
|
||||||
|
let db = SlashingProtectionDB.loadUnchecked(dir, filetrunc, readOnly = false)
|
||||||
|
db.exportSlashingInterchange(conf.outfile)
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
let conf = SlashProtConf.load()
|
||||||
|
|
||||||
|
case conf.cmd:
|
||||||
|
of dump: conf.doDump()
|
||||||
|
of restore: doAssert false, "unimplemented"
|
|
@ -37,4 +37,5 @@ fi
|
||||||
|
|
||||||
pushd "${SUBREPO_DIR}"
|
pushd "${SUBREPO_DIR}"
|
||||||
./download_test_vectors.sh
|
./download_test_vectors.sh
|
||||||
|
./download_slashing_interchange_tests.sh
|
||||||
popd
|
popd
|
||||||
|
|
|
@ -32,7 +32,8 @@ import # Unit test
|
||||||
./test_zero_signature,
|
./test_zero_signature,
|
||||||
./fork_choice/tests_fork_choice,
|
./fork_choice/tests_fork_choice,
|
||||||
./slashing_protection/test_slashing_interchange,
|
./slashing_protection/test_slashing_interchange,
|
||||||
./slashing_protection/test_slashing_protection_db
|
./slashing_protection/test_slashing_protection_db,
|
||||||
|
./slashing_protection/test_migration
|
||||||
|
|
||||||
import # Refactor state transition unit tests
|
import # Refactor state transition unit tests
|
||||||
# In mainnet these take 2 minutes and are empty TODOs
|
# In mainnet these take 2 minutes and are empty TODOs
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
{.used.}
|
||||||
|
|
||||||
|
import
|
||||||
|
# Standard library
|
||||||
|
std/[unittest, os],
|
||||||
|
# Status lib
|
||||||
|
eth/db/kvstore,
|
||||||
|
stew/results,
|
||||||
|
nimcrypto/utils,
|
||||||
|
serialization,
|
||||||
|
json_serialization,
|
||||||
|
# Internal
|
||||||
|
../../beacon_chain/validator_protection/[
|
||||||
|
slashing_protection,
|
||||||
|
slashing_protection_v1,
|
||||||
|
slashing_protection_v2
|
||||||
|
],
|
||||||
|
../../beacon_chain/spec/[datatypes, digest, crypto, presets],
|
||||||
|
# Test utilies
|
||||||
|
../testutil
|
||||||
|
|
||||||
|
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()
|
||||||
|
result.blob[0 ..< 8] = (1'u64 shl 48 + index.uint64).toBytesBE()
|
||||||
|
|
||||||
|
func hexToDigest(hex: string): Eth2Digest =
|
||||||
|
result = Eth2Digest.fromHex(hex)
|
||||||
|
|
||||||
|
proc sqlite3db_delete(basepath, dbname: string) =
|
||||||
|
removeFile(basepath / dbname&".sqlite3-shm")
|
||||||
|
removeFile(basepath / dbname&".sqlite3-wal")
|
||||||
|
removeFile(basepath / dbname&".sqlite3")
|
||||||
|
|
||||||
|
const TestDir = ""
|
||||||
|
const TestDbName = "t_slashprot_migration"
|
||||||
|
|
||||||
|
suiteReport "Slashing Protection DB - v1 and v2 migration" & preset():
|
||||||
|
# https://eips.ethereum.org/EIPS/eip-3076
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
|
wrappedTimedTest "Minimal format migration" & preset():
|
||||||
|
let genesis_validators_root = hexToDigest"0x04700007fabc8282644aed6d1c7c9e21d38a03a0c4ba193f3afe428824b3a673"
|
||||||
|
block: # export from a v1 DB
|
||||||
|
let db = SlashingProtectionDB_v1.init(
|
||||||
|
genesis_validators_root,
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
|
||||||
|
let pubkey = ValidatorPubKey
|
||||||
|
.fromHex"0xb845089a1457f811bfc000588fbb4e713669be8ce060ea6be3c6ece09afc3794106c91ca73acda5e5457122d58723bed"
|
||||||
|
.get()
|
||||||
|
db.registerBlock(
|
||||||
|
pubkey,
|
||||||
|
Slot 81952,
|
||||||
|
Eth2Digest()
|
||||||
|
)
|
||||||
|
|
||||||
|
db.registerAttestation(
|
||||||
|
pubkey,
|
||||||
|
source = Epoch 2290,
|
||||||
|
target = Epoch 3007,
|
||||||
|
Eth2Digest()
|
||||||
|
)
|
||||||
|
|
||||||
|
let spdir = db.toSPDIR_lowWatermark()
|
||||||
|
Json.saveFile(
|
||||||
|
currentSourcePath.parentDir/"t_migration_slashing_protection_v1.json",
|
||||||
|
spdir,
|
||||||
|
pretty = true
|
||||||
|
)
|
||||||
|
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
block: # Reopen as the new version
|
||||||
|
let db = SlashingProtectionDB.init(
|
||||||
|
genesis_validators_root,
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that v2 as been initialized (private field :/)
|
||||||
|
# doAssert: db.db_v2.getMetadataTable_DbV2().get() == genesis_validators_root
|
||||||
|
|
||||||
|
db.exportSlashingInterchange(
|
||||||
|
currentSourcePath.parentDir/"t_migration_slashing_protection_migrated.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
doAssert sameFileContent(
|
||||||
|
currentSourcePath.parentDir/"t_migration_slashing_protection_v1.json",
|
||||||
|
currentSourcePath.parentDir/"t_migration_slashing_protection_migrated.json"
|
||||||
|
)
|
|
@ -0,0 +1,216 @@
|
||||||
|
# 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
|
||||||
|
stew/[results, byteutils],
|
||||||
|
nimcrypto/utils,
|
||||||
|
chronicles,
|
||||||
|
# Internal
|
||||||
|
../../beacon_chain/validator_protection/slashing_protection,
|
||||||
|
../../beacon_chain/spec/[datatypes, digest, crypto, presets],
|
||||||
|
# Test utilies
|
||||||
|
../testutil,
|
||||||
|
../official/fixtures_utils
|
||||||
|
|
||||||
|
type
|
||||||
|
TestInterchange = object
|
||||||
|
name: string
|
||||||
|
## Name of the test case
|
||||||
|
genesis_validators_root: Eth2Digest0x
|
||||||
|
## Genesis validator root to use when creating the empty DB
|
||||||
|
## or to compare the import against
|
||||||
|
steps: seq[TestStep]
|
||||||
|
|
||||||
|
TestStep = object
|
||||||
|
should_succeed: bool
|
||||||
|
## Is "interchange" given a valid import
|
||||||
|
allow_partial_import: bool
|
||||||
|
## Does "interchange" contain slashable data either as standalone
|
||||||
|
## or with regards to previous steps
|
||||||
|
interchange: SPDIR
|
||||||
|
blocks: seq[CandidateBlock]
|
||||||
|
## Blocks to try as proposer after DB is imported
|
||||||
|
attestations: seq[CandidateVote]
|
||||||
|
## Attestations to try as validator after DB is imported
|
||||||
|
|
||||||
|
CandidateBlock = object
|
||||||
|
pubkey: PubKey0x
|
||||||
|
slot: SlotString
|
||||||
|
signing_root: Eth2Digest0x
|
||||||
|
should_succeed: bool
|
||||||
|
|
||||||
|
CandidateVote = object
|
||||||
|
pubkey: PubKey0x
|
||||||
|
source_epoch: EpochString
|
||||||
|
target_epoch: EpochString
|
||||||
|
signing_root: Eth2Digest0x
|
||||||
|
should_succeed: bool
|
||||||
|
|
||||||
|
func toHexLogs(v: CandidateBlock): auto =
|
||||||
|
(
|
||||||
|
pubkey: v.pubkey.PubKeyBytes.toHex(),
|
||||||
|
slot: $v.slot.Slot.shortLog(),
|
||||||
|
signing_root: v.signing_root.Eth2Digest.data.toHex(),
|
||||||
|
should_succeed: v.should_succeed
|
||||||
|
)
|
||||||
|
func toHexLogs(v: CandidateVote): auto =
|
||||||
|
(
|
||||||
|
pubkey: v.pubkey.PubKeyBytes.toHex(),
|
||||||
|
source_epoch: v.source_epoch.Epoch.shortLog(),
|
||||||
|
target_epoch: v.target_epoch.Epoch.shortLog(),
|
||||||
|
signing_root: v.signing_root.Eth2Digest.data.toHex(),
|
||||||
|
should_succeed: v.should_succeed
|
||||||
|
)
|
||||||
|
|
||||||
|
chronicles.formatIt CandidateBlock: it.toHexLogs
|
||||||
|
chronicles.formatIt CandidateVote: it.toHexLogs
|
||||||
|
|
||||||
|
proc sqlite3db_delete(basepath, dbname: string) =
|
||||||
|
removeFile(basepath/ dbname&".sqlite3-shm")
|
||||||
|
removeFile(basepath/ dbname&".sqlite3-wal")
|
||||||
|
removeFile(basepath/ dbname&".sqlite3")
|
||||||
|
|
||||||
|
const InterchangeTestsDir = FixturesDir / "tests-slashing-v5.0.0" / "generated"
|
||||||
|
const TestDir = ""
|
||||||
|
const TestDbPrefix = "test_slashprot_"
|
||||||
|
|
||||||
|
proc statusOkOrDuplicateOrMinSlotViolation(
|
||||||
|
status: Result[void, BadProposal], candidate: CandidateBlock): bool =
|
||||||
|
# 1. We might be importing a duplicate which EIP-3076 allows
|
||||||
|
# there is no reason during normal operation to integrate
|
||||||
|
# a duplicate so checkSlashableBlockProposal would have rejected it.
|
||||||
|
# 2. The last test "multiple_interchanges_single_validator_single_message_gap"
|
||||||
|
# requires implementing pruning in-between import to keep the
|
||||||
|
# MinSlotViolation check relevant.
|
||||||
|
# That check prevents duplicate because it doesn't keep history.
|
||||||
|
#
|
||||||
|
# We need to special-case those exceptions to pass all tests
|
||||||
|
if status.isOk:
|
||||||
|
return true
|
||||||
|
if status.error.kind == DoubleProposal and
|
||||||
|
candidate.signing_root.Eth2Digest != Eth2Digest() and
|
||||||
|
status.error.existingBlock == candidate.signing_root.Eth2Digest:
|
||||||
|
warn "Block already exists in the DB",
|
||||||
|
candidateBlock = candidate
|
||||||
|
return true
|
||||||
|
elif status.error.kind == MinSlotViolation:
|
||||||
|
# Note: we tested the codepath without pruning.
|
||||||
|
# Furthermore it's better to be to eager on MinSlotViolation
|
||||||
|
# than allow slashing (unless the MinSlot is too far in the future)
|
||||||
|
warn "Block violates low watermark requirement. It's likely a duplicate though.",
|
||||||
|
candidateBlock = candidate,
|
||||||
|
error = status.error
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
proc statusOkOrDuplicateOrMinEpochViolation(
|
||||||
|
status: Result[void, BadVote], candidate: CandidateVote): bool =
|
||||||
|
# We might be importing a duplicate which EIP-3076 allows
|
||||||
|
# there is no reason during normal operation to integrate
|
||||||
|
# a duplicate so checkSlashableAttestation would have rejected it.
|
||||||
|
# We special-case that for imports.
|
||||||
|
if status.isOk:
|
||||||
|
return true
|
||||||
|
if status.error.kind == DoubleVote and
|
||||||
|
candidate.signing_root.Eth2Digest != Eth2Digest() and
|
||||||
|
status.error.existingAttestation == candidate.signing_root.Eth2Digest:
|
||||||
|
warn "Attestation already exists in the DB",
|
||||||
|
candidateAttestation = candidate
|
||||||
|
return true
|
||||||
|
elif status.error.kind in {MinSourceViolation, MinTargetViolation}:
|
||||||
|
# Note: we tested the codepath without pruning.
|
||||||
|
# Furthermore it's better to be to eager on MinSlotViolation
|
||||||
|
# than allow slashing (unless the MinSlot is too far in the future)
|
||||||
|
warn "Attestation violates low watermark requirement. It's likely a duplicate though.",
|
||||||
|
candidateAttestation = candidate,
|
||||||
|
error = status.error
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
proc runTest(identifier: string) =
|
||||||
|
|
||||||
|
# The tests produce a lot of log noise
|
||||||
|
echo "\n\n===========================================\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
let testCase = InterchangeTestsDir / identifier
|
||||||
|
timedTest "Slashing test: " & identifier:
|
||||||
|
let t = parseTest(InterchangeTestsDir/identifier, Json, TestInterchange)
|
||||||
|
|
||||||
|
# Create a test specific DB
|
||||||
|
let dbname = TestDbPrefix & identifier.changeFileExt("")
|
||||||
|
|
||||||
|
# Delete existing db in case of previous test failure
|
||||||
|
sqlite3db_delete(TestDir, dbname)
|
||||||
|
|
||||||
|
let db = SlashingProtectionDB.init(
|
||||||
|
Eth2Digest t.genesis_validators_root,
|
||||||
|
TestDir,
|
||||||
|
dbname
|
||||||
|
)
|
||||||
|
# We don't use defer to auto-close+delete the DB
|
||||||
|
# as in case of issue we want to keep the DB around for investigation.
|
||||||
|
|
||||||
|
for step in t.steps:
|
||||||
|
let status = db.inclSPDIR(step.interchange)
|
||||||
|
if not step.should_succeed:
|
||||||
|
doAssert siFailure == status,
|
||||||
|
"Unexpected error:\n" &
|
||||||
|
" " & $status & "\n"
|
||||||
|
elif step.allow_partial_import:
|
||||||
|
doAssert siPartial == status,
|
||||||
|
"Unexpected error:\n" &
|
||||||
|
" " & $status & "\n"
|
||||||
|
else:
|
||||||
|
doAssert siSuccess == status,
|
||||||
|
"Unexpected error:\n" &
|
||||||
|
" " & $status & "\n"
|
||||||
|
|
||||||
|
for blck in step.blocks:
|
||||||
|
let status = db.checkSlashableBlockProposal(
|
||||||
|
ValidatorPubKey.fromRaw(blck.pubkey.PubKeyBytes).get(),
|
||||||
|
Slot blck.slot
|
||||||
|
)
|
||||||
|
if blck.should_succeed:
|
||||||
|
doAssert status.statusOkOrDuplicateOrMinSlotViolation(blck),
|
||||||
|
"Unexpected error:\n" &
|
||||||
|
" " & $status & "\n" &
|
||||||
|
" for " & $toHexLogs(blck)
|
||||||
|
else:
|
||||||
|
doAssert status.isErr(),
|
||||||
|
"Unexpected success:\n" &
|
||||||
|
" " & $status & "\n" &
|
||||||
|
" for " & $toHexLogs(blck)
|
||||||
|
|
||||||
|
for att in step.attestations:
|
||||||
|
let status = db.checkSlashableAttestation(
|
||||||
|
ValidatorPubKey.fromRaw(att.pubkey.PubKeyBytes).get(),
|
||||||
|
Epoch att.source_epoch,
|
||||||
|
Epoch att.target_epoch
|
||||||
|
)
|
||||||
|
if att.should_succeed:
|
||||||
|
doAssert status.statusOkOrDuplicateOrMinEpochViolation(att),
|
||||||
|
"Unexpected error:\n" &
|
||||||
|
" " & $status & "\n" &
|
||||||
|
" for " & $toHexLogs(att)
|
||||||
|
else:
|
||||||
|
doAssert status.isErr(),
|
||||||
|
"Unexpected success:\n" &
|
||||||
|
" " & $status & "\n" &
|
||||||
|
" for " & $toHexLogs(att)
|
||||||
|
|
||||||
|
# Now close and delete resources.
|
||||||
|
db.close()
|
||||||
|
sqlite3db_delete(TestDir, dbname)
|
||||||
|
|
||||||
|
|
||||||
|
suiteReport "Slashing Interchange tests " & preset():
|
||||||
|
for kind, path in walkDir(InterchangeTestsDir, true):
|
||||||
|
runTest(path)
|
|
@ -15,7 +15,7 @@ import
|
||||||
stew/results,
|
stew/results,
|
||||||
nimcrypto/utils,
|
nimcrypto/utils,
|
||||||
# Internal
|
# Internal
|
||||||
../../beacon_chain/validator_slashing_protection,
|
../../beacon_chain/validator_protection/slashing_protection,
|
||||||
../../beacon_chain/spec/[datatypes, digest, crypto, presets],
|
../../beacon_chain/spec/[datatypes, digest, crypto, presets],
|
||||||
# Test utilies
|
# Test utilies
|
||||||
../testutil
|
../testutil
|
||||||
|
@ -42,12 +42,30 @@ func fakeValidator(index: SomeInteger): ValidatorPubKey =
|
||||||
func hexToDigest(hex: string): Eth2Digest =
|
func hexToDigest(hex: string): Eth2Digest =
|
||||||
result = Eth2Digest.fromHex(hex)
|
result = Eth2Digest.fromHex(hex)
|
||||||
|
|
||||||
|
proc sqlite3db_delete(basepath, dbname: string) =
|
||||||
|
removeFile(basepath / dbname&".sqlite3-shm")
|
||||||
|
removeFile(basepath / dbname&".sqlite3-wal")
|
||||||
|
removeFile(basepath / dbname&".sqlite3")
|
||||||
|
|
||||||
|
const TestDir = ""
|
||||||
|
const TestDbName = "test_slashprot"
|
||||||
|
|
||||||
suiteReport "Slashing Protection DB - Interchange" & preset():
|
suiteReport "Slashing Protection DB - Interchange" & preset():
|
||||||
# https://hackmd.io/@sproul/Bk0Y0qdGD#Format-1-Complete
|
# https://hackmd.io/@sproul/Bk0Y0qdGD#Format-1-Complete
|
||||||
|
# https://eips.ethereum.org/EIPS/eip-3076
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
wrappedTimedTest "Smoke test - Complete format" & preset():
|
wrappedTimedTest "Smoke test - Complete format" & preset():
|
||||||
let genesis_validators_root = hexToDigest"0x04700007fabc8282644aed6d1c7c9e21d38a03a0c4ba193f3afe428824b3a673"
|
let genesis_validators_root = hexToDigest"0x04700007fabc8282644aed6d1c7c9e21d38a03a0c4ba193f3afe428824b3a673"
|
||||||
block: # export
|
block: # export
|
||||||
let db = SlashingProtectionDB.init(genesis_validators_root, kvStore MemStoreRef.init())
|
let db = SlashingProtectionDB.init(
|
||||||
|
genesis_validators_root,
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
let pubkey = ValidatorPubKey
|
let pubkey = ValidatorPubKey
|
||||||
.fromHex"0xb845089a1457f811bfc000588fbb4e713669be8ce060ea6be3c6ece09afc3794106c91ca73acda5e5457122d58723bed"
|
.fromHex"0xb845089a1457f811bfc000588fbb4e713669be8ce060ea6be3c6ece09afc3794106c91ca73acda5e5457122d58723bed"
|
||||||
|
@ -76,22 +94,44 @@ suiteReport "Slashing Protection DB - Interchange" & preset():
|
||||||
fakeRoot(65535)
|
fakeRoot(65535)
|
||||||
)
|
)
|
||||||
|
|
||||||
db.toSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")
|
db.exportSlashingInterchange(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")
|
||||||
|
|
||||||
block: # import - zero root db
|
block: # import - zero root db
|
||||||
let db2 = SlashingProtectionDB.init(Eth2Digest(), kvStore MemStoreRef.init())
|
let db2 = SlashingProtectionDB.init(
|
||||||
|
Eth2Digest(),
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db2.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
doAssert db2.fromSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")
|
doAssert siSuccess == db2.importSlashingInterchange(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")
|
||||||
db2.toSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection_roundtrip1.json")
|
db2.exportSlashingInterchange(currentSourcePath.parentDir/"test_complete_export_slashing_protection_roundtrip1.json")
|
||||||
|
|
||||||
block: # import - same root db
|
block: # import - same root db
|
||||||
let db3 = SlashingProtectionDB.init(genesis_validators_root, kvStore MemStoreRef.init())
|
let db3 = SlashingProtectionDB.init(
|
||||||
|
genesis_validators_root,
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db3.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
doAssert db3.fromSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")
|
doAssert siSuccess == db3.importSlashingInterchange(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")
|
||||||
db3.toSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection_roundtrip2.json")
|
db3.exportSlashingInterchange(currentSourcePath.parentDir/"test_complete_export_slashing_protection_roundtrip2.json")
|
||||||
|
|
||||||
|
wrappedTimedTest "Smoke test - Complete format - Invalid database is refused" & preset():
|
||||||
block: # import - invalid root db
|
block: # import - invalid root db
|
||||||
let invalid_genvalroot = hexToDigest"0x1234"
|
let invalid_genvalroot = hexToDigest"0x1234"
|
||||||
let db3 = SlashingProtectionDB.init(invalid_genvalroot, kvStore MemStoreRef.init())
|
let db4 = SlashingProtectionDB.init(
|
||||||
|
invalid_genvalroot,
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db4.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
doAssert not db3.fromSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")
|
doAssert siFailure == db4.importSlashingInterchange(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")
|
||||||
|
|
|
@ -9,12 +9,12 @@
|
||||||
|
|
||||||
import
|
import
|
||||||
# Standard library
|
# Standard library
|
||||||
std/unittest,
|
std/[unittest, os],
|
||||||
# Status lib
|
# Status lib
|
||||||
eth/db/kvstore,
|
eth/db/kvstore,
|
||||||
stew/results,
|
stew/results,
|
||||||
# Internal
|
# Internal
|
||||||
../../beacon_chain/validator_slashing_protection,
|
../../beacon_chain/validator_protection/slashing_protection,
|
||||||
../../beacon_chain/spec/[datatypes, digest, crypto, presets],
|
../../beacon_chain/spec/[datatypes, digest, crypto, presets],
|
||||||
# Test utilies
|
# Test utilies
|
||||||
../testutil
|
../testutil
|
||||||
|
@ -38,9 +38,35 @@ func fakeValidator(index: SomeInteger): ValidatorPubKey =
|
||||||
result = ValidatorPubKey()
|
result = ValidatorPubKey()
|
||||||
result.blob[0 ..< 8] = (1'u64 shl 48 + index.uint64).toBytesBE()
|
result.blob[0 ..< 8] = (1'u64 shl 48 + index.uint64).toBytesBE()
|
||||||
|
|
||||||
|
proc sqlite3db_delete(basepath, dbname: string) =
|
||||||
|
removeFile(basepath / dbname&".sqlite3-shm")
|
||||||
|
removeFile(basepath / dbname&".sqlite3-wal")
|
||||||
|
removeFile(basepath / dbname&".sqlite3")
|
||||||
|
|
||||||
|
const TestDir = ""
|
||||||
|
const TestDbName = "test_slashprot"
|
||||||
|
|
||||||
|
# Reminder of SQLite constraints for fake data:
|
||||||
|
# attestations:
|
||||||
|
# - all fields are NOT NULL
|
||||||
|
# - attestation_root is unique
|
||||||
|
# - (validator_id, target_epoch)
|
||||||
|
# blocks:
|
||||||
|
# - all fields are NOT NULL
|
||||||
|
# - block_root is unique
|
||||||
|
# - (validator_id, slot)
|
||||||
|
|
||||||
suiteReport "Slashing Protection DB" & preset():
|
suiteReport "Slashing Protection DB" & preset():
|
||||||
wrappedTimedTest "Empty database" & preset():
|
wrappedTimedTest "Empty database" & preset():
|
||||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
let db = SlashingProtectionDB.init(
|
||||||
|
default(Eth2Digest),
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
db.checkSlashableBlockProposal(
|
db.checkSlashableBlockProposal(
|
||||||
|
@ -58,10 +84,16 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
target = Epoch 1
|
target = Epoch 1
|
||||||
).error.kind == TargetPrecedesSource
|
).error.kind == TargetPrecedesSource
|
||||||
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
wrappedTimedTest "SP for block proposal - linear append":
|
wrappedTimedTest "SP for block proposal - linear append":
|
||||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
let db = SlashingProtectionDB.init(
|
||||||
|
default(Eth2Digest),
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
db.registerBlock(
|
db.registerBlock(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
|
@ -80,11 +112,6 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
slot = Slot 10
|
slot = Slot 10
|
||||||
).isErr()
|
).isErr()
|
||||||
# Slot occupied by another validator
|
# Slot occupied by another validator
|
||||||
db.checkSlashableBlockProposal(
|
|
||||||
fakeValidator(111),
|
|
||||||
slot = Slot 10
|
|
||||||
).isOk()
|
|
||||||
# Slot occupied by another validator
|
|
||||||
db.checkSlashableBlockProposal(
|
db.checkSlashableBlockProposal(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
slot = Slot 15
|
slot = Slot 15
|
||||||
|
@ -115,7 +142,15 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
).isErr()
|
).isErr()
|
||||||
|
|
||||||
wrappedTimedTest "SP for block proposal - backtracking append":
|
wrappedTimedTest "SP for block proposal - backtracking append":
|
||||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
let db = SlashingProtectionDB.init(
|
||||||
|
default(Eth2Digest),
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
# last finalized block
|
# last finalized block
|
||||||
db.registerBlock(
|
db.registerBlock(
|
||||||
|
@ -135,36 +170,37 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
fakeRoot(20)
|
fakeRoot(20)
|
||||||
)
|
)
|
||||||
for i in 0 ..< 30:
|
for i in 0 ..< 30:
|
||||||
if i notin {10, 20}:
|
if i > 10 and i != 20: # MinSlotViolation and DupSlot
|
||||||
check:
|
let status = db.checkSlashableBlockProposal(
|
||||||
db.checkSlashableBlockProposal(
|
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Slot i
|
Slot i
|
||||||
).isOk()
|
)
|
||||||
|
doAssert status.isOk, "error: " & $status
|
||||||
else:
|
else:
|
||||||
check:
|
let status = db.checkSlashableBlockProposal(
|
||||||
db.checkSlashableBlockProposal(
|
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Slot i
|
Slot i
|
||||||
).isErr()
|
)
|
||||||
|
doAssert status.isErr, "error: " & $status
|
||||||
db.registerBlock(
|
db.registerBlock(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Slot 15,
|
Slot 15,
|
||||||
fakeRoot(15)
|
fakeRoot(15)
|
||||||
)
|
)
|
||||||
for i in 0 ..< 30:
|
for i in 0 ..< 30:
|
||||||
if i notin {10, 15, 20}:
|
if i > 10 and i notin {15, 20}: # MinSlotViolation and DupSlot
|
||||||
check:
|
let status = db.checkSlashableBlockProposal(
|
||||||
db.checkSlashableBlockProposal(
|
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Slot i
|
Slot i
|
||||||
).isOk()
|
)
|
||||||
|
doAssert status.isOk, "error: " & $status
|
||||||
else:
|
else:
|
||||||
check:
|
let status = db.checkSlashableBlockProposal(
|
||||||
db.checkSlashableBlockProposal(
|
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Slot i
|
Slot i
|
||||||
).isErr()
|
)
|
||||||
|
doAssert status.isErr, "error: " & $status
|
||||||
|
check:
|
||||||
db.checkSlashableBlockProposal(
|
db.checkSlashableBlockProposal(
|
||||||
fakeValidator(0xDEADBEEF),
|
fakeValidator(0xDEADBEEF),
|
||||||
Slot i
|
Slot i
|
||||||
|
@ -180,50 +216,19 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
fakeRoot(17)
|
fakeRoot(17)
|
||||||
)
|
)
|
||||||
for i in 0 ..< 30:
|
for i in 0 ..< 30:
|
||||||
if i notin {10, 12, 15, 17, 20}:
|
if i > 10 and i notin {12, 15, 17, 20}:
|
||||||
check:
|
let status = db.checkSlashableBlockProposal(
|
||||||
db.checkSlashableBlockProposal(
|
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Slot i
|
Slot i
|
||||||
).isOk()
|
)
|
||||||
|
doAssert status.isOk, "error: " & $status
|
||||||
else:
|
else:
|
||||||
check:
|
let status = db.checkSlashableBlockProposal(
|
||||||
db.checkSlashableBlockProposal(
|
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Slot i
|
Slot i
|
||||||
).isErr()
|
|
||||||
db.checkSlashableBlockProposal(
|
|
||||||
fakeValidator(0xDEADBEEF),
|
|
||||||
Slot i
|
|
||||||
).isOk()
|
|
||||||
db.registerBlock(
|
|
||||||
fakeValidator(100),
|
|
||||||
Slot 9,
|
|
||||||
fakeRoot(9)
|
|
||||||
)
|
)
|
||||||
db.registerBlock(
|
doAssert status.isErr, "error: " & $status
|
||||||
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:
|
check:
|
||||||
db.checkSlashableBlockProposal(
|
|
||||||
fakeValidator(100),
|
|
||||||
Slot i
|
|
||||||
).isOk()
|
|
||||||
else:
|
|
||||||
check:
|
|
||||||
db.checkSlashableBlockProposal(
|
|
||||||
fakeValidator(100),
|
|
||||||
Slot i
|
|
||||||
).isErr()
|
|
||||||
db.checkSlashableBlockProposal(
|
db.checkSlashableBlockProposal(
|
||||||
fakeValidator(0xDEADBEEF),
|
fakeValidator(0xDEADBEEF),
|
||||||
Slot i
|
Slot i
|
||||||
|
@ -233,31 +238,35 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
Slot 29,
|
Slot 29,
|
||||||
fakeRoot(29)
|
fakeRoot(29)
|
||||||
)
|
)
|
||||||
db.registerBlock(
|
|
||||||
fakeValidator(100),
|
|
||||||
Slot 2,
|
|
||||||
fakeRoot(2)
|
|
||||||
)
|
|
||||||
for i in 0 ..< 30:
|
for i in 0 ..< 30:
|
||||||
if i notin {1, 2, 3, 9, 10, 12, 15, 17, 20, 29}:
|
if i > 10 and i notin {12, 15, 17, 20, 29}:
|
||||||
check:
|
let status = db.checkSlashableBlockProposal(
|
||||||
db.checkSlashableBlockProposal(
|
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Slot i
|
Slot i
|
||||||
).isOk()
|
)
|
||||||
|
doAssert status.isOk, "error: " & $status
|
||||||
else:
|
else:
|
||||||
check:
|
let status = db.checkSlashableBlockProposal(
|
||||||
db.checkSlashableBlockProposal(
|
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Slot i
|
Slot i
|
||||||
).isErr()
|
)
|
||||||
|
doAssert status.isErr, "error: " & $status
|
||||||
|
check:
|
||||||
db.checkSlashableBlockProposal(
|
db.checkSlashableBlockProposal(
|
||||||
fakeValidator(0xDEADBEEF),
|
fakeValidator(0xDEADBEEF),
|
||||||
Slot i
|
Slot i
|
||||||
).isOk()
|
).isOk()
|
||||||
|
|
||||||
wrappedTimedTest "SP for same epoch attestation target - linear append":
|
wrappedTimedTest "SP for same epoch attestation target - linear append":
|
||||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
let db = SlashingProtectionDB.init(
|
||||||
|
default(Eth2Digest),
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
db.registerAttestation(
|
db.registerAttestation(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
|
@ -276,11 +285,6 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
Epoch 0, Epoch 10,
|
Epoch 0, Epoch 10,
|
||||||
).error.kind == DoubleVote
|
).error.kind == DoubleVote
|
||||||
# Epoch occupied by another validator
|
# Epoch occupied by another validator
|
||||||
db.checkSlashableAttestation(
|
|
||||||
fakeValidator(111),
|
|
||||||
Epoch 0, Epoch 10
|
|
||||||
).isOk()
|
|
||||||
# Epoch occupied by another validator
|
|
||||||
db.checkSlashableAttestation(
|
db.checkSlashableAttestation(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Epoch 0, Epoch 15
|
Epoch 0, Epoch 15
|
||||||
|
@ -310,155 +314,17 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
Epoch 0, Epoch 20
|
Epoch 0, Epoch 20
|
||||||
).error.kind == DoubleVote
|
).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":
|
wrappedTimedTest "SP for surrounded attestations":
|
||||||
block:
|
block:
|
||||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
let db = SlashingProtectionDB.init(
|
||||||
|
default(Eth2Digest),
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
db.registerAttestation(
|
db.registerAttestation(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
|
@ -479,19 +345,22 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Epoch 11, Epoch 21
|
Epoch 11, Epoch 21
|
||||||
).isOk
|
).isOk
|
||||||
# TODO: is that possible?
|
|
||||||
db.checkSlashableAttestation(
|
|
||||||
fakeValidator(100),
|
|
||||||
Epoch 9, Epoch 19
|
|
||||||
).isOk
|
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
let db = SlashingProtectionDB.init(
|
||||||
|
default(Eth2Digest),
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
db.registerAttestation(
|
db.registerAttestation(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Epoch 0, Epoch 1,
|
Epoch 0, Epoch 1,
|
||||||
fakeRoot(0)
|
fakeRoot(1)
|
||||||
)
|
)
|
||||||
|
|
||||||
db.registerAttestation(
|
db.registerAttestation(
|
||||||
|
@ -522,7 +391,15 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
|
|
||||||
wrappedTimedTest "SP for surrounding attestations":
|
wrappedTimedTest "SP for surrounding attestations":
|
||||||
block:
|
block:
|
||||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
let db = SlashingProtectionDB.init(
|
||||||
|
default(Eth2Digest),
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
db.registerAttestation(
|
db.registerAttestation(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
|
@ -541,12 +418,20 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
).error.kind == SurroundingVote
|
).error.kind == SurroundingVote
|
||||||
|
|
||||||
block:
|
block:
|
||||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
let db = SlashingProtectionDB.init(
|
||||||
|
default(Eth2Digest),
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
db.registerAttestation(
|
db.registerAttestation(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Epoch 0, Epoch 1,
|
Epoch 0, Epoch 1,
|
||||||
fakeRoot(20)
|
fakeRoot(1)
|
||||||
)
|
)
|
||||||
|
|
||||||
db.registerAttestation(
|
db.registerAttestation(
|
||||||
|
@ -567,24 +452,32 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
|
|
||||||
wrappedTimedTest "Attestation ordering #1698":
|
wrappedTimedTest "Attestation ordering #1698":
|
||||||
block:
|
block:
|
||||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
let db = SlashingProtectionDB.init(
|
||||||
|
default(Eth2Digest),
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
db.registerAttestation(
|
db.registerAttestation(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Epoch 1, Epoch 2,
|
Epoch 1, Epoch 2,
|
||||||
fakeRoot(20)
|
fakeRoot(2)
|
||||||
)
|
)
|
||||||
|
|
||||||
db.registerAttestation(
|
db.registerAttestation(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Epoch 8, Epoch 10,
|
Epoch 8, Epoch 10,
|
||||||
fakeRoot(20)
|
fakeRoot(10)
|
||||||
)
|
)
|
||||||
|
|
||||||
db.registerAttestation(
|
db.registerAttestation(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Epoch 14, Epoch 15,
|
Epoch 14, Epoch 15,
|
||||||
fakeRoot(20)
|
fakeRoot(15)
|
||||||
)
|
)
|
||||||
|
|
||||||
# The current list is, 2 -> 10 -> 15
|
# The current list is, 2 -> 10 -> 15
|
||||||
|
@ -592,7 +485,7 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
db.registerAttestation(
|
db.registerAttestation(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Epoch 3, Epoch 6,
|
Epoch 3, Epoch 6,
|
||||||
fakeRoot(20)
|
fakeRoot(6)
|
||||||
)
|
)
|
||||||
|
|
||||||
# The current list is 2 -> 6 -> 10 -> 15
|
# The current list is 2 -> 6 -> 10 -> 15
|
||||||
|
@ -605,7 +498,15 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
|
|
||||||
wrappedTimedTest "Test valid attestation #1699":
|
wrappedTimedTest "Test valid attestation #1699":
|
||||||
block:
|
block:
|
||||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
let db = SlashingProtectionDB.init(
|
||||||
|
default(Eth2Digest),
|
||||||
|
TestDir,
|
||||||
|
TestDbName
|
||||||
|
)
|
||||||
|
defer:
|
||||||
|
db.close()
|
||||||
|
sqlite3db_delete(TestDir, TestDbName)
|
||||||
|
|
||||||
db.registerAttestation(
|
db.registerAttestation(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
|
@ -616,7 +517,7 @@ suiteReport "Slashing Protection DB" & preset():
|
||||||
db.registerAttestation(
|
db.registerAttestation(
|
||||||
fakeValidator(100),
|
fakeValidator(100),
|
||||||
Epoch 40, Epoch 50,
|
Epoch 40, Epoch 50,
|
||||||
fakeRoot(20)
|
fakeRoot(50)
|
||||||
)
|
)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 39966c051e0447d4ad6821d5a7646ad0f2aed842
|
Subproject commit 48fef39e8c1aa2aafc0a0b2847a9a05a62143ad2
|
|
@ -1 +1 @@
|
||||||
Subproject commit 1401c34374fe7606dbf1252e361725415890f5f0
|
Subproject commit 6d3e6a21caf4110d0d432b82f14e41e0271cd76b
|
Loading…
Reference in New Issue