Last active
November 27, 2020 18:34
-
-
Save mratsim/4e8e33914757a057117dc6837ecea34c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # beacon_chain | |
| # Copyright (c) 2018-2020 Status Research & Development GmbH | |
| # Licensed and distributed under either of | |
| # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). | |
| # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). | |
| # at your option. This file may not be copied, modified, or distributed except according to those terms. | |
| import | |
| # Standard library | |
| std/[tables, os, options], | |
| # Status | |
| eth/db/[kvstore, kvstore_sqlite3], | |
| chronicles, | |
| nimcrypto/[hash, utils], | |
| serialization, | |
| json_serialization, | |
| sqlite3_abi, | |
| # Internal | |
| ./spec/[datatypes, digest, crypto], | |
| ./ssz | |
| # Requirements | |
| # -------------------------------------------- | |
| # | |
| # Overview of slashing and how it ties in with the rest of Eth2.0 | |
| # | |
| # EIP 3076: | |
| # https://eips.ethereum.org/EIPS/eip-3076 | |
| # https://ethereum-magicians.org/t/eip-3076-validator-client-interchange-format-slashing-protection/ | |
| # | |
| # Phase 0 for humans - Validator responsibilities: | |
| # - https://notes.ethereum.org/@djrtwo/Bkn3zpwxB#Validator-responsibilities | |
| # | |
| # Phase 0 spec - Honest Validator - how to avoid slashing | |
| # - https://github.com/ethereum/eth2.0-specs/blob/v1.0.0/specs/phase0/validator.md#how-to-avoid-slashing | |
| # | |
| # In-depth reading on slashing conditions | |
| # | |
| # - Detecting slashing conditions https://hackmd.io/@n0ble/By897a5sH | |
| # - Open issue on writing a slashing detector https://github.com/ethereum/eth2.0-pm/issues/63 | |
| # - Casper the Friendly Finality Gadget, Vitalik Buterin and Virgil Griffith | |
| # https://arxiv.org/pdf/1710.09437.pdf | |
| # Figure 2 | |
| # An individual validator ν MUST NOT publish two distinct votes, | |
| # 〈ν,s1,t1,h(s1),h(t1) AND〈ν,s2,t2,h(s2),h(t2)〉, | |
| # such that either: | |
| # I. h(t1) = h(t2). | |
| # Equivalently, a validator MUST NOT publish two distinct votes for the same target height. | |
| # OR | |
| # II. h(s1) < h(s2) < h(t2) < h(t1). | |
| # Equivalently, a validator MUST NOT vote within the span of its other votes. | |
| # - Vitalik's annotated spec: https://github.com/ethereum/annotated-spec/blob/d8c51af84f9f309d91c37379c1fcb0810bc5f10a/phase0/beacon-chain.md#proposerslashing | |
| # 1. A proposer can get slashed for signing two distinct headers at the same slot. | |
| # 2. An attester can get slashed for signing | |
| # two attestations that together violate | |
| # the Casper FFG slashing conditions. | |
| # - https://github.com/ethereum/eth2.0-specs/blob/v1.0.0/specs/phase0/validator.md#ffg-vote | |
| # The "source" is the current_justified_epoch | |
| # The "target" is the current_epoch | |
| # | |
| # Reading on weak subjectivity | |
| # - https://notes.ethereum.org/@adiasg/weak-subjectvity-eth2 | |
| # - https://www.symphonious.net/2019/11/27/exploring-ethereum-2-weak-subjectivity-period/ | |
| # - https://ethresear.ch/t/weak-subjectivity-under-the-exit-queue-model/5187 | |
| # | |
| # Reading of interop serialization format | |
| # - Import/export format: https://hackmd.io/@sproul/Bk0Y0qdGD | |
| # - Tests: https://github.com/eth2-clients/slashing-protection-interchange-tests | |
| # | |
| # Relaxation for Nimbus | |
| # | |
| # We are not building a slashing detector but only protecting | |
| # attached validator from slashing, hence we make the following assumptions | |
| # | |
| # 1. We only need to store specific validators signed blocks and attestations | |
| # 2. We assume that our node is synced past | |
| # the last finalized epoch | |
| # hence we only need to keep track of blocks and attestations | |
| # since the last finalized epoch and we don't need to care | |
| # about the weak subjectivity period. | |
| # i.e. if `Node.isSynced()` returns false | |
| # a node skips its validator duties and doesn't invoke slashing protection. | |
| # and `isSynced` syncs at least up to the blockchain last finalized epoch. | |
| # | |
| # Hence the database or key-value store should support | |
| # | |
| # Queries | |
| # 1. db.signedBlockExistsFor(validator, slot) -> bool | |
| # 2. db.attestationExistsFor(validator, target_epoch) -> bool | |
| # 3. db.attestationSurrounding(validator, source_epoch, target_epoch) | |
| # 4. db.attestationSurrounded(validator, source_epoch, target_epoch) | |
| # | |
| # Update | |
| # 1. db.registerBlock(validator, slot, block_root) | |
| # 2. db.registerAttestation(validator, source_epoch, target_epoch, attestation_root) | |
| # | |
| # Maintenance | |
| # 1. db.prune(finalized_epoch) | |
| # | |
| # Interop | |
| # 1. db.import(json) | |
| # 2. db.export(json) | |
| # 3. db.export(json, validator) | |
| # 4. db.export(json, seq[validator]) | |
| # | |
| # Additionally after EIP3067 slashing protection requires | |
| # a "low watermark" protection that can be used | |
| # instead of keeping track of the whole history (and allows pruning) | |
| # In that case we need the following queries | |
| # | |
| # 1. db.signedBlockMinimalSlot (EIP3067 condition 1) | |
| # 2. db.signedAttMinimalSourceEpoch (EIP3067 condition 4) | |
| # 3. db.signedAttMinimalTargetEpoch (EIP3067 condition 5) | |
| # Technical Discussion | |
| # -------------------------------------------- | |
| # | |
| # TODO: Merge with BeaconChainDB? | |
| # - https://stackoverflow.com/questions/21844479/multiple-databases-vs-single-database-with-logically-partitioned-data | |
| # | |
| # Reasons for merging | |
| # - Single database | |
| # | |
| # Reasons for not merging | |
| # - BeaconChainDB is about the beacon node itself | |
| # while slashing protection is about validators | |
| # - BeaconChainDB is append-only | |
| # while slashing protection will be pruned | |
| # at each finalization. | |
| # Hence we might want different backend in the future | |
| # - In a VC/BN split configuration the slashing protection | |
| # may be better attached to the VC. (VC: Validator Client, BN: Beacon Node) | |
| # - The slashing protection DB only held cryptographic hashes | |
| # and epoch/slot integers which are uncompressible | |
| # while BeaconChainDB is snappy-compressed. | |
| # | |
| # TODO: if we enshrine the split we likely want to use | |
| # a relational DB instead of KV-Store, | |
| # for efficient pruning, range queries support | |
| # and filtering on validators | |
| # SQLite primitives | |
| # -------------------------------------------- | |
| # For now we choose to enforce the SQLite backend as a DB (and not a KV-Store) | |
| # | |
| # Cons | |
| # 1. Harder to switch away from a DB than from a KV-Store | |
| # | |
| # Pros | |
| # 1. No need for adhoc per-validator range queries implementation using LinkedList | |
| # with high potential of bug (as found in audit) | |
| # 2. uses robust and fuzzed SQLite codepath | |
| # 3. Straightforward pruning | |
| # 4. Can be maintained and inspected with standard tooling | |
| # | |
| # In particular the following query leads to complex code with a KV store | |
| # | |
| # Select 1 from attestations | |
| # where validator = '0x1234ABCDEF' | |
| # AND ( | |
| # -- Don't publish distinct vote for the same target | |
| # (target_epoch = candidate_target_epoch) | |
| # -- surrounded vote | |
| # OR | |
| # (source_epoch < candidate_source_epoch and candidate_target_epoch < target_epoch) | |
| # -- surrounding vote | |
| # OR | |
| # (candidate_source_epoch < source_epoch and target_epoch < candidate_target_epoch) | |
| # ) | |
| # | |
| # Note, with SQLite splitting into multiple small queries is also efficient | |
| # as it is embedded in the application: https://www.sqlite.org/np1queryprob.html | |
| # Future optimizations | |
| # -------------------------------------------- | |
| # To limit disk IO we might want to keep a data-structure in memory. | |
| # Surround voting detection is very similar to: | |
| # - Collision detection in games | |
| # - point of interest localisation in geographical DBs or maps | |
| # | |
| # A reasonable acceleration structure would be: | |
| # - O(log n) for adding new attestations | |
| # - O(log n) to check for surround voting. | |
| # - O(n) space usage | |
| # | |
| # Suitable inspirations may be: | |
| # - Bounding Volume Hierarchy and Axis-ligned Bounding Boxes from collision detection | |
| # - R-Trees from geospatial data processing and maps | |
| # - Kd-Trees from both | |
| # - less common structures like quadtrees and octrees | |
| # | |
| # See also optimizing a slashing detector for the whole chain | |
| # - https://github.com/protolambda/eth2-surround | |
| # - Detecting slashing conditions https://hackmd.io/@n0ble/By897a5sH | |
| # - Open issue on writing a slashing detector https://github.com/ethereum/eth2.0-pm/issues/63 | |
| type | |
| SlashingProtectionDB* = ref object | |
| ## Database storing the blocks attested | |
| ## by validators attached to a beacon node | |
| ## or validator client. | |
| # For now we commit to using SqLite | |
| # Splitting attestations queries | |
| # into small queries is fine with SqLite | |
| # https://www.sqlite.org/np1queryprob.html | |
| backend: SqStoreRef | |
| # Cached queries - write | |
| sqlInsertValidator: SqliteStmt[openarray[byte], void] | |
| sqlInsertAtt: SqliteStmt[(ValidatorInternalID, int64, int64, openarray[byte]), void] | |
| sqlInsertBlock: SqliteStmt[(ValidatorInternalID, int64, openarray[byte]), void] | |
| # Cached queries - read | |
| sqlGetValidatorInternalID: SqliteStmt[openarray[byte], ValidatorInternalID] | |
| sqlAttForSameTargetEpoch: SqliteStmt[(ValidatorInternalID, int64), openarray[byte]] | |
| sqlAttSurrounded: SqliteStmt[(ValidatorInternalID, int64, int64), (int64, int64, seq[byte])] | |
| sqlAttSurrounding: SqliteStmt[(ValidatorInternalID, int64, int64), (int64, int64, seq[byte])] | |
| sqlAttMinSourceEpoch: SqliteStmt[ValidatorInternalID, int64] | |
| sqlAttMinTargetEpoch: SqliteStmt[ValidatorInternalID, int64] | |
| sqlBlockForSameSlot: SqliteStmt[(ValidatorInternalID, int64), openarray[byte]] | |
| sqlBlockMinSlot: SqliteStmt[ValidatorInternalID, int64] | |
| ValidatorInternalID = int32 | |
| ## Validator internal ID in the DB | |
| ## This is cached to cost querying cost | |
| BadVoteKind* = enum | |
| ## Attestation bad vote kind | |
| # h: height (i.e. epoch for attestation, slot for blocks) | |
| # t: target | |
| # s: source | |
| # 1: existing attestations | |
| # 2: candidate attestation | |
| # Spec slashing condition | |
| DoubleVote # h(t1) = h(t2) | |
| SurroundedVote # h(s1) < h(s2) < h(t2) < h(t1) | |
| SurroundingVote # 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 | |
| BadVote* = object | |
| case kind*: BadVoteKind | |
| of DoubleVote: | |
| existingAttestation*: Eth2Digest | |
| of SurroundedVote, SurroundingVote: | |
| existingAttestationRoot*: Eth2Digest # Many roots might be in conflict | |
| sourceExisting*, targetExisting*: Epoch | |
| sourceSlashable*, targetSlashable*: Epoch | |
| of TargetPrecedesSource: | |
| discard | |
| SlotDesc = object | |
| # Using tuple instead of objects, crashes the Nim compiler | |
| # with SSZ serialization | |
| # Making this generic as well | |
| start, stop: Slot | |
| isInit: bool | |
| EpochDesc = object | |
| start, stop: Epoch | |
| isInit: bool | |
| KeysEpochs = object | |
| ## Per-validator linked lists start/stop | |
| blockSlots: SlotDesc | |
| sourceEpochs: EpochDesc | |
| targetEpochs: EpochDesc | |
| SlashingKeyKind = enum | |
| # Note: source epochs are not unique | |
| # and so cannot be used to build a key | |
| kBlock | |
| kTargetEpoch | |
| kLinkedListMeta | |
| # Interchange format | |
| kGenesisValidatorRoot | |
| kNumValidators | |
| kValidator | |
| BlockNode = object | |
| prev, next: Slot | |
| # TODO distinct type for block root vs all other ETH2Digest | |
| block_root: Eth2Digest | |
| TargetEpochNode = object | |
| prev, next: Epoch | |
| # TODO distinct type for attestation root vs all other ETH2Digest | |
| attestation_root: Eth2Digest | |
| source: Epoch | |
| ValID = array[RawPubKeySize, byte] | |
| ## This is the serialized byte representation | |
| ## of a Validator Public Key. | |
| ## Portable between Miracl/BLST | |
| ## and limits serialization/deserialization call | |
| {.push raises: [Defect].} | |
| logScope: | |
| topics = "antislash" | |
| # version history: | |
| # 0 -> https://github.com/status-im/nimbus-eth2/pull/1643, based on KV-store | |
| const SlashingDB_version = 1 | |
| template dispose(sqlStmt: SqliteStmt) = | |
| # TODO: This should be unnecessary but currently | |
| # the backend does not use the autocollected "extraStmts" field | |
| # as planned. | |
| discard sqlite3_finalize((ptr sqlite3_stmt) sqlStmt) | |
| proc setupDB(db: SlashingProtectionDB, genesis_validator_root: Eth2Digest) = | |
| ## Initial setup of the DB | |
| block: # Metadata | |
| db.backend.exec(""" | |
| CREATE TABLE metadata( | |
| slashing_db_version INTEGER, | |
| genesis_validator_root BLOB NOT NULL | |
| ); | |
| """).expect("DB should be working and \"metadata\" should not exist") | |
| # TODO: db.backend.exec does not take parameters | |
| let insertStmt = db.backend.prepareStmt( """ | |
| INSERT INTO | |
| metadata(slashing_db_version, genesis_validator_root) | |
| VALUES | |
| (""" & $SlashingDB_version & """, ?); | |
| """, openarray[byte], void | |
| ).get() | |
| insertStmt.exec(genesis_validator_root.data.toOpenArray(0, genesis_validator_root.data.high)) | |
| .expect("Metadata are initialized in the DB") | |
| insertStmt.dispose() | |
| block: # Tables | |
| db.backend.exec(""" | |
| CREATE TABLE validators( | |
| id INTEGER PRIMARY KEY, | |
| pubkey BLOB NOT NULL UNIQUE | |
| ); | |
| """).expect("DB should be working and \"validators\" should not exist") | |
| db.backend.exec(""" | |
| CREATE TABLE attestations( | |
| validator_id INTEGER NOT NULL, | |
| source_epoch INTEGER NOT NULL, | |
| target_epoch INTEGER NOT NULL, | |
| attestation_root BLOB NOT NULL UNIQUE, | |
| FOREIGN KEY(validator_id) REFERENCES validators(id) | |
| UNIQUE (validator_id, target_epoch) | |
| ); | |
| """).expect("DB should be working and \"attestations\" should not exist") | |
| db.backend.exec(""" | |
| CREATE TABLE blocks( | |
| validator_id INTEGER NOT NULL, | |
| slot INTEGER NOT NULL, | |
| block_root BLOB NOT NULL UNIQUE, | |
| FOREIGN KEY(validator_id) REFERENCES validators(id) | |
| UNIQUE (validator_id, slot) | |
| ); | |
| """).expect("DB should be working and \"blocks\" should not exist") | |
| proc checkDB(db: SlashingProtectionDB, genesis_validator_root: Eth2Digest) = | |
| ## Check the metadata of the DB | |
| let selectStmt = db.backend.prepareStmt( | |
| "SELECT * FROM metadata;", | |
| NoParams, (int32, seq[byte])).get() | |
| var version: int32 | |
| var root: Eth2Digest | |
| let status = selectStmt.exec do (res: (int32, seq[byte])): | |
| version = res[0] | |
| root.data[0..^1] = res[1].toOpenArray(0, res[1].high) | |
| selectStmt.dispose() | |
| doAssert status.isOk() | |
| doAssert version == SlashingDB_version, | |
| "Incorrect database version: " & $version & "\n" & | |
| "but expected: " & $SlashingDB_version | |
| doAssert root == genesis_validator_root, | |
| "Invalid database genesis validator root: " & root.data.toHex(lowercase = true) & "\n" & | |
| "but expected: " & genesis_validator_root.data.toHex(lowercase = true) | |
| proc setupCachedQueries(db: SlashingProtectionDB) = | |
| ## Create prepared queries for reuse | |
| # Insertions | |
| # -------------------------------------------------------- | |
| db.sqlInsertValidator = db.backend.prepareStmt(""" | |
| INSERT INTO | |
| validators(pubkey) | |
| VALUES | |
| (?); | |
| """, openarray[byte], void).get() | |
| db.sqlInsertAtt = db.backend.prepareStmt(""" | |
| INSERT INTO attestations( | |
| validator_id, | |
| source_epoch, | |
| target_epoch, | |
| attestation_root) | |
| VALUES | |
| (?,?,?,?); | |
| """, (ValidatorInternalID, int64, int64, openarray[byte]), void).get() | |
| db.sqlInsertBlock = db.backend.prepareStmt(""" | |
| INSERT INTO blocks( | |
| validator_id, | |
| slot, | |
| block_root) | |
| VALUES | |
| (?,?,?); | |
| """, (ValidatorInternalID, int64, openarray[byte]), void | |
| ).get() | |
| # Read internal validator ID | |
| # -------------------------------------------------------- | |
| db.sqlGetValidatorInternalID = db.backend.prepareStmt( | |
| "SELECT id from validators WHERE pubkey = ?;", | |
| openarray[byte], ValidatorInternalID | |
| ).get() | |
| # Inspect attestations | |
| # -------------------------------------------------------- | |
| # TODO: we use seq[byte] here which requires extra heap allocation | |
| # We can use openarray[byte] however | |
| # this requires only having attestation_root or block_root | |
| # as the unique returned column | |
| # For best logging we can't do that for attestations but can for blocks. | |
| # | |
| # Alternatively we could add support for ETH2Digest or array[32, byte] | |
| # to the DB backend. | |
| # Or supporting tuple returns like (int64, var openarray[byte]) | |
| # would require Nim 1.4 and views borrow-checking | |
| db.sqlAttForSameTargetEpoch = db.backend.prepareStmt(""" | |
| SELECT | |
| attestation_root | |
| FROM | |
| attestations | |
| WHERE | |
| validator_id = ? | |
| AND | |
| target_epoch = ?; | |
| """, (ValidatorInternalID, int64), openarray[byte] | |
| ).get() | |
| db.sqlAttSurrounded = db.backend.prepareStmt(""" | |
| SELECT | |
| source_epoch, target_epoch, attestation_root | |
| FROM | |
| attestations | |
| WHERE | |
| validator_id = ? | |
| AND | |
| source_epoch < ? AND ? < target_epoch | |
| LIMIT 1 | |
| """, (ValidatorInternalID, int64, int64), (int64, int64, seq[byte]) | |
| ).get() | |
| db.sqlAttSurrounding = db.backend.prepareStmt(""" | |
| SELECT | |
| source_epoch, target_epoch, attestation_root | |
| FROM | |
| attestations | |
| WHERE | |
| validator_id = ? | |
| AND | |
| ? < source_epoch AND target_epoch < ? | |
| LIMIT 1 | |
| """, (ValidatorInternalID, int64, int64), (int64, int64, seq[byte]) | |
| ).get() | |
| db.sqlAttMinSourceEpoch = db.backend.prepareStmt(""" | |
| SELECT | |
| MIN(source_epoch) | |
| FROM | |
| attestations | |
| WHERE | |
| validator_id = ?; | |
| """, ValidatorInternalID, int64 | |
| ).get() | |
| db.sqlAttMinTargetEpoch = db.backend.prepareStmt(""" | |
| SELECT | |
| MIN(source_epoch) | |
| FROM | |
| attestations | |
| WHERE | |
| validator_id = ?; | |
| """, ValidatorInternalID, int64 | |
| ).get() | |
| # Inspect blocks | |
| # -------------------------------------------------------- | |
| db.sqlBlockForSameSlot = db.backend.prepareStmt(""" | |
| SELECT | |
| block_root | |
| FROM | |
| blocks | |
| WHERE | |
| validator_id = ? | |
| AND | |
| slot = ?; | |
| """, (ValidatorInternalID, int64), openarray[byte] | |
| ).get() | |
| db.sqlBlockMinSlot = db.backend.prepareStmt(""" | |
| SELECT | |
| MIN(slot) | |
| FROM | |
| blocks | |
| WHERE | |
| validator_id = ?; | |
| """, ValidatorInternalID, int64 | |
| ).get() | |
| proc init*(T: type SlashingProtectionDB, | |
| genesis_validator_root: Eth2Digest, | |
| basePath: string, | |
| dbname: string): T = | |
| ## Initialize a new slashing protection database | |
| ## or load an existing one with matching genesis root | |
| ## `dbname` MUST not be ending with .sqlite3 | |
| let alreadyExists = fileExists(basepath/ dbname&".sqlite3") | |
| result = T(backend: SqStoreRef.init(basePath, dbname, keyspaces = []).get()) | |
| if alreadyExists: | |
| result.checkDB(genesis_validator_root) | |
| else: | |
| result.setupDB(genesis_validator_root) | |
| # Cached queries | |
| result.setupCachedQueries() | |
| proc load*( | |
| T: type SlashingProtectionDB, | |
| backend: KVStoreRef): SlashingProtectionDB = | |
| ## Load a slashing protection DB | |
| ## Note: This is for conversion usage | |
| ## this doesn't check the genesis validator root | |
| result = T(backend: backend) | |
| proc close*(db: SlashingProtectionDB) = | |
| ## Close a slashing protection database | |
| # Collect the cached statements | |
| # TODO: this will be handled by kvstore_sqlite3 in the future | |
| # the commit wasn't committed to the repo by mistake. | |
| for field in fields(db[]): | |
| when field is SqliteStmt: | |
| dispose(field) | |
| db.backend.close() | |
| # DB Queries | |
| # -------------------------------------------- | |
| proc foundAnyResult(status: KVResult[bool]): bool {.inline.}= | |
| ## Checks a DB query status for errors | |
| ## Then returns true if any result was found | |
| ## and false otherwise. | |
| ## There are 2 layers to a DB result | |
| ## 1. Did the query result in error. | |
| ## This is a logic bug and crashes NBC in this proc. | |
| ## 2. Did the query return any line. | |
| status.expect("DB is not corrupted and query is working") | |
| proc getValidatorInternalID( | |
| db: SlashingProtectionDB, | |
| validator: ValidatorPubKey): Option[ValidatorInternalID] = | |
| ## Retrieve a validator internal ID | |
| let serializedPubkey = validator.toRaw() # Miracl/BLST to bytes | |
| var valID: ValidatorInternalID | |
| let status = db.sqlGetValidatorInternalID.exec(serializedPubkey) do (res: ValidatorInternalID): | |
| valID = res | |
| # Note: we enforce at the DB level that if the pubkey exists it is unique. | |
| if status.foundAnyResult(): | |
| some(valID) | |
| else: | |
| none(ValidatorInternalID) | |
| proc checkSlashableBlockProposal*( | |
| db: SlashingProtectionDB, | |
| validator: ValidatorPubKey, | |
| slot: Slot | |
| ): Result[void, Eth2Digest] = | |
| ## 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(validator) | |
| if id.isNone(): | |
| notice "No slashing protection data - first block proposal?", | |
| validator = validator, | |
| slot = slot | |
| return ok() | |
| else: | |
| id.unsafeGet() | |
| var root: ETH2Digest | |
| let status = db.sqlBlockForSameSlot.exec( | |
| (valID, int64 slot) | |
| ) do (res: openarray[byte]): | |
| root.data[0..^1] = res | |
| # Note: we enforce at the DB level that if (pubkey, slot) exists it maps to a unique block root. | |
| if status.foundAnyResult(): | |
| # Conflicting block exist | |
| err(root) | |
| else: | |
| ok() | |
| # TODO: low-watermark check for EIP3067 | |
| proc checkSlashableAttestation*( | |
| db: SlashingProtectionDB, | |
| validator: ValidatorPubKey, | |
| source: Epoch, | |
| target: Epoch | |
| ): Result[void, BadVote] = | |
| ## 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 attestation root | |
| # 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) | |
| # --------------------------------- | |
| block: | |
| var root: ETH2Digest | |
| let status = db.sqlAttForSameTargetEpoch.exec( | |
| (valID, int64 target) | |
| ) do (res: openarray[byte]): | |
| root.data[0..^1] = res | |
| # 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 | |
| return err(BadVote( | |
| kind: DoubleVote, | |
| existingAttestation: root | |
| )) | |
| # Casper FFG 2nd slashing condition | |
| # -> Surrounded vote | |
| # Detect h(s1) < h(s2) < h(t2) < h(t1) | |
| # --------------------------------- | |
| block: | |
| var root: ETH2Digest | |
| var db_source, db_target: Epoch | |
| let status = db.sqlAttSurrounded.exec( | |
| (valID, int64 source, int64 target) | |
| ) do (res: tuple[source, target: int64, root: seq[byte]]): | |
| db_source = Epoch res.source | |
| db_target = Epoch res.target | |
| root.data[0..^1] = res.root.toOpenArray(0, res.root.high) | |
| # 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: 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: | |
| 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: seq[byte]]): | |
| db_source = Epoch res.source | |
| db_target = Epoch res.target | |
| root.data[0..^1] = res.root.toOpenArray(0, res.root.high) | |
| # 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, | |
| sourceExisting: db_source, | |
| targetExisting: db_target, | |
| sourceSlashable: source, | |
| targetSlashable: target | |
| )) | |
| # TODO: low-watermark check for EIP3067 | |
| return ok() | |
| # DB update | |
| # -------------------------------------------- | |
| proc registerValidator(db: SlashingProtectionDB, validator: ValidatorPubKey) = | |
| ## Get validator from the database | |
| ## or register it | |
| ## Assumes the validator does not exist | |
| let serializedPubkey = validator.toRaw() # Miracl/BLST to bytes | |
| let status = db.sqlInsertValidator.exec(serializedPubkey) | |
| doAssert status.isOk() | |
| proc getOrRegisterValidator( | |
| db: SlashingProtectionDB, | |
| validator: ValidatorPubKey): ValidatorInternalID = | |
| ## Get validator from the database | |
| ## or register it and then return it | |
| let id = db.getValidatorInternalID(validator) | |
| if id.isNone(): | |
| info "No slashing protection data for validator - initiating", | |
| validator = validator | |
| db.registerValidator(validator) | |
| let id = db.getValidatorInternalID(validator) | |
| doAssert id.isSome() | |
| id.unsafeGet() | |
| else: | |
| id.unsafeGet() | |
| proc registerBlock*( | |
| db: SlashingProtectionDB, | |
| validator: ValidatorPubKey, | |
| slot: Slot, block_root: Eth2Digest) = | |
| ## Add a block to the slashing protection DB | |
| ## `checkSlashableBlockProposal` MUST be run | |
| ## before to ensure no overwrite. | |
| let valID = db.getOrRegisterValidator(validator) | |
| let status = db.sqlInsertBlock.exec( | |
| (valID, int64 slot, | |
| block_root.data.toOpenArray(0, block_root.data.high))) | |
| doAssert status.isOk() | |
| proc registerAttestation*( | |
| db: SlashingProtectionDB, | |
| validator: ValidatorPubKey, | |
| source, target: Epoch, | |
| attestation_root: Eth2Digest) = | |
| ## Add an attestation to the slashing protection DB | |
| ## `checkSlashableAttestation` MUST be run | |
| ## before to ensure no overwrite. | |
| let valID = db.getOrRegisterValidator(validator) | |
| let status = db.sqlInsertAtt.exec( | |
| (valID, int64 source, int64 target, | |
| attestation_root.data.toOpenArray(0, attestation_root.data.high))) | |
| doAssert status.isOk() | |
| # DB maintenance | |
| # -------------------------------------------- | |
| # TODO: pruning | |
| # Interchange | |
| # -------------------------------------------- | |
| type | |
| SPDIF = object | |
| ## Slashing Protection Database Interchange Format | |
| metadata: SPDIF_Meta | |
| data: seq[SPDIF_Validator] | |
| Eth2Digest0x = distinct Eth2Digest | |
| ## The spec mandates "0x" prefix on serialization | |
| ## So we need to set custom read/write | |
| PubKey0x = distinct ValidatorPubKey | |
| ## The spec mandates "0x" prefix on serialization | |
| ## So we need to set custom read/write | |
| SPDIF_Meta = object | |
| interchange_format: string | |
| interchange_format_version: string | |
| genesis_validator_root: Eth2Digest0x | |
| SPDIF_Validator = object | |
| pubkey: PubKey0x | |
| signed_blocks: seq[SPDIF_SignedBlock] | |
| signed_attestations: seq[SPDIF_SignedAttestation] | |
| SPDIF_SignedBlock = object | |
| slot: Slot | |
| signing_root: Eth2Digest0x # compute_signing_root(block, domain) | |
| SPDIF_SignedAttestation = object | |
| source_epoch: Epoch | |
| target_epoch: Epoch | |
| signing_root: Eth2Digest0x # compute_signing_root(attestation, domain) | |
| proc writeValue*(writer: var JsonWriter, value: PubKey0x) | |
| {.inline, raises: [IOError, Defect].} = | |
| writer.writeValue("0x" & value.ValidatorPubKey.toHex()) | |
| proc readValue*(reader: var JsonReader, value: var PubKey0x) | |
| {.raises: [SerializationError, IOError, Defect].} = | |
| let key = ValidatorPubKey.fromHex(reader.readValue(string)) | |
| if key.isOk: | |
| value = PubKey0x key.get | |
| else: | |
| # TODO: Can we provide better diagnostic? | |
| raiseUnexpectedValue(reader, "Valid hex-encoded public key expected") | |
| proc writeValue*(w: var JsonWriter, a: Eth2Digest0x) | |
| {.inline, raises: [IOError, Defect].} = | |
| w.writeValue "0x" & a.Eth2Digest.data.toHex(lowercase = true) | |
| proc readValue*(r: var JsonReader, a: var Eth2Digest0x) | |
| {.raises: [SerializationError, IOError, Defect].} = | |
| try: | |
| a = Eth2Digest0x fromHex(Eth2Digest, r.readValue(string)) | |
| except ValueError: | |
| raiseUnexpectedValue(r, "Hex string expected") | |
| proc toSPDIF*(db: SlashingProtectionDB, path: string) | |
| {.raises: [IOError, Defect].} = | |
| ## Export the full slashing protection database | |
| ## to a json the Slashing Protection Database Interchange (Complete) Format | |
| # var extract: SPDIF | |
| # extract.metadata.interchange_format = "complete" | |
| # extract.metadata.interchange_format_version = "3" | |
| # extract.metadata.genesis_validator_root = Eth2Digest0x db.get( | |
| # subkey(kGenesisValidatorRoot), ETH2Digest | |
| # # Bug in results.nim | |
| # # ).expect("Slashing Protection requires genesis_validator_root at init") | |
| # ).unsafeGet() | |
| # let numValidators = db.get( | |
| # subkey(kNumValidators), | |
| # uint32 | |
| # ).get(otherwise = 0'u32) | |
| # for i in 0'u32 ..< numValidators: | |
| # var validator: SPDIF_Validator | |
| # validator.pubkey = PubKey0x db.get( | |
| # subkey(kValidator, i), | |
| # ValidatorPubKey | |
| # ).unsafeGet() | |
| # let valID = validator.pubkey.ValidatorPubKey.toRaw() | |
| # let ll = db.get( | |
| # subkey(kLinkedListMeta, valID), | |
| # KeysEpochs | |
| # ).unsafeGet() | |
| # if ll.blockSlots.isInit: | |
| # var curSlot = ll.blockSlots.start | |
| # while true: | |
| # let node = db.get( | |
| # subkey(kBlock, valID, curSlot), | |
| # BlockNode | |
| # ).unsafeGet() | |
| # validator.signed_blocks.add SPDIF_SignedBlock( | |
| # slot: curSlot, | |
| # signing_root: Eth2Digest0x node.block_root | |
| # ) | |
| # if curSlot == ll.blockSlots.stop: | |
| # break | |
| # else: | |
| # curSlot = node.next | |
| # if ll.targetEpochs.isInit: | |
| # var curEpoch = ll.targetEpochs.start | |
| # while true: | |
| # let node = db.get( | |
| # subkey(kTargetEpoch, valID, curEpoch), | |
| # TargetEpochNode | |
| # ).unsafeGet() | |
| # validator.signed_attestations.add SPDIF_SignedAttestation( | |
| # source_epoch: node.source, target_epoch: curEpoch, | |
| # signing_root: Eth2Digest0x node.attestation_root | |
| # ) | |
| # if curEpoch == ll.targetEpochs.stop: | |
| # break | |
| # else: | |
| # curEpoch = node.next | |
| # # Update extract without reallocating seqs | |
| # # by manually transferring ownership | |
| # extract.data.setLen(extract.data.len + 1) | |
| # shallowCopy(extract.data[^1], validator) | |
| # Json.saveFile(path, extract, pretty = true) | |
| # echo "Exported slashing protection DB to '", path, "'" | |
| proc fromSPDIF*(db: SlashingProtectionDB, path: string): bool | |
| {.raises: [SerializationError, IOError, Defect].} = | |
| ## Import a (Complete) Slashing Protection Database Interchange Format | |
| ## file into the specified slahsing protection DB | |
| ## | |
| ## The database must be initialized. | |
| ## The genesis_validator_root must match or | |
| ## the DB must have a zero root | |
| # let extract = Json.loadFile(path, SPDIF) | |
| # doAssert not db.isNil, "The Slashing Protection DB must be initialized." | |
| # doAssert not db.backend.isNil, "The Slashing Protection DB must be initialized." | |
| # let dbGenValRoot = db.get( | |
| # subkey(kGenesisValidatorRoot), ETH2Digest | |
| # ).unsafeGet() | |
| # if dbGenValRoot != default(Eth2Digest) and | |
| # dbGenValRoot != extract.metadata.genesis_validator_root.Eth2Digest: | |
| # echo "The slashing protection database and imported file refer to different blockchains." | |
| # return false | |
| # if dbGenValRoot == default(Eth2Digest): | |
| # db.put( | |
| # subkey(kGenesisValidatorRoot), | |
| # extract.metadata.genesis_validator_root.Eth2Digest | |
| # ) | |
| # for v in 0 ..< extract.data.len: | |
| # for b in 0 ..< extract.data[v].signed_blocks.len: | |
| # db.registerBlock( | |
| # extract.data[v].pubkey.ValidatorPubKey, | |
| # extract.data[v].signed_blocks[b].slot, | |
| # extract.data[v].signed_blocks[b].signing_root.Eth2Digest | |
| # ) | |
| # for a in 0 ..< extract.data[v].signed_attestations.len: | |
| # db.registerAttestation( | |
| # extract.data[v].pubkey.ValidatorPubKey, | |
| # extract.data[v].signed_attestations[a].source_epoch, | |
| # extract.data[v].signed_attestations[a].target_epoch, | |
| # extract.data[v].signed_attestations[a].signing_root.Eth2Digest | |
| # ) | |
| # return true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment