-
Notifications
You must be signed in to change notification settings - Fork 239
/
test_fixtures.nim
272 lines (243 loc) · 10.1 KB
/
test_fixtures.nim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# 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
# Status lib
chronicles,
# Internal
../../beacon_chain/validators/[slashing_protection, slashing_protection_v2],
../../beacon_chain/spec/datatypes/base,
# Test utilies
../testutil, ../testdbutil,
../consensus_spec/fixtures_utils
from std/os import changeFileExt, removeFile, walkDir, `/`
from stew/byteutils import toHex
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
contains_slashable_data: bool
## Does "interchange" contain slashable data either as standalone
## or with regards to previous steps
## 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.
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
should_succeed_complete: bool
CandidateVote = object
pubkey: PubKey0x
source_epoch: EpochString
target_epoch: EpochString
signing_root: Eth2Digest0x
should_succeed: bool
should_succeed_complete: bool
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) =
for extension in [".sqlite3-shm", ".sqlite3-wal", ".sqlite3"]:
try:
removeFile(basepath / dbname&extension)
except OSError:
discard
const InterchangeTestsDir = FixturesDir / "tests-slashing-v5.3.0" / "tests" / "generated"
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.
# 2. The test "multiple_interchanges_single_validator_single_message_gap"
# 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)
warn "Block violates low watermark requirement. It might be an already pruned block.",
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)
warn "Attestation violates low watermark requirement. It might be an already pruned attestation.",
candidateAttestation = candidate,
error = status.error
return true
return false
proc runTest(identifier: string) {.raises: [IOError, SerializationError].} =
# The tests produce a lot of log noise
# echo "\n\n===========================================\n\n"
let t = parseTest(InterchangeTestsDir/identifier, Json, TestInterchange)
# Create a test specific DB
let dbname = TestDbPrefix & identifier.changeFileExt("")
# Delete existing db in case of previous test failure
sqlite3db_delete(TestDir, dbname)
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:
doAssert status in {siPartial, siSuccess},
"Unexpected error:\n" &
" " & $status & "\n"
else:
doAssert siSuccess == status,
"Unexpected error:\n" &
" " & $status & "\n"
for blck in step.blocks:
let pubkey = ValidatorPubKey.fromRaw(blck.pubkey.PubKeyBytes).get()
let status = db.db_v2.checkSlashableBlockProposal(
Opt.none(ValidatorIndex),
pubkey,
Slot blck.slot
)
if blck.should_succeed:
doAssert status.statusOkOrDuplicateOrMinSlotViolation(blck),
"Unexpected error:\n" &
" " & $status & "\n" &
" for " & $toHexLogs(blck)
# 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(
Opt.none(ValidatorIndex),
pubkey, Slot blck.slot,
Eth2Digest blck.signing_root
)
doAssert status.isOk(),
"Failure to register block: " & $status
else:
doAssert status.isErr(),
"Unexpected success:\n" &
" status: " & $status & "\n" &
" for " & $toHexLogs(blck)
for att in step.attestations:
let pubkey = ValidatorPubKey.fromRaw(att.pubkey.PubKeyBytes).get()
let status = db.db_v2.checkSlashableAttestation(Opt.none(ValidatorIndex),
pubkey,
Epoch att.source_epoch,
Epoch att.target_epoch
)
if att.should_succeed:
doAssert status.statusOkOrDuplicateOrMinEpochViolation(att),
"Unexpected error:\n" &
" " & $status & "\n" &
" for " & $toHexLogs(att)
# 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(
Opt.none(ValidatorIndex),
pubkey,
Epoch att.source_epoch,
Epoch att.target_epoch,
Eth2Digest att.signing_root
)
doAssert status.isOk(),
"Failure to register attestation: " & $status
else:
doAssert status.isErr(),
"Unexpected success:\n" &
" " & $status & "\n" &
" for " & $toHexLogs(att)
# Now close and delete resources.
db.close()
sqlite3db_delete(TestDir, dbname)
suite "Slashing Interchange tests " & preset():
for kind, path in walkDir(
InterchangeTestsDir, relative = true, checkDir = true):
test "Slashing test: " & path:
if path == "single_validator_source_greater_than_target_surrounded.json":
# TODO: test relying on invalid behavior source > target
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()
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()
else:
runTest(path)