nimbus-eth2/beacon_chain/validators/slashing_protection_v1.nim

1147 lines
36 KiB
Nim
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# beacon_chain
# Copyright (c) 2018-2021 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
# Standard library
std/[tables, os],
# Status
eth/db/[kvstore, kvstore_sqlite3],
chronicles,
nimcrypto/[hash, utils],
serialization,
json_serialization,
# Internal
../spec/[datatypes, digest, crypto],
../ssz,
./slashing_protection_common
# Requirements
# --------------------------------------------
#
# Overview of slashing and how it ties in with the rest of Eth2.0
#
# Phase 0 for humans - Validator responsibilities:
# - https://notes.ethereum.org/@djrtwo/Bkn3zpwxB#Validator-responsibilities
#
# Phase 0 spec - Honest Validator - how to avoid slashing
# - https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/validator.md#how-to-avoid-slashing
#
# In-depth reading on slashing conditions
#
# - Detecting slashing conditions https://hackmd.io/@n0ble/By897a5sH
# - Open issue on writing a slashing detector https://github.com/ethereum/eth2.0-pm/issues/63
# - Casper the Friendly Finality Gadget, Vitalik Buterin and Virgil Griffith
# https://arxiv.org/pdf/1710.09437.pdf
# Figure 2
# An individual validator ν MUST NOT publish two distinct votes,
# 〈ν,s1,t1,h(s1),h(t1) AND〈ν,s2,t2,h(s2),h(t2)〉,
# such that either:
# I. h(t1) = h(t2).
# Equivalently, a validator MUST NOT publish two distinct votes for the same target height.
# OR
# II. h(s1) < h(s2) < h(t2) < h(t1).
# Equivalently, a validator MUST NOT vote within the span of its other votes.
# - Vitalik's annotated spec: https://github.com/ethereum/annotated-spec/blob/d8c51af84f9f309d91c37379c1fcb0810bc5f10a/phase0/beacon-chain.md#proposerslashing
# 1. A proposer can get slashed for signing two distinct headers at the same slot.
# 2. An attester can get slashed for signing
# two attestations that together violate
# the Casper FFG slashing conditions.
# - https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/validator.md#ffg-vote
# The "source" is the current_justified_epoch
# The "target" is the current_epoch
#
# Reading on weak subjectivity
# - https://notes.ethereum.org/@adiasg/weak-subjectvity-eth2
# - https://www.symphonious.net/2019/11/27/exploring-ethereum-2-weak-subjectivity-period/
# - https://ethresear.ch/t/weak-subjectivity-under-the-exit-queue-model/5187
#
# Reading of interop serialization format
# - Import/export format: https://hackmd.io/@sproul/Bk0Y0qdGD
# - Tests: https://github.com/eth2-clients/slashing-protection-interchange-tests
#
# Relaxation for Nimbus
#
# We are not building a slashing detector but only protecting
# attached validator from slashing, hence we make the following assumptions
#
# 1. We only need to store specific validators signed blocks and attestations
# 2. We assume that our node is synced past
# the last finalized epoch
# hence we only need to keep track of blocks and attestations
# since the last finalized epoch and we don't need to care
# about the weak subjectivity period.
# i.e. if `Node.isSynced()` returns false
# a node skips its validator duties and doesn't invoke slashing protection.
# and `isSynced` syncs at least up to the blockchain last finalized epoch.
#
# Hence the database or key-value store should support
#
# Queries
# 1. db.signedBlockExistsFor(validator, slot) -> bool
# 2. db.attestationExistsFor(validator, target_epoch) -> bool
# 3. db.attestationSurrounds(validator, source_epoch, target_epoch)
#
# Update
# 1. db.registerBlock(validator, slot, block_root)
# 2. db.registerAttestation(validator, source_epoch, target_epoch, attestation_root)
#
# Maintenance
# 1. db.prune(finalized_epoch)
#
# Interop
# 1. db.import(json)
# 2. db.export(json)
# 3. db.export(json, validator)
# 4. db.export(json, seq[validator])
# Technical Discussion
# --------------------------------------------
#
# TODO: Merge with BeaconChainDB?
# - https://stackoverflow.com/questions/21844479/multiple-databases-vs-single-database-with-logically-partitioned-data
#
# Reasons for merging
# - Single database
#
# Reasons for not merging
# - BeaconChainDB is about the beacon node itself
# while slashing protection is about validators
# - BeaconChainDB is append-only
# while slashing protection will be pruned
# at each finalization.
# Hence we might want different backend in the future
# - In a VC/BN split configuration the slashing protection
# may be better attached to the VC. (VC: Validator Client, BN: Beacon Node)
# - The slashing protection DB only held cryptographic hashes
# and epoch/slot integers which are uncompressible
# while BeaconChainDB is snappy-compressed.
#
# TODO: if we enshrine the split we likely want to use
# a relational DB instead of KV-Store,
# for efficient pruning and range queries support
# DB primitives
# --------------------------------------------
# Implementation
#
# As mentioned in the technical discussion
# we currently use a simple KV-store abstraction
# with no range queries or iterators.
#
# To support our requirements
# we store block proposals and attestations
# as per-validator linked lists
type
SlashingProtectionDB_v1* = ref object
## Database storing the blocks attested
## by validators attached to a beacon node
## or validator client.
backend: KvStoreRef
SlotDesc = object
# Using tuple instead of objects, crashes the Nim compiler
# with SSZ serialization
# Making this generic as well
start, stop: Slot
isInit: bool
EpochDesc = object
start, stop: Epoch
isInit: bool
KeysEpochs = object
## Per-validator linked lists start/stop
blockSlots: SlotDesc
sourceEpochs: EpochDesc
targetEpochs: EpochDesc
SlashingKeyKind = enum
# Note: source epochs are not unique
# and so cannot be used to build a key
kBlock
kTargetEpoch
kLinkedListMeta
# Interchange format
kGenesisValidatorsRoot
kNumValidators
kValidator
BlockNode = object
prev, next: Slot
# TODO distinct type for block root vs all other ETH2Digest
block_root: Eth2Digest
TargetEpochNode = object
prev, next: Epoch
# TODO distinct type for attestation root vs all other ETH2Digest
attestation_root: Eth2Digest
source: Epoch
ValID = array[RawPubKeySize, byte]
## This is the serialized byte representation
## of a Validator Public Key.
## Portable between Miracl/BLST
## and limits serialization/deserialization call
# Internal
# -------------------------------------------------------------
{.push raises: [Defect].}
logScope:
topics = "antislash"
func subkey(
kind: static SlashingKeyKind,
validator: ValID,
slot: Slot
): array[RawPubKeySize+8, byte] =
static: doAssert kind == kBlock
# Big endian to get a naturally ascending order on slots in sorted indices
result[0..<8] = toBytesBE(slot.uint64)
# .. but 7 bytes should be enough for slots - in return, we get a nicely
# rounded key length
result[0] = byte ord(kBlock)
result[8..<56] = validator
func subkey(
kind: static SlashingKeyKind,
validator: ValID,
epoch: Epoch
): array[RawPubKeySize+8, byte] =
static: doAssert kind == kTargetEpoch, "Got invalid kind " & $kind
# Big endian to get a naturally ascending order on slots in sorted indices
result[0..<8] = toBytesBE(epoch.uint64)
# .. but 7 bytes should be enough for slots - in return, we get a nicely
# rounded key length
result[0] = byte ord(kind)
result[8..<56] = validator
func subkey(
kind: static SlashingKeyKind,
validator: ValID
): array[RawPubKeySize+1, byte] =
static: doAssert kind == kLinkedListMeta
result[0] = byte ord(kLinkedListMeta)
result[1 .. ^1] = validator
func subkey(kind: static SlashingKeyKind): array[1, byte] =
static: doAssert kind in {kNumValidators, kGenesisValidatorsRoot}
result[0] = byte ord(kind)
func subkey(kind: static SlashingKeyKind, valIndex: uint32): array[5, byte] =
static: doAssert kind == kValidator
# Big endian to get a naturally ascending order on slots in sorted indices
result[1..<5] = toBytesBE(valIndex)
result[0] = byte ord(kind)
proc put(db: SlashingProtectionDB_v1, key: openArray[byte], v: auto) =
db.backend.put(
key,
SSZ.encode(v)
).expect("working database")
proc rawGet(rawdb: KvStoreRef,
key: openArray[byte],
T: typedesc): Opt[T] =
const ExpectedNodeSszSize = block:
when T is BlockNode:
2*sizeof(Epoch) + sizeof(Eth2Digest)
elif T is TargetEpochNode:
2*sizeof(Epoch) + sizeof(Eth2Digest) + sizeof(Epoch)
elif T is KeysEpochs:
2*sizeof(Slot) + 4*sizeof(Epoch) + 3*sizeof(bool)
elif T is Eth2Digest:
sizeof(Eth2Digest)
elif T is uint32:
sizeof(uint32)
elif T is ValidatorPubKey:
RawPubKeySize
elif T is PubKeyBytes:
RawPubKeySize
else:
{.error: "Invalid database node type: " & $T.}
## SSZ serialization is packed
## However in-memory, BlockNode, TargetEpochNode
## might be bigger due to alignment/compiler padding
var res: Opt[T]
proc decode(data: openArray[byte]) =
# We are capturing "result" and "T" from outer scope
# And allocating on the heap which are not ideal
# from a safety and performance point of view.
try:
if data.len == ExpectedNodeSszSize:
when T is ValidatorPubKey:
# symbol resolution bug
# SSZ.decode doesn't see "fromSSZBytes"
res.ok ValidatorPubKey.fromSszBytes(data)
else:
res.ok SSZ.decode(data, T) # captures from `get` scope
else:
# If the data can't be deserialized, it could be because it's from a
# version of the software that uses a different SSZ encoding
warn "Unable to deserialize data, old database?",
typ = $T,
dataLen = data.len,
expectedSize = ExpectedNodeSszSize
discard
except SerializationError:
# If the data can't be deserialized, it could be because it's from a
# version of the software that uses a different SSZ encoding
warn "Unable to deserialize data, old database?",
typ = $T,
dataLen = data.len,
expectedSize = ExpectedNodeSszSize
discard
discard rawdb.get(key, decode).expect("working database")
res
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(kGenesisValidatorsRoot),
genesis_validators_root
)
# DB Multiversioning
# -------------------------------------------------------------
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_v1,
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
# TODO distinct type for the result block root
let valID = validator.toRaw()
let foundBlock = db.get(
subkey(kBlock, valID, slot),
BlockNode
)
if foundBlock.isNone():
return ok()
return err(BadProposal(
kind: DoubleProposal,
existing_block: foundBlock.unsafeGet().block_root
))
proc checkSlashableAttestation*(
db: SlashingProtectionDB_v1,
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
# TODO distinct type for the result attestation root
let valID = validator.toRaw()
# Sanity
# ---------------------------------
if source > target:
return err(BadVote(kind: TargetPrecedesSource))
# Casper FFG 1st slashing condition
# Detect h(t1) = h(t2)
# ---------------------------------
let foundAttestation = db.get(
subkey(kTargetEpoch, valID, target),
TargetEpochNode
)
if foundAttestation.isSome():
# Logged by caller
return err(BadVote(
kind: DoubleVote,
existingAttestation: foundAttestation.unsafeGet().attestation_root
))
# TODO: we hack KV-store range queries
# ---------------------------------
let maybeLL = db.get(
subkey(kLinkedListMeta, valID),
KeysEpochs
)
if maybeLL.isNone:
info "No slashing protection data - first attestation?",
validator = validator,
attSource = source,
attTarget = target
return ok()
let ll = maybeLL.unsafeGet()
if not ll.targetEpochs.isInit:
info "No attestation slashing protection data - first attestation?",
validator = validator,
attSource = source,
attTarget = target
return ok()
# Chain reorg
# Detect h(s2) < h(s1)
# If the candidate attestation source precedes
# source(s) we have in the SlashingProtectionDB_v1
# we have a chain reorg
# ---------------------------------
if source < ll.sourceEpochs.stop:
warn "Detected a chain reorg",
earliestJustifiedEpoch = ll.sourceEpochs.start,
oldestJustifiedEpoch = ll.sourceEpochs.stop,
reorgJustifiedEpoch = source,
monitoredValidator = validator
# Casper FFG 2nd slashing condition
# -> Surrounded vote
# Detect h(s1) < h(s2) < h(t2) < h(t1)
# ---------------------------------
# Casper FFG 2nd slashing condition
# -> Surrounding vote
# Detect h(s2) < h(s1) < h(t1) < h(t2)
# ---------------------------------
template s2: untyped = source
template t2: untyped = target
# We start from the final target epoch
var t1: Epoch
var t1Node: TargetEpochNode
t1 = ll.targetEpochs.stop
t1Node = db.get(
subkey(kTargetEpoch, valID, t1),
TargetEpochNode
# bug in Nim results, ".e" field inaccessible
# ).expect("Consistent linked-list in DB")
).unsafeGet()
template s1: untyped = t1Node.source
template ar1: untyped = t1Node.attestation_root
# TODO: optimize so we don't scan the whole linked list
while true:
if s2 < s1 and s1 < t1 and t1 < t2:
# s2 < s1 < t1 < t2
# Logged by caller
return err(BadVote(
kind: SurroundingVote,
existingAttestationRoot: ar1,
sourceExisting: s1,
targetExisting: t1,
sourceSlashable: s2,
targetSlashable: t2
))
elif s1 < s2 and s2 < t2 and t2 < t1:
# s1 < s2 < t2 < t1
# Logged by caller
return err(BadVote(
kind: SurroundedVote,
existingAttestationRoot: ar1,
sourceExisting: s1,
targetExisting: t1,
sourceSlashable: s2,
targetSlashable: t2
))
# Next iteration
if t1Node.prev == default(Epoch) or
t1Node.prev == ll.targetEpochs.stop:
return ok()
else:
t1 = t1Node.prev
t1Node = db.get(
subkey(kTargetEpoch, valID, t1Node.prev),
TargetEpochNode
# bug in Nim results, ".e" field inaccessible
# ).expect("Consistent linked-list in DB")
).unsafeGet()
doAssert false, "Unreachable"
# DB update
# --------------------------------------------
proc registerValidator(db: SlashingProtectionDB_v1, validator: ValidatorPubKey) =
## Add a new validator to the database
## Assumes the validator does not exist
let maybeNumVals = db.get(
subkey(kNumValidators),
uint32
)
var valIndex = 0'u32
if maybeNumVals.isNone():
db.put(subkey(kNumValidators), 1'u32)
else:
valIndex = maybeNumVals.unsafeGet()
db.put(subkey(kNumValidators), valIndex + 1)
db.put(subkey(kValidator, valIndex), validator)
proc registerBlock*(
db: SlashingProtectionDB_v1,
validator: ValidatorPubKey,
slot: Slot, block_root: Eth2Digest) =
## Add a block to the slashing protection DB
## `checkSlashableBlockProposal` MUST be run
## before to ensure no overwrite.
let valID = validator.toRaw()
# We want to keep the linked-list ordered
# to ease pruning.
# TODO: DB instead of KV-store,
# at the very least we should isolate that logic
let maybeLL = db.get(
subkey(kLinkedListMeta, valID),
KeysEpochs
)
if maybeLL.isNone:
info "No slashing protection data - initiating block tracking for validator",
validator = validator
db.registerValidator(validator)
let node = BlockNode(
block_root: block_root
)
db.put(subkey(kBlock, valID, slot), node)
db.put(
subkey(kLinkedListMeta, valID),
KeysEpochs(
blockSlots: SlotDesc(start: slot, stop: slot, isInit: true),
# targetEpochs.isInit will be false
)
)
return
var ll = maybeLL.unsafeGet()
var cur = ll.blockSlots.stop
if not ll.blockSlots.isInit:
let node = BlockNode(
block_root: block_root
)
ll.blockSlots = SlotDesc(start: slot, stop: slot, isInit: true)
db.put(subkey(kBlock, valID, slot), node)
# TODO: what if crash here?
db.put(subkey(kLinkedListMeta, valID), ll)
return
if cur < slot:
# Adding a block later than all known blocks
let node = BlockNode(
prev: cur,
block_root: block_root
)
var prevNode = db.get(
subkey(kBlock, valID, cur),
BlockNode
# bug in Nim results, ".e" field inaccessible
# ).expect("Consistent linked-list in DB")
).unsafeGet()
prevNode.next = slot
ll.blockSlots.stop = slot
db.put(subkey(kBlock, valID, slot), node)
db.put(subkey(kBlock, valID, cur), prevNode)
# TODO: what if crash here?
db.put(subkey(kLinkedListMeta, valID), ll)
return
# TODO: we likely want a proper DB or better KV-store high-level API
# in the future.
while true:
var curNode = db.get(
subkey(kBlock, valID, cur),
BlockNode
# bug in Nim results, ".e" field inaccessible
# ).expect("Consistent linked-list in DB")
).unsafeGet()
if curNode.prev == ll.blockSlots.start:
# Reached the beginning
# Change: Metadata.start <-> cur
# to: Metadata.start <-> new <-> cur
# This should happen only if registerBlock
# is called out-of-order
warn "Validator proposal in the past - out-of-order antislash registration?",
validator = validator,
slot = slot,
blockroot = blockroot,
earliestBlockProposalSlotInDB = ll.blockSlots.start,
latestBlockProposalSlotInDB = ll.blockSlots.stop
var node = BlockNode(
prev: ll.blockSlots.start,
next: cur,
block_root: block_root
)
ll.blockSlots.start = slot
curNode.prev = slot
db.put(subkey(kBlock, valID, slot), node)
# TODO: what if crash here?
db.put(subkey(kBlock, valID, cur), curNode)
db.put(subkey(kLinkedListMeta, valID), ll)
return
elif slot > curNode.prev:
# Reached: prev < slot < cur
# Change: prev <-> cur
# to: prev <-> new <-> cur
let prev = curNode.prev
var node = BlockNode(
prev: prev, next: cur,
block_root: block_root
)
var prevNode = db.get(
subkey(kBlock, valID, prev),
BlockNode
# bug in Nim results, ".e" field inaccessible
# ).expect("Consistent linked-list in DB")
).unsafeGet()
prevNode.next = slot
curNode.prev = slot
db.put(subkey(kBlock, valID, slot), node)
# TODO: what if crash here?
db.put(subkey(kBlock, valID, cur), curNode)
db.put(subkey(kBlock, valID, prev), prevNode)
return
# Previous
cur = curNode.prev
curNode = db.get(
subkey(kBlock, valID, cur),
BlockNode
# bug in Nim results, ".e" field inaccessible
# ).expect("Consistent linked-list in DB")
).unsafeGet()
proc registerAttestation*(
db: SlashingProtectionDB_v1,
validator: ValidatorPubKey,
source, target: Epoch,
attestation_root: Eth2Digest) =
## Add an attestation to the slashing protection DB
## `checkSlashableAttestation` MUST be run
## before to ensure no overwrite.
let valID = validator.toRaw()
# We want to keep the linked-list ordered
# to ease pruning.
# TODO: DB instead of KV-store,
# at the very least we should isolate that logic
let maybeLL = db.get(
subkey(kLinkedListMeta, valID),
KeysEpochs
)
if maybeLL.isNone:
info "No slashing protection data - initiating attestation tracking for validator",
validator = validator
db.registerValidator(validator)
let node = TargetEpochNode(
source: source,
attestation_root: attestation_root
)
db.put(subkey(kTargetEpoch, valID, target), node)
db.put(
subkey(kLinkedListMeta, valID),
KeysEpochs(
# blockSlots.isInit will be false
sourceEpochs: EpochDesc(start: source, stop: source, isInit: true),
targetEpochs: EpochDesc(start: target, stop: target, isInit: true)
)
)
return
var ll = maybeLL.unsafeGet()
var cur = ll.targetEpochs.stop
if not ll.targetEpochs.isInit:
let node = TargetEpochNode(
attestation_root: attestation_root,
source: source
)
ll.targetEpochs = EpochDesc(start: target, stop: target, isInit: true)
ll.sourceEpochs = EpochDesc(start: source, stop: source, isInit: true)
db.put(subkey(kTargetEpoch, valID, target), node)
# TODO: what if crash here?
db.put(subkey(kLinkedListMeta, valID), ll)
return
block: # Update source epoch
if ll.sourceEpochs.stop < source:
ll.sourceEpochs.stop = source
if source < ll.sourceEpochs.start:
ll.sourceEpochs.start = source
if cur < target:
# Adding an attestation later than all known blocks
let node = TargetEpochNode(
prev: cur,
source: source,
attestation_root: attestation_root
)
var prevNode = db.get(
subkey(kTargetEpoch, valID, cur),
TargetEpochNode
# bug in Nim results, ".e" field inaccessible
# ).expect("Consistent linked-list in DB")
).unsafeGet()
prevNode.next = target
ll.targetEpochs.stop = target
db.put(subkey(kTargetEpoch, valID, target), node)
db.put(subkey(kTargetEpoch, valID, cur), prevNode)
# TODO: what if crash here?
db.put(subkey(kLinkedListMeta, valID), ll)
return
# TODO: we likely want a proper DB or better KV-store high-level API
# in the future.
while true:
var curNode = db.get(
subkey(kTargetEpoch, valID, cur),
TargetEpochNode
# bug in Nim results, ".e" field inaccessible
# ).expect("Consistent linked-list in DB")
).unsafeGet()
if curNode.prev == ll.targetEpochs.start:
# Reached the beginning
# Change: Metadata.start <-> cur
# to: Metadata.start <-> new <-> cur
# This should happen only if registerAttestation
# is called out-of-order or if the validator
# changes its vote for an earlier fork than its latest vote
warn "Validator vote targeting the past - out-of-order antislash registration or chain reorg?",
validator = validator,
source_epoch = source,
target_epoch = target,
attestation_root = attestation_root
var node = TargetEpochNode(
prev: ll.targetEpochs.start,
next: cur,
source: source,
attestation_root: attestation_root
)
ll.targetEpochs.start = target
curNode.prev = target
db.put(subkey(kTargetEpoch, valID, target), node)
# TODO: what if crash here?
db.put(subkey(kTargetEpoch, valID, cur), curNode)
db.put(subkey(kLinkedListMeta, valID), ll)
return
elif target > curNode.prev:
# Reached: prev < target < cur
# Change: prev <-> cur
# to: prev <-> new <-> cur
let prev = curNode.prev
var node = TargetEpochNode(
prev: prev, next: cur,
source: source,
attestation_root: attestation_root
)
var prevNode = db.get(
subkey(kTargetEpoch, valID, prev),
TargetEpochNode
# bug in Nim results, ".e" field inaccessible
# ).expect("Consistent linked-list in DB")
).unsafeGet()
prevNode.next = target
curNode.prev = target
db.put(subkey(kTargetEpoch, valID, target), node)
# TODO: what if crash here?
db.put(subkey(kTargetEpoch, valID, cur), curNode)
db.put(subkey(kTargetEpoch, valID, prev), prevNode)
return
# Previous
cur = curNode.prev
curNode = db.get(
subkey(kTargetEpoch, valID, cur),
TargetEpochNode
# bug in Nim results, ".e" field inaccessible
# ).expect("Consistent linked-list in DB")
).unsafeGet()
# Debug tools
# --------------------------------------------
proc dumpBlocks*(
db: SlashingProtectionDB_v1,
validator: ValidatorPubKey
): string =
## Dump the linked list of blocks proposd by a validator in a string
var blocks: seq[BlockNode]
let valID = validator.toRaw
let maybeLL = db.get(
subkey(kLinkedListMeta, valID),
KeysEpochs
)
if maybeLL.isNone:
return "No blocks in slashing protection DB for validator " & $validator
let ll = maybeLL.unsafeGet()
doAssert ll.blockSlots.isInit
var cur = ll.blockSlots.stop
while cur != ll.blockSlots.start:
blocks.add db.get(
subkey(kBlock, valID, cur),
BlockNode
).unsafeGet()
cur = blocks[^1].prev
blocks.add db.get(
subkey(kBlock, valID, ll.blockSlots.start),
BlockNode
).unsafeGet()
return $blocks
proc dumpAttestations*(
db: SlashingProtectionDB_v1,
validator: ValidatorPubKey
): string =
## Dump the linked list of blocks proposd by a validator in a string
var attestations: seq[TargetEpochNode]
let valID = validator.toRaw
let maybeLL = db.get(
subkey(kLinkedListMeta, valID),
KeysEpochs
)
if maybeLL.isNone:
return "No blocks in slashing protection DB for validator " & $validator
let ll = maybeLL.unsafeGet()
doAssert ll.targetEpochs.isInit
var cur = ll.targetEpochs.stop
while cur != ll.targetEpochs.start:
attestations.add db.get(
subkey(kTargetEpoch, valID, cur),
TargetEpochNode
).unsafeGet()
cur = attestations[^1].prev
attestations.add db.get(
subkey(kTargetEpoch, valID, ll.targetEpochs.start),
TargetEpochNode
).unsafeGet()
return $attestations
# DB maintenance
# --------------------------------------------
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: int64,
newMinTargetEpoch: int64) =
## Prune all blocks from a validator before the specified newMinSlot
## This is intended for interchange import.
##
## Note: the Database v1 does not support pruning.
##
## Negative source/target epoch of -1 can be received if no attestation was imported
## In that case nothing is done
warn "Slashing DB pruning is not supported on the v1 of our database. Request ignored.",
validator = shortLog(validator),
newMinSourceEpoch = newMinSourceEpoch,
newMinTargetEpoch = 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
# --------------------------------------------
proc toSPDIR_lowWatermark*(db: SlashingProtectionDB_v1): SPDIR
{.raises: [IOError, Defect].} =
## 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_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
).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
).unsafeGet()
if ll.blockSlots.isInit:
var curSlot = ll.blockSlots.start
while true:
let node = db.get(
subkey(kBlock, valID, curSlot),
BlockNode
).unsafeGet()
validator.signed_blocks.add SPDIR_SignedBlock(
slot: SlotString curSlot,
signing_root: Eth2Digest0x node.block_root
)
if curSlot == ll.blockSlots.stop:
break
else:
curSlot = node.next
if ll.targetEpochs.isInit:
var curEpoch = ll.targetEpochs.start
while true:
let node = db.get(
subkey(kTargetEpoch, valID, curEpoch),
TargetEpochNode
).unsafeGet()
validator.signed_attestations.add SPDIR_SignedAttestation(
source_epoch: EpochString node.source,
target_epoch: EpochString curEpoch,
signing_root: Eth2Digest0x node.attestation_root
)
if curEpoch == ll.targetEpochs.stop:
break
else:
curEpoch = node.next
# Update extract without reallocating seqs
# by manually transferring ownership
result.data.setLen(result.data.len + 1)
shallowCopy(result.data[^1], validator)
proc inclSPDIR*(db: SlashingProtectionDB_v1, spdir: SPDIR): SlashingImportStatus
{.raises: [SerializationError, IOError, Defect].} =
## Import a Slashing Protection Database Intermediate Representation
## file into the specified slashing protection DB
##
## The database must be initialized.
## The genesis_validators_root must match or
## the DB must have a zero root
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(kGenesisValidatorsRoot), ETH2Digest
).unsafeGet()
if dbGenValRoot != default(Eth2Digest) and
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(kGenesisValidatorsRoot),
spdir.metadata.genesis_validators_root.Eth2Digest
)
# Create a mutable copy for sorting
var spdir = spdir
return db.importInterchangeV5Impl(spdir)
# Sanity check
# --------------------------------------------------------------
static: doAssert SlashingProtectionDB_v1 is SlashingProtectionDB_Concept