nimbus-eth2/tests/slashing_protection/test_slashing_protection_db...

779 lines
21 KiB
Nim

# beacon_chain
# Copyright (c) 2018-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except according to those terms.
{.push raises: [].}
{.used.}
import
# Standard library
std/os,
# Status lib
eth/db/[kvstore, kvstore_sqlite3],
stew/endians2,
# Internal
../../beacon_chain/validators/slashing_protection,
../../beacon_chain/spec/[helpers],
../../beacon_chain/spec/datatypes/base,
# Test utilies
../testutil
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()
proc sqlite3db_delete(basepath, dbname: string) =
for extension in [".sqlite3-shm", ".sqlite3-wal", ".sqlite3"]:
try:
removeFile(basepath / dbname&extension)
except OSError:
discard
const TestDir = ""
const TestDbName = "test_slashprot"
# Reminder of SQLite constraints for fake data:
# attestations:
# - all fields are NOT NULL
# - attestation_root is unique
# - (validator_id, target_epoch)
# blocks:
# - all fields are NOT NULL
# - block_root is unique
# - (validator_id, slot)
suite "Slashing Protection DB" & preset():
test "Empty database" & preset():
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
check:
db.checkSlashableBlockProposal(
ValidatorIndex(1234),
fakeValidator(1234),
slot = Slot 1
).isOk()
db.checkSlashableAttestation(
ValidatorIndex(1234),
fakeValidator(1234),
source = Epoch 1,
target = Epoch 2
).isOk()
db.checkSlashableAttestation(
ValidatorIndex(1234),
fakeValidator(1234),
source = Epoch 2,
target = Epoch 1
).error.kind == TargetPrecedesSource
test "SP for block proposal - linear append":
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
check:
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 10,
fakeRoot(100)
).isOk()
db.registerBlock(
ValidatorIndex(111),
fakeValidator(111),
Slot 15,
fakeRoot(111)
).isOk()
# Slot occupied by same validator
db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
slot = Slot 10
).isErr()
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
slot = Slot 10,
fakeRoot(101)
).isErr()
# Slot occupied by another validator
db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
slot = Slot 15
).isOk()
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
slot = Slot 15,
fakeRoot(150)
).isOk()
# Slot occupied by same validator
db.checkSlashableBlockProposal(
ValidatorIndex(111),
fakeValidator(111),
slot = Slot 15
).isErr()
db.registerBlock(
ValidatorIndex(111),
fakeValidator(111),
slot = Slot 15,
fakeRoot(151)
).isErr()
# Slot inoccupied
db.checkSlashableBlockProposal(
ValidatorIndex(255),
fakeValidator(255),
slot = Slot 20
).isOk()
db.registerBlock(
ValidatorIndex(255),
fakeValidator(255),
slot = Slot 20,
fakeRoot(4321)
).isOk()
check:
# Slot now occupied
db.checkSlashableBlockProposal(
ValidatorIndex(255),
fakeValidator(255),
slot = Slot 20
).isErr()
db.registerBlock(
ValidatorIndex(255),
fakeValidator(255),
slot = Slot 20,
fakeRoot(4322)
).isErr()
test "SP for block proposal - backtracking append":
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
# last finalized block
check:
db.registerBlock(
ValidatorIndex(0),
fakeValidator(0),
Slot 0,
fakeRoot(0)
).isOk()
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 10,
fakeRoot(10)
).isOk()
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 20,
fakeRoot(20)
).isOk()
for i in 0 ..< 30:
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
if i > 10 and i != 20: # MinSlotViolation and DupSlot
doAssert status.isOk, "error: " & $status
else:
doAssert status.isErr, "error: " & $status
check:
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 15,
fakeRoot(15)
).isOk()
for i in 0 ..< 30:
if i > 10 and i notin {15, 20}: # MinSlotViolation and DupSlot
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
doAssert status.isOk, "error: " & $status
else:
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
doAssert status.isErr, "error: " & $status
check:
db.checkSlashableBlockProposal(
ValidatorIndex(0xDEADBEEF),
fakeValidator(0xDEADBEEF),
Slot i
).isOk()
check:
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 12,
fakeRoot(12)
).isOk()
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 17,
fakeRoot(17)
).isOk()
for i in 0 ..< 30:
if i > 10 and i notin {12, 15, 17, 20}:
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
doAssert status.isOk, "error: " & $status
else:
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
doAssert status.isErr, "error: " & $status
check:
db.checkSlashableBlockProposal(
ValidatorIndex(0xDEADBEEF),
fakeValidator(0xDEADBEEF),
Slot i
).isOk()
check:
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 29,
fakeRoot(29)
).isOk()
for i in 0 ..< 30:
if i > 10 and i notin {12, 15, 17, 20, 29}:
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
doAssert status.isOk, "error: " & $status
else:
let status = db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot i
)
doAssert status.isErr, "error: " & $status
check:
db.checkSlashableBlockProposal(
ValidatorIndex(0xDEADBEEF),
fakeValidator(0xDEADBEEF),
Slot i
).isOk()
test "SP for same epoch attestation target - linear append":
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
check:
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 10,
fakeRoot(100)
).isOk()
db.registerAttestation(
ValidatorIndex(111),
fakeValidator(111),
Epoch 0, Epoch 15,
fakeRoot(111)
).isOk()
# Epoch occupied by same validator
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 10,
).error.kind == DoubleVote
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 10, fakeRoot(101)
).error.kind == DoubleVote
# Epoch occupied by another validator
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 15
).isOk()
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 15, fakeRoot(151)
).isOk()
# Epoch occupied by same validator
db.checkSlashableAttestation(
ValidatorIndex(111),
fakeValidator(111),
Epoch 0, Epoch 15
).error.kind == DoubleVote
db.registerAttestation(
ValidatorIndex(111),
fakeValidator(111),
Epoch 0, Epoch 15, fakeRoot(161)
).error.kind == DoubleVote
# Epoch inoccupied
db.checkSlashableAttestation(
ValidatorIndex(255),
fakeValidator(255),
Epoch 0, Epoch 20
).isOk()
db.registerAttestation(
ValidatorIndex(255),
fakeValidator(255),
Epoch 0, Epoch 20, fakeRoot(4321)
).isOk()
# Epoch now occupied
db.checkSlashableAttestation(
ValidatorIndex(255),
fakeValidator(255),
Epoch 0, Epoch 20
).error.kind == DoubleVote
db.registerAttestation(
ValidatorIndex(255),
fakeValidator(255),
Epoch 0, Epoch 20, fakeRoot(4322)
).error.kind == DoubleVote
test "SP for surrounded attestations":
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
check:
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
).isOk()
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 11, Epoch 19
).error.kind == SurroundVote
db.checkSlashableAttestation(
ValidatorIndex(200),
fakeValidator(200),
Epoch 11, Epoch 19
).isOk
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 11, Epoch 21
).isOk
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
check:
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 1,
fakeRoot(1)
).isOk()
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
).isOk()
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 11, Epoch 19
).error.kind == SurroundVote
db.checkSlashableAttestation(
ValidatorIndex(200),
fakeValidator(200),
Epoch 11, Epoch 19
).isOk
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 11, Epoch 21
).isOk
# TODO: is that possible?
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 9, Epoch 19
).isOk
test "SP for surrounding attestations":
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
check:
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
).isOk()
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 9, Epoch 21
).error.kind == SurroundVote
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 21
).error.kind == SurroundVote
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
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()
check:
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 9, Epoch 21
).error.kind == SurroundVote
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 0, Epoch 21
).error.kind == SurroundVote
test "Attestation ordering #1698":
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
check:
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 1, Epoch 2,
fakeRoot(2)
).isOk()
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 8, Epoch 10,
fakeRoot(10)
).isOk()
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 14, Epoch 15,
fakeRoot(15)
).isOk()
# The current list is, 2 -> 10 -> 15
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 3, Epoch 6,
fakeRoot(6)
).isOk()
# The current list is 2 -> 6 -> 10 -> 15
check:
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 7, Epoch 11
).error.kind == SurroundVote
test "Test valid attestation #1699":
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
check:
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
).isOk()
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 40, Epoch 50,
fakeRoot(50)
).isOk()
check:
db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 20, Epoch 30
).isOk
test "Pruning blocks works":
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 10,
fakeRoot(10)
).expect("registered block")
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 1000,
fakeRoot(20)
).expect("registered block")
# After pruning, duplicate becomes a min slot violation
doAssert db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot 10,
).error.kind == DoubleProposal
db.pruneAfterFinalization(
epoch(Slot 1000)
)
doAssert db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot 10,
).error.kind == MinSlotViolation
test "Don't prune the very last block even by mistake":
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 10,
fakeRoot(10)
).expect("registered block")
db.registerBlock(
ValidatorIndex(100),
fakeValidator(100),
Slot 1000,
fakeRoot(20)
).expect("registered block")
doAssert db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot 1000,
).error.kind == DoubleProposal
# Pruning far in the future
db.pruneAfterFinalization(
epoch(Slot 10000)
)
# Last block is still there
doAssert db.checkSlashableBlockProposal(
ValidatorIndex(100),
fakeValidator(100),
Slot 1000,
).error.kind == DoubleProposal
test "Pruning attestations works":
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
).expect("registered block")
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 40, Epoch 50,
fakeRoot(50)
).expect("registered block")
# After pruning, duplicate becomes a min source epoch violation
doAssert db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 10, Epoch 20
).error.kind == DoubleVote
# After pruning, surrounding vote becomes a min source epoch violation
doAssert db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 9, Epoch 21
).error.kind == SurroundVote
# After pruning, surrounded vote becomes a min source epoch violation
doAssert db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 11, Epoch 19
).error.kind == SurroundVote
# --------------------------------
db.pruneAfterFinalization(
Epoch 40
)
# --------------------------------
doAssert db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 10, Epoch 20
).error.kind == MinSourceViolation
doAssert db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 9, Epoch 21
).error.kind == MinSourceViolation
doAssert db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 11, Epoch 19
).error.kind == MinSourceViolation
# TODO is it possible to actually trigger MinTargetViolation
# given all the other constraints?
test "Don't prune the very last attestation(s) even by mistake":
block:
sqlite3db_delete(TestDir, TestDbName)
let db = SlashingProtectionDB.init(ZERO_HASH, TestDir, TestDbName)
defer:
db.close()
sqlite3db_delete(TestDir, TestDbName)
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 10, Epoch 20,
fakeRoot(20)
).expect("registered block")
db.registerAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 40, Epoch 50,
fakeRoot(50)
).expect("registered block")
# --------------------------------
db.pruneAfterFinalization(
epoch(Slot 10000)
)
# --------------------------------
doAssert db.checkSlashableAttestation(
ValidatorIndex(100),
fakeValidator(100),
Epoch 40, Epoch 50
).error.kind == DoubleVote
# TODO is it possible to actually to have
# the MAX(SourceEpoch) and MAX(TargetEpoch)
# on 2 different attestations
# given all the other constraints?