Skip to content

Commit 583b484

Browse files
committed
Fix critical network stall and recovery deadlock issues
- Add time-based emergency recovery to prevent infinite deadlock * Introduce EMERGENCY_STOP_TIME for wall-clock tracking * Recovery now triggers after 10 seconds OR 10 blocks (whichever first) * Prevents nodes from being stuck forever when blocks stop - Implement global network stall detection * Add LAST_BLOCK_PRODUCED_TIME/HEIGHT for tracking * Check every 30 seconds if no blocks produced for 10+ seconds * Automatically trigger emergency failover after 15 seconds - Lower fast sync threshold from 50 to 10 blocks * Faster detection of node desynchronization * Prevents gradual divergence accumulation - Improve fork detection and resolution * Immediate sync when own blocks rejected as forks * Bypass rate limiting for critical fork scenarios * Aggressive sync with network majority Architecture preserved: - Uses existing methods (select_emergency_producer, broadcast_emergency_producer_change) - Lock-free atomic operations for scalability - No additional network overhead in normal operation - Supports Super/Full/Light node types correctly Scalability for millions of nodes: - O(1) atomic operations, no locks - Local detection without network queries - Emergency broadcasts only when needed - No continuous heartbeat required
1 parent 81d8303 commit 583b484

2 files changed

Lines changed: 100 additions & 13 deletions

File tree

development/qnet-integration/src/node.rs

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub const MIN_COMPATIBLE_VERSION: u32 = 1; // Minimum version we can work with
1515
// PRODUCTION CONSTANTS - No hardcoded magic numbers!
1616
const ROTATION_INTERVAL_BLOCKS: u64 = 30; // Producer rotation every 30 blocks
1717
const MIN_BYZANTINE_NODES: usize = 4; // 3f+1 where f=1
18-
const FAST_SYNC_THRESHOLD: u64 = 50; // Trigger fast sync if behind by 50+ blocks
18+
const FAST_SYNC_THRESHOLD: u64 = 10; // Trigger fast sync if behind by 10+ blocks (lowered from 50 for faster detection)
1919
const FAST_SYNC_TIMEOUT_SECS: u64 = 60; // Fast sync timeout
2020
const BACKGROUND_SYNC_TIMEOUT_SECS: u64 = 30; // Background sync timeout
2121
const SNAPSHOT_FULL_INTERVAL: u64 = 10000; // Full snapshot every 10k blocks
@@ -69,11 +69,16 @@ pub fn set_emergency_producer_flag(block_height: u64, producer: String) {
6969
}
7070

7171
// CRITICAL: Global synchronization flags for API access
72-
use std::sync::atomic::{AtomicBool, Ordering};
72+
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
7373
static SYNC_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
7474
static FAST_SYNC_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
7575
pub static NODE_IS_SYNCHRONIZED: AtomicBool = AtomicBool::new(false);
7676

77+
// CRITICAL FIX: Track last block production time globally for stall detection
78+
// This prevents network from getting stuck when all nodes stop producing
79+
static LAST_BLOCK_PRODUCED_TIME: AtomicU64 = AtomicU64::new(0);
80+
static LAST_BLOCK_PRODUCED_HEIGHT: AtomicU64 = AtomicU64::new(0);
81+
7782
// NOTE: Removed ROTATION_NOTIFY - simple 1-second timing is more reliable
7883
// Testing showed that natural timing without interrupts prevents race conditions
7984

@@ -1444,9 +1449,14 @@ impl BlockchainNode {
14441449
continue;
14451450
}
14461451

1447-
// CRITICAL: Rate limit fork attempts (prevent DoS)
1452+
// CRITICAL: Rate limit fork attempts (prevent DoS)
1453+
// But allow immediate sync if it's our own fork
14481454
let last_attempt = *last_fork_attempt.read().await;
1449-
if last_attempt.elapsed().as_secs() < FORK_ATTEMPT_COOLDOWN_SECS {
1455+
let own_fork = if let Some(p2p) = &unified_p2p {
1456+
fork_producer == p2p.get_node_id()
1457+
} else { false };
1458+
1459+
if !own_fork && last_attempt.elapsed().as_secs() < FORK_ATTEMPT_COOLDOWN_SECS {
14501460
println!("[REORG] 🛡️ Fork attempt too soon - rate limited ({}s cooldown)", FORK_ATTEMPT_COOLDOWN_SECS);
14511461

14521462
// CRITICAL FIX: If we detect our own blocks being rejected as forks,
@@ -1689,6 +1699,10 @@ impl BlockchainNode {
16891699
*height.write().await = received_block.height;
16901700
println!("[BLOCKS] 📊 Global height updated to {}", received_block.height);
16911701

1702+
// CRITICAL FIX: Update last block time for stall detection
1703+
LAST_BLOCK_PRODUCED_TIME.store(get_timestamp_safe(), Ordering::Relaxed);
1704+
LAST_BLOCK_PRODUCED_HEIGHT.store(received_block.height, Ordering::Relaxed);
1705+
16921706
// CRITICAL FIX: Update P2P local height for message filtering
16931707
crate::unified_p2p::LOCAL_BLOCKCHAIN_HEIGHT.store(
16941708
received_block.height,
@@ -2875,6 +2889,50 @@ impl BlockchainNode {
28752889
last_production_height = microblock_height;
28762890
last_production_time = std::time::Instant::now();
28772891
}
2892+
2893+
// CRITICAL FIX: Global network stall detection
2894+
// Check if ANY block has been produced recently (by us or others)
2895+
let last_block_time = LAST_BLOCK_PRODUCED_TIME.load(Ordering::Relaxed);
2896+
let last_block_height = LAST_BLOCK_PRODUCED_HEIGHT.load(Ordering::Relaxed);
2897+
let current_time = get_timestamp_safe();
2898+
2899+
if last_block_time > 0 && current_time > last_block_time {
2900+
let time_since_last_block = current_time - last_block_time;
2901+
2902+
// CRITICAL: Trigger emergency if no blocks for 10+ seconds
2903+
// This is GLOBAL stall detection, not just local
2904+
if time_since_last_block > 10 && microblock_height > 0 {
2905+
println!("[STALL] 🚨 NETWORK STALL DETECTED! No blocks for {} seconds", time_since_last_block);
2906+
println!("[STALL] 📊 Last block: #{} at timestamp {}", last_block_height, last_block_time);
2907+
2908+
// Force emergency producer selection if we're supposed to be producing
2909+
if let Some(p2p) = &unified_p2p {
2910+
let next_height = microblock_height + 1;
2911+
let expected_producer = Self::select_microblock_producer(
2912+
next_height, &unified_p2p, &node_id, node_type,
2913+
Some(&storage), &quantum_poh
2914+
).await;
2915+
2916+
if time_since_last_block > 15 {
2917+
println!("[STALL] 🔥 Triggering emergency failover for producer: {}", expected_producer);
2918+
2919+
// Select emergency producer
2920+
let emergency_producer = Self::select_emergency_producer(
2921+
&expected_producer, next_height, &unified_p2p,
2922+
&node_id, node_type, Some(storage.clone())
2923+
).await;
2924+
2925+
// Broadcast emergency change
2926+
if let Err(e) = p2p.broadcast_emergency_producer_change(
2927+
&expected_producer, &emergency_producer,
2928+
next_height, "network_stall"
2929+
) {
2930+
println!("[STALL] ⚠️ Failed to broadcast emergency: {}", e);
2931+
}
2932+
}
2933+
}
2934+
}
2935+
}
28782936
}
28792937
// SYNC FIX: Fast catch-up mode for nodes that are far behind
28802938
// Using global flags defined at module level
@@ -2936,8 +2994,8 @@ impl BlockchainNode {
29362994
println!("[SYNC] ⚠️ Node is {} blocks behind network (local: {}, network: {})",
29372995
height_difference, microblock_height, network_height);
29382996

2939-
// For extreme lag (>50 blocks), use fast sync
2940-
if height_difference > 50 {
2997+
// For significant lag (>10 blocks), use fast sync
2998+
if height_difference > 10 {
29412999
// RACE CONDITION FIX: Only start fast sync if not already running
29423000
if !FAST_SYNC_IN_PROGRESS.swap(true, Ordering::SeqCst) {
29433001
println!("[SYNC] ⚡ FAST SYNC MODE: {} blocks behind, catching up...", height_difference);
@@ -4049,6 +4107,10 @@ impl BlockchainNode {
40494107
// This prevents phantom height where node claims height N without having block N
40504108
println!("[PRODUCER] ✅ Created and saved block #{}", microblock.height);
40514109

4110+
// CRITICAL FIX: Update global last block time for stall detection
4111+
LAST_BLOCK_PRODUCED_TIME.store(get_timestamp_safe(), Ordering::Relaxed);
4112+
LAST_BLOCK_PRODUCED_HEIGHT.store(microblock.height, Ordering::Relaxed);
4113+
40524114
// CRITICAL: Increment height for next iteration
40534115
// We only advance after successfully creating and storing the block
40544116
microblock_height = microblock.height; // Set to the block we just created

development/qnet-integration/src/unified_p2p.rs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ pub static EMERGENCY_STOP_PRODUCTION: Lazy<Arc<AtomicBool>> =
101101
pub static EMERGENCY_STOP_HEIGHT: Lazy<Arc<AtomicU64>> =
102102
Lazy::new(|| Arc::new(AtomicU64::new(0)));
103103

104+
// CRITICAL FIX: Track TIME of emergency stop to prevent deadlock
105+
// Recovery after 10 seconds (not blocks) to avoid infinite wait
106+
pub static EMERGENCY_STOP_TIME: Lazy<Arc<AtomicU64>> =
107+
Lazy::new(|| Arc::new(AtomicU64::new(0)));
108+
104109
// CRITICAL: Track emergency failovers in progress to prevent race conditions
105110
// Format: "emergency_failover_{height}" -> prevents multiple nodes from initiating same failover
106111
// SCALABILITY: DashSet for lock-free concurrent access with millions of nodes
@@ -7753,7 +7758,13 @@ impl SimplifiedP2P {
77537758
let current_stop_height = EMERGENCY_STOP_HEIGHT.load(Ordering::Relaxed);
77547759
if current_stop_height == 0 {
77557760
EMERGENCY_STOP_HEIGHT.store(block_height, Ordering::Relaxed);
7756-
println!("[RECOVERY] 📍 Will auto-recover after 10 blocks (at block #{})", block_height + 10);
7761+
// CRITICAL FIX: Also store TIME to prevent deadlock when blocks stop
7762+
let current_time = std::time::SystemTime::now()
7763+
.duration_since(std::time::UNIX_EPOCH)
7764+
.unwrap_or_default()
7765+
.as_secs();
7766+
EMERGENCY_STOP_TIME.store(current_time, Ordering::Relaxed);
7767+
println!("[RECOVERY] 📍 Will auto-recover after 10 seconds (time-based) or 10 blocks");
77577768
} else {
77587769
println!("[RECOVERY] ⚠️ Already stopped at block #{}, not resetting timer", current_stop_height);
77597770
}
@@ -7767,19 +7778,33 @@ impl SimplifiedP2P {
77677778
}
77687779
}
77697780

7770-
// Check if we should clear the emergency stop (been stopped for 10+ blocks)
7781+
// Check if we should clear the emergency stop (been stopped for 10+ blocks OR 10+ seconds)
77717782
// This applies to Super/Full nodes that were previously stopped
77727783
if EMERGENCY_STOP_PRODUCTION.load(Ordering::Relaxed) {
77737784
let stop_height = EMERGENCY_STOP_HEIGHT.load(Ordering::Relaxed);
7774-
if stop_height > 0 && block_height >= stop_height + 10 {
7775-
println!("[RECOVERY] ✅ Auto-clearing emergency stop after 10 blocks (stopped at #{}, now at #{})",
7776-
stop_height, block_height);
7785+
let stop_time = EMERGENCY_STOP_TIME.load(Ordering::Relaxed);
7786+
let current_time = std::time::SystemTime::now()
7787+
.duration_since(std::time::UNIX_EPOCH)
7788+
.unwrap_or_default()
7789+
.as_secs();
7790+
7791+
// CRITICAL FIX: Clear stop EITHER after 10 blocks OR 10 seconds (whichever comes first)
7792+
// This prevents deadlock when network stops producing blocks
7793+
let blocks_passed = if block_height > stop_height { block_height - stop_height } else { 0 };
7794+
let seconds_passed = if current_time > stop_time { current_time - stop_time } else { 0 };
7795+
7796+
if stop_height > 0 && (blocks_passed >= 10 || seconds_passed >= 10) {
7797+
println!("[RECOVERY] ✅ Auto-clearing emergency stop after {} blocks / {} seconds",
7798+
blocks_passed, seconds_passed);
77777799
EMERGENCY_STOP_PRODUCTION.store(false, Ordering::Relaxed);
77787800
EMERGENCY_STOP_HEIGHT.store(0, Ordering::Relaxed);
7801+
EMERGENCY_STOP_TIME.store(0, Ordering::Relaxed);
77797802
println!("[RECOVERY] 🚀 Node can now resume block production");
77807803
} else if stop_height > 0 {
7781-
let blocks_remaining = 10 - (block_height - stop_height);
7782-
println!("[RECOVERY] ⏳ Emergency stop active for {} more blocks", blocks_remaining);
7804+
let blocks_remaining = 10_u64.saturating_sub(blocks_passed);
7805+
let seconds_remaining = 10_u64.saturating_sub(seconds_passed);
7806+
println!("[RECOVERY] ⏳ Emergency stop active for {} more blocks OR {} more seconds",
7807+
blocks_remaining, seconds_remaining);
77837808
}
77847809
}
77857810

0 commit comments

Comments
 (0)