Skip to content

Commit 1dce06c

Browse files
committed
feat(light-nodes): identical retry/confirmation tracking for BitmapTX as HeartbeatCommitment
- Replace fire-and-forget HashSet with DashMap<u64, HeartbeatCommitmentStatus> for BitmapTX - Add retry logic: 3 retries with 10-block timeout, identical to HeartbeatCommitment - Add confirmation scan: detect BitmapTX inclusion in chain within 20-block window - Replace send_network_message with send_critical_tx_with_ack for producer forwarding - Add Gulf Stream logging and forwarded_to_producer tracking for BitmapTX - Remove RAM attestation fallback: reward proof is on-chain BitmapTX only - Fix light node ping response routing: include response_url in FCM and UnifiedPush payload - Mobile wallet: respond to pinging genesis node URL instead of random bootstrap node - Add QNET_HALT_HEIGHT for coordinated upgrades (Cosmos-style halt at target block) - All 10 lifecycle steps of BitmapTX now identical to HeartbeatCommitment Made-with: Cursor
1 parent 6c7f0b7 commit 1dce06c

4 files changed

Lines changed: 242 additions & 94 deletions

File tree

applications/qnet-mobile/src/services/PushService.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ export async function getNextPingTime() {
337337
* Respond to ping challenge (sign and send)
338338
* MANDATORY: Dilithium3 (ML-DSA-65) quantum signature — no Ed25519 fallback
339339
*/
340-
export async function respondToChallenge(nodeId, challenge) {
340+
export async function respondToChallenge(nodeId, challenge, responseUrl) {
341341
try {
342342
const Keychain = require('react-native-keychain');
343343

@@ -359,7 +359,7 @@ export async function respondToChallenge(nodeId, challenge) {
359359
const dilithiumSig = await signWithDilithium(challenge, pingSkHex, pingPkHex, pingNodeId);
360360
const formattedSignature = `ping_dilithium:${dilithiumSig}`;
361361

362-
const apiUrl = await getRandomBootstrapNodeAsync();
362+
const apiUrl = responseUrl || await getRandomBootstrapNodeAsync();
363363
const response = await fetch(
364364
`${apiUrl}/api/v1/light-node/ping-response`,
365365
{
@@ -405,8 +405,8 @@ export async function respondToChallenge(nodeId, challenge) {
405405
*/
406406
export async function handlePushMessage(data) {
407407
if (data?.action === 'ping_response' && data?.challenge && data?.node_id) {
408-
console.log('[Push] 📥 Ping received:', data.node_id);
409-
return await respondToChallenge(data.node_id, data.challenge);
408+
console.log('[Push] 📥 Ping received:', data.node_id, 'from:', data.response_url || 'random');
409+
return await respondToChallenge(data.node_id, data.challenge, data.response_url);
410410
}
411411
return false;
412412
}

development/qnet-integration/src/bin/qnet-node.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2902,6 +2902,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
29022902
}
29032903

29042904
// Start background node monitoring
2905+
// v8.0: Read QNET_HALT_HEIGHT once at startup for coordinated upgrades.
2906+
// Cosmos equivalent: halt-height flag. When ALL nodes set the same halt height,
2907+
// they all stop gracefully at the same block → operator updates binaries → restarts.
2908+
// Use case: breaking consensus changes (hard fork).
2909+
// Normal rolling updates: leave unset (nodes restart one-by-one, catch up via snapshot).
2910+
let halt_height: Option<u64> = std::env::var("QNET_HALT_HEIGHT")
2911+
.ok()
2912+
.and_then(|s| s.trim().parse::<u64>().ok());
2913+
2914+
if let Some(h) = halt_height {
2915+
println!("[INFO][HALT] QNET_HALT_HEIGHT={} — node will stop at this block (coordinated upgrade)", h);
2916+
}
2917+
29052918
let node_clone = node.clone();
29062919
let node_handle = tokio::spawn(async move {
29072920
// Keep node running and monitor
@@ -2916,6 +2929,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
29162929
println!("[MONITOR] ⚠️ No peers connected - running standalone");
29172930
}
29182931
}
2932+
2933+
// v8.0: QNET_HALT_HEIGHT — coordinated upgrade stop
2934+
// Works like Cosmos halt-height: set same value on all nodes,
2935+
// they all stop at that block → update binaries → restart.
2936+
if let Some(stop_at) = halt_height {
2937+
let current_height = node_clone.get_height().await;
2938+
if current_height >= stop_at {
2939+
println!("[INFO][HALT] Reached halt_height={} current={} — flushing and stopping for coordinated upgrade",
2940+
stop_at, current_height);
2941+
let storage = node_clone.get_storage();
2942+
match storage.flush_all() {
2943+
Ok(()) => println!("[INFO][HALT] storage.flush_all() complete"),
2944+
Err(e) => println!("[ERR][HALT] storage.flush_all() failed: {}", e),
2945+
}
2946+
println!("[INFO][HALT] Node stopped. Update binary and restart (remove QNET_HALT_HEIGHT).");
2947+
std::process::exit(0);
2948+
}
2949+
}
29192950
}
29202951
});
29212952

0 commit comments

Comments
 (0)