Skip to content

Commit 949abfd

Browse files
dicethedevgreptile-apps[bot]pablodeymo
authored
fix: skip validator duties while syncing (lambdaclass#418)
## 🗒️ Description / Motivation - Prevents validators from proposing blocks or producing attestations while their local view is syncing. - Signing with a stale local view can place proposals and attestations on a chain the network has already left. - Builds on the leanEthereum/leanSpec#708 sync heuristic added in the preceding PR (lambdaclass#417 ). ## What Changed - Gate scheduled block proposals using the node’s sync status. - Report `has_proposal = false` to the store when a proposal is skipped. - Gate attestation production at interval 1. - Log skipped proposal and attestation duties. - Keep block processing, fork choice, aggregation, metrics, and key advancement active while syncing. - Add tests covering gating, recovery, and network-wide stalls. ## Correctness / Behavior Guarantees - Proposals and attestations are disabled only while the local node is considered syncing. - Duties resume once the node catches up past the hysteresis recovery boundary. - Duties remain enabled during a network-wide stall so validators can help the chain recover. - Incoming network data and synchronization processing remain unaffected. - A skipped proposal is not reported to fork choice as locally available. ## Tests Added / Run - Added tests verifying: - Proposals and attestations are gated while syncing. - Duties resume after catching up. - Duties remain enabled during network-wide stalls. - Ran `cargo test -p ethlambda-blockchain --lib --offline`. - 31 passed; 0 failed. - Ran `cargo clippy -p ethlambda-blockchain --lib --offline -- -D warnings`. - Ran `cargo fmt --all -- --check`. - Ran `git diff --check`. ## Related Issues / PRs - Closes lambdaclass#236 - Depends on the sync-status heuristic PR. - Follows leanEthereum/leanSpec#708. ## ✅ Verification Checklist - [x] Ran `make fmt` — clean - [x] Ran `make lint` (clippy with `-D warnings`) — clean - [ ] Ran `cargo test --workspace --release` — all passing --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Pablo Deymonnaz <pdeymon@fi.uba.ar>
1 parent 7208707 commit 949abfd

1 file changed

Lines changed: 55 additions & 2 deletions

File tree

‎crates/blockchain/src/lib.rs‎

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ impl SyncStatusTracker {
9696
metrics::SyncStatus::Synced
9797
}
9898
}
99+
100+
fn duties_allowed(&self) -> bool {
101+
!self.syncing
102+
}
103+
104+
fn gate_proposer(&self, proposer: Option<u64>) -> Option<u64> {
105+
proposer.filter(|_| self.duties_allowed())
106+
}
99107
}
100108

101109
/// Milliseconds until the next interval boundary, measured relative to genesis.
@@ -273,9 +281,16 @@ impl BlockChainServer {
273281
// At interval 0, check if we will propose (but don't build the block yet).
274282
// Tick forkchoice first to accept attestations, then build the block
275283
// using the freshly-accepted attestations.
276-
let proposer_validator_id = (interval == 0 && slot > 0)
284+
let scheduled_proposer = (interval == 0 && slot > 0)
277285
.then(|| self.get_our_proposer(slot))
278286
.flatten();
287+
let proposer_validator_id = self.sync_status.gate_proposer(scheduled_proposer);
288+
289+
if let Some(validator_id) = scheduled_proposer
290+
&& proposer_validator_id.is_none()
291+
{
292+
info!(%slot, %validator_id, "Skipping block proposal while syncing");
293+
}
279294

280295
// Snapshot the pre-merge `new_payloads` set at the end-of-slot promote
281296
// (interval 4), so the post-block report for this round sees its
@@ -334,7 +349,11 @@ impl BlockChainServer {
334349
slot - 1,
335350
);
336351
}
337-
self.produce_attestations(slot, is_aggregator);
352+
if self.sync_status.duties_allowed() {
353+
self.produce_attestations(slot, is_aggregator);
354+
} else if !self.key_manager.validator_ids().is_empty() {
355+
info!(%slot, "Skipping attestations while syncing");
356+
}
338357
}
339358

340359
// Update safe target slot metric (updated by store.on_tick at interval 3)
@@ -1068,6 +1087,12 @@ mod tests {
10681087
metrics::SyncStatus::Synced
10691088
);
10701089
}
1090+
1091+
let first_syncing_slot = 10 + SYNC_LAG_THRESHOLD + 1;
1092+
assert_eq!(
1093+
tracker.update(first_syncing_slot, 10, first_syncing_slot),
1094+
metrics::SyncStatus::Syncing
1095+
);
10711096
}
10721097

10731098
#[test]
@@ -1112,4 +1137,32 @@ mod tests {
11121137

11131138
assert_eq!(tracker.update(15, 20, 20), metrics::SyncStatus::Synced);
11141139
}
1140+
1141+
#[test]
1142+
fn syncing_gates_proposals_and_attestations() {
1143+
let mut tracker = SyncStatusTracker::default();
1144+
tracker.update(20, 0, 20);
1145+
1146+
assert!(!tracker.duties_allowed());
1147+
assert_eq!(tracker.gate_proposer(Some(3)), None);
1148+
}
1149+
1150+
#[test]
1151+
fn caught_up_node_allows_proposals_and_attestations() {
1152+
let mut tracker = SyncStatusTracker::default();
1153+
tracker.update(20, 0, 20);
1154+
tracker.update(20, 18, 20);
1155+
1156+
assert!(tracker.duties_allowed());
1157+
assert_eq!(tracker.gate_proposer(Some(3)), Some(3));
1158+
}
1159+
1160+
#[test]
1161+
fn network_stall_keeps_proposals_and_attestations_enabled() {
1162+
let mut tracker = SyncStatusTracker::default();
1163+
tracker.update(100, 0, 0);
1164+
1165+
assert!(tracker.duties_allowed());
1166+
assert_eq!(tracker.gate_proposer(Some(3)), Some(3));
1167+
}
11151168
}

0 commit comments

Comments
 (0)