avoid some slashing protection queries (#2528)

This PR reduces the number of database queries for slashing protection
from 5 reads and 1 write to 2 reads and 1 write in the optimistic case.

In the process, it removes user-level support for writing the database
in the version 1 format in order to simplify the code flow, and prevent
code rot. In particular, the v1 format was not covered by any unit tests
and has no advantages over v2. The concrete code to read and write it
remains for now, in particular to support upgrades from v1 to v2.

The branch also removes the use of concepts which doesn't work with
checked exceptions - in particular, this highlights code that both
raises exceptions and returns error codes, which could be cleaned up in
the future.

* Cache internal validator ID
* Rely on unique index to check for trivial duplicate votes
* Combine two surround vote queries into one
* Combine API for checking and registering slashing into single function

The slashing DB is normally not a bottleneck, but may become one with
high attached validator counts.
This commit is contained in:
Jacek Sieka 2021-05-04 15:17:28 +02:00 committed by GitHub
parent 290b889ce6
commit efdf759cc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 705 additions and 732 deletions

View File

@ -145,7 +145,7 @@ type
slashingDbKind* {.
hidden
defaultValue: SlashingDbKind.v2
desc: "The slashing DB flavour to use (v1, v2 or both) [=both]"
desc: "The slashing DB flavour to use (v2) [=v2]"
name: "slashing-db-kind" }: SlashingDbKind
stateDbKind* {.

View File

@ -322,33 +322,24 @@ proc init*(T: type BeaconNode,
attestationPool = newClone(AttestationPool.init(chainDag, quarantine))
exitPool = newClone(ExitPool.init(chainDag, quarantine))
case config.slashingDbKind
of SlashingDbKind.v2:
discard
of SlashingDbKind.v1:
error "Slashing DB v1 is no longer supported for writing"
quit 1
of SlashingDbKind.both:
warn "Slashing DB v1 deprecated, writing only v2"
info "Loading slashing protection database (v2)",
path = config.validatorsDir()
let
slashingProtectionDB =
case config.slashingDbKind
of SlashingDbKind.v1:
info "Loading slashing protection database",
path = config.validatorsDir()
SlashingProtectionDB.init(
getStateField(chainDag.headState, genesis_validators_root),
config.validatorsDir(), "slashing_protection",
modes = {kCompleteArchiveV1},
disagreementBehavior = kChooseV1
)
of SlashingDbKind.v2:
info "Loading slashing protection database (v2)",
path = config.validatorsDir()
SlashingProtectionDB.init(
SlashingProtectionDB.init(
getStateField(chainDag.headState, genesis_validators_root),
config.validatorsDir(), "slashing_protection"
)
of SlashingDbKind.both:
info "Loading slashing protection database (dual DB mode)",
path = config.validatorsDir()
SlashingProtectionDB.init(
getStateField(chainDag.headState, genesis_validators_root),
config.validatorsDir(), "slashing_protection",
modes = {kCompleteArchiveV1, kCompleteArchiveV2},
disagreementBehavior = kChooseV2
)
validatorPool = newClone(ValidatorPool.init(slashingProtectionDB))
consensusManager = ConsensusManager.new(

View File

@ -135,25 +135,25 @@ proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, a
if vc.proposalsForCurrentEpoch.contains slot:
let public_key = vc.proposalsForCurrentEpoch[slot]
notice "Proposing block", slot = slot, public_key = public_key
let validator = vc.attachedValidators.validators[public_key]
let randao_reveal = await validator.genRandaoReveal(
vc.fork, vc.beaconGenesis.genesis_validators_root, slot)
var newBlock = SignedBeaconBlock(
message: await vc.client.get_v1_validator_block(slot, vc.graffitiBytes, randao_reveal)
)
newBlock.root = hash_tree_root(newBlock.message)
# TODO: signing_root is recomputed in signBlockProposal just after
let signing_root = compute_block_root(vc.fork, vc.beaconGenesis.genesis_validators_root, slot, newBlock.root)
let notSlashable = vc.attachedValidators
.slashingProtection
.checkSlashableBlockProposal(public_key, slot)
.slashingProtection
.registerBlock(
newBlock.message.proposer_index.ValidatorIndex, public_key, slot,
signing_root)
if notSlashable.isOk:
let validator = vc.attachedValidators.validators[public_key]
notice "Proposing block", slot = slot, public_key = public_key
let randao_reveal = await validator.genRandaoReveal(
vc.fork, vc.beaconGenesis.genesis_validators_root, slot)
var newBlock = SignedBeaconBlock(
message: await vc.client.get_v1_validator_block(slot, vc.graffitiBytes, randao_reveal)
)
newBlock.root = hash_tree_root(newBlock.message)
# TODO: signing_root is recomputed in signBlockProposal just after
let signing_root = compute_block_root(vc.fork, vc.beaconGenesis.genesis_validators_root, slot, newBlock.root)
vc.attachedValidators
.slashingProtection
.registerBlock(public_key, slot, signing_root)
newBlock.signature = await validator.signBlockProposal(
vc.fork, vc.beaconGenesis.genesis_validators_root, slot, newBlock.root)
@ -181,21 +181,14 @@ proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, a
let validator = vc.attachedValidators.validators[a.public_key]
let ad = await vc.client.get_v1_validator_attestation_data(slot, a.committee_index)
# TODO signing_root is recomputed in produceAndSignAttestation/signAttestation just after
let signing_root = compute_attestation_root(
vc.fork, vc.beaconGenesis.genesis_validators_root, ad)
let notSlashable = vc.attachedValidators
.slashingProtection
.checkSlashableAttestation(
a.public_key,
ad.source.epoch,
ad.target.epoch)
.slashingProtection
.registerAttestation(
a.validator_index, a.public_key, ad.source.epoch, ad.target.epoch, signing_root)
if notSlashable.isOk():
# TODO signing_root is recomputed in produceAndSignAttestation/signAttestation just after
let signing_root = compute_attestation_root(
vc.fork, vc.beaconGenesis.genesis_validators_root, ad)
vc.attachedValidators
.slashingProtection
.registerAttestation(
a.public_key, ad.source.epoch, ad.target.epoch, signing_root)
# TODO I don't like these (u)int64-to-int conversions...
let attestation = await validator.produceAndSignAttestation(
ad, a.committee_length.int, a.validator_committee_index,

View File

@ -5,8 +5,7 @@
# * 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.
# TODO doesn't work with concepts (sigh)
# {.push raises: [Defect].}
{.push raises: [Defect].}
import
# stdlib
@ -21,6 +20,7 @@ import
./slashing_protection_v2
export slashing_protection_common
# Generic sandwich
export chronicles
@ -52,8 +52,7 @@ type
## Database storing the blocks attested
## by validators attached to a beacon node
## or validator client.
db_v1: SlashingProtectionDB_v1
db_v2: SlashingProtectionDB_v2
db_v2*: SlashingProtectionDB_v2
modes: set[SlashProtDBMode]
disagreementBehavior: DisagreementBehavior
@ -99,17 +98,27 @@ proc init*(
)
result.db_v2 = db
var db_v1: SlashingProtectionDB_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)
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)
let spdir = try: db_v1.toSPDIR_lowWatermark()
except IOError as exc:
fatal "Cannot migrate v1 database", err = exc.msg
quit 1
let status = try: result.db_v2.inclSPDIR(spdir)
except CatchableError as exc:
fatal "Writing DB v2 failed", err = exc.msg
quit 1
case status
of siSuccess:
info "Slashing DB migration successful."
@ -163,77 +172,9 @@ proc close*(db: SlashingProtectionDB) =
# 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,
index: ValidatorIndex,
validator: ValidatorPubKey,
slot: Slot
): Result[void, BadProposal] =
@ -243,12 +184,11 @@ proc checkSlashableBlockProposal*(
## The error contains the blockroot that was already proposed
##
## Returns success otherwise
db.queryVersions(
checkSlashableBlockProposal(db_version, validator, slot)
)
checkSlashableBlockProposal(db.db_v2, some(index), validator, slot)
proc checkSlashableAttestation*(
db: SlashingProtectionDB,
index: ValidatorIndex,
validator: ValidatorPubKey,
source: Epoch,
target: Epoch
@ -259,65 +199,36 @@ proc checkSlashableAttestation*(
## (surrounding vote or surrounded vote).
##
## Returns success otherwise
db.queryVersions(
checkSlashableAttestation(db_version, validator, source, target)
)
checkSlashableAttestation(db.db_v2, some(index), validator, source, target)
# DB Updates
# DB Updates - only v2 supported here
# --------------------------------------------
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,
index: ValidatorIndex,
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.
slot: Slot, block_signing_root: Eth2Digest): Result[void, BadProposal] =
## Add a block to the slashing protection DB - the registration will
## fail if it would violate a slashing protection rule.
##
## block_signing_root is the output of
## compute_signing_root(block, domain)
db.updateVersions(
registerBlock(db_version, validator, slot, block_signing_root)
)
registerBlock(db.db_v2, some(index), validator, slot, block_signing_root)
proc registerAttestation*(
db: SlashingProtectionDB,
index: ValidatorIndex,
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: Eth2Digest): Result[void, BadVote] =
## Add an attestation to the slashing protection DB - the registration will
## fail if it would violate a slashing protection rule.
##
## attestation_signing_root is the output of
## compute_signing_root(attestation, domain)
db.updateVersions(
registerAttestation(db_version, validator,
source, target, attestation_signing_root)
)
registerAttestation(db.db_v2, some(index), validator,
source, target, attestation_signing_root)
# DB maintenance
# --------------------------------------------
@ -366,41 +277,6 @@ proc pruneAfterFinalization*(
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
@ -409,7 +285,10 @@ proc inclSPDIR*(db: SlashingProtectionDB, spdir: SPDIR): SlashingImportStatus
# That builds on a DB backend inclSPDIR and toSPDIR
# SPDIR being a common Intermediate Representation
# Sanity check
# --------------------------------------------------------------
proc inclSPDIR*(db: SlashingProtectionDB, spdir: SPDIR): SlashingImportStatus
{.raises: [SerializationError, IOError, Defect].} =
db.db_v2.inclSPDIR(spdir)
static: doAssert SlashingProtectionDB is SlashingProtectionDB_Concept
proc toSPDIR*(db: SlashingProtectionDB): SPDIR
{.raises: [IOError, Defect].} =
db.db_v2.toSPDIR()

View File

@ -82,56 +82,6 @@ type
# 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, int64, int64)
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
@ -146,9 +96,8 @@ type
# 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)
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
@ -156,12 +105,13 @@ type
# 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* = object
BadVote* {.pure.} = object
case kind*: BadVoteKind
of DoubleVote:
existingAttestation*: Eth2Digest
of SurroundedVote, SurroundingVote:
of SurroundVote:
existingAttestationRoot*: Eth2Digest # Many roots might be in conflict
sourceExisting*, targetExisting*: Epoch
sourceSlashable*, targetSlashable*: Epoch
@ -173,12 +123,15 @@ type
of MinTargetViolation:
minTarget*: Epoch
candidateTarget*: Epoch
of BadVoteKind.DatabaseError:
message*: string
BadProposalKind* = enum
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
@ -187,6 +140,8 @@ type
of MinSlotViolation:
minSlot*: Slot
candidateSlot*: Slot
of BadProposalKind.DatabaseError:
message*: string
func `==`*(a, b: BadVote): bool =
## Comparison operator.
@ -194,24 +149,26 @@ func `==`*(a, b: BadVote): bool =
## 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
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
func `==`*(a, b: BadProposal): bool =
## Comparison operator.
@ -266,7 +223,7 @@ proc readValue*(r: var JsonReader, a: var (SlotString or EpochString))
raiseUnexpectedValue(r, "Integer in a string expected")
proc exportSlashingInterchange*(
db: SlashingProtectionDB_Concept,
db: auto,
path: string, prettify = true) {.raises: [Defect, IOError].} =
## Export a database to the Slashing Protection Database Interchange Format
let spdir = db.toSPDIR()
@ -274,7 +231,7 @@ proc exportSlashingInterchange*(
echo "Exported slashing protection DB to '", path, "'"
proc importSlashingInterchange*(
db: SlashingProtectionDB_Concept,
db: auto,
path: string): SlashingImportStatus {.raises: [Defect, IOError, SerializationError].} =
## Import a Slashing Protection Database Interchange Format
## into a Nimbus DB.
@ -307,7 +264,7 @@ chronicles.formatIt SPDIR_SignedAttestation: it.shortLog
# --------------------------------------------
proc importInterchangeV5Impl*(
db: SlashingProtectionDB_Concept,
db: auto,
spdir: var SPDIR
): SlashingImportStatus
{.raises: [SerializationError, IOError, Defect].} =
@ -359,8 +316,8 @@ proc importInterchangeV5Impl*(
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
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
@ -388,12 +345,6 @@ proc importInterchangeV5Impl*(
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)
@ -409,10 +360,11 @@ proc importInterchangeV5Impl*(
for a in 0 ..< spdir.data[v].signed_attestations.len:
template A: untyped = spdir.data[v].signed_attestations[a]
let status = db.checkSlashableAttestation(
let status = db.registerAttestation(
parsedKey,
A.source_epoch.Epoch,
A.target_epoch.Epoch
A.target_epoch.Epoch,
A.signing_root.Eth2Digest
)
if status.isErr():
# We might be importing a duplicate which EIP-3076 allows
@ -439,13 +391,6 @@ proc importInterchangeV5Impl*(
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
if maxValidSourceEpochSeen < 0 or maxValidTargetEpochSeen < 0:

View File

@ -531,7 +531,7 @@ proc checkSlashableAttestation*(
# s2 < s1 < t1 < t2
# Logged by caller
return err(BadVote(
kind: SurroundingVote,
kind: SurroundVote,
existingAttestationRoot: ar1,
sourceExisting: s1,
targetExisting: t1,
@ -542,7 +542,7 @@ proc checkSlashableAttestation*(
# s1 < s2 < t2 < t1
# Logged by caller
return err(BadVote(
kind: SurroundedVote,
kind: SurroundVote,
existingAttestationRoot: ar1,
sourceExisting: s1,
targetExisting: t1,
@ -587,10 +587,10 @@ proc registerValidator(db: SlashingProtectionDB_v1, validator: ValidatorPubKey)
proc registerBlock*(
db: SlashingProtectionDB_v1,
validator: ValidatorPubKey,
slot: Slot, block_root: Eth2Digest) =
slot: Slot, block_root: Eth2Digest): Result[void, BadProposal] =
## Add a block to the slashing protection DB
## `checkSlashableBlockProposal` MUST be run
## before to ensure no overwrite.
? checkSlashableBlockProposal(db, validator, slot)
let valID = validator.toRaw()
@ -620,7 +620,7 @@ proc registerBlock*(
# targetEpochs.isInit will be false
)
)
return
return ok()
var ll = maybeLL.unsafeGet()
var cur = ll.blockSlots.stop
@ -632,7 +632,7 @@ proc registerBlock*(
db.put(subkey(kBlock, valID, slot), node)
# TODO: what if crash here?
db.put(subkey(kLinkedListMeta, valID), ll)
return
return ok()
if cur < slot:
# Adding a block later than all known blocks
@ -652,7 +652,7 @@ proc registerBlock*(
db.put(subkey(kBlock, valID, cur), prevNode)
# TODO: what if crash here?
db.put(subkey(kLinkedListMeta, valID), ll)
return
return ok()
# TODO: we likely want a proper DB or better KV-store high-level API
# in the future.
@ -687,7 +687,7 @@ proc registerBlock*(
# TODO: what if crash here?
db.put(subkey(kBlock, valID, cur), curNode)
db.put(subkey(kLinkedListMeta, valID), ll)
return
return ok()
elif slot > curNode.prev:
# Reached: prev < slot < cur
# Change: prev <-> cur
@ -709,7 +709,7 @@ proc registerBlock*(
# TODO: what if crash here?
db.put(subkey(kBlock, valID, cur), curNode)
db.put(subkey(kBlock, valID, prev), prevNode)
return
return ok()
# Previous
cur = curNode.prev
@ -720,15 +720,19 @@ proc registerBlock*(
# ).expect("Consistent linked-list in DB")
).unsafeGet()
ok()
proc registerAttestation*(
db: SlashingProtectionDB_v1,
validator: ValidatorPubKey,
source, target: Epoch,
attestation_root: Eth2Digest) =
attestation_root: Eth2Digest): Result[void, BadVote] =
## Add an attestation to the slashing protection DB
## `checkSlashableAttestation` MUST be run
## before to ensure no overwrite.
? checkSlashableAttestation(db, validator, source, target)
let valID = validator.toRaw()
# We want to keep the linked-list ordered
@ -759,7 +763,7 @@ proc registerAttestation*(
targetEpochs: EpochDesc(start: target, stop: target, isInit: true)
)
)
return
return ok()
var ll = maybeLL.unsafeGet()
var cur = ll.targetEpochs.stop
@ -773,7 +777,7 @@ proc registerAttestation*(
db.put(subkey(kTargetEpoch, valID, target), node)
# TODO: what if crash here?
db.put(subkey(kLinkedListMeta, valID), ll)
return
return ok()
block: # Update source epoch
if ll.sourceEpochs.stop < source:
@ -800,7 +804,7 @@ proc registerAttestation*(
db.put(subkey(kTargetEpoch, valID, cur), prevNode)
# TODO: what if crash here?
db.put(subkey(kLinkedListMeta, valID), ll)
return
return ok()
# TODO: we likely want a proper DB or better KV-store high-level API
# in the future.
@ -835,7 +839,7 @@ proc registerAttestation*(
# TODO: what if crash here?
db.put(subkey(kTargetEpoch, valID, cur), curNode)
db.put(subkey(kLinkedListMeta, valID), ll)
return
return ok()
elif target > curNode.prev:
# Reached: prev < target < cur
# Change: prev <-> cur
@ -858,7 +862,7 @@ proc registerAttestation*(
# TODO: what if crash here?
db.put(subkey(kTargetEpoch, valID, cur), curNode)
db.put(subkey(kTargetEpoch, valID, prev), prevNode)
return
return ok()
# Previous
cur = curNode.prev
@ -869,6 +873,8 @@ proc registerAttestation*(
# ).expect("Consistent linked-list in DB")
).unsafeGet()
ok()
# Debug tools
# --------------------------------------------
@ -1141,8 +1147,3 @@ proc inclSPDIR*(db: SlashingProtectionDB_v1, spdir: SPDIR): SlashingImportStatus
# Create a mutable copy for sorting
var spdir = spdir
return db.importInterchangeV5Impl(spdir)
# Sanity check
# --------------------------------------------------------------
static: doAssert SlashingProtectionDB_v1 is SlashingProtectionDB_Concept

View File

@ -9,7 +9,7 @@
import
# Standard library
std/[os, options, typetraits, decls],
std/[os, options, typetraits, decls, tables],
# Status
stew/byteutils,
eth/db/[kvstore, kvstore_sqlite3],
@ -211,12 +211,13 @@ type
# Cached queries - read
sqlGetValidatorInternalID: SqliteStmt[PubKeyBytes, ValidatorInternalID]
sqlAttForSameTargetEpoch: SqliteStmt[(ValidatorInternalID, int64), Hash32]
sqlAttSurrounded: SqliteStmt[(ValidatorInternalID, int64, int64), (int64, int64, Hash32)]
sqlAttSurrounding: SqliteStmt[(ValidatorInternalID, int64, int64), (int64, int64, Hash32)]
sqlAttSurrounds: SqliteStmt[(ValidatorInternalID, int64, int64, int64, int64), (int64, int64, Hash32)]
sqlAttMinSourceTargetEpochs: SqliteStmt[ValidatorInternalID, (int64, int64)]
sqlBlockForSameSlot: SqliteStmt[(ValidatorInternalID, int64), Hash32]
sqlBlockMinSlot: SqliteStmt[ValidatorInternalID, int64]
internalIds: Table[ValidatorIndex, ValidatorInternalID]
ValidatorInternalID = int32
## Validator internal ID in the DB
## This is cached to cost querying cost
@ -382,30 +383,17 @@ proc setupCachedQueries(db: SlashingProtectionDB_v2) =
""", (ValidatorInternalID, int64), Hash32
).get()
db.sqlAttSurrounded = db.backend.prepareStmt("""
db.sqlAttSurrounds = db.backend.prepareStmt("""
SELECT
source_epoch, target_epoch, signing_root
FROM
signed_attestations
WHERE 1=1
and validator_id = ?
and source_epoch < ?
and ? < target_epoch
and ((source_epoch < ? and ? < target_epoch) OR
(? < source_epoch and target_epoch < ?))
LIMIT 1
""", (ValidatorInternalID, int64, int64), (int64, int64, Hash32)
).get()
db.sqlAttSurrounding = db.backend.prepareStmt("""
SELECT
source_epoch, target_epoch, signing_root
FROM
signed_attestations
WHERE 1=1
and validator_id = ?
and ? < source_epoch
and target_epoch < ?
LIMIT 1
""", (ValidatorInternalID, int64, int64), (int64, int64, Hash32)
""", (ValidatorInternalID, int64, int64, int64, int64), (int64, int64, Hash32)
).get()
# By default an aggregate always return a value
@ -680,8 +668,20 @@ proc foundAnyResult(status: KVResult[bool]): bool {.inline.}=
proc getValidatorInternalID(
db: SlashingProtectionDB_v2,
index: Option[ValidatorIndex],
validator: ValidatorPubKey): Option[ValidatorInternalID] =
## Retrieve a validator internal ID
if index.isSome():
# Validator keys are mapped to internal id:s instead of using the
# validator index - this allows importing keys without knowing the
# state but has the unfortunate consequence of introducing an indirection
# that must be kept updated at some cost. In a future version of the
# database, one could consider a simplified design that directly uses the
# validator index. In the meantime, this cache avoids some of the
# unnecessary read traffic when checking and registering entries.
db.internalIds.withValue(index.get(), internal) do:
return some(internal[])
let serializedPubkey = validator.toRaw() # Miracl/BLST to bytes
var valID: ValidatorInternalID
let status = db.sqlGetValidatorInternalID.exec(serializedPubkey) do (res: ValidatorInternalID):
@ -689,13 +689,15 @@ proc getValidatorInternalID(
# Note: we enforce at the DB level that if the pubkey exists it is unique.
if status.foundAnyResult():
if index.isSome():
db.internalIds[index.get()] = valID
some(valID)
else:
none(ValidatorInternalID)
proc checkSlashableBlockProposal*(
proc checkSlashableBlockProposalOther(
db: SlashingProtectionDB_v2,
validator: ValidatorPubKey,
valID: ValidatorInternalID,
slot: Slot
): Result[void, BadProposal] =
## Returns an error if the specified validator
@ -706,49 +708,6 @@ proc checkSlashableBlockProposal*(
## Returns success otherwise
# TODO distinct type for the result block root
let valID = block:
let id = db.getValidatorInternalID(validator)
if id.isNone():
notice "No slashing protection data - first block proposal?",
validator = validator,
slot = slot
return ok()
else:
id.unsafeGet()
# Casper FFG 1st slashing condition
# Detect h(t1) = h(t2)
# ---------------------------------
block:
# Condition 1 at https://eips.ethereum.org/EIPS/eip-3076
var root: ETH2Digest
# 6 second (minimal preset) slots => overflow at ~1.75 trillion years under
# minimal preset, and twice that with mainnet preset
doAssert slot <= high(int64).uint64
let status = db.sqlBlockForSameSlot.exec(
(valID, int64 slot)
) do (res: Hash32):
root.data = res
# Note: we enforce at the DB level that if (pubkey, slot) exists it maps to a unique block root.
#
# It's possible to allow republishing an already signed block here (Lighthouse does it)
# AFAIK repeat signing only happens if the node crashes after saving to the DB and
# there is still time to redo the validator work but:
# - will the validator have reconstructed the same state in memory?
# for example if the validator has different attestations
# it can't reconstruct the previous signed block anyway.
# - it is useful if the validator couldn't gossip.
# Rather than adding Result "Ok" and Result "OkRepeatSigning"
# and an extra Eth2Digest comparison for that case, we just refuse repeat signing.
if status.foundAnyResult():
# Conflicting block exist
return err(BadProposal(
kind: DoubleProposal,
existing_block: root))
# EIP-3067 - Low-watermark
# Detect h(t1) <= h(t2)
# ---------------------------------
@ -778,38 +737,88 @@ proc checkSlashableBlockProposal*(
ok()
proc checkSlashableAttestation*(
proc checkSlashableBlockProposalDoubleProposal(
db: SlashingProtectionDB_v2,
validator: ValidatorPubKey,
source: Epoch,
target: Epoch
): Result[void, BadVote] =
valID: ValidatorInternalID,
slot: Slot
): Result[void, BadProposal] =
## 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).
## 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 attestation root
# TODO distinct type for the result block root
# Casper FFG 1st slashing condition
# Detect h(t1) = h(t2)
# ---------------------------------
block:
# Condition 1 at https://eips.ethereum.org/EIPS/eip-3076
var root: ETH2Digest
let status = db.sqlBlockForSameSlot.exec(
(valID, int64 slot)
) do (res: Hash32):
root.data = res
# Note: we enforce at the DB level that if (pubkey, slot) exists it maps to a unique block root.
#
# It's possible to allow republishing an already signed block here (Lighthouse does it)
# AFAIK repeat signing only happens if the node crashes after saving to the DB and
# there is still time to redo the validator work but:
# - will the validator have reconstructed the same state in memory?
# for example if the validator has different attestations
# it can't reconstruct the previous signed block anyway.
# - it is useful if the validator couldn't gossip.
# Rather than adding Result "Ok" and Result "OkRepeatSigning"
# and an extra Eth2Digest comparison for that case, we just refuse repeat signing.
if status.foundAnyResult():
# Conflicting block exist
return err(BadProposal(
kind: DoubleProposal,
existing_block: root))
ok()
proc checkSlashableBlockProposal*(
db: SlashingProtectionDB_v2,
index: Option[ValidatorIndex],
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 = block:
let id = db.getValidatorInternalID(index, validator)
if id.isNone():
notice "No slashing protection data - first block proposal?",
validator = validator,
slot = slot
return ok()
else:
id.unsafeGet()
? checkSlashableBlockProposalDoubleProposal(db, valID, slot)
? checkSlashableBlockProposalOther(db, valID, slot)
ok()
proc checkSlashableAttestationDoubleVote(
db: SlashingProtectionDB_v2,
valID: ValidatorInternalID,
source: Epoch,
target: Epoch): Result[void, BadVote] =
# Sanity
# ---------------------------------
if source > target:
return err(BadVote(kind: TargetPrecedesSource))
# Internal metadata
# ---------------------------------
let valID = block:
let id = db.getValidatorInternalID(validator)
if id.isNone():
notice "No slashing protection data - first attestation?",
validator = validator,
attSource = source,
attTarget = target
return ok()
else:
id.unsafeGet()
# Casper FFG 1st slashing condition
# Detect h(t1) = h(t2)
# ---------------------------------
@ -833,12 +842,38 @@ proc checkSlashableAttestation*(
existingAttestation: root
))
ok()
proc checkSlashableAttestationOther(
db: SlashingProtectionDB_v2,
valID: ValidatorInternalID,
source: Epoch,
target: Epoch): Result[void, BadVote] =
# Simple double votes are protected by the unique index on the database table
# - this function checks everything else!
## 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
# Sanity
# ---------------------------------
if source > target:
return err(BadVote(kind: TargetPrecedesSource))
# Casper FFG 2nd slashing condition
# -> Surrounded vote
# Detect h(s1) < h(s2) < h(t2) < h(t1)
# -> Surrounding vote
# Detect h(s2) < h(s1) < h(t1) < h(t2)
# ---------------------------------
block:
# Condition 3 part 2/3 at https://eips.ethereum.org/EIPS/eip-3076
# Condition 3 part 3/3 at https://eips.ethereum.org/EIPS/eip-3076
var root: ETH2Digest
var db_source, db_target: Epoch
@ -846,8 +881,8 @@ proc checkSlashableAttestation*(
doAssert source <= high(int64).uint64
doAssert target <= high(int64).uint64
let status = db.sqlAttSurrounded.exec(
(valID, int64 source, int64 target)
let status = db.sqlAttSurrounds.exec(
(valID, int64 source, int64 target, int64 source, int64 target)
) do (res: tuple[source, target: int64, root: Hash32]):
db_source = Epoch res.source
db_target = Epoch res.target
@ -858,35 +893,7 @@ proc checkSlashableAttestation*(
# Conflicting attestation exist, log by caller
# s1 < s2 < t2 < t1
return err(BadVote(
kind: SurroundedVote,
existingAttestationRoot: root,
sourceExisting: db_source,
targetExisting: db_target,
sourceSlashable: source,
targetSlashable: target
))
# Casper FFG 2nd slashing condition
# -> Surrounding vote
# Detect h(s2) < h(s1) < h(t1) < h(t2)
# ---------------------------------
block:
# Condition 3 part 3/3 at https://eips.ethereum.org/EIPS/eip-3076
var root: ETH2Digest
var db_source, db_target: Epoch
let status = db.sqlAttSurrounding.exec(
(valID, int64 source, int64 target)
) do (res: tuple[source, target: int64, root: Hash32]):
db_source = Epoch res.source
db_target = Epoch res.target
root.data = res.root
# Note: we enforce at the DB level that if (pubkey, target) exists it maps to a unique block root.
if status.foundAnyResult():
# Conflicting attestation exist, log by caller
# s1 < s2 < t2 < t1
return err(BadVote(
kind: SurroundingVote,
kind: SurroundVote,
existingAttestationRoot: root,
sourceExisting: db_source,
targetExisting: db_target,
@ -933,7 +940,31 @@ proc checkSlashableAttestation*(
candidateTarget: target
))
return ok()
ok()
proc checkSlashableAttestation*(
db: SlashingProtectionDB_v2,
index: Option[ValidatorIndex],
validator: ValidatorPubKey,
source: Epoch,
target: Epoch
): Result[void, BadVote] =
if source > target:
return err(BadVote(kind: TargetPrecedesSource))
let valID = block:
let id = db.getValidatorInternalID(index, validator)
if id.isNone():
notice "No slashing protection data - first attestation?",
validator, source, target
return ok()
else:
id.unsafeGet()
? checkSlashableAttestationDoubleVote(db, valID, source, target)
? checkSlashableAttestationOther(db, valID, source, target)
ok()
# DB update
# --------------------------------------------
@ -948,16 +979,17 @@ proc registerValidator(db: SlashingProtectionDB_v2, validator: ValidatorPubKey)
proc getOrRegisterValidator(
db: SlashingProtectionDB_v2,
index: Option[ValidatorIndex],
validator: ValidatorPubKey): ValidatorInternalID =
## Get validator from the database
## or register it and then return it
let id = db.getValidatorInternalID(validator)
let id = db.getValidatorInternalID(index, validator)
if id.isNone():
info "No slashing protection data for validator - initiating",
validator = validator
db.registerValidator(validator)
let id = db.getValidatorInternalID(validator)
let id = db.getValidatorInternalID(index, validator)
doAssert id.isSome()
id.unsafeGet()
else:
@ -965,33 +997,65 @@ proc getOrRegisterValidator(
proc registerBlock*(
db: SlashingProtectionDB_v2,
index: Option[ValidatorIndex],
validator: ValidatorPubKey,
slot: Slot, block_root: Eth2Digest) =
slot: Slot, block_root: Eth2Digest): Result[void, BadProposal] =
## Add a block to the slashing protection DB
## `checkSlashableBlockProposal` MUST be run
## before to ensure no overwrite.
let valID = db.getOrRegisterValidator(validator)
let valID = db.getOrRegisterValidator(index, validator)
# 6 second (minimal preset) slots => overflow at ~1.75 trillion years under
# minimal preset, and twice that with mainnet preset
doAssert slot <= high(int64).uint64
let check = checkSlashableBlockProposalOther(db, valID, slot)
if check.isErr():
# Check for double vote to get more accurate error information
? checkSlashableBlockProposalDoubleProposal(db, valID, slot)
return check
let status = db.sqlInsertBlock.exec(
(valID, int64 slot,
block_root.data))
doAssert status.isOk(),
"SQLite error when registering block: " & $status.error & "\n" &
"for validator: 0x" & validator.toHex() & ", slot: " & $slot
(valID, int64 slot, block_root.data))
if status.isErr():
# Inserting primarily fails when the constraint for double proposals is
# violated but may also happen due to disk full and other storage issues -
# in any case, we'll return an error so that production is halted
? checkSlashableBlockProposalDoubleProposal(db, valID, slot)
# If this was not a slashing error, it must have been a database error
return err(BadProposal(
kind: BadProposalKind.DatabaseError,
message: status.error))
ok()
proc registerBlock*(
db: SlashingProtectionDB_v2,
validator: ValidatorPubKey,
slot: Slot, block_root: Eth2Digest): Result[void, BadProposal] =
registerBlock(db, none(ValidatorIndex), validator, slot, block_root)
proc registerAttestation*(
db: SlashingProtectionDB_v2,
index: Option[ValidatorIndex],
validator: ValidatorPubKey,
source, target: Epoch,
attestation_root: Eth2Digest) =
attestation_root: Eth2Digest): Result[void, BadVote] =
## Add an attestation to the slashing protection DB
## `checkSlashableAttestation` MUST be run
## before to ensure no overwrite.
let valID = db.getOrRegisterValidator(validator)
if source > target:
return err(BadVote(kind: TargetPrecedesSource))
let valID = db.getOrRegisterValidator(index, validator)
# Double votes caught by database index!
let check = checkSlashableAttestationOther(db, valID, source, target)
if check.isErr():
# Check for double vote to get more accurate error information
? checkSlashableAttestationDoubleVote(db, valID, source, target)
return check
# Overflows in 14 trillion years (minimal) or 112 trillion years (mainnet)
doAssert source <= high(int64).uint64
@ -1000,27 +1064,49 @@ proc registerAttestation*(
let status = db.sqlInsertAtt.exec(
(valID, int64 source, int64 target,
attestation_root.data))
doAssert status.isOk(),
"SQLite error when registering attestation: " & $status.error & "\n" &
"for validator: 0x" & validator.toHex() &
", sourceEpoch: " & $source &
", targetEpoch: " & $target
if status.isErr():
# Inserting primarily fails when the constraint for double votes is
# violated but may also happen due to disk full and other storage issues -
# in any case, we'll return an error so that production is halted
? checkSlashableAttestationDoubleVote(db, valID, source, target)
# If this was not a slashing error, it must have been a database error
return err(BadVote(
kind: BadVoteKind.DatabaseError,
message: status.error))
ok()
proc registerAttestation*(
db: SlashingProtectionDB_v2,
validator: ValidatorPubKey,
source, target: Epoch,
attestation_root: Eth2Digest): Result[void, BadVote] =
registerAttestation(
db, none(ValidatorIndex), validator, source, target, attestation_root)
# DB maintenance
# --------------------------------------------
proc pruneBlocks*(db: SlashingProtectionDB_v2, validator: ValidatorPubkey, newMinSlot: Slot) =
proc pruneBlocks*(
db: SlashingProtectionDB_v2,
index: Option[ValidatorIndex],
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.
let valID = db.getOrRegisterValidator(validator)
let valID = db.getOrRegisterValidator(index, validator)
let status = db.sqlPruneValidatorBlocks.exec(
(valID, int64 newMinSlot))
doAssert status.isOk(),
"SQLite error when pruning validator blocks: " & $status.error & "\n" &
"for validator: 0x" & validator.toHex() & ", newMinSlot: " & $newMinSlot
proc pruneBlocks*(
db: SlashingProtectionDB_v2,
validator: ValidatorPubkey, newMinSlot: Slot) =
pruneBlocks(db, none(ValidatorIndex), validator, newMinSlot)
proc pruneAttestations*(
db: SlashingProtectionDB_v2,
index: Option[ValidatorIndex],
validator: ValidatorPubkey,
newMinSourceEpoch: int64,
newMinTargetEpoch: int64) =
@ -1028,7 +1114,7 @@ proc pruneAttestations*(
## This is intended for interchange import.
## Negative source/target epoch of -1 can be received if no attestation was imported
## In that case nothing is done (since we used signed int in SQLite)
let valID = db.getOrRegisterValidator(validator)
let valID = db.getOrRegisterValidator(index, validator)
let status = db.sqlPruneValidatorAttestations.exec(
(valID, newMinSourceEpoch, newMinTargetEpoch))
@ -1038,6 +1124,14 @@ proc pruneAttestations*(
", newSourceEpoch: " & $newMinSourceEpoch &
", newTargetEpoch: " & $newMinTargetEpoch
proc pruneAttestations*(
db: SlashingProtectionDB_v2,
validator: ValidatorPubkey,
newMinSourceEpoch: int64,
newMinTargetEpoch: int64) =
pruneAttestations(
db, none(ValidatorIndex), validator, newMinSourceEpoch, newMinTargetEpoch)
proc pruneAfterFinalization*(
db: SlashingProtectionDB_v2,
finalizedEpoch: Epoch
@ -1205,8 +1299,3 @@ proc inclSPDIR*(db: SlashingProtectionDB_v2, spdir: SPDIR): SlashingImportStatus
# Create a mutable copy for sorting
var spdir = spdir
return db.importInterchangeV5Impl(spdir)
# Sanity check
# --------------------------------------------------------------
static: doAssert SlashingProtectionDB_v2 is SlashingProtectionDB_Concept

View File

@ -338,27 +338,18 @@ proc proposeBlock(node: BeaconNode,
slot = shortLog(slot)
return head
let notSlashable = node.attachedValidators
.slashingProtection
.checkSlashableBlockProposal(validator.pubkey, slot)
if notSlashable.isErr:
warn "Slashing protection activated",
validator = validator.pubkey,
slot = slot,
existingProposal = notSlashable.error
return head
let
fork = getStateField(node.chainDag.headState, fork)
genesis_validators_root =
getStateField(node.chainDag.headState, genesis_validators_root)
let
randao = await validator.genRandaoReveal(
fork, genesis_validators_root, slot)
message = makeBeaconBlockForHeadAndSlot(
node, randao, validator_index, node.graffitiBytes, head, slot)
if not message.isSome():
return head # already logged elsewhere!
var
newBlock = SignedBeaconBlock(
message: message.get()
@ -369,9 +360,16 @@ proc proposeBlock(node: BeaconNode,
# TODO: recomputed in block proposal
let signing_root = compute_block_root(
fork, genesis_validators_root, slot, newBlock.root)
node.attachedValidators
let notSlashable = node.attachedValidators
.slashingProtection
.registerBlock(validator.pubkey, slot, signing_root)
.registerBlock(validator_index, validator.pubkey, slot, signing_root)
if notSlashable.isErr:
warn "Slashing protection activated",
validator = validator.pubkey,
slot = slot,
existingProposal = notSlashable.error
return head
newBlock.signature = await validator.signBlockProposal(
fork, genesis_validators_root, slot, newBlock.root)
@ -409,7 +407,7 @@ proc handleAttestations(node: BeaconNode, head: BlockRef, slot: Slot) =
var attestations: seq[tuple[
data: AttestationData, committeeLen, indexInCommittee: int,
validator: AttachedValidator]]
validator: AttachedValidator, validator_index: ValidatorIndex]]
# We need to run attestations exactly for the slot that we're attesting to.
# In case blocks went missing, this means advancing past the latest block
@ -429,34 +427,28 @@ proc handleAttestations(node: BeaconNode, head: BlockRef, slot: Slot) =
let committee = get_beacon_committee(
epochRef, slot, committee_index.CommitteeIndex)
for index_in_committee, validatorIdx in committee:
let validator = node.getAttachedValidator(epochRef, validatorIdx)
for index_in_committee, validator_index in committee:
let validator = node.getAttachedValidator(epochRef, validator_index)
if validator != nil:
let ad = makeAttestationData(
epochRef, attestationHead, committee_index.CommitteeIndex)
attestations.add((ad, committee.len, index_in_committee, validator))
attestations.add(
(ad, committee.len, index_in_committee, validator, validator_index))
for a in attestations:
# TODO signing_root is recomputed in produceAndSignAttestation/signAttestation just after
let signing_root = compute_attestation_root(
fork, genesis_validators_root, a.data)
let notSlashable = node.attachedValidators
.slashingProtection
.checkSlashableAttestation(
a.validator.pubkey,
a.data.source.epoch,
a.data.target.epoch)
.slashingProtection
.registerAttestation(
a.validator_index,
a.validator.pubkey,
a.data.source.epoch,
a.data.target.epoch,
signing_root
)
if notSlashable.isOk():
# TODO signing_root is recomputed in produceAndSignAttestation/signAttestation just after
let signing_root = compute_attestation_root(
fork, genesis_validators_root, a.data)
node.attachedValidators
.slashingProtection
.registerAttestation(
a.validator.pubkey,
a.data.source.epoch,
a.data.target.epoch,
signing_root
)
traceAsyncErrors createAndSendAttestation(
node, fork, genesis_validators_root, a.validator, a.data,
a.committeeLen, a.indexInCommittee, num_active_validators)

View File

@ -11,7 +11,7 @@ import
# Standard library
std/[os],
# Status lib
eth/db/kvstore,
eth/db/[kvstore, kvstore_sqlite3],
stew/results,
nimcrypto/utils,
serialization,
@ -25,25 +25,6 @@ import
# 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() =
test 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)
@ -59,7 +40,7 @@ suite "Slashing Protection DB - v1 and v2 migration" & preset():
# https://eips.ethereum.org/EIPS/eip-3076
sqlite3db_delete(TestDir, TestDbName)
wrappedTimedTest "Minimal format migration" & preset():
test "Minimal format migration" & preset():
let genesis_validators_root = hexToDigest"0x04700007fabc8282644aed6d1c7c9e21d38a03a0c4ba193f3afe428824b3a673"
block: # export from a v1 DB
let db = SlashingProtectionDB_v1.init(
@ -71,18 +52,19 @@ suite "Slashing Protection DB - v1 and v2 migration" & preset():
let pubkey = ValidatorPubKey
.fromHex"0xb845089a1457f811bfc000588fbb4e713669be8ce060ea6be3c6ece09afc3794106c91ca73acda5e5457122d58723bed"
.get()
db.registerBlock(
pubkey,
Slot 81952,
Eth2Digest()
)
check:
db.registerBlock(
pubkey,
Slot 81952,
Eth2Digest()
).isOk()
db.registerAttestation(
pubkey,
source = Epoch 2290,
target = Epoch 3007,
Eth2Digest()
)
db.registerAttestation(
pubkey,
source = Epoch 2290,
target = Epoch 3007,
Eth2Digest()
).isOk()
let spdir = db.toSPDIR_lowWatermark()
Json.saveFile(

View File

@ -13,7 +13,7 @@ import
nimcrypto/utils,
chronicles,
# Internal
../../beacon_chain/validators/slashing_protection,
../../beacon_chain/validators/[slashing_protection, slashing_protection_v2],
../../beacon_chain/spec/[datatypes, digest, crypto, presets],
# Test utilies
../testutil, ../testdbutil,
@ -174,7 +174,7 @@ proc runTest(identifier: string) =
" " & $status & "\n"
for blck in step.blocks:
let status = db.checkSlashableBlockProposal(
let status = db.db_v2.checkSlashableBlockProposal(none(ValidatorIndex),
ValidatorPubKey.fromRaw(blck.pubkey.PubKeyBytes).get(),
Slot blck.slot
)
@ -190,7 +190,7 @@ proc runTest(identifier: string) =
" for " & $toHexLogs(blck)
for att in step.attestations:
let status = db.checkSlashableAttestation(
let status = db.db_v2.checkSlashableAttestation(none(ValidatorIndex),
ValidatorPubKey.fromRaw(att.pubkey.PubKeyBytes).get(),
Epoch att.source_epoch,
Epoch att.target_epoch

View File

@ -11,11 +11,11 @@ import
# Standard library
std/[os],
# Status lib
eth/db/kvstore,
eth/db/[kvstore, kvstore_sqlite3],
stew/results,
nimcrypto/utils,
# Internal
../../beacon_chain/validators/slashing_protection,
../../beacon_chain/validators/[slashing_protection, slashing_protection_v2],
../../beacon_chain/spec/[datatypes, digest, crypto, presets],
# Test utilies
../testutil
@ -62,29 +62,30 @@ suite "Slashing Protection DB - Interchange" & preset():
let pubkey = ValidatorPubKey
.fromHex"0xb845089a1457f811bfc000588fbb4e713669be8ce060ea6be3c6ece09afc3794106c91ca73acda5e5457122d58723bed"
.get()
db.registerBlock(
pubkey,
Slot 81952,
hexToDigest"0x4ff6f743a43f3b4f95350831aeaf0a122a1a392922c45d804280284a69eb850b"
)
# db.registerBlock(
# pubkey,
# Slot 81951,
# fakeRoot(65535)
# )
check:
db.db_v2.registerBlock(
pubkey,
Slot 81952,
hexToDigest"0x4ff6f743a43f3b4f95350831aeaf0a122a1a392922c45d804280284a69eb850b"
).isOk()
# db.registerBlock(
# pubkey,
# Slot 81951,
# fakeRoot(65535)
# )
db.registerAttestation(
pubkey,
source = Epoch 2290,
target = Epoch 3007,
hexToDigest"0x587d6a4f59a58fe24f406e0502413e77fe1babddee641fda30034ed37ecc884d"
)
db.registerAttestation(
pubkey,
source = Epoch 2290,
target = Epoch 3008,
fakeRoot(65535)
)
db.db_v2.registerAttestation(
pubkey,
source = Epoch 2290,
target = Epoch 3007,
hexToDigest"0x587d6a4f59a58fe24f406e0502413e77fe1babddee641fda30034ed37ecc884d"
).isOk()
db.db_v2.registerAttestation(
pubkey,
source = Epoch 2290,
target = Epoch 3008,
fakeRoot(65535)
).isOk()
db.exportSlashingInterchange(currentSourcePath.parentDir/"test_complete_export_slashing_protection.json")

View File

@ -11,7 +11,7 @@ import
# Standard library
std/[os],
# Status lib
eth/db/kvstore,
eth/db/[kvstore, kvstore_sqlite3],
stew/results,
# Internal
../../beacon_chain/validators/slashing_protection,
@ -19,14 +19,6 @@ import
# 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() =
test name:
body
wrappedTest()
func fakeRoot(index: SomeInteger): Eth2Digest =
## Create fake roots
## Those are just the value serialized in big-endian
@ -57,7 +49,7 @@ const TestDbName = "test_slashprot"
# - (validator_id, slot)
suite "Slashing Protection DB" & preset():
wrappedTimedTest "Empty database" & preset():
test "Empty database" & preset():
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(
default(Eth2Digest),
@ -70,21 +62,24 @@ suite "Slashing Protection DB" & preset():
check:
db.checkSlashableBlockProposal(
ValidatorIndex(1234),
fakeValidator(1234),
slot = Slot 1
).isOk()
db.checkSlashableAttestation(
ValidatorIndex(1234),
fakeValidator(1234),
source = Epoch 1,
target = Epoch 2
).isOk()
db.checkSlashableAttestation(
ValidatorIndex(1234),
fakeValidator(1234),
source = Epoch 2,
target = Epoch 1
).error.kind == TargetPrecedesSource
wrappedTimedTest "SP for block proposal - linear append":
test "SP for block proposal - linear append":
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(
default(Eth2Digest),
@ -95,53 +90,86 @@ suite "Slashing Protection DB" & preset():
db.close()
sqlite3db_delete(TestDir, TestDbName)
db.registerBlock(
fakeValidator(100),
Slot 10,
fakeRoot(100)
)
db.registerBlock(
fakeValidator(111),
Slot 15,
fakeRoot(111)
)
check:
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 10,
fakeRoot(100)
).isOk()
db.registerBlock(
ValidatorIndex(111),
fakeValidator(111),
Slot 15,
fakeRoot(111)
).isOk()
# Slot occupied by same validator
db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
slot = Slot 10
).isErr()
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
slot = Slot 10,
fakeRoot(101)
).isErr()
# Slot occupied by another validator
db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
slot = Slot 15
).isOk()
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
slot = Slot 15,
fakeRoot(150)
).isOk()
# Slot occupied by same validator
db.checkSlashableBlockProposal(
ValidatorIndex(111),
fakeValidator(111),
slot = Slot 15
).isErr()
db.registerBlock(
ValidatorIndex(111),
fakeValidator(111),
slot = Slot 15,
fakeRoot(151)
).isErr()
# Slot inoccupied
db.checkSlashableBlockProposal(
ValidatorIndex(255),
fakeValidator(255),
slot = Slot 20
).isOk()
db.registerBlock(
fakeValidator(255),
slot = Slot 20,
fakeRoot(4321)
)
db.registerBlock(
ValidatorIndex(255),
fakeValidator(255),
slot = Slot 20,
fakeRoot(4321)
).isOk()
check:
# Slot now occupied
db.checkSlashableBlockProposal(
ValidatorIndex(255),
fakeValidator(255),
slot = Slot 20
).isErr()
db.registerBlock(
ValidatorIndex(255),
fakeValidator(255),
slot = Slot 20,
fakeRoot(4322)
).isErr()
wrappedTimedTest "SP for block proposal - backtracking append":
test "SP for block proposal - backtracking append":
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(
default(Eth2Digest),
@ -153,111 +181,129 @@ suite "Slashing Protection DB" & preset():
sqlite3db_delete(TestDir, TestDbName)
# last finalized block
db.registerBlock(
fakeValidator(0),
Slot 0,
fakeRoot(0)
)
check:
db.registerBlock(
ValidatorIndex(0),
fakeValidator(0),
Slot 0,
fakeRoot(0)
).isOk()
db.registerBlock(
fakeValidator(100),
Slot 10,
fakeRoot(10)
)
db.registerBlock(
fakeValidator(100),
Slot 20,
fakeRoot(20)
)
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 10,
fakeRoot(10)
).isOk()
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 20,
fakeRoot(20)
).isOk()
for i in 0 ..< 30:
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
if i > 10 and i != 20: # MinSlotViolation and DupSlot
let status = db.checkSlashableBlockProposal(
fakeValidator(100),
Slot i
)
doAssert status.isOk, "error: " & $status
else:
let status = db.checkSlashableBlockProposal(
fakeValidator(100),
Slot i
)
doAssert status.isErr, "error: " & $status
db.registerBlock(
fakeValidator(100),
Slot 15,
fakeRoot(15)
)
check:
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 15,
fakeRoot(15)
).isOk()
for i in 0 ..< 30:
if i > 10 and i notin {15, 20}: # MinSlotViolation and DupSlot
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
doAssert status.isOk, "error: " & $status
else:
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
doAssert status.isErr, "error: " & $status
check:
db.checkSlashableBlockProposal(
ValidatorIndex(0xDEADBEEF),
fakeValidator(0xDEADBEEF),
Slot i
).isOk()
db.registerBlock(
fakeValidator(100),
Slot 12,
fakeRoot(12)
)
db.registerBlock(
fakeValidator(100),
Slot 17,
fakeRoot(17)
)
check:
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 12,
fakeRoot(12)
).isOk()
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 17,
fakeRoot(17)
).isOk()
for i in 0 ..< 30:
if i > 10 and i notin {12, 15, 17, 20}:
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
doAssert status.isOk, "error: " & $status
else:
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
doAssert status.isErr, "error: " & $status
check:
db.checkSlashableBlockProposal(
ValidatorIndex(0xDEADBEEF),
fakeValidator(0xDEADBEEF),
Slot i
).isOk()
db.registerBlock(
fakeValidator(100),
Slot 29,
fakeRoot(29)
)
check:
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 29,
fakeRoot(29)
).isOk()
for i in 0 ..< 30:
if i > 10 and i notin {12, 15, 17, 20, 29}:
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
doAssert status.isOk, "error: " & $status
else:
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
doAssert status.isErr, "error: " & $status
check:
db.checkSlashableBlockProposal(
ValidatorIndex(0xDEADBEEF),
fakeValidator(0xDEADBEEF),
Slot i
).isOk()
wrappedTimedTest "SP for same epoch attestation target - linear append":
test "SP for same epoch attestation target - linear append":
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(
default(Eth2Digest),
@ -268,53 +314,81 @@ suite "Slashing Protection DB" & preset():
db.close()
sqlite3db_delete(TestDir, TestDbName)
db.registerAttestation(
fakeValidator(100),
Epoch 0, Epoch 10,
fakeRoot(100)
)
db.registerAttestation(
fakeValidator(111),
Epoch 0, Epoch 15,
fakeRoot(111)
)
check:
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 10,
fakeRoot(100)
).isOk()
db.registerAttestation(
ValidatorIndex(111),
fakeValidator(111),
Epoch 0, Epoch 15,
fakeRoot(111)
).isOk()
# Epoch occupied by same validator
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 10,
).error.kind == DoubleVote
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 10, fakeRoot(101)
).error.kind == DoubleVote
# Epoch occupied by another validator
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 15
).isOk()
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 15, fakeRoot(151)
).isOk()
# Epoch occupied by same validator
db.checkSlashableAttestation(
ValidatorIndex(111),
fakeValidator(111),
Epoch 0, Epoch 15
).error.kind == DoubleVote
db.registerAttestation(
ValidatorIndex(111),
fakeValidator(111),
Epoch 0, Epoch 15, fakeRoot(161)
).error.kind == DoubleVote
# Epoch inoccupied
db.checkSlashableAttestation(
ValidatorIndex(255),
fakeValidator(255),
Epoch 0, Epoch 20
).isOk()
db.registerAttestation(
ValidatorIndex(255),
fakeValidator(255),
Epoch 0, Epoch 20, fakeRoot(4321)
).isOk()
db.registerAttestation(
fakeValidator(255),
Epoch 0, Epoch 20,
fakeRoot(4321)
)
check:
# Epoch now occupied
db.checkSlashableAttestation(
ValidatorIndex(255),
fakeValidator(255),
Epoch 0, Epoch 20
).error.kind == DoubleVote
db.registerAttestation(
ValidatorIndex(255),
fakeValidator(255),
Epoch 0, Epoch 20, fakeRoot(4322)
).error.kind == DoubleVote
wrappedTimedTest "SP for surrounded attestations":
test "SP for surrounded attestations":
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(
@ -326,22 +400,26 @@ suite "Slashing Protection DB" & preset():
db.close()
sqlite3db_delete(TestDir, TestDbName)
db.registerAttestation(
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
)
check:
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
).isOk()
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 11, Epoch 19
).error.kind == SurroundedVote
).error.kind == SurroundVote
db.checkSlashableAttestation(
ValidatorIndex(200),
fakeValidator(200),
Epoch 11, Epoch 19
).isOk
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 11, Epoch 21
).isOk
@ -357,39 +435,44 @@ suite "Slashing Protection DB" & preset():
db.close()
sqlite3db_delete(TestDir, TestDbName)
db.registerAttestation(
fakeValidator(100),
Epoch 0, Epoch 1,
fakeRoot(1)
)
db.registerAttestation(
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
)
check:
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 1,
fakeRoot(1)
).isOk()
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
).isOk()
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 11, Epoch 19
).error.kind == SurroundedVote
).error.kind == SurroundVote
db.checkSlashableAttestation(
ValidatorIndex(200),
fakeValidator(200),
Epoch 11, Epoch 19
).isOk
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 11, Epoch 21
).isOk
# TODO: is that possible?
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 9, Epoch 19
).isOk
wrappedTimedTest "SP for surrounding attestations":
test "SP for surrounding attestations":
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(
@ -400,22 +483,24 @@ suite "Slashing Protection DB" & preset():
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
db.registerAttestation(
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
)
check:
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
).isOk()
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 9, Epoch 21
).error.kind == SurroundingVote
).error.kind == SurroundVote
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 21
).error.kind == SurroundingVote
).error.kind == SurroundVote
block:
sqlite3db_delete(TestDir, TestDbName)
@ -428,29 +513,34 @@ suite "Slashing Protection DB" & preset():
db.close()
sqlite3db_delete(TestDir, TestDbName)
db.registerAttestation(
fakeValidator(100),
Epoch 0, Epoch 1,
fakeRoot(1)
)
check:
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 1,
fakeRoot(1)
).isOk()
db.registerAttestation(
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
)
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
).isOk()
check:
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 9, Epoch 21
).error.kind == SurroundingVote
).error.kind == SurroundVote
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 21
).error.kind == SurroundingVote
).error.kind == SurroundVote
wrappedTimedTest "Attestation ordering #1698":
test "Attestation ordering #1698":
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(
@ -462,41 +552,47 @@ suite "Slashing Protection DB" & preset():
db.close()
sqlite3db_delete(TestDir, TestDbName)
db.registerAttestation(
fakeValidator(100),
Epoch 1, Epoch 2,
fakeRoot(2)
)
check:
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 1, Epoch 2,
fakeRoot(2)
).isOk()
db.registerAttestation(
fakeValidator(100),
Epoch 8, Epoch 10,
fakeRoot(10)
)
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 8, Epoch 10,
fakeRoot(10)
).isOk()
db.registerAttestation(
fakeValidator(100),
Epoch 14, Epoch 15,
fakeRoot(15)
)
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 14, Epoch 15,
fakeRoot(15)
).isOk()
# The current list is, 2 -> 10 -> 15
# The current list is, 2 -> 10 -> 15
db.registerAttestation(
fakeValidator(100),
Epoch 3, Epoch 6,
fakeRoot(6)
)
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 3, Epoch 6,
fakeRoot(6)
).isOk()
# The current list is 2 -> 6 -> 10 -> 15
# The current list is 2 -> 6 -> 10 -> 15
check:
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 7, Epoch 11
).error.kind == SurroundingVote
).error.kind == SurroundVote
wrappedTimedTest "Test valid attestation #1699":
test "Test valid attestation #1699":
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(
@ -508,20 +604,24 @@ suite "Slashing Protection DB" & preset():
db.close()
sqlite3db_delete(TestDir, TestDbName)
db.registerAttestation(
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
)
check:
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
).isOk()
db.registerAttestation(
fakeValidator(100),
Epoch 40, Epoch 50,
fakeRoot(50)
)
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 40, Epoch 50,
fakeRoot(50)
).isOk()
check:
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 20, Epoch 30
).isOk