mirror of
https://github.com/status-im/nimbus-eth2.git
synced 2025-01-09 22:06:21 +00:00
f70ff38b53
Some upstream repos still need fixes, but this gets us close enough that style hints can be enabled by default. In general, "canonical" spellings are preferred even if they violate nep-1 - this applies in particular to spec-related stuff like `genesis_validators_root` which appears throughout the codebase.
404 lines
14 KiB
Nim
404 lines
14 KiB
Nim
# 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.
|
|
|
|
{.push raises: [Defect].}
|
|
|
|
import
|
|
# Stdlib
|
|
std/[typetraits, strutils, algorithm],
|
|
# Status
|
|
stew/[byteutils, results],
|
|
serialization,
|
|
json_serialization,
|
|
chronicles,
|
|
# Internal
|
|
../spec/datatypes/base
|
|
|
|
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
|
|
# --------------------------------------------
|
|
|
|
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)
|
|
SurroundVote # h(s1) < h(s2) < h(t2) < h(t1) or 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
|
|
DatabaseError # Cannot read/write the slashing protection db
|
|
|
|
BadVote* {.pure.} = object
|
|
case kind*: BadVoteKind
|
|
of DoubleVote:
|
|
existingAttestation*: Eth2Digest
|
|
of SurroundVote:
|
|
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
|
|
of BadVoteKind.DatabaseError:
|
|
message*: string
|
|
|
|
BadProposalKind* {.pure.} = enum
|
|
# Spec slashing condition
|
|
DoubleProposal # h(t1) == h(t2)
|
|
# EIP-3067 (https://eips.ethereum.org/EIPS/eip-3076)
|
|
MinSlotViolation # h(t2) <= h(t1)
|
|
DatabaseError # Cannot read/write the slashing protection db
|
|
|
|
BadProposal* = object
|
|
case kind*: BadProposalKind
|
|
of DoubleProposal:
|
|
existingBlock*: Eth2Digest
|
|
of MinSlotViolation:
|
|
minSlot*: Slot
|
|
candidateSlot*: Slot
|
|
of BadProposalKind.DatabaseError:
|
|
message*: string
|
|
|
|
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
|
|
else:
|
|
case a.kind
|
|
of DoubleVote:
|
|
a.existingAttestation == b.existingAttestation
|
|
of SurroundVote:
|
|
(a.existingAttestationRoot == b.existingAttestationRoot) and
|
|
(a.sourceExisting == b.sourceExisting) and
|
|
(a.targetExisting == b.targetExisting) and
|
|
(a.sourceSlashable == b.sourceSlashable) and
|
|
(a.targetSlashable == b.targetSlashable)
|
|
of TargetPrecedesSource:
|
|
true
|
|
of MinSourceViolation:
|
|
(a.minSource == b.minSource) and
|
|
(a.candidateSource == b.candidateSource)
|
|
of MinTargetViolation:
|
|
(a.minTarget == b.minTarget) and
|
|
(a.candidateTarget == b.candidateTarget)
|
|
of BadVoteKind.DatabaseError:
|
|
true
|
|
|
|
template `==`*(a, b: PubKey0x): bool =
|
|
PubKeyBytes(a) == PubKeyBytes(b)
|
|
|
|
template `<`*(a, b: PubKey0x): bool =
|
|
PubKeyBytes(a) < PubKeyBytes(b)
|
|
|
|
template cmp*(a, b: PubKey0x): bool =
|
|
cmp(PubKeyBytes(a), PubKeyBytes(b))
|
|
|
|
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 hexToByteArray(reader.readValue(string), 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 importSlashingInterchange*(
|
|
db: auto,
|
|
path: string): SlashingImportStatus {.raises: [Defect, IOError, SerializationError].} =
|
|
## 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: auto,
|
|
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().load().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()
|
|
|
|
# TODO: with minification sorting is unnecessary, cleanup
|
|
# 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()
|
|
|
|
let (dbSlot, dbSource, dbTarget) = db.retrieveLatestValidatorData(parsedKey)
|
|
|
|
# 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
|
|
if dbSlot.isSome():
|
|
maxValidSlotSeen = int dbSlot.get()
|
|
|
|
if spdir.data[v].signed_blocks.len >= 1:
|
|
# Minification, to limit Sqlite IO we only import the last block after sorting
|
|
template B: untyped = spdir.data[v].signed_blocks[^1]
|
|
let status = db.registerBlock(
|
|
parsedKey, B.slot.Slot, B.signing_root.Eth2Digest
|
|
)
|
|
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
|
|
else:
|
|
error "Slashable block. Skipping its import.",
|
|
pubkey = spdir.data[v].pubkey.PubKeyBytes.toHex(),
|
|
candidateBlock = B,
|
|
conflict = status.error()
|
|
result = siPartial
|
|
|
|
if B.slot.int > maxValidSlotSeen:
|
|
maxValidSlotSeen = int B.slot
|
|
|
|
# Now prune everything that predates
|
|
# this DB or interchange file max slot
|
|
# Even if the block is not imported, pruning will keep the latest one.
|
|
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
|
|
|
|
if dbSource.isSome():
|
|
maxValidSourceEpochSeen = int dbSource.get()
|
|
if dbTarget.isSome():
|
|
maxValidTargetEpochSeen = int dbTarget.get()
|
|
|
|
# We do a first pass over the data to find the max source/target seen
|
|
for a in 0 ..< spdir.data[v].signed_attestations.len:
|
|
template A: untyped = spdir.data[v].signed_attestations[a]
|
|
|
|
if A.source_epoch.int > maxValidSourceEpochSeen:
|
|
maxValidSourceEpochSeen = A.source_epoch.int
|
|
if A.target_epoch.int > maxValidTargetEpochSeen:
|
|
maxValidTargetEpochSeen = A.target_epoch.int
|
|
|
|
if maxValidSourceEpochSeen < 0 or maxValidTargetEpochSeen < 0:
|
|
doAssert maxValidSourceEpochSeen == -1 and maxValidTargetEpochSeen == -1
|
|
notice "No attestation found in slashing interchange file for validator",
|
|
pubkey = spdir.data[v].pubkey.PubKeyBytes.toHex()
|
|
continue
|
|
|
|
# See formal proof https://github.com/michaelsproul/slashing-proofs
|
|
# of synthetic attestation
|
|
if not(maxValidSourceEpochSeen < maxValidTargetEpochSeen) and
|
|
not(maxValidSourceEpochSeen == 0 and maxValidTargetEpochSeen == 0):
|
|
# Special-case genesis (Slashing prot is deactivated anyway)
|
|
warn "Invalid attestation(s), source epochs should be less than target epochs, skipping import",
|
|
pubkey = spdir.data[v].pubkey.PubKeyBytes.toHex(),
|
|
maxValidSourceEpochSeen = maxValidSourceEpochSeen,
|
|
maxValidTargetEpochSeen = maxValidTargetEpochSeen
|
|
result = siPartial
|
|
continue
|
|
|
|
db.registerSyntheticAttestation(
|
|
parsedKey,
|
|
Epoch maxValidSourceEpochSeen,
|
|
Epoch maxValidTargetEpochSeen
|
|
)
|
|
|
|
db.pruneAttestations(parsedKey, maxValidSourceEpochSeen, maxValidTargetEpochSeen)
|