@@ -242,55 +242,58 @@ async fn batch_insert_produced_blocks(pool: &SqlitePool, blocks: &[ProducedBlock
242
242
Ok ( ( ) )
243
243
}
244
244
245
- /// Marks heartbeat presence entries as outdated (disabled) based on global slot comparisons.
245
+ /// Marks heartbeat presence entries as outdated (disabled) based on block height comparisons.
246
246
///
247
247
/// This function performs the following steps:
248
- /// 1. Finds the maximum global slot for each window (considering only non-disabled entries).
249
- /// 2. Identifies the previous window's maximum global slot for each window.
250
- /// 3. Marks a presence entry as disabled if its global slot is less than:
251
- /// - The maximum global slot of the previous window (if it exists).
248
+ /// 1. Finds the maximum block height for each window (considering only non-disabled entries).
249
+ /// 2. Identifies the previous window's maximum block height for each window.
250
+ /// 3. Marks a presence entry as disabled if its block height is less than:
251
+ /// - The maximum block height of the previous window minus a tolerance of $HEIGHT_TOLERANCE blocks (if it exists).
252
252
///
253
- /// This approach allows for a full window of tolerance in synchronization:
254
- /// - Entries matching or exceeding the previous window's max slot are considered up-to-date.
255
- /// - This allows for slight delays in propagation between windows.
253
+ /// This approach allows for a reasonable tolerance in synchronization:
254
+ /// - Entries matching or exceeding the previous window's max height - $HEIGHT_TOLERANCE are considered up-to-date.
255
+ /// - This allows for slight delays in block propagation between windows.
256
256
///
257
257
/// Note: The first window in the sequence will not have any entries marked as disabled,
258
258
/// as there is no previous window to compare against.
259
259
///
260
260
/// Returns the number of presence entries marked as disabled.
261
261
async fn mark_outdated_presence ( pool : & SqlitePool ) -> Result < usize > {
262
+ const HEIGHT_TOLERANCE : i64 = 5 ;
263
+
262
264
let affected = sqlx:: query!(
263
265
r#"
264
- WITH MaxSlots AS (
266
+ WITH MaxHeights AS (
265
267
SELECT
266
268
window_id,
267
- MAX(best_tip_global_slot ) as max_slot
269
+ MAX(best_tip_height ) as max_height
268
270
FROM heartbeat_presence
269
271
WHERE disabled = FALSE
270
272
GROUP BY window_id
271
273
),
272
- PrevMaxSlots AS (
273
- -- Get the max slot from the immediate previous window
274
+ PrevMaxHeights AS (
275
+ -- Get the max height from the immediate previous window
274
276
SELECT
275
277
tw.id as window_id,
276
- prev.max_slot as prev_max_slot
278
+ prev.max_height as prev_max_height
277
279
FROM time_windows tw
278
280
LEFT JOIN time_windows prev_tw ON prev_tw.id = tw.id - 1
279
- LEFT JOIN MaxSlots prev ON prev.window_id = prev_tw.id
281
+ LEFT JOIN MaxHeights prev ON prev.window_id = prev_tw.id
280
282
)
281
283
UPDATE heartbeat_presence
282
284
SET disabled = TRUE
283
- WHERE (window_id, best_tip_global_slot ) IN (
285
+ WHERE (window_id, best_tip_height ) IN (
284
286
SELECT
285
287
hp.window_id,
286
- hp.best_tip_global_slot
288
+ hp.best_tip_height
287
289
FROM heartbeat_presence hp
288
- JOIN PrevMaxSlots pms ON pms .window_id = hp.window_id
290
+ JOIN PrevMaxHeights pmh ON pmh .window_id = hp.window_id
289
291
WHERE hp.disabled = FALSE
290
- AND pms.prev_max_slot IS NOT NULL -- Ensure there is a previous window
291
- AND hp.best_tip_global_slot < pms.prev_max_slot -- Less than previous window max
292
+ AND pmh.prev_max_height IS NOT NULL -- Ensure there is a previous window
293
+ AND hp.best_tip_height < (pmh.prev_max_height - ?)
292
294
)
293
- "#
295
+ "# ,
296
+ HEIGHT_TOLERANCE
294
297
)
295
298
. execute ( pool)
296
299
. await ?;
@@ -388,9 +391,11 @@ pub async fn process_heartbeats(
388
391
389
392
let best_tip = entry. best_tip_block ( ) ;
390
393
let public_key_id = * public_key_map. get ( & entry. submitter ) . unwrap ( ) ;
394
+ let has_presence =
395
+ ( entry. is_synced ( ) || entry. is_catchup ( ) ) && best_tip. is_some ( ) ;
391
396
392
397
// Record presence only if node is synced and has a best tip
393
- if entry . is_synced ( ) && best_tip . is_some ( ) {
398
+ if has_presence {
394
399
presence_batch. push ( HeartbeatPresence {
395
400
window_id : window. id . unwrap ( ) ,
396
401
public_key_id,
@@ -426,9 +431,9 @@ pub async fn process_heartbeats(
426
431
}
427
432
428
433
// Verify that the block slot matches the expected one for the current time
429
- // TODO: maybe we can be a bit more lenient here?
434
+ // Allow a difference of 1 in either direction
430
435
let expected_slot = global_slot_at_time ( entry. create_time ) ;
431
- if block_info. global_slot != expected_slot {
436
+ if ( block_info. global_slot as i64 - expected_slot as i64 ) . abs ( ) > 1 {
432
437
println ! (
433
438
"WARNING: Invalid block slot: {} (height: {}, producer: {}, expected slot: {}, actual slot: {})" ,
434
439
block_info. hash, block_info. height, entry. submitter, expected_slot, block_info. global_slot
@@ -445,15 +450,25 @@ pub async fn process_heartbeats(
445
450
continue ;
446
451
}
447
452
448
- seen_blocks. insert ( key. clone ( ) , entry. create_time ) ;
449
- produced_blocks_batch. push ( ProducedBlock {
450
- window_id : window. id . unwrap ( ) ,
451
- public_key_id,
452
- block_hash : block_info. hash ,
453
- block_height : block_info. height ,
454
- block_global_slot : block_info. global_slot ,
455
- block_data : block_info. base64_encoded_header ,
456
- } ) ;
453
+ if has_presence {
454
+ seen_blocks. insert ( key. clone ( ) , entry. create_time ) ;
455
+ produced_blocks_batch. push ( ProducedBlock {
456
+ window_id : window. id . unwrap ( ) ,
457
+ public_key_id,
458
+ block_hash : block_info. hash ,
459
+ block_height : block_info. height ,
460
+ block_global_slot : block_info. global_slot ,
461
+ block_data : block_info. base64_encoded_header ,
462
+ } ) ;
463
+ } else {
464
+ println ! (
465
+ "WARNING: Block produced by unsynced node: {} (height: {}, producer: {})" ,
466
+ block_info. hash, block_info. height, entry. submitter
467
+ ) ;
468
+ println ! ( "Submitter: {:?}" , entry. submitter) ;
469
+ println ! ( "Sync status: {}" , entry. sync_phase( ) . unwrap_or_default( ) ) ;
470
+ println ! ( "Best tip: {:?}" , entry. best_tip_block( ) . map( |b| b. hash) ) ;
471
+ }
457
472
}
458
473
Some ( ( _block_info, Err ( e) ) ) => {
459
474
println ! (
@@ -698,21 +713,29 @@ pub async fn view_scores(pool: &SqlitePool, config: &Config) -> Result<()> {
698
713
699
714
let max_scores = get_max_scores ( pool) . await ?;
700
715
716
+ println ! ( "\n Submitter Scores Summary:" ) ;
717
+ println ! ( "Current maximum score possible: {}" , max_scores. current) ;
718
+ println ! ( "Total maximum score possible: {}" , max_scores. total) ;
701
719
println ! ( "\n Submitter Scores:" ) ;
702
720
println ! ( "--------------------------------------------------------" ) ;
703
721
println ! (
704
- "Public Key | Score | Blocks | Current Max | Total Max | Last Updated | Last Heartbeat"
722
+ "Public Key | Score | Score % | Blocks | Last Updated | Last Heartbeat"
705
723
) ;
706
724
println ! ( "--------------------------------------------------------" ) ;
707
725
708
726
for row in scores {
727
+ let percentage = if max_scores. current > 0 {
728
+ ( row. score as f64 / max_scores. current as f64 ) * 100.0
729
+ } else {
730
+ 0.0
731
+ } ;
732
+
709
733
println ! (
710
- "{:<40} | {:>5} | {:>6} | {:>11} | {:>9 } | {} | {}" ,
734
+ "{:<40} | {:>5} | {:>6.2}% | {:>6 } | {} | {}" ,
711
735
row. public_key,
712
736
row. score,
737
+ percentage,
713
738
row. blocks_produced,
714
- max_scores. current,
715
- max_scores. total,
716
739
row. last_updated. unwrap_or_default( ) ,
717
740
row. last_heartbeat. unwrap_or_default( )
718
741
) ;
0 commit comments