use std::time::Duration;
use rand::{rngs::StdRng, Rng, SeedableRng};
use zakura_chain::block;
use crate::zakura::{BlockSyncStatus, ZakuraBlockSyncConfig, ZakuraPeerId, MAX_BS_RESPONSE_BYTES};
#[derive(Clone, Copy, Debug)]
pub(crate) enum LatencyDist {
Fixed(Duration),
Uniform { low: Duration, high: Duration },
}
impl LatencyDist {
pub(crate) fn zero() -> Self {
Self::Fixed(Duration::ZERO)
}
pub(crate) fn sample(&self, rng: &mut StdRng) -> Duration {
match *self {
Self::Fixed(duration) => duration,
Self::Uniform { low, high } => {
let lo = low.as_micros().min(high.as_micros());
let hi = low.as_micros().max(high.as_micros());
if hi == lo {
Duration::from_micros(u64::try_from(lo).unwrap_or(u64::MAX))
} else {
let micros = rng.gen_range(lo..=hi);
Duration::from_micros(u64::try_from(micros).unwrap_or(u64::MAX))
}
}
}
}
fn is_zero(&self) -> bool {
matches!(self, Self::Fixed(d) if d.is_zero())
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct IdleGap {
pub(crate) every_responses: u64,
pub(crate) duration: Duration,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Degrade {
pub(crate) at: Duration,
pub(crate) mode: DegradeMode,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum DegradeMode {
GoSilent,
Wedge,
SlowTo {
base_rtt: Duration,
bandwidth_bytes_per_sec: u64,
},
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct ServeProfile {
pub(crate) first_block_latency: LatencyDist,
pub(crate) per_block_latency: LatencyDist,
pub(crate) bandwidth_bytes_per_sec: Option<u64>,
pub(crate) idle_gap: Option<IdleGap>,
pub(crate) drop_probability: f64,
pub(crate) withhold: Option<(block::Height, block::Height)>,
pub(crate) reorder: bool,
pub(crate) degrade: Option<Degrade>,
}
impl ServeProfile {
pub(crate) fn fast() -> Self {
Self {
first_block_latency: LatencyDist::zero(),
per_block_latency: LatencyDist::zero(),
bandwidth_bytes_per_sec: None,
idle_gap: None,
drop_probability: 0.0,
withhold: None,
reorder: false,
degrade: None,
}
}
pub(crate) fn slow(rtt: Duration, per_block: Duration) -> Self {
Self {
first_block_latency: LatencyDist::Fixed(rtt),
per_block_latency: LatencyDist::Fixed(per_block),
..Self::fast()
}
}
pub(crate) fn byte_rate(base_rtt: Duration, bandwidth_bytes_per_sec: u64) -> Self {
Self {
first_block_latency: LatencyDist::Fixed(base_rtt),
bandwidth_bytes_per_sec: Some(bandwidth_bytes_per_sec.max(1)),
..Self::fast()
}
}
pub(crate) fn first_block_is_zero(&self) -> bool {
self.first_block_latency.is_zero()
}
pub(crate) fn per_block_is_zero(&self) -> bool {
self.per_block_latency.is_zero()
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct CommitProfile {
pub(crate) per_commit_delay: Duration,
pub(crate) burst: Option<CommitBurstStall>,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct CommitBurstStall {
pub(crate) every_commits: u64,
pub(crate) duration: Duration,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct PeerSpec {
pub(crate) id_byte: u8,
pub(crate) servable_low: block::Height,
pub(crate) servable_high: block::Height,
pub(crate) max_blocks_per_response: u32,
pub(crate) max_inflight_requests: u32,
pub(crate) connect_at: Duration,
pub(crate) disconnect_at: Option<Duration>,
pub(crate) serve: ServeProfile,
}
impl PeerSpec {
pub(crate) fn fast(id_byte: u8, servable_high: block::Height) -> Self {
Self {
id_byte,
servable_low: block::Height(1),
servable_high,
max_blocks_per_response: 16,
max_inflight_requests: 64,
connect_at: Duration::ZERO,
disconnect_at: None,
serve: ServeProfile::fast(),
}
}
pub(crate) fn with_serve(
id_byte: u8,
servable_high: block::Height,
serve: ServeProfile,
) -> Self {
Self {
serve,
..Self::fast(id_byte, servable_high)
}
}
pub(crate) fn status(&self, tip_hash: block::Hash) -> BlockSyncStatus {
BlockSyncStatus {
servable_low: self.servable_low,
servable_high: self.servable_high,
tip_hash,
max_blocks_per_response: self.max_blocks_per_response,
max_inflight_requests: self.max_inflight_requests,
max_response_bytes: MAX_BS_RESPONSE_BYTES,
}
}
pub(crate) fn peer_id(&self) -> ZakuraPeerId {
ZakuraPeerId::new(vec![self.id_byte; 32]).expect("synthetic peer id is within bounds")
}
pub(crate) fn rng_seed(&self, scenario_seed: u64) -> u64 {
scenario_seed ^ u64::from(self.id_byte).wrapping_mul(0x9e37_79b9_7f4a_7c15)
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum TipEventKind {
GrowTo(block::Height),
HeaderReanchor(block::Height),
VerifiedReset(block::Height),
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct TipEvent {
pub(crate) at: Duration,
pub(crate) kind: TipEventKind,
}
#[derive(Clone, Debug)]
pub(crate) struct Scenario {
pub(crate) blocks: u32,
pub(crate) seed: u64,
pub(crate) target_block_bytes: Option<usize>,
pub(crate) initial_best_header: block::Height,
pub(crate) config: ZakuraBlockSyncConfig,
pub(crate) peers: Vec<PeerSpec>,
pub(crate) timeline: Vec<TipEvent>,
pub(crate) commit: CommitProfile,
pub(crate) transport_queue_depth: Option<usize>,
pub(crate) deadline: Duration,
}
impl Scenario {
pub(crate) fn new(
blocks: u32,
seed: u64,
config: ZakuraBlockSyncConfig,
peers: Vec<PeerSpec>,
) -> Self {
Self {
blocks,
seed,
target_block_bytes: None,
initial_best_header: block::Height(blocks),
config,
peers,
timeline: Vec::new(),
commit: CommitProfile::default(),
transport_queue_depth: None,
deadline: Duration::from_secs(30),
}
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct FuzzOutcome {
pub(crate) committed_tip: block::Height,
pub(crate) target: block::Height,
}
impl FuzzOutcome {
pub(crate) fn reached_target(&self) -> bool {
self.committed_tip >= self.target
}
}
pub(crate) fn fuzz_config() -> ZakuraBlockSyncConfig {
ZakuraBlockSyncConfig {
max_blocks_per_response: 16,
max_inflight_requests: 256,
max_inflight_block_bytes: u64::MAX,
request_timeout: Duration::from_secs(30),
bbr_probe_rtt_interval: Duration::from_millis(150),
bbr_probe_rtt_duration: Duration::from_millis(30),
bbr_rtprop_window: Duration::from_millis(150),
..ZakuraBlockSyncConfig::default()
}
}
pub(crate) fn peer_rng(scenario_seed: u64, spec: &PeerSpec) -> StdRng {
StdRng::seed_from_u64(spec.rng_seed(scenario_seed))
}