413 lines
12 KiB
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
|