Skip to content

Commit 3ea5c20

Browse files
committed
refactor: replace grace period with slot-based operator doppelgänger detection
Refactors the operator doppelgänger protection system to use slot comparison instead of time-based grace period, improving startup time and reliability. Changes: State Management: - Replace DoppelgangerState enum with AtomicBool for simpler state tracking - Add startup_slot field to track service initialization time - Remove grace period logic (saves 411 seconds at startup) Detection Logic: - Implement slot-based detection: msg_slot > startup_slot indicates twin - Messages at or before startup_slot are ignored (our own old messages) - Add extract_message_slot() helper supporting both QBFT and PartialSignatureMessages - No race conditions possible since all outgoing messages blocked during monitoring Benefits: - Faster startup: eliminates 411-second grace period wait time - More reliable: slot comparison is deterministic, unaffected by network delays - Simpler code: fewer states, cleaner logic with lock-free atomic operations - Better edge case handling: restart in same slot, network delays, clock skew Testing: - Replace time-based async tests with slot-based sync tests - All 8 tests pass verifying correct twin detection behavior
1 parent a1a59b3 commit 3ea5c20

File tree

6 files changed

+190
-298
lines changed

6 files changed

+190
-298
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

anchor/client/src/cli.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -495,8 +495,8 @@ pub struct Node {
495495
#[clap(
496496
long,
497497
help = "Enable operator doppelgänger protection. When enabled, the node blocks all \
498-
outgoing messages during a grace period (to let old messages expire), then \
499-
monitors for messages with its operator ID. Shuts down if a twin is detected \
498+
outgoing messages and monitors the network for messages signed with its operator ID \
499+
that reference slots after startup. Shuts down if a twin operator is detected \
500500
to prevent QBFT protocol violations. Enabled by default.",
501501
display_order = 0,
502502
default_value_t = true,
@@ -508,9 +508,9 @@ pub struct Node {
508508
#[clap(
509509
long,
510510
value_name = "EPOCHS",
511-
help = "Number of epochs to monitor for twin operators after the grace period. \
512-
During monitoring, outgoing messages remain blocked and the node listens \
513-
for messages with its operator ID to detect duplicate instances.",
511+
help = "Number of epochs to monitor for twin operators using slot-based detection. \
512+
During monitoring, outgoing messages remain blocked and the node checks incoming \
513+
messages for slots after startup to detect duplicate operator instances.",
514514
display_order = 0,
515515
default_value_t = 2,
516516
requires = "operator_dg"

anchor/client/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,8 +437,16 @@ impl Client {
437437

438438
// Create operator doppelgänger protection if enabled (will be started after sync)
439439
let doppelganger_service = if config.operator_dg && config.impostor.is_none() {
440+
// Get current slot for slot-based detection baseline
441+
let current_slot = slot_clock.now().ok_or_else(|| {
442+
"Failed to get current slot for doppelgänger protection".to_string()
443+
})?;
444+
// Convert types::Slot to ssv_types::Slot
445+
let startup_slot = ssv_types::Slot::new(current_slot.as_u64());
446+
440447
Some(Arc::new(OperatorDoppelgangerService::new(
441448
operator_id.clone(),
449+
startup_slot,
442450
E::slots_per_epoch(),
443451
Duration::from_secs(spec.seconds_per_slot),
444452
executor.shutdown_sender(),

anchor/message_sender/src/network.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ impl<S: SlotClock + 'static, D: DutiesProvider> MessageSender for Arc<NetworkMes
5454
additional_message_callback: Option<Box<MessageCallback>>,
5555
) -> Result<(), Error> {
5656
// Check if doppelgänger protection is active - block outgoing messages
57-
// This includes both grace period (waiting for old messages to expire) and
58-
// monitoring period (actively detecting twins)
57+
// During the entire monitoring period, we block all outgoing messages to prevent
58+
// competition with potential twin operators
5959
if let Some(dg) = &self.doppelganger_service
60-
&& dg.is_active()
60+
&& dg.is_monitoring()
6161
{
6262
trace!("Dropping message send - doppelgänger protection active");
6363
return Ok(());
@@ -109,10 +109,10 @@ impl<S: SlotClock + 'static, D: DutiesProvider> MessageSender for Arc<NetworkMes
109109

110110
fn send(&self, message: SignedSSVMessage, committee_id: CommitteeId) -> Result<(), Error> {
111111
// Check if doppelgänger protection is active - block outgoing messages
112-
// This includes both grace period (waiting for old messages to expire) and
113-
// monitoring period (actively detecting twins)
112+
// During the entire monitoring period, we block all outgoing messages to prevent
113+
// competition with potential twin operators
114114
if let Some(dg) = &self.doppelganger_service
115-
&& dg.is_active()
115+
&& dg.is_monitoring()
116116
{
117117
trace!("Dropping message send - doppelgänger protection active");
118118
return Ok(());

anchor/operator_doppelganger/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ edition = { workspace = true }
55

66
[dependencies]
77
database = { workspace = true }
8+
ethereum_ssz = { workspace = true }
89
futures = { workspace = true }
9-
message_validator = { workspace = true }
1010
parking_lot = { workspace = true }
1111
ssv_types = { workspace = true }
1212
task_executor = { workspace = true }
1313
tokio = { workspace = true }
1414
tracing = { workspace = true }
15+
types = { workspace = true }
1516

1617
[dev-dependencies]
1718
async-channel = { workspace = true }

0 commit comments

Comments
 (0)