Skip to content

Commit beee015

Browse files
committed
pre-emptive duplicate validator detection heuristic
1 parent 602f344 commit beee015

File tree

5 files changed

+90
-2
lines changed

5 files changed

+90
-2
lines changed

beacon_chain/conf.nim

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,11 @@ type
231231
desc: "Write SSZ dumps of blocks, attestations and states to data dir"
232232
name: "dump" }: bool
233233

234+
selfSlashingDetectionEpochs* {.
235+
defaultValue: 2
236+
desc: "Number of epochs of gossip to which to listen before broadcasting"
237+
name: "self-slashing-detection-epochs" }: uint64
238+
234239
of createTestnet:
235240
testnetDepositsFile* {.
236241
desc: "A LaunchPad deposits file for the genesis state validators"

beacon_chain/eth2_processor.nim

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import
44
chronicles, chronicles/chronos_tools, chronos, metrics,
55
./spec/[crypto, datatypes, digest],
66
./block_pools/[clearance, chain_dag],
7-
./attestation_aggregation, ./exit_pool,
7+
./attestation_aggregation, ./exit_pool, ./validator_pool,
88
./beacon_node_types, ./attestation_pool,
99
./time, ./conf, ./sszdump
1010

@@ -62,12 +62,15 @@ type
6262
chainDag*: ChainDAGRef
6363
attestationPool*: ref AttestationPool
6464
exitPool: ref ExitPool
65+
validatorPool: ref ValidatorPool
6566
quarantine*: QuarantineRef
6667

6768
blocksQueue*: AsyncQueue[BlockEntry]
6869
attestationsQueue*: AsyncQueue[AttestationEntry]
6970
aggregatesQueue*: AsyncQueue[AggregateEntry]
7071

72+
selfSlashingDetection*: SelfSlashingDetection
73+
7174
proc updateHead*(self: var Eth2Processor, wallSlot: Slot) =
7275
## Trigger fork choice and returns the new head block.
7376
## Can return `nil`
@@ -285,6 +288,34 @@ proc blockValidator*(
285288

286289
ValidationResult.Accept
287290

291+
proc checkForPotentialSelfSlashing(
292+
self: var Eth2Processor, attestationData: AttestationData,
293+
attesterIndices: HashSet[ValidatorIndex], wallSlot: Slot) =
294+
# Attestations remain valid for 32 slots, so avoid confusing with one's own
295+
# reflections, for a ATTESTATION_PROPAGATION_SLOT_RANGE div SLOTS_PER_EPOCH
296+
# period after the attestation slot. For mainnet this can be one additional
297+
# epoch, and for minimal, four epochs.
298+
const GUARD_EPOCHS = ATTESTATION_PROPAGATION_SLOT_RANGE div SLOTS_PER_EPOCH
299+
300+
let epoch = wallSlot.epoch
301+
# Can skip this whole conditional by setting relevant config value to 0
302+
if epoch < self.selfSlashingDetection.broadcastStartEpoch and
303+
epoch >= self.selfSlashingDetection.probeEpoch and
304+
epoch <= self.selfSlashingDetection.probeEpoch + GUARD_EPOCHS:
305+
let tgtBlck = self.chainDag.getRef(attestationData.target.root)
306+
doAssert not tgtBlck.isNil # because attestation is valid above
307+
308+
let epochRef = self.chainDag.getEpochRef(
309+
tgtBlck, attestationData.target.epoch)
310+
for validatorIndex in attesterIndices:
311+
let validatorPubkey = epochRef.validator_keys[validatorIndex]
312+
if self.validatorPool[].getValidator(validatorPubkey) !=
313+
default(AttachedValidator):
314+
notice "Found another validator using same public key; would be slashed",
315+
validatorIndex,
316+
validatorPubkey
317+
quit 1
318+
288319
proc attestationValidator*(
289320
self: var Eth2Processor,
290321
attestation: Attestation,
@@ -315,6 +346,8 @@ proc attestationValidator*(
315346
beacon_attestations_received.inc()
316347
beacon_attestation_delay.observe(delay.toFloatSeconds())
317348

349+
self.checkForPotentialSelfSlashing(attestation.data, v.value, wallSlot)
350+
318351
while self.attestationsQueue.full():
319352
let dropped = self.attestationsQueue.popFirst()
320353
doAssert dropped.finished, "popFirst sanity"
@@ -358,6 +391,9 @@ proc aggregateValidator*(
358391
beacon_aggregates_received.inc()
359392
beacon_aggregate_delay.observe(delay.toFloatSeconds())
360393

394+
self.checkForPotentialSelfSlashing(
395+
signedAggregateAndProof.message.aggregate.data, v.value, wallSlot)
396+
361397
while self.aggregatesQueue.full():
362398
let dropped = self.aggregatesQueue.popFirst()
363399
doAssert dropped.finished, "popFirst sanity"
@@ -453,6 +489,7 @@ proc new*(T: type Eth2Processor,
453489
chainDag: ChainDAGRef,
454490
attestationPool: ref AttestationPool,
455491
exitPool: ref ExitPool,
492+
validatorPool: ref ValidatorPool,
456493
quarantine: QuarantineRef,
457494
getWallTime: GetWallTimeFn): ref Eth2Processor =
458495
(ref Eth2Processor)(
@@ -461,6 +498,7 @@ proc new*(T: type Eth2Processor,
461498
chainDag: chainDag,
462499
attestationPool: attestationPool,
463500
exitPool: exitPool,
501+
validatorPool: validatorPool,
464502
quarantine: quarantine,
465503
blocksQueue: newAsyncQueue[BlockEntry](1),
466504
aggregatesQueue: newAsyncQueue[AggregateEntry](MAX_ATTESTATIONS.int),

beacon_chain/nimbus_beacon_node.nim

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,8 @@ proc init*(T: type BeaconNode,
288288
proc getWallTime(): BeaconTime = res.beaconClock.now()
289289

290290
res.processor = Eth2Processor.new(
291-
conf, chainDag, attestationPool, exitPool, quarantine, getWallTime)
291+
conf, chainDag, attestationPool, exitPool, newClone(res.attachedValidators),
292+
quarantine, getWallTime)
292293

293294
res.requestManager = RequestManager.init(
294295
network, res.processor.blocksQueue)
@@ -449,6 +450,33 @@ proc removeMessageHandlers(node: BeaconNode): Future[void] =
449450

450451
allFutures(unsubscriptions)
451452

453+
proc setupSelfSlashingProtection(node: BeaconNode, slot: Slot) =
454+
# When another client's already running, this is very likely to detect
455+
# potential duplicate validators, which can trigger slashing. Assuming
456+
# the most pessimal case of two validators started simultaneously, the
457+
# probability of triggering a slashable condition is up to 1/n, with n
458+
# being the number of epochs one waits before proposing or attesting.
459+
#
460+
# Every missed attestation costs approximately 3*get_base_reward(), which
461+
# can be up to around 10,000 Wei. Thus, skipping attestations isn't cheap
462+
# and one should gauge the likelihood of this simultaneous launch to tune
463+
# the epoch delay to one's perceived risk.
464+
#
465+
# This approach catches both startup and network outage conditions.
466+
467+
node.processor.selfSlashingDetection.broadcastStartEpoch =
468+
slot.epoch + node.config.selfSlashingDetectionEpochs
469+
# randomize() already called
470+
node.processor.selfSlashingDetection.probeEpoch =
471+
slot.epoch + rand(node.config.selfSlashingDetectionEpochs.int - 1).uint64
472+
doAssert node.processor.selfSlashingDetection.probeEpoch <
473+
node.processor.selfSlashingDetection.broadcastStartEpoch
474+
475+
debug "Setting up self-slashing protection",
476+
epoch = slot.epoch,
477+
probeEpoch = node.processor.selfSlashingDetection.probeEpoch,
478+
broadcastStartEpoch = node.processor.selfSlashingDetection.broadcastStartEpoch
479+
452480
proc onSlotStart(node: BeaconNode, lastSlot, scheduledSlot: Slot) {.async.} =
453481
## Called at the beginning of a slot - usually every slot, but sometimes might
454482
## skip a few in case we're running late.
@@ -588,6 +616,7 @@ proc onSlotStart(node: BeaconNode, lastSlot, scheduledSlot: Slot) {.async.} =
588616
headSlot = node.chainDag.head.slot,
589617
syncQueueLen
590618

619+
node.setupSelfSlashingProtection(slot)
591620
await node.addMessageHandlers()
592621
doAssert node.getTopicSubscriptionEnabled()
593622
elif

beacon_chain/spec/datatypes.nim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,10 @@ type
460460
stabilitySubnet*: uint64
461461
stabilitySubnetExpirationEpoch*: Epoch
462462

463+
SelfSlashingDetection* = object
464+
broadcastStartEpoch*: Epoch
465+
probeEpoch*: Epoch
466+
463467
func shortValidatorKey*(state: BeaconState, validatorIdx: int): string =
464468
($state.validators[validatorIdx].pubkey)[0..7]
465469

beacon_chain/validator_duties.nim

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,18 @@ proc handleValidatorDuties*(node: BeaconNode, lastSlot, slot: Slot) {.async.} =
503503

504504
var curSlot = lastSlot + 1
505505

506+
# In general, the default values of 0 are fine, since it will at most prevent
507+
# from gossiping (proposing beacon blocks, attesting, etc) for epoch 0.
508+
doAssert node.processor[].selfSlashingDetection.probeEpoch == 0 or (
509+
node.processor[].selfSlashingDetection.probeEpoch <
510+
node.processor[].selfSlashingDetection.broadcastStartEpoch)
511+
if curSlot.epoch < node.processor[].selfSlashingDetection.broadcastStartEpoch and
512+
curSlot.epoch != node.processor[].selfSlashingDetection.probeEpoch:
513+
notice "Waiting to gossip out to detect potential duplicate validators",
514+
broadcastStartEpoch = node.processor[].selfSlashingDetection.broadcastStartEpoch,
515+
probeEpoch = node.processor[].selfSlashingDetection.probeEpoch
516+
return
517+
506518
# Start by checking if there's work we should have done in the past that we
507519
# can still meaningfully do
508520
while curSlot < slot:

0 commit comments

Comments
 (0)