use std::collections::HashSet;
use freenet_stdlib::prelude::ContractInstanceId;
use serde::{Deserialize, Serialize};
pub const DEFAULT_MAX_FOCUS_CONTRACTS: usize = 2;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FocusSelector {
salt: [u8; 32],
max_focus: usize,
epoch: u64,
}
impl FocusSelector {
pub fn new(salt: [u8; 32], max_focus: usize) -> Self {
Self {
salt,
max_focus,
epoch: 0,
}
}
pub fn resuming_at(salt: [u8; 32], max_focus: usize, epoch: u64) -> Self {
Self {
salt,
max_focus: max_focus.max(1),
epoch,
}
}
pub fn epoch(&self) -> u64 {
self.epoch
}
pub fn max_focus(&self) -> usize {
self.max_focus
}
pub fn rotate(&mut self) {
self.epoch = self.epoch.wrapping_add(1);
}
pub fn select(&self, candidates: &[ContractInstanceId]) -> Vec<ContractInstanceId> {
let mut scored: Vec<([u8; 32], ContractInstanceId)> = candidates
.iter()
.collect::<HashSet<_>>()
.into_iter()
.map(|id| (self.score(id), *id))
.collect();
scored.sort_by(|(sa, ia), (sb, ib)| {
sa.cmp(sb).then_with(|| ia.as_bytes().cmp(ib.as_bytes()))
});
scored
.into_iter()
.take(self.max_focus)
.map(|(_, id)| id)
.collect()
}
fn score(&self, id: &ContractInstanceId) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(b"freenet-conformance-focus-v1");
hasher.update(&self.salt);
hasher.update(&self.epoch.to_le_bytes());
hasher.update(id.as_bytes());
*hasher.finalize().as_bytes()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn id(byte: u8) -> ContractInstanceId {
ContractInstanceId::new([byte; 32])
}
fn candidates(n: u8) -> Vec<ContractInstanceId> {
(0..n).map(id).collect()
}
#[test]
fn selection_is_bounded_by_max_focus() {
let selector = FocusSelector::new([1; 32], 2);
assert_eq!(selector.select(&candidates(50)).len(), 2);
assert_eq!(selector.select(&candidates(1)).len(), 1);
assert!(selector.select(&[]).is_empty());
}
#[test]
fn selection_is_stable_within_an_epoch() {
let selector = FocusSelector::new([2; 32], 3);
let pool = candidates(20);
let first = selector.select(&pool);
assert_eq!(first, selector.select(&pool));
let mut shuffled = pool.clone();
shuffled.reverse();
assert_eq!(first, selector.select(&shuffled));
let mut duplicated = pool.clone();
duplicated.extend(pool.iter().copied());
assert_eq!(first, selector.select(&duplicated));
}
#[test]
fn rotation_redraws_the_selection() {
let mut selector = FocusSelector::new([3; 32], 2);
let pool = candidates(40);
let before = selector.select(&pool);
let mut changed = false;
for _ in 0..8 {
selector.rotate();
if selector.select(&pool) != before {
changed = true;
break;
}
}
assert!(
changed,
"rotating did not change the focus set in eight epochs, so rotation is \
not reshuffling anything"
);
assert!(selector.epoch() > 0, "rotate must advance the epoch");
}
#[test]
fn different_peers_watch_different_contracts() {
let pool = candidates(60);
let a = FocusSelector::new([0xAA; 32], 2).select(&pool);
let b = FocusSelector::new([0xBB; 32], 2).select(&pool);
let c = FocusSelector::new([0xCC; 32], 2).select(&pool);
assert!(
!(a == b && b == c),
"three peers with different salts chose identical focus sets, so the \
salt is not affecting selection and an author could predict it"
);
}
#[test]
fn a_contract_cannot_tell_whether_it_is_watched_without_the_salt() {
let target = id(7);
let pool = candidates(10);
let peers = 100u8;
let watched = (0..peers)
.filter(|i| {
FocusSelector::new([*i; 32], 2)
.select(&pool)
.contains(&target)
})
.count();
assert!(
watched > 0 && watched < peers as usize,
"this contract was watched by {watched} of {peers} peers; being watched by \
none or by all means selection is decided by the id, and an author \
could grind for either"
);
}
}