use super::{load_test_vectors_and_sync_chain_index, poll::poll_until, MockchainMode};
use crate::chain_index::{finalized_height_floor, ChainIndex};
use std::time::Duration;
use tokio::time::sleep;
#[tokio::test(flavor = "multi_thread")]
async fn nfs_lowest_block_matches_finalized_db_tip() {
let (_blocks, _indexer, index_reader, mockchain) =
load_test_vectors_and_sync_chain_index(MockchainMode::Active).await;
let snapshot = index_reader.snapshot_nonfinalized_state().await.unwrap();
let nfs = snapshot
.get_nfs_snapshot()
.expect("NFS exists after harness completes finalized sync");
let seam_height = finalized_height_floor(mockchain.active_height());
let nfs_seam_hash = nfs
.heights_to_hashes
.get(&seam_height)
.copied()
.expect("NFS retains the block at the finalized-DB tip height");
let finalized_db_tip_block = index_reader
.finalized_state
.get_chain_block_by_height(seam_height)
.await
.expect("read finalized DB")
.expect("finalized DB has a block at its tip height");
assert_eq!(
nfs_seam_hash,
*finalized_db_tip_block.hash(),
"block at seam height {} must match between NFS and finalized DB",
seam_height.0,
);
}
#[tokio::test(flavor = "multi_thread")]
async fn block_is_evicted_from_nfs_when_finalized_advances_past_it() {
let (_blocks, _indexer, index_reader, mockchain) =
load_test_vectors_and_sync_chain_index(MockchainMode::Active).await;
let initial_seam_height = finalized_height_floor(mockchain.active_height());
let initial_snapshot = index_reader.snapshot_nonfinalized_state().await.unwrap();
let initial_nfs = initial_snapshot
.get_nfs_snapshot()
.expect("NFS exists after harness");
let target_hash = *initial_nfs
.heights_to_hashes
.get(&initial_seam_height)
.expect("NFS retains the block at the finalized-DB tip height");
assert!(
initial_nfs.blocks.contains_key(&target_hash),
"precondition: block at seam height is in NFS",
);
mockchain.mine_blocks(20);
let post_mine_active_height = mockchain.active_height();
poll_until(
"NFS tip to catch up to the mined chain (post-trim state)",
Duration::from_secs(10),
Duration::from_millis(25),
|| async {
let snapshot = index_reader.snapshot_nonfinalized_state().await.ok()?;
let nfs = snapshot.get_nfs_snapshot()?;
(nfs.best_tip.height.0 == post_mine_active_height).then_some(())
},
)
.await;
let later_snapshot = index_reader.snapshot_nonfinalized_state().await.unwrap();
let later_nfs = later_snapshot
.get_nfs_snapshot()
.expect("NFS still exists after advance");
assert!(
!later_nfs.blocks.contains_key(&target_hash),
"block at original seam height must have been evicted from NFS",
);
assert!(
!later_nfs
.heights_to_hashes
.contains_key(&initial_seam_height),
"heights_to_hashes must no longer reference the original seam height",
);
}
#[tokio::test(flavor = "multi_thread")]
async fn nfs_slot_is_monotonic_post_init() {
let (_blocks, _indexer, index_reader, _mockchain) =
load_test_vectors_and_sync_chain_index(MockchainMode::Active).await;
for i in 0..10 {
let snapshot = index_reader.snapshot_nonfinalized_state().await.unwrap();
assert!(
snapshot.get_nfs_snapshot().is_some(),
"iteration {i}: post-init snapshot must contain an NFS",
);
sleep(Duration::from_millis(100)).await;
}
}
#[tokio::test(flavor = "multi_thread")]
async fn shutdown_terminates_sync_loop_cleanly() {
let (_blocks, mut indexer, index_reader, mockchain) =
load_test_vectors_and_sync_chain_index(MockchainMode::Active).await;
let target_tip = mockchain.active_height();
poll_until(
"indexer to publish NFS at chain tip (loop settled in interval sleep)",
Duration::from_secs(10),
Duration::from_millis(50),
|| async {
let snapshot = index_reader.snapshot_nonfinalized_state().await.ok()?;
let nfs = snapshot.get_nfs_snapshot()?;
(nfs.best_tip.height.0 == target_tip).then_some(())
},
)
.await;
let handle = indexer
.sync_loop_handle
.take()
.expect("sync loop handle present after construction");
indexer
.shutdown()
.await
.expect("shutdown completes without error");
let join_outcome = tokio::time::timeout(Duration::from_secs(5), handle)
.await
.expect("sync loop did not exit within 5 s of shutdown");
let sync_result = join_outcome.expect("sync loop task panicked");
assert!(
sync_result.is_ok(),
"sync loop returned Err on clean shutdown: {sync_result:?}",
);
}
#[tokio::test(flavor = "multi_thread")]
async fn race_pre_mine_finalized_height_block_is_evicted_when_source_advances_mid_iter() {
let (_blocks, _indexer, index_reader, mockchain) =
load_test_vectors_and_sync_chain_index(MockchainMode::Active).await;
let initial_active = mockchain.active_height();
let pre_mine_finalized_height = finalized_height_floor(initial_active);
let initial_snapshot = index_reader.snapshot_nonfinalized_state().await.unwrap();
let initial_nfs = initial_snapshot
.get_nfs_snapshot()
.expect("NFS exists after harness");
let target_hash = *initial_nfs
.heights_to_hashes
.get(&pre_mine_finalized_height)
.expect("NFS retains the block at the finalized-DB tip height");
assert!(
initial_nfs.blocks.contains_key(&target_hash),
"precondition: block at pre-mine finalized height is in NFS",
);
let advance: u32 = 20;
let mc = mockchain.clone();
mockchain.arm_one_shot_get_block_hook(Box::new(move || mc.mine_blocks(advance)));
let post_mine_active = initial_active + advance;
poll_until(
"NFS tip to reach post-mine height (race window forced)",
Duration::from_secs(10),
Duration::from_millis(25),
|| async {
let snapshot = index_reader.snapshot_nonfinalized_state().await.ok()?;
let nfs = snapshot.get_nfs_snapshot()?;
(nfs.best_tip.height.0 == post_mine_active).then_some(())
},
)
.await;
let later_snapshot = index_reader.snapshot_nonfinalized_state().await.unwrap();
let later_nfs = later_snapshot
.get_nfs_snapshot()
.expect("NFS still exists after advance");
assert!(
!later_nfs.blocks.contains_key(&target_hash),
"block at pre-mine finalized height (height {}) must be evicted after the \
source advances mid-iter; published NFS overshoots its iter-committed \
seam (#1126)",
pre_mine_finalized_height.0,
);
}