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
|
||||
## Slashing Protection DB - Interchange [Preset: mainnet]
|
||||
```diff
|
||||
+ Smoke test - Complete format - Invalid database is refused [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
|
||||
## Slashing Protection DB [Preset: mainnet]
|
||||
```diff
|
||||
|
@ -203,13 +209,12 @@ OK: 1/1 Fail: 0/1 Skip: 0/1
|
|||
+ Empty database [Preset: mainnet] OK
|
||||
+ SP for block proposal - backtracking 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 surrounded attestations OK
|
||||
+ SP for surrounding attestations 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
|
||||
```diff
|
||||
+ 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
|
||||
|
||||
---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
|
||||
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
|
||||
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]""""
|
||||
|
|
|
@ -6,7 +6,7 @@ import
|
|||
spec/[datatypes, digest, crypto],
|
||||
block_pools/block_pools_types,
|
||||
fork_choice/fork_choice_types,
|
||||
validator_slashing_protection
|
||||
validator_protection/slashing_protection
|
||||
|
||||
from libp2p/protocols/pubsub/pubsub import ValidationResult
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ import
|
|||
eth1_monitor, version, ssz/merkleization,
|
||||
sync_protocol, request_manager, keystore_management, interop, statusbar,
|
||||
sync_manager, validator_duties, filepath,
|
||||
validator_slashing_protection, ./eth2_processor
|
||||
validator_protection/slashing_protection, ./eth2_processor
|
||||
|
||||
from eth/common/eth_types import BlockHashOrNumber
|
||||
|
||||
|
@ -324,7 +324,7 @@ proc init*(T: type BeaconNode,
|
|||
res.attachedValidators = ValidatorPool.init(
|
||||
SlashingProtectionDB.init(
|
||||
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,
|
||||
spec/eth2_apis/callsigs_types,
|
||||
eth2_json_rpc_serialization,
|
||||
validator_slashing_protection,
|
||||
validator_protection/slashing_protection,
|
||||
eth/db/[kvstore, kvstore_sqlite3]
|
||||
|
||||
logScope: topics = "vc"
|
||||
|
@ -314,7 +314,7 @@ programMain:
|
|||
vc.attachedValidators.slashingProtection =
|
||||
SlashingProtectionDB.init(
|
||||
vc.beaconGenesis.genesis_validators_root,
|
||||
kvStore SqStoreRef.init(config.validatorsDir(), "slashing_protection").tryGet()
|
||||
config.validatorsDir(), "slashing_protection"
|
||||
)
|
||||
|
||||
let
|
||||
|
|
|
@ -27,7 +27,7 @@ import
|
|||
./eth2_network, ./keystore_management, ./beacon_node_common,
|
||||
./beacon_node_types, ./nimbus_binary_common, ./eth1_monitor, ./version,
|
||||
./ssz/merkleization, ./attestation_aggregation, ./sync_manager, ./sszdump,
|
||||
./validator_slashing_protection
|
||||
./validator_protection/slashing_protection
|
||||
|
||||
# Metrics for tracking attestation and beacon block loss
|
||||
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],
|
||||
eth/db/[kvstore, kvstore_sqlite3],
|
||||
./spec/[datatypes, crypto, digest, signatures, helpers],
|
||||
./beacon_node_types, validator_slashing_protection
|
||||
./beacon_node_types, validator_protection/slashing_protection
|
||||
|
||||
declareGauge validators,
|
||||
"Number of validators attached to the beacon node"
|
||||
|
@ -12,7 +12,7 @@ declareGauge validators,
|
|||
func init*(T: type ValidatorPool,
|
||||
slashingProtectionDB: SlashingProtectionDB): T =
|
||||
## Initialize the validator pool and the slashing protection service
|
||||
## `genesis_validator_root` is used as an unique ID for the
|
||||
## `genesis_validators_root` is used as an unique ID for the
|
||||
## blockchain
|
||||
## `backend` is the KeyValue Store backend
|
||||
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
|
||||
# Standard library
|
||||
std/tables,
|
||||
std/[tables, os],
|
||||
# Status
|
||||
eth/db/kvstore,
|
||||
eth/db/[kvstore, kvstore_sqlite3],
|
||||
chronicles,
|
||||
nimcrypto/[hash, utils],
|
||||
serialization,
|
||||
json_serialization,
|
||||
# Internal
|
||||
./spec/[datatypes, digest, crypto],
|
||||
./ssz
|
||||
../spec/[datatypes, digest, crypto],
|
||||
../ssz,
|
||||
./slashing_protection_common
|
||||
|
||||
# Requirements
|
||||
# --------------------------------------------
|
||||
|
@ -136,38 +137,12 @@ import
|
|||
# as per-validator linked lists
|
||||
|
||||
type
|
||||
SlashingProtectionDB* = ref object
|
||||
SlashingProtectionDB_v1* = ref object
|
||||
## Database storing the blocks attested
|
||||
## by validators attached to a beacon node
|
||||
## or validator client.
|
||||
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
|
||||
# Using tuple instead of objects, crashes the Nim compiler
|
||||
# with SSZ serialization
|
||||
|
@ -191,7 +166,7 @@ type
|
|||
kTargetEpoch
|
||||
kLinkedListMeta
|
||||
# Interchange format
|
||||
kGenesisValidatorRoot
|
||||
kGenesisValidatorsRoot
|
||||
kNumValidators
|
||||
kValidator
|
||||
|
||||
|
@ -212,6 +187,9 @@ type
|
|||
## Portable between Miracl/BLST
|
||||
## and limits serialization/deserialization call
|
||||
|
||||
# Internal
|
||||
# -------------------------------------------------------------
|
||||
|
||||
{.push raises: [Defect].}
|
||||
logScope:
|
||||
topics = "antislash"
|
||||
|
@ -254,7 +232,7 @@ func subkey(
|
|||
result[1 .. ^1] = validator
|
||||
|
||||
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)
|
||||
|
||||
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[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(
|
||||
key,
|
||||
SSZ.encode(v)
|
||||
).expect("working database")
|
||||
|
||||
proc get(db: SlashingProtectionDB,
|
||||
proc rawGet(rawdb: KvStoreRef,
|
||||
key: openArray[byte],
|
||||
T: typedesc): Opt[T] =
|
||||
|
||||
|
@ -286,6 +264,8 @@ proc get(db: SlashingProtectionDB,
|
|||
sizeof(uint32)
|
||||
elif T is ValidatorPubKey:
|
||||
RawPubKeySize
|
||||
elif T is PubKeyBytes:
|
||||
RawPubKeySize
|
||||
else:
|
||||
{.error: "Invalid database node type: " & $T.}
|
||||
## SSZ serialization is packed
|
||||
|
@ -322,37 +302,113 @@ proc get(db: SlashingProtectionDB,
|
|||
expectedSize = ExpectedNodeSszSize
|
||||
discard
|
||||
|
||||
discard db.backend.get(key, decode).expect("working database")
|
||||
discard rawdb.get(key, decode).expect("working database")
|
||||
|
||||
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
|
||||
# "template WriterType(T: type SSZ): type"
|
||||
# by having a non-generic proc
|
||||
db.put(
|
||||
subkey(kGenesisValidatorRoot),
|
||||
genesis_validator_root
|
||||
subkey(kGenesisValidatorsRoot),
|
||||
genesis_validators_root
|
||||
)
|
||||
|
||||
proc init*(
|
||||
T: type SlashingProtectionDB,
|
||||
genesis_validator_root: Eth2Digest,
|
||||
backend: KVStoreRef): SlashingProtectionDB =
|
||||
result = T(backend: backend)
|
||||
result.setGenesis(genesis_validator_root)
|
||||
# DB Multiversioning
|
||||
# -------------------------------------------------------------
|
||||
|
||||
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()
|
||||
|
||||
# DB Queries
|
||||
# --------------------------------------------
|
||||
|
||||
proc checkSlashableBlockProposal*(
|
||||
db: SlashingProtectionDB,
|
||||
db: SlashingProtectionDB_v1,
|
||||
validator: ValidatorPubKey,
|
||||
slot: Slot
|
||||
): Result[void, Eth2Digest] =
|
||||
): Result[void, BadProposal] =
|
||||
## Returns an error if the specified validator
|
||||
## already proposed a block for the specified slot.
|
||||
## This would lead to slashing.
|
||||
|
@ -367,18 +423,21 @@ proc checkSlashableBlockProposal*(
|
|||
)
|
||||
if foundBlock.isNone():
|
||||
return ok()
|
||||
return err(foundBlock.unsafeGet().block_root)
|
||||
return err(BadProposal(
|
||||
kind: DoubleProposal,
|
||||
existing_block: foundBlock.unsafeGet().block_root
|
||||
))
|
||||
|
||||
proc checkSlashableAttestation*(
|
||||
db: SlashingProtectionDB,
|
||||
db: SlashingProtectionDB_v1,
|
||||
validator: ValidatorPubKey,
|
||||
source: Epoch,
|
||||
target: Epoch
|
||||
): Result[void, BadVote] =
|
||||
## 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
|
||||
## already voted for the specified slot
|
||||
## or would vote in a contradiction to previous votes
|
||||
## (surrounding vote or surrounded vote).
|
||||
##
|
||||
## Returns success otherwise
|
||||
# TODO distinct type for the result attestation root
|
||||
|
@ -428,7 +487,7 @@ proc checkSlashableAttestation*(
|
|||
# Chain reorg
|
||||
# Detect h(s2) < h(s1)
|
||||
# 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
|
||||
# ---------------------------------
|
||||
if source < ll.sourceEpochs.stop:
|
||||
|
@ -507,7 +566,7 @@ proc checkSlashableAttestation*(
|
|||
# DB update
|
||||
# --------------------------------------------
|
||||
|
||||
proc registerValidator(db: SlashingProtectionDB, validator: ValidatorPubKey) =
|
||||
proc registerValidator(db: SlashingProtectionDB_v1, validator: ValidatorPubKey) =
|
||||
## Add a new validator to the database
|
||||
## Assumes the validator does not exist
|
||||
let maybeNumVals = db.get(
|
||||
|
@ -524,7 +583,7 @@ proc registerValidator(db: SlashingProtectionDB, validator: ValidatorPubKey) =
|
|||
db.put(subkey(kValidator, valIndex), validator)
|
||||
|
||||
proc registerBlock*(
|
||||
db: SlashingProtectionDB,
|
||||
db: SlashingProtectionDB_v1,
|
||||
validator: ValidatorPubKey,
|
||||
slot: Slot, block_root: Eth2Digest) =
|
||||
## Add a block to the slashing protection DB
|
||||
|
@ -660,7 +719,7 @@ proc registerBlock*(
|
|||
).unsafeGet()
|
||||
|
||||
proc registerAttestation*(
|
||||
db: SlashingProtectionDB,
|
||||
db: SlashingProtectionDB_v1,
|
||||
validator: ValidatorPubKey,
|
||||
source, target: Epoch,
|
||||
attestation_root: Eth2Digest) =
|
||||
|
@ -812,7 +871,7 @@ proc registerAttestation*(
|
|||
# --------------------------------------------
|
||||
|
||||
proc dumpBlocks*(
|
||||
db: SlashingProtectionDB,
|
||||
db: SlashingProtectionDB_v1,
|
||||
validator: ValidatorPubKey
|
||||
): string =
|
||||
## Dump the linked list of blocks proposd by a validator in a string
|
||||
|
@ -847,7 +906,7 @@ proc dumpBlocks*(
|
|||
return $blocks
|
||||
|
||||
proc dumpAttestations*(
|
||||
db: SlashingProtectionDB,
|
||||
db: SlashingProtectionDB_v1,
|
||||
validator: ValidatorPubKey
|
||||
): string =
|
||||
## Dump the linked list of blocks proposd by a validator in a string
|
||||
|
@ -883,80 +942,52 @@ proc dumpAttestations*(
|
|||
|
||||
# DB maintenance
|
||||
# --------------------------------------------
|
||||
# TODO: pruning
|
||||
# Note that the complete interchange format
|
||||
# requires all proposals/attestations ever and so prevent pruning.
|
||||
proc pruneBlocks*(db: SlashingProtectionDB_v1, 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: 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
|
||||
# --------------------------------------------
|
||||
|
||||
type
|
||||
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)
|
||||
proc toSPDIR_lowWatermark*(db: SlashingProtectionDB_v1): SPDIR
|
||||
{.raises: [IOError, Defect].} =
|
||||
## Export the full slashing protection database
|
||||
## to a json the Slashing Protection Database Interchange (Complete) Format
|
||||
var extract: SPDIF
|
||||
extract.metadata.interchange_format = "complete"
|
||||
extract.metadata.interchange_format_version = "3"
|
||||
extract.metadata.genesis_validator_root = Eth2Digest0x db.get(
|
||||
subkey(kGenesisValidatorRoot), ETH2Digest
|
||||
## Export only the low watermark metadata
|
||||
## to the Nimbus Slashing Protection Database Intermediate Representation
|
||||
##
|
||||
## The full history is lost.
|
||||
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_validator_root at init")
|
||||
# ).expect("Slashing Protection requires genesis_validators_root at init")
|
||||
).unsafeGet()
|
||||
|
||||
let numValidators = db.get(
|
||||
|
@ -965,13 +996,68 @@ proc toSPDIF*(db: SlashingProtectionDB, path: string)
|
|||
).get(otherwise = 0'u32)
|
||||
|
||||
for i in 0'u32 ..< numValidators:
|
||||
var validator: SPDIF_Validator
|
||||
var validator: SPDIR_Validator
|
||||
validator.pubkey = PubKey0x db.get(
|
||||
subkey(kValidator, i),
|
||||
ValidatorPubKey
|
||||
PubKeyBytes
|
||||
).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(
|
||||
subkey(kLinkedListMeta, valID),
|
||||
KeysEpochs
|
||||
|
@ -985,8 +1071,8 @@ proc toSPDIF*(db: SlashingProtectionDB, path: string)
|
|||
BlockNode
|
||||
).unsafeGet()
|
||||
|
||||
validator.signed_blocks.add SPDIF_SignedBlock(
|
||||
slot: curSlot,
|
||||
validator.signed_blocks.add SPDIR_SignedBlock(
|
||||
slot: SlotString curSlot,
|
||||
signing_root: Eth2Digest0x node.block_root
|
||||
)
|
||||
|
||||
|
@ -997,15 +1083,15 @@ proc toSPDIF*(db: SlashingProtectionDB, path: string)
|
|||
|
||||
if ll.targetEpochs.isInit:
|
||||
var curEpoch = ll.targetEpochs.start
|
||||
var count = 0
|
||||
while true:
|
||||
let node = db.get(
|
||||
subkey(kTargetEpoch, valID, curEpoch),
|
||||
TargetEpochNode
|
||||
).unsafeGet()
|
||||
|
||||
validator.signed_attestations.add SPDIF_SignedAttestation(
|
||||
source_epoch: node.source, target_epoch: curEpoch,
|
||||
validator.signed_attestations.add SPDIR_SignedAttestation(
|
||||
source_epoch: EpochString node.source,
|
||||
target_epoch: EpochString curEpoch,
|
||||
signing_root: Eth2Digest0x node.attestation_root
|
||||
)
|
||||
|
||||
|
@ -1014,59 +1100,44 @@ proc toSPDIF*(db: SlashingProtectionDB, path: string)
|
|||
else:
|
||||
curEpoch = node.next
|
||||
|
||||
inc count
|
||||
doAssert count < 5
|
||||
|
||||
# Update extract without reallocating seqs
|
||||
# by manually transferring ownership
|
||||
extract.data.setLen(extract.data.len + 1)
|
||||
shallowCopy(extract.data[^1], validator)
|
||||
result.data.setLen(result.data.len + 1)
|
||||
shallowCopy(result.data[^1], validator)
|
||||
|
||||
Json.saveFile(path, extract, pretty = true)
|
||||
echo "Exported slashing protection DB to '", path, "'"
|
||||
|
||||
proc fromSPDIF*(db: SlashingProtectionDB, path: string): bool
|
||||
proc inclSPDIR*(db: SlashingProtectionDB_v1, spdir: SPDIR): SlashingImportStatus
|
||||
{.raises: [SerializationError, IOError, Defect].} =
|
||||
## Import a (Complete) Slashing Protection Database Interchange Format
|
||||
## file into the specified slahsing protection DB
|
||||
## Import a Slashing Protection Database Intermediate Representation
|
||||
## file into the specified slashing protection DB
|
||||
##
|
||||
## 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
|
||||
|
||||
let extract = Json.loadFile(path, SPDIF)
|
||||
|
||||
doAssert not db.isNil, "The Slashing Protection DB must be initialized."
|
||||
doAssert not db.backend.isNil, "The Slashing Protection DB must be initialized."
|
||||
|
||||
let dbGenValRoot = db.get(
|
||||
subkey(kGenesisValidatorRoot), ETH2Digest
|
||||
subkey(kGenesisValidatorsRoot), ETH2Digest
|
||||
).unsafeGet()
|
||||
|
||||
if dbGenValRoot != default(Eth2Digest) and
|
||||
dbGenValRoot != extract.metadata.genesis_validator_root.Eth2Digest:
|
||||
echo "The slashing protection database and imported file refer to different blockchains."
|
||||
return false
|
||||
dbGenValRoot != spdir.metadata.genesis_validators_root.Eth2Digest:
|
||||
error "The slashing protection database and imported file refer to different blockchains.",
|
||||
DB_genesis_validators_root = dbGenValRoot,
|
||||
Imported_genesis_validators_root = spdir.metadata.genesis_validators_root.Eth2Digest
|
||||
return siFailure
|
||||
|
||||
if dbGenValRoot == default(Eth2Digest):
|
||||
db.put(
|
||||
subkey(kGenesisValidatorRoot),
|
||||
extract.metadata.genesis_validator_root.Eth2Digest
|
||||
subkey(kGenesisValidatorsRoot),
|
||||
spdir.metadata.genesis_validators_root.Eth2Digest
|
||||
)
|
||||
|
||||
for v in 0 ..< extract.data.len:
|
||||
for b in 0 ..< extract.data[v].signed_blocks.len:
|
||||
db.registerBlock(
|
||||
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
|
||||
)
|
||||
# Create a mutable copy for sorting
|
||||
var spdir = spdir
|
||||
return db.importInterchangeV5Impl(spdir)
|
||||
|
||||
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}"
|
||||
./download_test_vectors.sh
|
||||
./download_slashing_interchange_tests.sh
|
||||
popd
|
||||
|
|
|
@ -32,7 +32,8 @@ import # Unit test
|
|||
./test_zero_signature,
|
||||
./fork_choice/tests_fork_choice,
|
||||
./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
|
||||
# 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,
|
||||
nimcrypto/utils,
|
||||
# Internal
|
||||
../../beacon_chain/validator_slashing_protection,
|
||||
../../beacon_chain/validator_protection/slashing_protection,
|
||||
../../beacon_chain/spec/[datatypes, digest, crypto, presets],
|
||||
# Test utilies
|
||||
../testutil
|
||||
|
@ -42,12 +42,30 @@ func fakeValidator(index: SomeInteger): ValidatorPubKey =
|
|||
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 = "test_slashprot"
|
||||
|
||||
suiteReport "Slashing Protection DB - Interchange" & preset():
|
||||
# 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():
|
||||
let genesis_validators_root = hexToDigest"0x04700007fabc8282644aed6d1c7c9e21d38a03a0c4ba193f3afe428824b3a673"
|
||||
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
|
||||
.fromHex"0xb845089a1457f811bfc000588fbb4e713669be8ce060ea6be3c6ece09afc3794106c91ca73acda5e5457122d58723bed"
|
||||
|
@ -76,22 +94,44 @@ suiteReport "Slashing Protection DB - Interchange" & preset():
|
|||
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
|
||||
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")
|
||||
db2.toSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection_roundtrip1.json")
|
||||
doAssert siSuccess == db2.importSlashingInterchange(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")
|
||||
db2.exportSlashingInterchange(currentSourcePath.parentDir/"test_complete_export_slashing_protection_roundtrip1.json")
|
||||
|
||||
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")
|
||||
db3.toSPDIF(currentSourcePath.parentDir/"test_complete_export_slashing_protection_roundtrip2.json")
|
||||
doAssert siSuccess == db3.importSlashingInterchange(currentSourcePath.parentDir/"test_complete_export_slashing_protection.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
|
||||
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
|
||||
# Standard library
|
||||
std/unittest,
|
||||
std/[unittest, os],
|
||||
# Status lib
|
||||
eth/db/kvstore,
|
||||
stew/results,
|
||||
# Internal
|
||||
../../beacon_chain/validator_slashing_protection,
|
||||
../../beacon_chain/validator_protection/slashing_protection,
|
||||
../../beacon_chain/spec/[datatypes, digest, crypto, presets],
|
||||
# Test utilies
|
||||
../testutil
|
||||
|
@ -38,9 +38,35 @@ func fakeValidator(index: SomeInteger): ValidatorPubKey =
|
|||
result = ValidatorPubKey()
|
||||
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():
|
||||
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:
|
||||
db.checkSlashableBlockProposal(
|
||||
|
@ -58,10 +84,16 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
target = Epoch 1
|
||||
).error.kind == TargetPrecedesSource
|
||||
|
||||
db.close()
|
||||
|
||||
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(
|
||||
fakeValidator(100),
|
||||
|
@ -80,11 +112,6 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
slot = Slot 10
|
||||
).isErr()
|
||||
# Slot occupied by another validator
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(111),
|
||||
slot = Slot 10
|
||||
).isOk()
|
||||
# Slot occupied by another validator
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
slot = Slot 15
|
||||
|
@ -115,7 +142,15 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
).isErr()
|
||||
|
||||
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
|
||||
db.registerBlock(
|
||||
|
@ -135,36 +170,37 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
fakeRoot(20)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {10, 20}:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
if i > 10 and i != 20: # MinSlotViolation and DupSlot
|
||||
let status = db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isOk()
|
||||
)
|
||||
doAssert status.isOk, "error: " & $status
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
let status = db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isErr()
|
||||
)
|
||||
doAssert status.isErr, "error: " & $status
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 15,
|
||||
fakeRoot(15)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {10, 15, 20}:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
if i > 10 and i notin {15, 20}: # MinSlotViolation and DupSlot
|
||||
let status = db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isOk()
|
||||
)
|
||||
doAssert status.isOk, "error: " & $status
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
let status = db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isErr()
|
||||
)
|
||||
doAssert status.isErr, "error: " & $status
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Slot i
|
||||
|
@ -180,50 +216,19 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
fakeRoot(17)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {10, 12, 15, 17, 20}:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
if i > 10 and i notin {12, 15, 17, 20}:
|
||||
let status = db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isOk()
|
||||
)
|
||||
doAssert status.isOk, "error: " & $status
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
let status = db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isErr()
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Slot i
|
||||
).isOk()
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 9,
|
||||
fakeRoot(9)
|
||||
)
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 1,
|
||||
fakeRoot(1)
|
||||
)
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 3,
|
||||
fakeRoot(3)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {1, 3, 9, 10, 12, 15, 17, 20}:
|
||||
doAssert status.isErr, "error: " & $status
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isErr()
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Slot i
|
||||
|
@ -233,31 +238,35 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
Slot 29,
|
||||
fakeRoot(29)
|
||||
)
|
||||
db.registerBlock(
|
||||
fakeValidator(100),
|
||||
Slot 2,
|
||||
fakeRoot(2)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {1, 2, 3, 9, 10, 12, 15, 17, 20, 29}:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
if i > 10 and i notin {12, 15, 17, 20, 29}:
|
||||
let status = db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isOk()
|
||||
)
|
||||
doAssert status.isOk, "error: " & $status
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
let status = db.checkSlashableBlockProposal(
|
||||
fakeValidator(100),
|
||||
Slot i
|
||||
).isErr()
|
||||
)
|
||||
doAssert status.isErr, "error: " & $status
|
||||
check:
|
||||
db.checkSlashableBlockProposal(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Slot i
|
||||
).isOk()
|
||||
|
||||
wrappedTimedTest "SP for same epoch attestation target - linear append":
|
||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
||||
sqlite3db_delete(TestDir, TestDbName)
|
||||
let db = SlashingProtectionDB.init(
|
||||
default(Eth2Digest),
|
||||
TestDir,
|
||||
TestDbName
|
||||
)
|
||||
defer:
|
||||
db.close()
|
||||
sqlite3db_delete(TestDir, TestDbName)
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
|
@ -276,11 +285,6 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
Epoch 0, Epoch 10,
|
||||
).error.kind == DoubleVote
|
||||
# Epoch occupied by another validator
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(111),
|
||||
Epoch 0, Epoch 10
|
||||
).isOk()
|
||||
# Epoch occupied by another validator
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 15
|
||||
|
@ -310,155 +314,17 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
Epoch 0, Epoch 20
|
||||
).error.kind == DoubleVote
|
||||
|
||||
wrappedTimedTest "SP for same epoch attestation target - backtracking append":
|
||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
||||
|
||||
# last finalized block
|
||||
db.registerAttestation(
|
||||
fakeValidator(0),
|
||||
Epoch 0, Epoch 0,
|
||||
fakeRoot(0)
|
||||
)
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 10,
|
||||
fakeRoot(10)
|
||||
)
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 20,
|
||||
fakeRoot(20)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {10, 20}:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).error.kind == DoubleVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 15,
|
||||
fakeRoot(15)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {10, 15, 20}:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).error.kind == DoubleVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 12,
|
||||
fakeRoot(12)
|
||||
)
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 17,
|
||||
fakeRoot(17)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {10, 12, 15, 17, 20}:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).error.kind == DoubleVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 9,
|
||||
fakeRoot(9)
|
||||
)
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 1,
|
||||
fakeRoot(1)
|
||||
)
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 3,
|
||||
fakeRoot(3)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {1, 3, 9, 10, 12, 15, 17, 20}:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).error.kind == DoubleVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 29,
|
||||
fakeRoot(29)
|
||||
)
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 2,
|
||||
fakeRoot(2)
|
||||
)
|
||||
for i in 0 ..< 30:
|
||||
if i notin {1, 2, 3, 9, 10, 12, 15, 17, 20, 29}:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
else:
|
||||
check:
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch i
|
||||
).error.kind == DoubleVote
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(0xDEADBEEF),
|
||||
Epoch 0, Epoch i
|
||||
).isOk()
|
||||
|
||||
wrappedTimedTest "SP for surrounded attestations":
|
||||
block:
|
||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
||||
sqlite3db_delete(TestDir, TestDbName)
|
||||
let db = SlashingProtectionDB.init(
|
||||
default(Eth2Digest),
|
||||
TestDir,
|
||||
TestDbName
|
||||
)
|
||||
defer:
|
||||
db.close()
|
||||
sqlite3db_delete(TestDir, TestDbName)
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
|
@ -479,19 +345,22 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
fakeValidator(100),
|
||||
Epoch 11, Epoch 21
|
||||
).isOk
|
||||
# TODO: is that possible?
|
||||
db.checkSlashableAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 9, Epoch 19
|
||||
).isOk
|
||||
|
||||
block:
|
||||
let db = SlashingProtectionDB.init(default(Eth2Digest), kvStore MemStoreRef.init())
|
||||
sqlite3db_delete(TestDir, TestDbName)
|
||||
let db = SlashingProtectionDB.init(
|
||||
default(Eth2Digest),
|
||||
TestDir,
|
||||
TestDbName
|
||||
)
|
||||
defer:
|
||||
db.close()
|
||||
sqlite3db_delete(TestDir, TestDbName)
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 1,
|
||||
fakeRoot(0)
|
||||
fakeRoot(1)
|
||||
)
|
||||
|
||||
db.registerAttestation(
|
||||
|
@ -522,7 +391,15 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
|
||||
wrappedTimedTest "SP for surrounding attestations":
|
||||
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(
|
||||
fakeValidator(100),
|
||||
|
@ -541,12 +418,20 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
).error.kind == SurroundingVote
|
||||
|
||||
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(
|
||||
fakeValidator(100),
|
||||
Epoch 0, Epoch 1,
|
||||
fakeRoot(20)
|
||||
fakeRoot(1)
|
||||
)
|
||||
|
||||
db.registerAttestation(
|
||||
|
@ -567,24 +452,32 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
|
||||
wrappedTimedTest "Attestation ordering #1698":
|
||||
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(
|
||||
fakeValidator(100),
|
||||
Epoch 1, Epoch 2,
|
||||
fakeRoot(20)
|
||||
fakeRoot(2)
|
||||
)
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 8, Epoch 10,
|
||||
fakeRoot(20)
|
||||
fakeRoot(10)
|
||||
)
|
||||
|
||||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 14, Epoch 15,
|
||||
fakeRoot(20)
|
||||
fakeRoot(15)
|
||||
)
|
||||
|
||||
# The current list is, 2 -> 10 -> 15
|
||||
|
@ -592,7 +485,7 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 3, Epoch 6,
|
||||
fakeRoot(20)
|
||||
fakeRoot(6)
|
||||
)
|
||||
|
||||
# The current list is 2 -> 6 -> 10 -> 15
|
||||
|
@ -605,7 +498,15 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
|
||||
wrappedTimedTest "Test valid attestation #1699":
|
||||
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(
|
||||
fakeValidator(100),
|
||||
|
@ -616,7 +517,7 @@ suiteReport "Slashing Protection DB" & preset():
|
|||
db.registerAttestation(
|
||||
fakeValidator(100),
|
||||
Epoch 40, Epoch 50,
|
||||
fakeRoot(20)
|
||||
fakeRoot(50)
|
||||
)
|
||||
|
||||
check:
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 39966c051e0447d4ad6821d5a7646ad0f2aed842
|
||||
Subproject commit 48fef39e8c1aa2aafc0a0b2847a9a05a62143ad2
|
|
@ -1 +1 @@
|
|||
Subproject commit 1401c34374fe7606dbf1252e361725415890f5f0
|
||||
Subproject commit 6d3e6a21caf4110d0d432b82f14e41e0271cd76b
|
Loading…
Reference in New Issue