2024-02-28 17:28:04 +00:00
|
|
|
# beacon_chain
|
2024-01-06 14:26:56 +00:00
|
|
|
# Copyright (c) 2018-2024 Status Research & Development GmbH
|
2021-02-09 15:23:06 +00:00
|
|
|
# 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.
|
|
|
|
|
2024-02-28 17:28:04 +00:00
|
|
|
{.push raises: [].}
|
2021-10-12 11:36:52 +00:00
|
|
|
{.used.}
|
|
|
|
|
2021-02-09 15:23:06 +00:00
|
|
|
import
|
|
|
|
# Status lib
|
|
|
|
chronicles,
|
|
|
|
# Internal
|
2021-05-04 13:17:28 +00:00
|
|
|
../../beacon_chain/validators/[slashing_protection, slashing_protection_v2],
|
2021-06-23 14:43:18 +00:00
|
|
|
../../beacon_chain/spec/datatypes/base,
|
2021-02-09 15:23:06 +00:00
|
|
|
# Test utilies
|
2021-04-28 16:41:02 +00:00
|
|
|
../testutil, ../testdbutil,
|
2021-10-12 11:36:52 +00:00
|
|
|
../consensus_spec/fixtures_utils
|
2021-02-09 15:23:06 +00:00
|
|
|
|
2023-12-04 11:55:56 +00:00
|
|
|
from std/os import changeFileExt, removeFile, walkDir, `/`
|
|
|
|
from stew/byteutils import toHex
|
|
|
|
|
2021-02-09 15:23:06 +00:00
|
|
|
type
|
|
|
|
TestInterchange = object
|
|
|
|
name: string
|
|
|
|
## Name of the test case
|
|
|
|
genesis_validators_root: Eth2Digest0x
|
|
|
|
## Genesis validator root to use when creating the empty DB
|
|
|
|
## or to compare the import against
|
|
|
|
steps: seq[TestStep]
|
|
|
|
|
|
|
|
TestStep = object
|
|
|
|
should_succeed: bool
|
|
|
|
## Is "interchange" given a valid import
|
2022-01-20 16:14:06 +00:00
|
|
|
contains_slashable_data: bool
|
2021-02-09 15:23:06 +00:00
|
|
|
## Does "interchange" contain slashable data either as standalone
|
|
|
|
## or with regards to previous steps
|
2022-01-20 16:14:06 +00:00
|
|
|
## If contains_slashable_data is false, then the given interchange must be imported
|
|
|
|
## successfully, and the given block/attestation checks must pass.
|
|
|
|
## If contains_slashable_data is true, then implementations have the option to do one of two
|
|
|
|
## things:
|
|
|
|
## - Import the interchange successfully, working around the slashable data by minification
|
|
|
|
## or some other mechanism. If the import succeeds, all checks must pass and the test
|
|
|
|
## should continue to the next step.
|
|
|
|
## - Reject the interchange (or partially import it), in which case the block/attestation
|
|
|
|
## checks and all future steps should be ignored.
|
2021-02-09 15:23:06 +00:00
|
|
|
interchange: SPDIR
|
|
|
|
blocks: seq[CandidateBlock]
|
|
|
|
## Blocks to try as proposer after DB is imported
|
|
|
|
attestations: seq[CandidateVote]
|
|
|
|
## Attestations to try as validator after DB is imported
|
|
|
|
|
|
|
|
CandidateBlock = object
|
|
|
|
pubkey: PubKey0x
|
|
|
|
slot: SlotString
|
|
|
|
signing_root: Eth2Digest0x
|
|
|
|
should_succeed: bool
|
2023-12-04 11:55:56 +00:00
|
|
|
should_succeed_complete: bool
|
2021-02-09 15:23:06 +00:00
|
|
|
|
|
|
|
CandidateVote = object
|
|
|
|
pubkey: PubKey0x
|
|
|
|
source_epoch: EpochString
|
|
|
|
target_epoch: EpochString
|
|
|
|
signing_root: Eth2Digest0x
|
|
|
|
should_succeed: bool
|
2023-12-04 11:55:56 +00:00
|
|
|
should_succeed_complete: bool
|
2021-02-09 15:23:06 +00:00
|
|
|
|
|
|
|
func toHexLogs(v: CandidateBlock): auto =
|
|
|
|
(
|
|
|
|
pubkey: v.pubkey.PubKeyBytes.toHex(),
|
|
|
|
slot: $v.slot.Slot.shortLog(),
|
|
|
|
signing_root: v.signing_root.Eth2Digest.data.toHex(),
|
|
|
|
should_succeed: v.should_succeed
|
|
|
|
)
|
|
|
|
func toHexLogs(v: CandidateVote): auto =
|
|
|
|
(
|
|
|
|
pubkey: v.pubkey.PubKeyBytes.toHex(),
|
|
|
|
source_epoch: v.source_epoch.Epoch.shortLog(),
|
|
|
|
target_epoch: v.target_epoch.Epoch.shortLog(),
|
|
|
|
signing_root: v.signing_root.Eth2Digest.data.toHex(),
|
|
|
|
should_succeed: v.should_succeed
|
|
|
|
)
|
|
|
|
|
|
|
|
chronicles.formatIt CandidateBlock: it.toHexLogs
|
|
|
|
chronicles.formatIt CandidateVote: it.toHexLogs
|
|
|
|
|
|
|
|
proc sqlite3db_delete(basepath, dbname: string) =
|
2024-02-28 17:28:04 +00:00
|
|
|
for extension in [".sqlite3-shm", ".sqlite3-wal", ".sqlite3"]:
|
|
|
|
try:
|
|
|
|
removeFile(basepath / dbname&extension)
|
|
|
|
except OSError:
|
|
|
|
discard
|
2021-02-09 15:23:06 +00:00
|
|
|
|
2023-12-04 11:55:56 +00:00
|
|
|
const InterchangeTestsDir = FixturesDir / "tests-slashing-v5.3.0" / "tests" / "generated"
|
2021-02-09 15:23:06 +00:00
|
|
|
const TestDir = ""
|
|
|
|
const TestDbPrefix = "test_slashprot_"
|
|
|
|
|
|
|
|
proc statusOkOrDuplicateOrMinSlotViolation(
|
|
|
|
status: Result[void, BadProposal], candidate: CandidateBlock): bool =
|
|
|
|
# 1. We might be importing a duplicate which EIP-3076 allows
|
|
|
|
# there is no reason during normal operation to integrate
|
|
|
|
# a duplicate so checkSlashableBlockProposal would have rejected it.
|
2022-01-20 16:14:06 +00:00
|
|
|
# 2. The test "multiple_interchanges_single_validator_single_message_gap"
|
2021-02-09 15:23:06 +00:00
|
|
|
# requires implementing pruning in-between import to keep the
|
|
|
|
# MinSlotViolation check relevant.
|
|
|
|
# That check prevents duplicate because it doesn't keep history.
|
|
|
|
#
|
|
|
|
# We need to special-case those exceptions to pass all tests
|
|
|
|
if status.isOk:
|
|
|
|
return true
|
|
|
|
if status.error.kind == DoubleProposal and
|
|
|
|
candidate.signing_root.Eth2Digest != Eth2Digest() and
|
|
|
|
status.error.existingBlock == candidate.signing_root.Eth2Digest:
|
|
|
|
warn "Block already exists in the DB",
|
|
|
|
candidateBlock = candidate
|
|
|
|
return true
|
|
|
|
elif status.error.kind == MinSlotViolation:
|
|
|
|
# Note: we tested the codepath without pruning.
|
|
|
|
# Furthermore it's better to be to eager on MinSlotViolation
|
|
|
|
# than allow slashing (unless the MinSlot is too far in the future)
|
2022-01-20 16:14:06 +00:00
|
|
|
warn "Block violates low watermark requirement. It might be an already pruned block.",
|
2021-02-09 15:23:06 +00:00
|
|
|
candidateBlock = candidate,
|
|
|
|
error = status.error
|
|
|
|
return true
|
|
|
|
return false
|
|
|
|
|
|
|
|
proc statusOkOrDuplicateOrMinEpochViolation(
|
|
|
|
status: Result[void, BadVote], candidate: CandidateVote): bool =
|
|
|
|
# We might be importing a duplicate which EIP-3076 allows
|
|
|
|
# there is no reason during normal operation to integrate
|
|
|
|
# a duplicate so checkSlashableAttestation would have rejected it.
|
|
|
|
# We special-case that for imports.
|
|
|
|
if status.isOk:
|
|
|
|
return true
|
|
|
|
if status.error.kind == DoubleVote and
|
|
|
|
candidate.signing_root.Eth2Digest != Eth2Digest() and
|
|
|
|
status.error.existingAttestation == candidate.signing_root.Eth2Digest:
|
|
|
|
warn "Attestation already exists in the DB",
|
|
|
|
candidateAttestation = candidate
|
|
|
|
return true
|
|
|
|
elif status.error.kind in {MinSourceViolation, MinTargetViolation}:
|
|
|
|
# Note: we tested the codepath without pruning.
|
|
|
|
# Furthermore it's better to be to eager on MinSlotViolation
|
|
|
|
# than allow slashing (unless the MinSlot is too far in the future)
|
2022-01-20 16:14:06 +00:00
|
|
|
warn "Attestation violates low watermark requirement. It might be an already pruned attestation.",
|
2021-02-09 15:23:06 +00:00
|
|
|
candidateAttestation = candidate,
|
|
|
|
error = status.error
|
|
|
|
return true
|
|
|
|
return false
|
|
|
|
|
2024-02-28 17:28:04 +00:00
|
|
|
proc runTest(identifier: string) {.raises: [IOError, SerializationError].} =
|
2021-02-09 15:23:06 +00:00
|
|
|
|
|
|
|
# The tests produce a lot of log noise
|
2021-10-12 11:36:52 +00:00
|
|
|
# echo "\n\n===========================================\n\n"
|
2021-02-09 15:23:06 +00:00
|
|
|
|
2022-01-20 16:14:06 +00:00
|
|
|
let t = parseTest(InterchangeTestsDir/identifier, Json, TestInterchange)
|
2021-02-09 15:23:06 +00:00
|
|
|
|
2022-01-20 16:14:06 +00:00
|
|
|
# Create a test specific DB
|
|
|
|
let dbname = TestDbPrefix & identifier.changeFileExt("")
|
2021-02-09 15:23:06 +00:00
|
|
|
|
2022-01-20 16:14:06 +00:00
|
|
|
# Delete existing db in case of previous test failure
|
|
|
|
sqlite3db_delete(TestDir, dbname)
|
2021-02-09 15:23:06 +00:00
|
|
|
|
2022-01-20 16:14:06 +00:00
|
|
|
let db = SlashingProtectionDB.init(
|
|
|
|
Eth2Digest t.genesis_validators_root,
|
|
|
|
TestDir,
|
|
|
|
dbname
|
|
|
|
)
|
|
|
|
# We don't use defer to auto-close+delete the DB
|
|
|
|
# as in case of issue we want to keep the DB around for investigation.
|
|
|
|
|
|
|
|
for step in t.steps:
|
|
|
|
let status = db.inclSPDIR(step.interchange)
|
|
|
|
if not step.should_succeed:
|
|
|
|
doAssert siFailure == status,
|
|
|
|
"Unexpected error:\n" &
|
|
|
|
" " & $status & "\n"
|
|
|
|
elif step.contains_slashable_data:
|
2022-03-04 14:43:34 +00:00
|
|
|
doAssert status in {siPartial, siSuccess},
|
2022-01-20 16:14:06 +00:00
|
|
|
"Unexpected error:\n" &
|
|
|
|
" " & $status & "\n"
|
|
|
|
else:
|
|
|
|
doAssert siSuccess == status,
|
|
|
|
"Unexpected error:\n" &
|
|
|
|
" " & $status & "\n"
|
|
|
|
|
|
|
|
for blck in step.blocks:
|
2022-03-04 14:43:34 +00:00
|
|
|
let pubkey = ValidatorPubKey.fromRaw(blck.pubkey.PubKeyBytes).get()
|
2023-11-19 13:08:07 +00:00
|
|
|
let status = db.db_v2.checkSlashableBlockProposal(
|
|
|
|
Opt.none(ValidatorIndex),
|
2022-03-04 14:43:34 +00:00
|
|
|
pubkey,
|
2022-01-20 16:14:06 +00:00
|
|
|
Slot blck.slot
|
|
|
|
)
|
|
|
|
if blck.should_succeed:
|
|
|
|
doAssert status.statusOkOrDuplicateOrMinSlotViolation(blck),
|
2021-02-09 15:23:06 +00:00
|
|
|
"Unexpected error:\n" &
|
2022-01-20 16:14:06 +00:00
|
|
|
" " & $status & "\n" &
|
|
|
|
" for " & $toHexLogs(blck)
|
2022-03-04 14:43:34 +00:00
|
|
|
|
|
|
|
# https://github.com/eth-clients/slashing-protection-interchange-tests/pull/14
|
|
|
|
# Successful blocks are to be incoporated in the DB
|
|
|
|
if status.isOk(): # Skip duplicates
|
|
|
|
let status = db.db_v2.registerBlock(
|
2023-11-19 13:08:07 +00:00
|
|
|
Opt.none(ValidatorIndex),
|
2022-03-04 14:43:34 +00:00
|
|
|
pubkey, Slot blck.slot,
|
|
|
|
Eth2Digest blck.signing_root
|
|
|
|
)
|
|
|
|
doAssert status.isOk(),
|
|
|
|
"Failure to register block: " & $status
|
|
|
|
|
2021-02-09 15:23:06 +00:00
|
|
|
else:
|
2022-01-20 16:14:06 +00:00
|
|
|
doAssert status.isErr(),
|
|
|
|
"Unexpected success:\n" &
|
|
|
|
" status: " & $status & "\n" &
|
|
|
|
" for " & $toHexLogs(blck)
|
|
|
|
|
|
|
|
for att in step.attestations:
|
2022-03-04 14:43:34 +00:00
|
|
|
let pubkey = ValidatorPubKey.fromRaw(att.pubkey.PubKeyBytes).get()
|
|
|
|
|
2023-11-19 13:08:07 +00:00
|
|
|
let status = db.db_v2.checkSlashableAttestation(Opt.none(ValidatorIndex),
|
2022-03-04 14:43:34 +00:00
|
|
|
pubkey,
|
2022-01-20 16:14:06 +00:00
|
|
|
Epoch att.source_epoch,
|
|
|
|
Epoch att.target_epoch
|
|
|
|
)
|
|
|
|
if att.should_succeed:
|
|
|
|
doAssert status.statusOkOrDuplicateOrMinEpochViolation(att),
|
2021-02-09 15:23:06 +00:00
|
|
|
"Unexpected error:\n" &
|
2022-01-20 16:14:06 +00:00
|
|
|
" " & $status & "\n" &
|
|
|
|
" for " & $toHexLogs(att)
|
2022-03-04 14:43:34 +00:00
|
|
|
|
|
|
|
# https://github.com/eth-clients/slashing-protection-interchange-tests/pull/14
|
|
|
|
# Successful attestations are to be incoporated in the DB
|
|
|
|
if status.isOk(): # Skip duplicates
|
|
|
|
let status = db.db_v2.registerAttestation(
|
2023-11-19 13:08:07 +00:00
|
|
|
Opt.none(ValidatorIndex),
|
2022-03-04 14:43:34 +00:00
|
|
|
pubkey,
|
|
|
|
Epoch att.source_epoch,
|
|
|
|
Epoch att.target_epoch,
|
|
|
|
Eth2Digest att.signing_root
|
|
|
|
)
|
|
|
|
doAssert status.isOk(),
|
|
|
|
"Failure to register attestation: " & $status
|
2022-01-20 16:14:06 +00:00
|
|
|
else:
|
|
|
|
doAssert status.isErr(),
|
|
|
|
"Unexpected success:\n" &
|
|
|
|
" " & $status & "\n" &
|
|
|
|
" for " & $toHexLogs(att)
|
|
|
|
|
|
|
|
# Now close and delete resources.
|
|
|
|
db.close()
|
|
|
|
sqlite3db_delete(TestDir, dbname)
|
2021-02-09 15:23:06 +00:00
|
|
|
|
2021-04-28 16:41:02 +00:00
|
|
|
suite "Slashing Interchange tests " & preset():
|
2021-07-02 17:33:59 +00:00
|
|
|
for kind, path in walkDir(
|
|
|
|
InterchangeTestsDir, relative = true, checkDir = true):
|
2022-01-20 16:14:06 +00:00
|
|
|
test "Slashing test: " & path:
|
|
|
|
|
2022-03-04 14:43:34 +00:00
|
|
|
if path == "single_validator_source_greater_than_target_surrounded.json":
|
|
|
|
# TODO: test relying on invalid behavior source > target
|
2022-01-20 16:14:06 +00:00
|
|
|
skip()
|
|
|
|
elif path == "single_validator_source_greater_than_target_surrounding.json":
|
|
|
|
# TODO: test relying on unclear minification behavior:
|
|
|
|
# creating an invalid minified attestation with source > target
|
|
|
|
# or setting target = max(source, target)
|
|
|
|
skip()
|
2022-03-04 14:43:34 +00:00
|
|
|
elif path == "single_validator_resign_attestation.json":
|
|
|
|
# It's simpler to just disallow register an attestation twice for the same (source, target)
|
|
|
|
# rather than also checking the actual signing_root
|
|
|
|
skip()
|
2022-01-20 16:14:06 +00:00
|
|
|
else:
|
2022-06-07 17:05:06 +00:00
|
|
|
runTest(path)
|