use libp2p::PeerId;
use crate::shard_cmd::BlockServerEntry;
pub fn check_rebalance(
chain: &[BlockServerEntry],
our_peer_id: &PeerId,
our_start: usize,
our_end: usize,
total_blocks: usize,
target_blocks: usize,
min_redundancy: usize,
) -> Option<(usize, usize)> {
if total_blocks == 0 || our_start >= our_end {
return None;
}
let mut coverage = vec![0usize; total_blocks];
for entry in chain {
if &entry.peer_id == our_peer_id {
continue;
}
let s = entry.start_block.min(total_blocks);
let e = entry.end_block.min(total_blocks);
for c in &mut coverage[s..e] {
*c += 1;
}
}
let our_min_coverage = coverage[our_start.min(total_blocks)..our_end.min(total_blocks)]
.iter()
.copied()
.min()
.unwrap_or(0);
if our_min_coverage < min_redundancy {
return None;
}
let gap_start = coverage.iter().position(|&c| c == 0)?;
let gap_end = (gap_start + target_blocks).min(total_blocks);
Some((gap_start, gap_end))
}
pub fn pick_gap_from_chain(
chain: &[BlockServerEntry],
our_peer_id: &PeerId,
total_blocks: usize,
target_blocks: usize,
) -> (usize, usize) {
if total_blocks == 0 {
return (0, 0);
}
let target = target_blocks.min(total_blocks);
let mut coverage = vec![0usize; total_blocks];
for e in chain {
if &e.peer_id == our_peer_id {
continue;
}
let s = e.start_block.min(total_blocks);
let end = e.end_block.min(total_blocks);
for c in &mut coverage[s..end] {
*c += 1;
}
}
let min_cov = coverage.iter().copied().min().unwrap_or(0);
let start = if min_cov == 0 {
coverage.iter().position(|&c| c == 0).unwrap_or(0)
} else {
let n_windows = total_blocks.saturating_sub(target) + 1;
(0..n_windows)
.min_by_key(|&i| coverage[i..i + target].iter().sum::<usize>())
.unwrap_or(0)
};
let end = (start + target).min(total_blocks);
(start, end)
}
#[cfg(test)]
mod tests {
use super::*;
fn fake_peer(n: u8) -> PeerId {
let mut seed = [0u8; 32];
seed[0] = n;
let sk = libp2p::identity::ed25519::SecretKey::try_from_bytes(&mut seed)
.expect("valid 32-byte seed");
let kp = libp2p::identity::Keypair::from(libp2p::identity::ed25519::Keypair::from(sk));
kp.public().to_peer_id()
}
fn make_entry(peer: PeerId, start: usize, end: usize) -> BlockServerEntry {
BlockServerEntry {
peer_id: peer,
start_block: start,
end_block: end,
public_name: format!("node-{}", start),
}
}
#[test]
fn no_rebalance_when_alone() {
let our_peer = fake_peer(1);
let chain = vec![make_entry(our_peer.clone(), 0, 8)];
let result = check_rebalance(&chain, &our_peer, 0, 8, 32, 8, 2);
assert_eq!(result, None, "Should not rebalance when alone");
}
#[test]
fn no_rebalance_when_full_coverage() {
let our_peer = fake_peer(1);
let peer_b = fake_peer(2);
let peer_c = fake_peer(3);
let chain = vec![
make_entry(our_peer.clone(), 0, 8),
make_entry(peer_b.clone(), 0, 32),
make_entry(peer_c.clone(), 0, 32),
];
let result = check_rebalance(&chain, &our_peer, 0, 8, 32, 8, 2);
assert_eq!(result, None, "No gap → no rebalance");
}
#[test]
fn rebalance_when_gap_and_redundant() {
let our_peer = fake_peer(1);
let peer_b = fake_peer(2);
let peer_c = fake_peer(3);
let chain = vec![
make_entry(our_peer.clone(), 0, 8),
make_entry(peer_b.clone(), 0, 8),
make_entry(peer_c.clone(), 0, 8),
];
let result = check_rebalance(&chain, &our_peer, 0, 8, 32, 8, 2);
assert_eq!(result, Some((8, 16)), "Should move to fill gap at 8");
}
#[test]
fn no_rebalance_when_gap_but_not_redundant() {
let our_peer = fake_peer(1);
let peer_b = fake_peer(2);
let chain = vec![
make_entry(our_peer.clone(), 0, 8),
make_entry(peer_b.clone(), 8, 32),
];
let result = check_rebalance(&chain, &our_peer, 0, 8, 32, 8, 2);
assert_eq!(result, None, "Only coverage of our range — must not move");
}
#[test]
fn rebalance_picks_lowest_gap() {
let our_peer = fake_peer(1);
let peer_b = fake_peer(2);
let peer_c = fake_peer(3);
let chain = vec![
make_entry(our_peer.clone(), 0, 8),
make_entry(peer_b.clone(), 0, 8),
make_entry(peer_b.clone(), 16, 24),
make_entry(peer_c.clone(), 0, 8),
make_entry(peer_c.clone(), 16, 24),
];
let result = check_rebalance(&chain, &our_peer, 0, 8, 32, 8, 2);
assert_eq!(result, Some((8, 16)), "Should pick the lowest gap first");
}
#[test]
fn gap_from_chain_never_panics_on_empty_chain() {
let our_peer = fake_peer(1);
let result = pick_gap_from_chain(&[], &our_peer, 32, 8);
assert_eq!(result, (0, 8), "Empty chain → start at block 0");
}
#[test]
fn gap_from_chain_finds_genuine_gap() {
let our_peer = fake_peer(1);
let peer_b = fake_peer(2);
let chain = vec![make_entry(peer_b.clone(), 0, 16)];
let result = pick_gap_from_chain(&chain, &our_peer, 32, 8);
assert_eq!(result, (16, 24), "Should pick the first uncovered block");
}
#[test]
fn gap_from_chain_joins_least_covered_when_full() {
let our_peer = fake_peer(1);
let peer_b = fake_peer(2);
let peer_c = fake_peer(3);
let chain = vec![
make_entry(peer_b.clone(), 0, 32),
make_entry(peer_c.clone(), 0, 8),
make_entry(peer_c.clone(), 16, 32),
];
let result = pick_gap_from_chain(&chain, &our_peer, 32, 8);
assert_eq!(result, (8, 16), "Should join least-covered window");
}
#[test]
fn gap_from_chain_excludes_self() {
let our_peer = fake_peer(1);
let chain = vec![make_entry(our_peer.clone(), 0, 32)];
let result = pick_gap_from_chain(&chain, &our_peer, 32, 8);
assert_eq!(
result,
(0, 8),
"Stale self entry must not count as coverage"
);
}
}