nimbus-eth2/beacon_chain/validators/slashing_protection.nim

413 lines
12 KiB
Nim

# 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
# 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
let (db, requiresMigration) = SlashingProtectionDB_v2.initCompatV1(
genesis_validators_root,
basePath, dbname
)
result.db_v2 = db
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: int64,
newMinTargetEpoch: int64) =
## 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
# --------------------------------------------------------------
static: doAssert SlashingProtectionDB is SlashingProtectionDB_Concept