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* {. slashingDbKind* {.
hidden hidden
defaultValue: SlashingDbKind.v2 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 name: "slashing-db-kind" }: SlashingDbKind
stateDbKind* {. stateDbKind* {.

View File

@ -322,33 +322,24 @@ proc init*(T: type BeaconNode,
attestationPool = newClone(AttestationPool.init(chainDag, quarantine)) attestationPool = newClone(AttestationPool.init(chainDag, quarantine))
exitPool = newClone(ExitPool.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 = slashingProtectionDB =
case config.slashingDbKind SlashingProtectionDB.init(
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(
getStateField(chainDag.headState, genesis_validators_root), getStateField(chainDag.headState, genesis_validators_root),
config.validatorsDir(), "slashing_protection" 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)) validatorPool = newClone(ValidatorPool.init(slashingProtectionDB))
consensusManager = ConsensusManager.new( consensusManager = ConsensusManager.new(

View File

@ -135,25 +135,25 @@ proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, a
if vc.proposalsForCurrentEpoch.contains slot: if vc.proposalsForCurrentEpoch.contains slot:
let public_key = vc.proposalsForCurrentEpoch[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 let notSlashable = vc.attachedValidators
.slashingProtection .slashingProtection
.checkSlashableBlockProposal(public_key, slot) .registerBlock(
newBlock.message.proposer_index.ValidatorIndex, public_key, slot,
signing_root)
if notSlashable.isOk: 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( newBlock.signature = await validator.signBlockProposal(
vc.fork, vc.beaconGenesis.genesis_validators_root, slot, newBlock.root) 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 validator = vc.attachedValidators.validators[a.public_key]
let ad = await vc.client.get_v1_validator_attestation_data(slot, a.committee_index) 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 let notSlashable = vc.attachedValidators
.slashingProtection .slashingProtection
.checkSlashableAttestation( .registerAttestation(
a.public_key, a.validator_index, a.public_key, ad.source.epoch, ad.target.epoch, signing_root)
ad.source.epoch,
ad.target.epoch)
if notSlashable.isOk(): 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... # TODO I don't like these (u)int64-to-int conversions...
let attestation = await validator.produceAndSignAttestation( let attestation = await validator.produceAndSignAttestation(
ad, a.committee_length.int, a.validator_committee_index, 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). # * 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. # 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 import
# stdlib # stdlib
@ -21,6 +20,7 @@ import
./slashing_protection_v2 ./slashing_protection_v2
export slashing_protection_common export slashing_protection_common
# Generic sandwich # Generic sandwich
export chronicles export chronicles
@ -52,8 +52,7 @@ type
## Database storing the blocks attested ## Database storing the blocks attested
## by validators attached to a beacon node ## by validators attached to a beacon node
## or validator client. ## or validator client.
db_v1: SlashingProtectionDB_v1 db_v2*: SlashingProtectionDB_v2
db_v2: SlashingProtectionDB_v2
modes: set[SlashProtDBMode] modes: set[SlashProtDBMode]
disagreementBehavior: DisagreementBehavior disagreementBehavior: DisagreementBehavior
@ -99,17 +98,27 @@ proc init*(
) )
result.db_v2 = db result.db_v2 = db
var db_v1: SlashingProtectionDB_v1
let rawdb = kvstore result.db_v2.getRawDBHandle() let rawdb = kvstore result.db_v2.getRawDBHandle()
if not rawdb.checkOrPutGenesis_DbV1(genesis_validators_root): if not rawdb.checkOrPutGenesis_DbV1(genesis_validators_root):
fatal "The slashing database refers to another chain/mainnet/testnet", fatal "The slashing database refers to another chain/mainnet/testnet",
path = basePath/dbname, path = basePath/dbname,
genesis_validators_root = genesis_validators_root genesis_validators_root = genesis_validators_root
result.db_v1.fromRawDB(rawdb) db_v1.fromRawDB(rawdb)
if requiresMigration: if requiresMigration:
info "Migrating local validators slashing DB from v1 to v2" info "Migrating local validators slashing DB from v1 to v2"
let spdir = result.db_v1.toSPDIR_lowWatermark() let spdir = try: db_v1.toSPDIR_lowWatermark()
let status = result.db_v2.inclSPDIR(spdir) 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 case status
of siSuccess: of siSuccess:
info "Slashing DB migration successful." info "Slashing DB migration successful."
@ -163,77 +172,9 @@ proc close*(db: SlashingProtectionDB) =
# DB Queries # 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*( proc checkSlashableBlockProposal*(
db: SlashingProtectionDB, db: SlashingProtectionDB,
index: ValidatorIndex,
validator: ValidatorPubKey, validator: ValidatorPubKey,
slot: Slot slot: Slot
): Result[void, BadProposal] = ): Result[void, BadProposal] =
@ -243,12 +184,11 @@ proc checkSlashableBlockProposal*(
## The error contains the blockroot that was already proposed ## The error contains the blockroot that was already proposed
## ##
## Returns success otherwise ## Returns success otherwise
db.queryVersions( checkSlashableBlockProposal(db.db_v2, some(index), validator, slot)
checkSlashableBlockProposal(db_version, validator, slot)
)
proc checkSlashableAttestation*( proc checkSlashableAttestation*(
db: SlashingProtectionDB, db: SlashingProtectionDB,
index: ValidatorIndex,
validator: ValidatorPubKey, validator: ValidatorPubKey,
source: Epoch, source: Epoch,
target: Epoch target: Epoch
@ -259,65 +199,36 @@ proc checkSlashableAttestation*(
## (surrounding vote or surrounded vote). ## (surrounding vote or surrounded vote).
## ##
## Returns success otherwise ## Returns success otherwise
db.queryVersions( checkSlashableAttestation(db.db_v2, some(index), validator, source, target)
checkSlashableAttestation(db_version, 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*( proc registerBlock*(
db: SlashingProtectionDB, db: SlashingProtectionDB,
index: ValidatorIndex,
validator: ValidatorPubKey, validator: ValidatorPubKey,
slot: Slot, block_signing_root: Eth2Digest) = slot: Slot, block_signing_root: Eth2Digest): Result[void, BadProposal] =
## Add a block to the slashing protection DB ## Add a block to the slashing protection DB - the registration will
## `checkSlashableBlockProposal` MUST be run ## fail if it would violate a slashing protection rule.
## before to ensure no overwrite.
## ##
## block_signing_root is the output of ## block_signing_root is the output of
## compute_signing_root(block, domain) ## compute_signing_root(block, domain)
db.updateVersions( registerBlock(db.db_v2, some(index), validator, slot, block_signing_root)
registerBlock(db_version, validator, slot, block_signing_root)
)
proc registerAttestation*( proc registerAttestation*(
db: SlashingProtectionDB, db: SlashingProtectionDB,
index: ValidatorIndex,
validator: ValidatorPubKey, validator: ValidatorPubKey,
source, target: Epoch, source, target: Epoch,
attestation_signing_root: Eth2Digest) = attestation_signing_root: Eth2Digest): Result[void, BadVote] =
## Add an attestation to the slashing protection DB ## Add an attestation to the slashing protection DB - the registration will
## `checkSlashableAttestation` MUST be run ## fail if it would violate a slashing protection rule.
## before to ensure no overwrite.
## ##
## attestation_signing_root is the output of ## attestation_signing_root is the output of
## compute_signing_root(attestation, domain) ## compute_signing_root(attestation, domain)
db.updateVersions( registerAttestation(db.db_v2, some(index), validator,
registerAttestation(db_version, validator, source, target, attestation_signing_root)
source, target, attestation_signing_root)
)
# DB maintenance # DB maintenance
# -------------------------------------------- # --------------------------------------------
@ -366,41 +277,6 @@ proc pruneAfterFinalization*(
fatal "Pruning is not implemented" fatal "Pruning is not implemented"
quit 1 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 # The high-level import/export functions are
# - importSlashingInterchange # - importSlashingInterchange
# - exportSlashingInterchange # - exportSlashingInterchange
@ -409,7 +285,10 @@ proc inclSPDIR*(db: SlashingProtectionDB, spdir: SPDIR): SlashingImportStatus
# That builds on a DB backend inclSPDIR and toSPDIR # That builds on a DB backend inclSPDIR and toSPDIR
# SPDIR being a common Intermediate Representation # 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 # 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 SlashingImportStatus* = enum
siSuccess siSuccess
siFailure siFailure
@ -146,9 +96,8 @@ type
# 2: candidate attestation # 2: candidate attestation
# Spec slashing condition # Spec slashing condition
DoubleVote # h(t1) == h(t2) DoubleVote # h(t1) == h(t2)
SurroundedVote # h(s1) < h(s2) < h(t2) < h(t1) SurroundVote # h(s1) < h(s2) < h(t2) < h(t1) or h(s2) < h(s1) < h(t1) < h(t2)
SurroundingVote # h(s2) < h(s1) < h(t1) < h(t2)
# Non-spec, should never happen in a well functioning client # Non-spec, should never happen in a well functioning client
TargetPrecedesSource # h(t1) < h(s1) - current epoch precedes last justified epoch 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) # EIP-3067 (https://eips.ethereum.org/EIPS/eip-3076)
MinSourceViolation # h(s2) < h(s1) - EIP3067 condition 4 (strict inequality) MinSourceViolation # h(s2) < h(s1) - EIP3067 condition 4 (strict inequality)
MinTargetViolation # h(t2) <= h(t1) - EIP3067 condition 5 MinTargetViolation # h(t2) <= h(t1) - EIP3067 condition 5
DatabaseError # Cannot read/write the slashing protection db
BadVote* = object BadVote* {.pure.} = object
case kind*: BadVoteKind case kind*: BadVoteKind
of DoubleVote: of DoubleVote:
existingAttestation*: Eth2Digest existingAttestation*: Eth2Digest
of SurroundedVote, SurroundingVote: of SurroundVote:
existingAttestationRoot*: Eth2Digest # Many roots might be in conflict existingAttestationRoot*: Eth2Digest # Many roots might be in conflict
sourceExisting*, targetExisting*: Epoch sourceExisting*, targetExisting*: Epoch
sourceSlashable*, targetSlashable*: Epoch sourceSlashable*, targetSlashable*: Epoch
@ -173,12 +123,15 @@ type
of MinTargetViolation: of MinTargetViolation:
minTarget*: Epoch minTarget*: Epoch
candidateTarget*: Epoch candidateTarget*: Epoch
of BadVoteKind.DatabaseError:
message*: string
BadProposalKind* = enum BadProposalKind* {.pure.} = enum
# Spec slashing condition # Spec slashing condition
DoubleProposal # h(t1) == h(t2) DoubleProposal # h(t1) == h(t2)
# EIP-3067 (https://eips.ethereum.org/EIPS/eip-3076) # EIP-3067 (https://eips.ethereum.org/EIPS/eip-3076)
MinSlotViolation # h(t2) <= h(t1) MinSlotViolation # h(t2) <= h(t1)
DatabaseError # Cannot read/write the slashing protection db
BadProposal* = object BadProposal* = object
case kind*: BadProposalKind case kind*: BadProposalKind
@ -187,6 +140,8 @@ type
of MinSlotViolation: of MinSlotViolation:
minSlot*: Slot minSlot*: Slot
candidateSlot*: Slot candidateSlot*: Slot
of BadProposalKind.DatabaseError:
message*: string
func `==`*(a, b: BadVote): bool = func `==`*(a, b: BadVote): bool =
## Comparison operator. ## Comparison operator.
@ -194,24 +149,26 @@ func `==`*(a, b: BadVote): bool =
## result of multiple DB versions ## result of multiple DB versions
if a.kind != b.kind: if a.kind != b.kind:
false false
elif a.kind == DoubleVote: else:
a.existingAttestation == b.existingAttestation case a.kind
elif a.kind in {SurroundedVote, SurroundingVote}: of DoubleVote:
(a.existingAttestationRoot == b.existingAttestationRoot) and a.existingAttestation == b.existingAttestation
(a.sourceExisting == b.sourceExisting) and of SurroundVote:
(a.targetExisting == b.targetExisting) and (a.existingAttestationRoot == b.existingAttestationRoot) and
(a.sourceSlashable == b.sourceSlashable) and (a.sourceExisting == b.sourceExisting) and
(a.targetSlashable == b.targetSlashable) (a.targetExisting == b.targetExisting) and
elif a.kind == TargetPrecedesSource: (a.sourceSlashable == b.sourceSlashable) and
true (a.targetSlashable == b.targetSlashable)
elif a.kind == MinSourceViolation: of TargetPrecedesSource:
(a.minSource == b.minSource) and true
(a.candidateSource == b.candidateSource) of MinSourceViolation:
elif a.kind == MinTargetViolation: (a.minSource == b.minSource) and
(a.minTarget == b.minTarget) and (a.candidateSource == b.candidateSource)
(a.candidateTarget == b.candidateTarget) of MinTargetViolation:
else: # Unreachable (a.minTarget == b.minTarget) and
false (a.candidateTarget == b.candidateTarget)
of BadVoteKind.DatabaseError:
true
func `==`*(a, b: BadProposal): bool = func `==`*(a, b: BadProposal): bool =
## Comparison operator. ## Comparison operator.
@ -266,7 +223,7 @@ proc readValue*(r: var JsonReader, a: var (SlotString or EpochString))
raiseUnexpectedValue(r, "Integer in a string expected") raiseUnexpectedValue(r, "Integer in a string expected")
proc exportSlashingInterchange*( proc exportSlashingInterchange*(
db: SlashingProtectionDB_Concept, db: auto,
path: string, prettify = true) {.raises: [Defect, IOError].} = path: string, prettify = true) {.raises: [Defect, IOError].} =
## Export a database to the Slashing Protection Database Interchange Format ## Export a database to the Slashing Protection Database Interchange Format
let spdir = db.toSPDIR() let spdir = db.toSPDIR()
@ -274,7 +231,7 @@ proc exportSlashingInterchange*(
echo "Exported slashing protection DB to '", path, "'" echo "Exported slashing protection DB to '", path, "'"
proc importSlashingInterchange*( proc importSlashingInterchange*(
db: SlashingProtectionDB_Concept, db: auto,
path: string): SlashingImportStatus {.raises: [Defect, IOError, SerializationError].} = path: string): SlashingImportStatus {.raises: [Defect, IOError, SerializationError].} =
## Import a Slashing Protection Database Interchange Format ## Import a Slashing Protection Database Interchange Format
## into a Nimbus DB. ## into a Nimbus DB.
@ -307,7 +264,7 @@ chronicles.formatIt SPDIR_SignedAttestation: it.shortLog
# -------------------------------------------- # --------------------------------------------
proc importInterchangeV5Impl*( proc importInterchangeV5Impl*(
db: SlashingProtectionDB_Concept, db: auto,
spdir: var SPDIR spdir: var SPDIR
): SlashingImportStatus ): SlashingImportStatus
{.raises: [SerializationError, IOError, Defect].} = {.raises: [SerializationError, IOError, Defect].} =
@ -359,8 +316,8 @@ proc importInterchangeV5Impl*(
for b in 0 ..< spdir.data[v].signed_blocks.len: for b in 0 ..< spdir.data[v].signed_blocks.len:
template B: untyped = spdir.data[v].signed_blocks[b] template B: untyped = spdir.data[v].signed_blocks[b]
let status = db.checkSlashableBlockProposal( let status = db.registerBlock(
parsedKey, B.slot.Slot parsedKey, B.slot.Slot, B.signing_root.Eth2Digest
) )
if status.isErr(): if status.isErr():
# We might be importing a duplicate which EIP-3076 allows # We might be importing a duplicate which EIP-3076 allows
@ -388,12 +345,6 @@ proc importInterchangeV5Impl*(
if B.slot.int > maxValidSlotSeen: if B.slot.int > maxValidSlotSeen:
maxValidSlotSeen = B.slot.int maxValidSlotSeen = B.slot.int
db.registerBlock(
parsedKey,
B.slot.Slot,
B.signing_root.Eth2Digest
)
# Now prune everything that predates # Now prune everything that predates
# this interchange file max slot # this interchange file max slot
db.pruneBlocks(parsedKey, Slot maxValidSlotSeen) db.pruneBlocks(parsedKey, Slot maxValidSlotSeen)
@ -409,10 +360,11 @@ proc importInterchangeV5Impl*(
for a in 0 ..< spdir.data[v].signed_attestations.len: for a in 0 ..< spdir.data[v].signed_attestations.len:
template A: untyped = spdir.data[v].signed_attestations[a] template A: untyped = spdir.data[v].signed_attestations[a]
let status = db.checkSlashableAttestation( let status = db.registerAttestation(
parsedKey, parsedKey,
A.source_epoch.Epoch, A.source_epoch.Epoch,
A.target_epoch.Epoch A.target_epoch.Epoch,
A.signing_root.Eth2Digest
) )
if status.isErr(): if status.isErr():
# We might be importing a duplicate which EIP-3076 allows # We might be importing a duplicate which EIP-3076 allows
@ -439,13 +391,6 @@ proc importInterchangeV5Impl*(
if A.target_epoch.int > maxValidTargetEpochSeen: if A.target_epoch.int > maxValidTargetEpochSeen:
maxValidTargetEpochSeen = A.target_epoch.int 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 # Now prune everything that predates
# this interchange file max slot # this interchange file max slot
if maxValidSourceEpochSeen < 0 or maxValidTargetEpochSeen < 0: if maxValidSourceEpochSeen < 0 or maxValidTargetEpochSeen < 0:

View File

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

View File

@ -9,7 +9,7 @@
import import
# Standard library # Standard library
std/[os, options, typetraits, decls], std/[os, options, typetraits, decls, tables],
# Status # Status
stew/byteutils, stew/byteutils,
eth/db/[kvstore, kvstore_sqlite3], eth/db/[kvstore, kvstore_sqlite3],
@ -211,12 +211,13 @@ type
# Cached queries - read # Cached queries - read
sqlGetValidatorInternalID: SqliteStmt[PubKeyBytes, ValidatorInternalID] sqlGetValidatorInternalID: SqliteStmt[PubKeyBytes, ValidatorInternalID]
sqlAttForSameTargetEpoch: SqliteStmt[(ValidatorInternalID, int64), Hash32] sqlAttForSameTargetEpoch: SqliteStmt[(ValidatorInternalID, int64), Hash32]
sqlAttSurrounded: SqliteStmt[(ValidatorInternalID, int64, int64), (int64, int64, Hash32)] sqlAttSurrounds: SqliteStmt[(ValidatorInternalID, int64, int64, int64, int64), (int64, int64, Hash32)]
sqlAttSurrounding: SqliteStmt[(ValidatorInternalID, int64, int64), (int64, int64, Hash32)]
sqlAttMinSourceTargetEpochs: SqliteStmt[ValidatorInternalID, (int64, int64)] sqlAttMinSourceTargetEpochs: SqliteStmt[ValidatorInternalID, (int64, int64)]
sqlBlockForSameSlot: SqliteStmt[(ValidatorInternalID, int64), Hash32] sqlBlockForSameSlot: SqliteStmt[(ValidatorInternalID, int64), Hash32]
sqlBlockMinSlot: SqliteStmt[ValidatorInternalID, int64] sqlBlockMinSlot: SqliteStmt[ValidatorInternalID, int64]
internalIds: Table[ValidatorIndex, ValidatorInternalID]
ValidatorInternalID = int32 ValidatorInternalID = int32
## Validator internal ID in the DB ## Validator internal ID in the DB
## This is cached to cost querying cost ## This is cached to cost querying cost
@ -382,30 +383,17 @@ proc setupCachedQueries(db: SlashingProtectionDB_v2) =
""", (ValidatorInternalID, int64), Hash32 """, (ValidatorInternalID, int64), Hash32
).get() ).get()
db.sqlAttSurrounded = db.backend.prepareStmt(""" db.sqlAttSurrounds = db.backend.prepareStmt("""
SELECT SELECT
source_epoch, target_epoch, signing_root source_epoch, target_epoch, signing_root
FROM FROM
signed_attestations signed_attestations
WHERE 1=1 WHERE 1=1
and validator_id = ? and validator_id = ?
and source_epoch < ? and ((source_epoch < ? and ? < target_epoch) OR
and ? < target_epoch (? < source_epoch and target_epoch < ?))
LIMIT 1 LIMIT 1
""", (ValidatorInternalID, int64, int64), (int64, int64, Hash32) """, (ValidatorInternalID, int64, int64, 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)
).get() ).get()
# By default an aggregate always return a value # By default an aggregate always return a value
@ -680,8 +668,20 @@ proc foundAnyResult(status: KVResult[bool]): bool {.inline.}=
proc getValidatorInternalID( proc getValidatorInternalID(
db: SlashingProtectionDB_v2, db: SlashingProtectionDB_v2,
index: Option[ValidatorIndex],
validator: ValidatorPubKey): Option[ValidatorInternalID] = validator: ValidatorPubKey): Option[ValidatorInternalID] =
## Retrieve a validator internal ID ## 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 let serializedPubkey = validator.toRaw() # Miracl/BLST to bytes
var valID: ValidatorInternalID var valID: ValidatorInternalID
let status = db.sqlGetValidatorInternalID.exec(serializedPubkey) do (res: 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. # Note: we enforce at the DB level that if the pubkey exists it is unique.
if status.foundAnyResult(): if status.foundAnyResult():
if index.isSome():
db.internalIds[index.get()] = valID
some(valID) some(valID)
else: else:
none(ValidatorInternalID) none(ValidatorInternalID)
proc checkSlashableBlockProposal*( proc checkSlashableBlockProposalOther(
db: SlashingProtectionDB_v2, db: SlashingProtectionDB_v2,
validator: ValidatorPubKey, valID: ValidatorInternalID,
slot: Slot slot: Slot
): Result[void, BadProposal] = ): Result[void, BadProposal] =
## Returns an error if the specified validator ## Returns an error if the specified validator
@ -706,49 +708,6 @@ proc checkSlashableBlockProposal*(
## Returns success otherwise ## Returns success otherwise
# TODO distinct type for the result block root # 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 # EIP-3067 - Low-watermark
# Detect h(t1) <= h(t2) # Detect h(t1) <= h(t2)
# --------------------------------- # ---------------------------------
@ -778,38 +737,88 @@ proc checkSlashableBlockProposal*(
ok() ok()
proc checkSlashableAttestation*( proc checkSlashableBlockProposalDoubleProposal(
db: SlashingProtectionDB_v2, db: SlashingProtectionDB_v2,
validator: ValidatorPubKey, valID: ValidatorInternalID,
source: Epoch, slot: Slot
target: Epoch ): Result[void, BadProposal] =
): Result[void, BadVote] =
## Returns an error if the specified validator ## Returns an error if the specified validator
## already voted for the specified slot ## already proposed a block for the specified slot.
## or would vote in a contradiction to previous votes ## This would lead to slashing.
## (surrounding vote or surrounded vote). ## The error contains the blockroot that was already proposed
## ##
## Returns success otherwise ## 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 # Sanity
# --------------------------------- # ---------------------------------
if source > target: if source > target:
return err(BadVote(kind: TargetPrecedesSource)) 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 # Casper FFG 1st slashing condition
# Detect h(t1) = h(t2) # Detect h(t1) = h(t2)
# --------------------------------- # ---------------------------------
@ -833,12 +842,38 @@ proc checkSlashableAttestation*(
existingAttestation: root 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 # Casper FFG 2nd slashing condition
# -> Surrounded vote # -> Surrounded vote
# Detect h(s1) < h(s2) < h(t2) < h(t1) # Detect h(s1) < h(s2) < h(t2) < h(t1)
# -> Surrounding vote
# Detect h(s2) < h(s1) < h(t1) < h(t2)
# --------------------------------- # ---------------------------------
block: block:
# Condition 3 part 2/3 at https://eips.ethereum.org/EIPS/eip-3076 # 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 root: ETH2Digest
var db_source, db_target: Epoch var db_source, db_target: Epoch
@ -846,8 +881,8 @@ proc checkSlashableAttestation*(
doAssert source <= high(int64).uint64 doAssert source <= high(int64).uint64
doAssert target <= high(int64).uint64 doAssert target <= high(int64).uint64
let status = db.sqlAttSurrounded.exec( let status = db.sqlAttSurrounds.exec(
(valID, int64 source, int64 target) (valID, int64 source, int64 target, int64 source, int64 target)
) do (res: tuple[source, target: int64, root: Hash32]): ) do (res: tuple[source, target: int64, root: Hash32]):
db_source = Epoch res.source db_source = Epoch res.source
db_target = Epoch res.target db_target = Epoch res.target
@ -858,35 +893,7 @@ proc checkSlashableAttestation*(
# Conflicting attestation exist, log by caller # Conflicting attestation exist, log by caller
# s1 < s2 < t2 < t1 # s1 < s2 < t2 < t1
return err(BadVote( return err(BadVote(
kind: SurroundedVote, kind: SurroundVote,
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,
existingAttestationRoot: root, existingAttestationRoot: root,
sourceExisting: db_source, sourceExisting: db_source,
targetExisting: db_target, targetExisting: db_target,
@ -933,7 +940,31 @@ proc checkSlashableAttestation*(
candidateTarget: target 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 # DB update
# -------------------------------------------- # --------------------------------------------
@ -948,16 +979,17 @@ proc registerValidator(db: SlashingProtectionDB_v2, validator: ValidatorPubKey)
proc getOrRegisterValidator( proc getOrRegisterValidator(
db: SlashingProtectionDB_v2, db: SlashingProtectionDB_v2,
index: Option[ValidatorIndex],
validator: ValidatorPubKey): ValidatorInternalID = validator: ValidatorPubKey): ValidatorInternalID =
## Get validator from the database ## Get validator from the database
## or register it and then return it ## or register it and then return it
let id = db.getValidatorInternalID(validator) let id = db.getValidatorInternalID(index, validator)
if id.isNone(): if id.isNone():
info "No slashing protection data for validator - initiating", info "No slashing protection data for validator - initiating",
validator = validator validator = validator
db.registerValidator(validator) db.registerValidator(validator)
let id = db.getValidatorInternalID(validator) let id = db.getValidatorInternalID(index, validator)
doAssert id.isSome() doAssert id.isSome()
id.unsafeGet() id.unsafeGet()
else: else:
@ -965,33 +997,65 @@ proc getOrRegisterValidator(
proc registerBlock*( proc registerBlock*(
db: SlashingProtectionDB_v2, db: SlashingProtectionDB_v2,
index: Option[ValidatorIndex],
validator: ValidatorPubKey, validator: ValidatorPubKey,
slot: Slot, block_root: Eth2Digest) = slot: Slot, block_root: Eth2Digest): Result[void, BadProposal] =
## Add a block to the slashing protection DB ## Add a block to the slashing protection DB
## `checkSlashableBlockProposal` MUST be run ## `checkSlashableBlockProposal` MUST be run
## before to ensure no overwrite. ## 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 # 6 second (minimal preset) slots => overflow at ~1.75 trillion years under
# minimal preset, and twice that with mainnet preset # minimal preset, and twice that with mainnet preset
doAssert slot <= high(int64).uint64 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( let status = db.sqlInsertBlock.exec(
(valID, int64 slot, (valID, int64 slot, block_root.data))
block_root.data)) if status.isErr():
doAssert status.isOk(), # Inserting primarily fails when the constraint for double proposals is
"SQLite error when registering block: " & $status.error & "\n" & # violated but may also happen due to disk full and other storage issues -
"for validator: 0x" & validator.toHex() & ", slot: " & $slot # 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*( proc registerAttestation*(
db: SlashingProtectionDB_v2, db: SlashingProtectionDB_v2,
index: Option[ValidatorIndex],
validator: ValidatorPubKey, validator: ValidatorPubKey,
source, target: Epoch, source, target: Epoch,
attestation_root: Eth2Digest) = attestation_root: Eth2Digest): Result[void, BadVote] =
## Add an attestation to the slashing protection DB ## Add an attestation to the slashing protection DB
## `checkSlashableAttestation` MUST be run ## `checkSlashableAttestation` MUST be run
## before to ensure no overwrite. ## 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) # Overflows in 14 trillion years (minimal) or 112 trillion years (mainnet)
doAssert source <= high(int64).uint64 doAssert source <= high(int64).uint64
@ -1000,27 +1064,49 @@ proc registerAttestation*(
let status = db.sqlInsertAtt.exec( let status = db.sqlInsertAtt.exec(
(valID, int64 source, int64 target, (valID, int64 source, int64 target,
attestation_root.data)) attestation_root.data))
doAssert status.isOk(), if status.isErr():
"SQLite error when registering attestation: " & $status.error & "\n" & # Inserting primarily fails when the constraint for double votes is
"for validator: 0x" & validator.toHex() & # violated but may also happen due to disk full and other storage issues -
", sourceEpoch: " & $source & # in any case, we'll return an error so that production is halted
", targetEpoch: " & $target ? 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 # 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 ## Prune all blocks from a validator before the specified newMinSlot
## This is intended for interchange import to ensure ## This is intended for interchange import to ensure
## that in case of a gap, we don't allow signing in that gap. ## 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( let status = db.sqlPruneValidatorBlocks.exec(
(valID, int64 newMinSlot)) (valID, int64 newMinSlot))
doAssert status.isOk(), doAssert status.isOk(),
"SQLite error when pruning validator blocks: " & $status.error & "\n" & "SQLite error when pruning validator blocks: " & $status.error & "\n" &
"for validator: 0x" & validator.toHex() & ", newMinSlot: " & $newMinSlot "for validator: 0x" & validator.toHex() & ", newMinSlot: " & $newMinSlot
proc pruneBlocks*(
db: SlashingProtectionDB_v2,
validator: ValidatorPubkey, newMinSlot: Slot) =
pruneBlocks(db, none(ValidatorIndex), validator, newMinSlot)
proc pruneAttestations*( proc pruneAttestations*(
db: SlashingProtectionDB_v2, db: SlashingProtectionDB_v2,
index: Option[ValidatorIndex],
validator: ValidatorPubkey, validator: ValidatorPubkey,
newMinSourceEpoch: int64, newMinSourceEpoch: int64,
newMinTargetEpoch: int64) = newMinTargetEpoch: int64) =
@ -1028,7 +1114,7 @@ proc pruneAttestations*(
## This is intended for interchange import. ## This is intended for interchange import.
## Negative source/target epoch of -1 can be received if no attestation was imported ## 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) ## 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( let status = db.sqlPruneValidatorAttestations.exec(
(valID, newMinSourceEpoch, newMinTargetEpoch)) (valID, newMinSourceEpoch, newMinTargetEpoch))
@ -1038,6 +1124,14 @@ proc pruneAttestations*(
", newSourceEpoch: " & $newMinSourceEpoch & ", newSourceEpoch: " & $newMinSourceEpoch &
", newTargetEpoch: " & $newMinTargetEpoch ", newTargetEpoch: " & $newMinTargetEpoch
proc pruneAttestations*(
db: SlashingProtectionDB_v2,
validator: ValidatorPubkey,
newMinSourceEpoch: int64,
newMinTargetEpoch: int64) =
pruneAttestations(
db, none(ValidatorIndex), validator, newMinSourceEpoch, newMinTargetEpoch)
proc pruneAfterFinalization*( proc pruneAfterFinalization*(
db: SlashingProtectionDB_v2, db: SlashingProtectionDB_v2,
finalizedEpoch: Epoch finalizedEpoch: Epoch
@ -1205,8 +1299,3 @@ proc inclSPDIR*(db: SlashingProtectionDB_v2, spdir: SPDIR): SlashingImportStatus
# Create a mutable copy for sorting # Create a mutable copy for sorting
var spdir = spdir var spdir = spdir
return db.importInterchangeV5Impl(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) slot = shortLog(slot)
return head 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 let
fork = getStateField(node.chainDag.headState, fork) fork = getStateField(node.chainDag.headState, fork)
genesis_validators_root = genesis_validators_root =
getStateField(node.chainDag.headState, genesis_validators_root) getStateField(node.chainDag.headState, genesis_validators_root)
let
randao = await validator.genRandaoReveal( randao = await validator.genRandaoReveal(
fork, genesis_validators_root, slot) fork, genesis_validators_root, slot)
message = makeBeaconBlockForHeadAndSlot( message = makeBeaconBlockForHeadAndSlot(
node, randao, validator_index, node.graffitiBytes, head, slot) node, randao, validator_index, node.graffitiBytes, head, slot)
if not message.isSome(): if not message.isSome():
return head # already logged elsewhere! return head # already logged elsewhere!
var var
newBlock = SignedBeaconBlock( newBlock = SignedBeaconBlock(
message: message.get() message: message.get()
@ -369,9 +360,16 @@ proc proposeBlock(node: BeaconNode,
# TODO: recomputed in block proposal # TODO: recomputed in block proposal
let signing_root = compute_block_root( let signing_root = compute_block_root(
fork, genesis_validators_root, slot, newBlock.root) fork, genesis_validators_root, slot, newBlock.root)
node.attachedValidators let notSlashable = node.attachedValidators
.slashingProtection .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( newBlock.signature = await validator.signBlockProposal(
fork, genesis_validators_root, slot, newBlock.root) fork, genesis_validators_root, slot, newBlock.root)
@ -409,7 +407,7 @@ proc handleAttestations(node: BeaconNode, head: BlockRef, slot: Slot) =
var attestations: seq[tuple[ var attestations: seq[tuple[
data: AttestationData, committeeLen, indexInCommittee: int, 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. # 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 # 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( let committee = get_beacon_committee(
epochRef, slot, committee_index.CommitteeIndex) epochRef, slot, committee_index.CommitteeIndex)
for index_in_committee, validatorIdx in committee: for index_in_committee, validator_index in committee:
let validator = node.getAttachedValidator(epochRef, validatorIdx) let validator = node.getAttachedValidator(epochRef, validator_index)
if validator != nil: if validator != nil:
let ad = makeAttestationData( let ad = makeAttestationData(
epochRef, attestationHead, committee_index.CommitteeIndex) 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: 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 let notSlashable = node.attachedValidators
.slashingProtection .slashingProtection
.checkSlashableAttestation( .registerAttestation(
a.validator.pubkey, a.validator_index,
a.data.source.epoch, a.validator.pubkey,
a.data.target.epoch) a.data.source.epoch,
a.data.target.epoch,
signing_root
)
if notSlashable.isOk(): 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( traceAsyncErrors createAndSendAttestation(
node, fork, genesis_validators_root, a.validator, a.data, node, fork, genesis_validators_root, a.validator, a.data,
a.committeeLen, a.indexInCommittee, num_active_validators) a.committeeLen, a.indexInCommittee, num_active_validators)

View File

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

View File

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

View File

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

View File

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