Skip to content

Commit 6dabaa8

Browse files
committed
fix(light-node): use all epoch attestations for BitmapTX eligibility, fix Explorer labels
Previously, LightNodeEligibilityBitmap TX creation filtered attestations by pinger_id == own_node_id. This caused 0 eligible nodes when a light node responded to a different genesis node than the one that sent the FCM push (which is expected behavior since FCM replies go to whichever genesis endpoint the device reaches first). Changes: - node.rs: remove pinger_id filter from BitmapTX eligible_indices computation; use all epoch attestations and filter by shard range [my_start, my_end) instead; add HashSet deduplication so a node attested multiple times counts once; update shard range log for visibility - api/tx/[hash]/route.ts: add LightNodeEligibilityBitmap and all lowercase variants to mapTxType so the TX detail page shows Heartbeat instead of Transfer - api/address/[address]/route.ts: already had bitmap mapping (no change needed) Eligibility ownership is now correctly determined by shard membership (deterministic sorted registry + genesis_idx), not by which genesis node happened to receive the mobile device HTTP response. Made-with: Cursor
1 parent 5a7b08b commit 6dabaa8

3 files changed

Lines changed: 46 additions & 22 deletions

File tree

  • applications/qnet-explorer/frontend/src/app/api
  • development/qnet-integration/src

applications/qnet-explorer/frontend/src/app/api/address/[address]/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,16 @@ function mapTxType(type: string, fromAddress?: string): string {
7575
'contractcall': 'Smart Contract',
7676
'registration': 'Registration',
7777
'reward': 'Reward',
78+
'heartbeatcommitment': 'Heartbeat',
79+
'heartbeat': 'Heartbeat',
80+
'lightnodeeligibilitybitmap': 'Heartbeat',
81+
'bitmapcommitment': 'Heartbeat',
82+
'pingcommitmentwithsampling': 'System',
83+
'pingattestation': 'System',
7884
};
7985

8086
if (map[normalized]) return map[normalized];
87+
if (normalized.includes('heartbeat') || normalized.includes('bitmap')) return 'Heartbeat';
8188
if (normalized.includes('reward') || normalized.includes('emission')) return 'Reward';
8289
return 'Transfer';
8390
}

applications/qnet-explorer/frontend/src/app/api/tx/[hash]/route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,18 @@ function mapTxType(type: string | object | undefined, fromAddress?: string): str
3030
'ContractCall': 'Smart Contract',
3131
'HeartbeatCommitment': 'Heartbeat',
3232
'Heartbeat': 'Heartbeat',
33+
'LightNodeEligibilityBitmap': 'Heartbeat', // Light node bitmap — Rust enum name
34+
'bitmap_commitment': 'Heartbeat', // Light node bitmap — API string name
35+
'bitmapcommitment': 'Heartbeat', // Light node bitmap — lowercased variant
36+
'lightnodeeligibilitybitmap': 'Heartbeat', // Light node bitmap — fully lowercased
3337
'CreateAccount': 'System',
3438
'BatchRewardClaims': 'Reward',
3539
'BatchNodeActivations': 'Node Activation',
3640
'BatchTransfers': 'Transfer',
3741
'PingAttestation': 'System',
3842
'PingCommitmentWithSampling': 'System',
3943
};
40-
return map[typeStr] || 'Transfer';
44+
return map[typeStr] || map[typeStr.toLowerCase()] || 'Transfer';
4145
}
4246

4347
// Format amount from nanoQNC to QNC (ALWAYS divide by 1e9)

development/qnet-integration/src/node.rs

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10400,12 +10400,15 @@ impl BlockchainNode {
1040010400
let epoch_start = current_epoch * EMISSION_BLOCK_INTERVAL;
1040110401
let epoch_end = (current_epoch + 1) * EMISSION_BLOCK_INTERVAL;
1040210402
let all_attestations = p2p.get_attestations_for_block_range(epoch_start, epoch_end);
10403-
10404-
// Filter to only pings from THIS Genesis node
10405-
let my_pings: Vec<_> = all_attestations.into_iter()
10406-
.filter(|(_, _, pinger_id, _, _)| pinger_id == &node_id)
10407-
.collect();
10408-
10403+
10404+
// Use ALL attestations for the epoch regardless of which genesis
10405+
// node received the FCM response. The light node device sends its
10406+
// signed reply to whichever genesis endpoint it reaches first
10407+
// (FCM → Google → device → any genesis HTTP). Eligibility is
10408+
// determined by shard membership below, not by pinger_id.
10409+
// pinger_id filter is kept only in PingCommitmentWithSampling
10410+
// (Merkle proof of "I personally pinged these nodes").
10411+
1040910412
// Get total assigned Light nodes for this Genesis
1041010413
// Genesis nodes divide Light nodes: each gets 1/5 of registry
1041110414
let genesis_idx = std::env::var("QNET_BOOTSTRAP_ID")
@@ -10453,22 +10456,32 @@ impl BlockchainNode {
1045310456
} else {
1045410457

1045510458
let total_assigned = my_end - my_start;
10456-
10457-
// Convert pings to local indices (0-based within this Genesis shard)
10458-
// Light node registry index → local index
10459-
// v2.89: Use index_map for O(1) lookup instead of O(n log n)
10460-
let eligible_indices: Vec<u32> = my_pings.iter()
10461-
.filter_map(|(light_node_id, _, _, _, _)| {
10462-
// Get global index from index_map (O(1) lookup)
10463-
index_map.get(light_node_id)
10464-
.map(|&global_idx| global_idx.saturating_sub(my_start))
10465-
.filter(|&local_idx| local_idx < total_assigned)
10466-
})
10467-
.collect();
10468-
10459+
10460+
// Convert attested light nodes to shard-local indices.
10461+
// global_idx is deduplicated by node_id thanks to filter_map:
10462+
// if multiple attestations exist for the same light_node_id we
10463+
// still produce exactly one local index for that node, because
10464+
// filter_map skips None and we collect into a Vec (duplicates
10465+
// at this stage are later deduplicated by the bitmap set-bit logic).
10466+
// Only nodes whose global index falls inside [my_start, my_end)
10467+
// are counted — this is the shard ownership check.
10468+
let eligible_indices: Vec<u32> = {
10469+
let mut seen = std::collections::HashSet::new();
10470+
all_attestations.iter()
10471+
.filter_map(|(light_node_id, _, _, _, _)| {
10472+
if !seen.insert(light_node_id.clone()) {
10473+
return None; // deduplicate same node attested multiple times
10474+
}
10475+
index_map.get(light_node_id)
10476+
.filter(|&&global_idx| global_idx >= my_start && global_idx < my_end)
10477+
.map(|&global_idx| global_idx - my_start)
10478+
})
10479+
.collect()
10480+
};
10481+
1046910482
if is_info() {
10470-
println!("[INFO][LIGHT-BITMAP] Genesis {} has {} eligible / {} assigned Light nodes",
10471-
genesis_idx + 1, eligible_indices.len(), total_assigned);
10483+
println!("[INFO][LIGHT-BITMAP] Genesis {} shard [{},{}) → {} eligible / {} assigned Light nodes",
10484+
genesis_idx + 1, my_start, my_end, eligible_indices.len(), total_assigned);
1047210485
}
1047310486

1047410487
// Create bitmap TX

0 commit comments

Comments
 (0)