use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::time::Duration;
use freenet_stdlib::prelude::ContractInstanceId;
use tokio::time::Instant;
use super::bundle::ReplayBundle;
use super::focus::FocusSelector;
use super::generator::{GeneratorConfig, generate_cases};
use super::policy::{ConformanceAction, EnforcementMode, decide};
use super::property::{ConformanceProperty, PropertyOutcome};
use super::runtime_oracle::RuntimeOracle;
use super::verifier::verify_case;
const MAX_FOCUS_CONTRACTS: usize = 2;
pub const PROBE_INTERVAL: Duration = Duration::from_secs(15 * 60);
const ROTATE_EVERY_TICKS: u32 = 8;
const MAX_CASES_PER_PROBE: usize = 64;
const PROBE_TIME_BUDGET: Duration = Duration::from_secs(10);
const SALT_FILE: &str = "focus-salt";
const EPOCH_FILE: &str = "focus-epoch";
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ShadowReport {
pub focused: usize,
pub probed: usize,
pub skipped_no_code: usize,
pub skipped_no_samples: usize,
pub cases: usize,
pub inconclusive: usize,
pub would_remove: usize,
pub reported: usize,
pub timed_out: usize,
pub judged: Vec<JudgedContract>,
pub without_verdict: usize,
}
pub(crate) fn count_awaiting_samples(report: &mut ShadowReport, awaiting: usize) {
report.focused += awaiting;
report.skipped_no_samples += awaiting;
report.without_verdict += awaiting;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JudgedContract {
pub contract: ContractInstanceId,
pub verdicts: usize,
pub inconclusive: usize,
}
impl ShadowReport {
pub fn judged_contains(&self, contract: &ContractInstanceId) -> bool {
self.judged.iter().any(|j| j.contract == *contract)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum CandidateSource {
Hosted,
#[default]
SamplerFallback,
}
impl CandidateSource {
pub(crate) fn as_str(self) -> &'static str {
match self {
CandidateSource::Hosted => "hosted",
CandidateSource::SamplerFallback => "sampler",
}
}
}
pub(crate) fn candidate_pool(
hosted: Option<Vec<ContractInstanceId>>,
fallback: &[ContractInstanceId],
) -> (Vec<ContractInstanceId>, CandidateSource) {
match hosted {
Some(hosted) => (hosted, CandidateSource::Hosted),
None => (fallback.to_vec(), CandidateSource::SamplerFallback),
}
}
pub(crate) fn needs_hosted_warmup(
wide: bool,
last: &FocusTick,
attempts: u32,
budget: u32,
) -> bool {
!wide && last.source != CandidateSource::Hosted && attempts < budget
}
#[derive(Debug, Clone, Default)]
pub(crate) struct FocusTick {
pub selected: Vec<ContractInstanceId>,
pub candidates: usize,
pub source: CandidateSource,
}
pub struct ShadowRunner {
mode: EnforcementMode,
selector: FocusSelector,
ticks: u32,
dir: PathBuf,
logged: HashSet<(ContractInstanceId, ConformanceProperty)>,
}
impl ShadowRunner {
pub fn new(dir: &Path, mode: EnforcementMode) -> Self {
let salt = load_or_create_salt(dir);
let epoch = load_epoch(dir);
Self {
mode,
selector: FocusSelector::resuming_at(salt, MAX_FOCUS_CONTRACTS, epoch),
ticks: 0,
dir: dir.to_path_buf(),
logged: HashSet::new(),
}
}
pub fn epoch(&self) -> u64 {
self.selector.epoch()
}
pub(crate) fn advance(&mut self) {
self.ticks = self.ticks.wrapping_add(1);
if self.ticks % ROTATE_EVERY_TICKS == 0 {
self.selector.rotate();
self.logged.clear();
store_epoch(&self.dir, self.selector.epoch());
}
}
pub(crate) fn focus(
&self,
hosted: Option<Vec<ContractInstanceId>>,
fallback: &[ContractInstanceId],
) -> FocusTick {
if self.mode == EnforcementMode::Disabled {
return FocusTick::default();
}
let (candidates, source) = candidate_pool(hosted, fallback);
FocusTick {
selected: self.selector.select(&candidates),
candidates: candidates.len(),
source,
}
}
pub(crate) fn select(
&self,
focus: &FocusTick,
samplers: &HashMap<ContractInstanceId, super::capture::TrackedContract>,
) -> (Vec<(ContractInstanceId, ReplayBundle)>, usize) {
if self.mode == EnforcementMode::Disabled {
return (Vec::new(), 0);
}
let work: Vec<_> = focus
.selected
.iter()
.filter_map(|instance| {
let tracked = samplers.get(instance)?;
Some((*instance, super::capture::bundle_for(*instance, tracked)))
})
.collect();
let awaiting_samples = focus.selected.len().saturating_sub(work.len());
(work, awaiting_samples)
}
pub(crate) fn mode(&self) -> EnforcementMode {
self.mode
}
pub(crate) fn record(&mut self, findings: &[Finding]) {
for finding in findings {
if !self
.logged
.insert((finding.contract, finding.violation.property))
{
continue;
}
tracing::info!(
contract = %finding.contract,
property = ?finding.violation.property,
epoch = self.selector.epoch(),
would_remove = finding.would_remove,
detail = %finding.violation.detail,
"conformance shadow finding"
);
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Finding {
pub contract: ContractInstanceId,
pub violation: super::property::Violation,
pub would_remove: bool,
}
pub(crate) async fn probe(
work: Vec<(ContractInstanceId, ReplayBundle)>,
store: Option<PathBuf>,
mode: EnforcementMode,
) -> (ShadowReport, Vec<Finding>) {
probe_with_budget(work, store, mode, PROBE_TIME_BUDGET).await
}
pub(crate) async fn probe_with_budget(
work: Vec<(ContractInstanceId, ReplayBundle)>,
store: Option<PathBuf>,
mode: EnforcementMode,
budget: Duration,
) -> (ShadowReport, Vec<Finding>) {
let mut report = ShadowReport {
focused: work.len(),
..ShadowReport::default()
};
let mut findings = Vec::new();
let Some(store) = store else {
report.skipped_no_code = work.len();
report.without_verdict += work.len();
return (report, findings);
};
for (instance, bundle) in work {
probe_one(
&instance,
&bundle,
&store,
mode,
budget,
&mut report,
&mut findings,
)
.await;
}
(report, findings)
}
#[allow(clippy::too_many_arguments)]
async fn probe_one(
instance: &ContractInstanceId,
bundle: &ReplayBundle,
store: &Path,
mode: EnforcementMode,
budget: Duration,
report: &mut ShadowReport,
findings: &mut Vec<Finding>,
) {
let started = Instant::now();
let corpus = bundle.to_corpus();
if corpus.is_empty() {
report.skipped_no_samples += 1;
report.without_verdict += 1;
return;
}
let code = match bundle.resolve_code_from_store(store) {
Ok(code) => code,
Err(err) => {
tracing::debug!(%instance, error = %err, "shadow probe has no code for a focus contract");
report.skipped_no_code += 1;
report.without_verdict += 1;
return;
}
};
let mut oracle = match RuntimeOracle::standalone(code, bundle.parameters.clone()).await {
Ok(oracle) => oracle,
Err(err) => {
tracing::debug!(%instance, error = %err, "shadow probe could not build an oracle");
report.skipped_no_code += 1;
report.without_verdict += 1;
return;
}
};
let config = GeneratorConfig {
max_cases: MAX_CASES_PER_PROBE,
..Default::default()
};
let cases = generate_cases(&corpus, &config);
report.probed += 1;
let cases: Vec<_> = cases.into_iter().take(MAX_CASES_PER_PROBE).collect();
let instance_copy = *instance;
let Some(remaining) = budget.checked_sub(started.elapsed()) else {
report.timed_out += 1;
report.without_verdict += 1;
return;
};
let joined = tokio::task::spawn_blocking(move || {
let mut out = ProbeOutcome::default();
let loop_started = Instant::now();
for case in &cases {
if loop_started.elapsed() >= remaining {
out.timed_out = true;
break;
}
let outcome = verify_case(&mut oracle, case);
out.cases += 1;
out.decisions.push(decide(mode, &outcome));
let inconclusive = matches!(outcome, PropertyOutcome::Inconclusive(_));
out.inconclusive += usize::from(inconclusive);
if !inconclusive {
out.reached_verdict = true;
}
}
out
})
.await;
let outcome = match joined {
Ok(outcome) => outcome,
Err(err) => {
tracing::warn!(%instance, error = %err, "shadow probe thread failed");
report.without_verdict += 1;
return;
}
};
report.cases += outcome.cases;
report.inconclusive += outcome.inconclusive;
if outcome.timed_out {
report.timed_out += 1;
}
if outcome.reached_verdict {
report.judged.push(JudgedContract {
contract: instance_copy,
verdicts: outcome.cases.saturating_sub(outcome.inconclusive),
inconclusive: outcome.inconclusive,
});
} else {
report.without_verdict += 1;
}
for action in outcome.decisions {
match action {
ConformanceAction::Nothing => {}
ConformanceAction::Report(violation) => {
report.reported += 1;
findings.push(Finding {
contract: instance_copy,
violation,
would_remove: false,
});
}
ConformanceAction::WouldRemove(violation) => {
report.would_remove += 1;
findings.push(Finding {
contract: instance_copy,
violation,
would_remove: true,
});
}
ConformanceAction::Remove(violation) => {
report.would_remove += 1;
findings.push(Finding {
contract: instance_copy,
violation,
would_remove: true,
});
}
}
}
}
#[derive(Default)]
struct ProbeOutcome {
cases: usize,
inconclusive: usize,
timed_out: bool,
decisions: Vec<ConformanceAction>,
reached_verdict: bool,
}
fn load_or_create_salt(dir: &Path) -> [u8; 32] {
let path = dir.join(SALT_FILE);
if let Ok(bytes) = std::fs::read(&path) {
if let Ok(salt) = <[u8; 32]>::try_from(bytes.as_slice()) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = std::fs::metadata(&path) {
if meta.permissions().mode() & 0o077 != 0 {
if let Err(err) =
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))
{
tracing::warn!(
path = %path.display(),
error = %err,
"could not tighten permissions on an existing focus \
salt; it stays readable beyond its owner"
);
}
}
}
}
return salt;
}
tracing::warn!(
path = %path.display(),
"focus salt file is not 32 bytes; generating a new one"
);
}
let mut salt = [0u8; 32];
chacha20poly1305::aead::rand_core::RngCore::fill_bytes(
&mut chacha20poly1305::aead::OsRng,
&mut salt,
);
if let Err(err) = write_salt(&path, &salt) {
tracing::warn!(
path = %path.display(),
error = %err,
"could not persist the conformance focus salt; focus will reshuffle on restart"
);
}
salt
}
fn write_salt(path: &Path, salt: &[u8; 32]) -> std::io::Result<()> {
use std::io::Write;
let mut file = crate::wasm_runtime::create_owner_only(path)?;
file.write_all(salt)?;
file.sync_all()
}
fn load_epoch(dir: &Path) -> u64 {
std::fs::read_to_string(dir.join(EPOCH_FILE))
.ok()
.and_then(|raw| raw.trim().parse().ok())
.unwrap_or(0)
}
fn store_epoch(dir: &Path, epoch: u64) {
let path = dir.join(EPOCH_FILE);
let written = crate::wasm_runtime::create_owner_only(&path).and_then(|mut file| {
use std::io::Write;
file.write_all(epoch.to_string().as_bytes())
});
if let Err(err) = written {
tracing::warn!(
path = %path.display(),
error = %err,
"could not persist the conformance focus epoch; focus rotation will \
restart from zero after a restart"
);
}
}
#[cfg(test)]
pub(crate) async fn probe_fixture_contract(
mode: u8,
) -> Result<(ContractInstanceId, ShadowReport, Vec<Finding>), Box<dyn std::error::Error>> {
use freenet_stdlib::prelude::{
ContractCode, ContractContainer, ContractWasmAPIVersion, Parameters, WrappedContract,
};
use std::sync::Arc;
use crate::conformance::capture::{Observation, SamplingScope, TrackedContract, record};
let wasm = crate::wasm_runtime::tests::get_test_module("test_contract_conformance")?;
let code_hash = *blake3::hash(&wasm).as_bytes();
let store_dir = crate::util::tests::get_temp_dir();
std::fs::create_dir_all(store_dir.path())?;
let db = crate::contract::storages::Storage::new(store_dir.path()).await?;
let mut store =
crate::wasm_runtime::ContractStore::new(store_dir.path().into(), 10_000_000, db)?;
let parameters: Parameters<'static> = Parameters::from(vec![mode]);
let contract = WrappedContract::new(Arc::new(ContractCode::from(wasm)), parameters.clone());
let id = *contract.key().id();
store.store_contract(ContractContainer::Wasm(ContractWasmAPIVersion::V1(
contract,
)))?;
let transitions: &[(Vec<u8>, Vec<u8>)] = &[
(vec![1], vec![1, 2]),
(vec![2], vec![2, 3]),
(vec![3], vec![3, 4]),
(vec![1, 2], vec![1, 2, 5]),
];
let mut samplers: HashMap<ContractInstanceId, TrackedContract> = HashMap::new();
for (base, result) in transitions {
let _evicted = record(
&mut samplers,
&SamplingScope::Wide,
Observation {
contract: id,
code_hash,
parameters: parameters.as_ref().to_vec(),
base_state: base.clone(),
incoming_state: Some(result.clone()),
delta: None,
result_state: result.clone(),
related: Vec::new(),
},
);
}
let dir = tempfile::TempDir::new()?;
let runner = ShadowRunner::new(dir.path(), EnforcementMode::Shadow);
let focus = runner.focus(None, &samplers.keys().copied().collect::<Vec<_>>());
let (work, awaiting) = runner.select(&focus, &samplers);
assert!(
!work.is_empty(),
"the fixture contract was not selected, so a probe never ran and any \
assertion downstream of this proves nothing"
);
let (mut report, findings) = probe(
work,
Some(store_dir.path().to_path_buf()),
EnforcementMode::Shadow,
)
.await;
count_awaiting_samples(&mut report, awaiting);
Ok((id, report, findings))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::conformance::capture::{Observation, TrackedContract, record};
async fn tick(
runner: &mut ShadowRunner,
samplers: &HashMap<ContractInstanceId, TrackedContract>,
store: Option<&Path>,
) -> ShadowReport {
runner.advance();
let focus = runner.focus(None, &samplers.keys().copied().collect::<Vec<_>>());
let (work, awaiting) = runner.select(&focus, samplers);
let (mut report, findings) =
probe(work, store.map(|p| p.to_path_buf()), runner.mode()).await;
count_awaiting_samples(&mut report, awaiting);
runner.record(&findings);
report
}
#[test]
fn awaiting_samples_reach_focused_skipped_and_without_verdict() {
let mut report = ShadowReport {
focused: 1,
skipped_no_samples: 0,
without_verdict: 0,
probed: 1,
..Default::default()
};
count_awaiting_samples(&mut report, 3);
assert_eq!(
(
report.focused,
report.skipped_no_samples,
report.without_verdict
),
(4, 3, 3),
"a warming-up focus contract must reach `focused` (or the focus set \
reports as the smaller subset that had samples), `skipped_no_samples` \
(or the reason is unstated) and `without_verdict` (or it counts as \
neither judged nor unjudged): {report:?}"
);
assert_eq!(
report.probed, 1,
"the fold must not touch `probed` — no case ran for a warming-up contract"
);
}
#[test]
fn a_default_focus_tick_does_not_claim_the_hosted_source() {
assert_eq!(
FocusTick::default().source,
CandidateSource::SamplerFallback,
"a focus decision that has not happened yet must not claim the hosted set"
);
}
#[test]
fn warmup_retries_while_focus_is_still_on_the_fallback() {
let fallback = FocusTick {
source: CandidateSource::SamplerFallback,
..FocusTick::default()
};
assert!(
needs_hosted_warmup(false, &fallback, 0, 12),
"warm-up gave up while focus was still on the sampler fallback"
);
assert!(
needs_hosted_warmup(false, &FocusTick::default(), 0, 12),
"warm-up was disabled before the first draw had even happened"
);
}
#[test]
fn warmup_stops_once_focus_reaches_the_hosted_set() {
let hosted = FocusTick {
source: CandidateSource::Hosted,
..FocusTick::default()
};
assert!(!needs_hosted_warmup(false, &hosted, 0, 12));
}
#[test]
fn warmup_does_not_run_for_wide_capture() {
assert!(!needs_hosted_warmup(true, &FocusTick::default(), 0, 12));
}
#[test]
fn warmup_gives_up_after_its_attempt_budget() {
assert!(
!needs_hosted_warmup(false, &FocusTick::default(), 12, 12),
"warm-up would retry forever on a node that never builds a ring"
);
}
#[tokio::test]
async fn focus_selects_from_the_hosted_set_it_is_given() -> anyhow::Result<()> {
let dir = tempfile::TempDir::new()?;
let runner = ShadowRunner::new(dir.path(), EnforcementMode::Shadow);
let hosted: Vec<_> = (10..40u8).map(instance).collect();
let tick = runner.focus(Some(hosted.clone()), &[instance(1), instance(2)]);
assert_eq!(tick.source, CandidateSource::Hosted);
assert_eq!(tick.candidates, hosted.len());
assert!(
!tick.selected.is_empty(),
"hosted candidates selected nothing"
);
for picked in &tick.selected {
assert!(
hosted.contains(picked),
"focus selected {picked} which the peer does not host"
);
}
Ok(())
}
#[test]
fn candidates_come_from_the_hosted_set_when_one_is_available() {
let hosted = vec![instance(7), instance(8), instance(9)];
let sampler_keys = vec![instance(1), instance(2)];
let (pool, source) = candidate_pool(Some(hosted.clone()), &sampler_keys);
assert_eq!(pool, hosted, "focus did not draw from the hosted set");
assert_eq!(source, CandidateSource::Hosted);
}
#[test]
fn candidates_fall_back_to_the_sampler_and_report_it() {
let sampler_keys = vec![instance(1), instance(2)];
let (pool, source) = candidate_pool(None, &sampler_keys);
assert_eq!(pool, sampler_keys);
assert_eq!(source, CandidateSource::SamplerFallback);
assert_eq!(source.as_str(), "sampler");
}
#[test]
fn an_empty_hosted_set_does_not_fall_back_to_the_sampler() {
let sampler_keys = vec![instance(1), instance(2)];
let (pool, source) = candidate_pool(Some(Vec::new()), &sampler_keys);
assert!(pool.is_empty(), "an empty hosted set borrowed sampler keys");
assert_eq!(source, CandidateSource::Hosted);
}
fn instance(n: u8) -> ContractInstanceId {
ContractInstanceId::new([n; 32])
}
fn samplers_for(
id: ContractInstanceId,
parameters: Vec<u8>,
code_hash: [u8; 32],
transitions: &[(Vec<u8>, Vec<u8>)],
) -> HashMap<ContractInstanceId, TrackedContract> {
let mut samplers = HashMap::new();
for (base, result) in transitions {
let _evicted = record(
&mut samplers,
&crate::conformance::capture::SamplingScope::Wide,
Observation {
contract: id,
code_hash,
parameters: parameters.clone(),
base_state: base.clone(),
incoming_state: Some(result.clone()),
delta: None,
result_state: result.clone(),
related: Vec::new(),
},
);
}
samplers
}
#[test]
fn the_focus_salt_persists_across_runners() {
let dir = tempfile::TempDir::new().expect("tempdir");
let first = load_or_create_salt(dir.path());
let second = load_or_create_salt(dir.path());
assert_eq!(
first, second,
"a second runner in the same directory drew a different salt, so focus \
reshuffles on every restart"
);
}
#[cfg(unix)]
#[test]
fn the_focus_salt_is_not_world_readable() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::TempDir::new().expect("tempdir");
let _ = load_or_create_salt(dir.path());
let mode = std::fs::metadata(dir.path().join(SALT_FILE))
.expect("salt file")
.permissions()
.mode()
& 0o777;
assert_eq!(
mode, 0o600,
"the focus salt is readable beyond its owner (mode {mode:o})"
);
}
#[test]
fn two_peers_draw_different_salts() {
let a = tempfile::TempDir::new().expect("tempdir");
let b = tempfile::TempDir::new().expect("tempdir");
assert_ne!(
load_or_create_salt(a.path()),
load_or_create_salt(b.path()),
"two peers drew the same salt, so focus selection is not per-peer"
);
}
#[test]
fn a_short_salt_file_is_replaced_rather_than_used() {
let dir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(dir.path().join(SALT_FILE), [1u8, 2, 3]).expect("write short salt");
let salt = load_or_create_salt(dir.path());
assert_ne!(salt, [0u8; 32], "a replacement salt must not be all zeroes");
assert_eq!(
salt,
load_or_create_salt(dir.path()),
"the replacement salt was not persisted"
);
}
#[tokio::test]
async fn no_contract_store_reports_skips_rather_than_a_clean_run() {
let dir = tempfile::TempDir::new().expect("tempdir");
let mut runner = ShadowRunner::new(dir.path(), EnforcementMode::Shadow);
let samplers = samplers_for(
instance(1),
vec![0],
[0u8; 32],
&[(vec![1], vec![1, 2]), (vec![2], vec![2, 3])],
);
let report = tick(&mut runner, &samplers, None).await;
assert!(
report.focused > 0,
"nothing was selected, so this proves nothing"
);
assert_eq!(
report.skipped_no_code, report.focused,
"focus contracts with no resolvable code were not counted as skipped"
);
assert_eq!(
report.cases, 0,
"cases were checked without any contract code"
);
assert_eq!(report.would_remove, 0);
assert!(
report.judged.is_empty(),
"a contract with no resolvable code was recorded as judged: {report:?}"
);
assert_eq!(
report.without_verdict, report.focused,
"a contract skipped for no code did not count toward without_verdict: \
{report:?}"
);
assert_eq!(
report.judged.len() + report.without_verdict,
report.focused,
"judged and without_verdict must be complements over the focus set, or \
the dashboard silently under- or over-reports: {report:?}"
);
}
#[tokio::test]
async fn disabled_does_no_work_whatsoever() {
let dir = tempfile::TempDir::new().expect("tempdir");
let mut runner = ShadowRunner::new(dir.path(), EnforcementMode::Disabled);
let samplers = samplers_for(instance(1), vec![0], [0u8; 32], &[(vec![1], vec![1, 2])]);
assert_eq!(
tick(&mut runner, &samplers, None).await,
ShadowReport::default(),
"Disabled mode still selected or skipped contracts"
);
}
#[tokio::test]
async fn focus_rotates_on_schedule_and_not_sooner() {
let dir = tempfile::TempDir::new().expect("tempdir");
let mut runner = ShadowRunner::new(dir.path(), EnforcementMode::Shadow);
let samplers = samplers_for(instance(1), vec![0], [0u8; 32], &[(vec![1], vec![1, 2])]);
for nth in 1..ROTATE_EVERY_TICKS {
tick(&mut runner, &samplers, None).await;
assert_eq!(
runner.epoch(),
0,
"focus rotated early, on tick {nth} of {ROTATE_EVERY_TICKS}"
);
}
tick(&mut runner, &samplers, None).await;
assert_eq!(
runner.epoch(),
1,
"focus did not rotate after {ROTATE_EVERY_TICKS} ticks"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn the_time_budget_stops_a_probe() -> Result<(), Box<dyn std::error::Error>> {
use freenet_stdlib::prelude::{
ContractCode, ContractContainer, ContractWasmAPIVersion, Parameters, WrappedContract,
};
use std::sync::Arc;
let wasm = crate::wasm_runtime::tests::get_test_module("test_contract_conformance")?;
let code_hash = *blake3::hash(&wasm).as_bytes();
let store_dir = crate::util::tests::get_temp_dir();
std::fs::create_dir_all(store_dir.path())?;
let db = crate::contract::storages::Storage::new(store_dir.path()).await?;
let mut store =
crate::wasm_runtime::ContractStore::new(store_dir.path().into(), 10_000_000, db)?;
let parameters: Parameters<'static> = Parameters::from(vec![0u8]);
let contract = WrappedContract::new(Arc::new(ContractCode::from(wasm)), parameters.clone());
let id = *contract.key().id();
store.store_contract(ContractContainer::Wasm(ContractWasmAPIVersion::V1(
contract,
)))?;
let samplers = samplers_for(
id,
vec![0],
code_hash,
&[(vec![1], vec![1, 2]), (vec![2], vec![2, 3])],
);
let dir = tempfile::TempDir::new()?;
let runner = ShadowRunner::new(dir.path(), EnforcementMode::Shadow);
let focus = runner.focus(None, &samplers.keys().copied().collect::<Vec<_>>());
let (work, _awaiting) = runner.select(&focus, &samplers);
assert!(!work.is_empty(), "nothing selected, so this proves nothing");
let (report, _) = probe_with_budget(
work,
Some(store_dir.path().to_path_buf()),
EnforcementMode::Shadow,
Duration::ZERO,
)
.await;
assert_eq!(
report.timed_out, 1,
"an exhausted budget did not stop the probe: {report:?}"
);
assert_eq!(
report.cases, 0,
"cases ran after the budget was already exhausted: {report:?}"
);
Ok(())
}
#[tokio::test]
async fn the_focus_epoch_survives_a_restart() {
let dir = tempfile::TempDir::new().expect("tempdir");
let samplers = samplers_for(instance(1), vec![0], [0u8; 32], &[(vec![1], vec![1, 2])]);
let mut first = ShadowRunner::new(dir.path(), EnforcementMode::Shadow);
for _ in 0..ROTATE_EVERY_TICKS {
tick(&mut first, &samplers, None).await;
}
let rotated = first.epoch();
assert!(rotated > 0, "the first runner never rotated");
drop(first);
let resumed = ShadowRunner::new(dir.path(), EnforcementMode::Shadow);
assert_eq!(
resumed.epoch(),
rotated,
"a restart lost the focus epoch, so rotation restarts from zero every time \
the node bounces"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_real_defect_reaches_would_remove_and_a_conforming_contract_does_not()
-> Result<(), Box<dyn std::error::Error>> {
use freenet_stdlib::prelude::{
ContractCode, ContractContainer, ContractWasmAPIVersion, Parameters, WrappedContract,
};
use std::sync::Arc;
const LAST_WRITE_WINS: u8 = 1;
const CONFORMING: u8 = 0;
let wasm = crate::wasm_runtime::tests::get_test_module("test_contract_conformance")?;
let code_hash = *blake3::hash(&wasm).as_bytes();
let store_dir = crate::util::tests::get_temp_dir();
std::fs::create_dir_all(store_dir.path())?;
let db = crate::contract::storages::Storage::new(store_dir.path()).await?;
let mut store =
crate::wasm_runtime::ContractStore::new(store_dir.path().into(), 10_000_000, db)?;
let states = vec![
(vec![1u8], vec![1u8, 2]),
(vec![2u8], vec![2u8, 3]),
(vec![1u8, 2], vec![1u8, 2, 3]),
(vec![4u8], vec![4u8, 5]),
];
let mut outcomes = Vec::new();
for mode in [LAST_WRITE_WINS, CONFORMING] {
let parameters: Parameters<'static> = Parameters::from(vec![mode]);
let contract = WrappedContract::new(
Arc::new(ContractCode::from(wasm.clone())),
parameters.clone(),
);
let key = *contract.key();
let id = *key.id();
store.store_contract(ContractContainer::Wasm(ContractWasmAPIVersion::V1(
contract,
)))?;
let samplers = samplers_for(id, vec![mode], code_hash, &states);
let before = samplers.len();
let dir = tempfile::TempDir::new()?;
let mut runner = ShadowRunner::new(dir.path(), EnforcementMode::Shadow);
let report = tick(&mut runner, &samplers, Some(store_dir.path())).await;
assert_eq!(
samplers.len(),
before,
"a probe changed the sampler set; shadow mode must not alter what the \
peer would have captured"
);
assert_eq!(
report.probed, 1,
"mode {mode} was not probed at all (skipped_no_code={}, \
skipped_no_samples={}), so its verdict means nothing",
report.skipped_no_code, report.skipped_no_samples
);
assert!(
report.cases > 0,
"mode {mode} was probed but no case ran, so nothing was checked"
);
outcomes.push((mode, report));
}
let broken = &outcomes[0].1;
let honest = &outcomes[1].1;
for (mode, report) in [(LAST_WRITE_WINS, broken), (CONFORMING, honest)] {
assert_eq!(
report.judged.len() + report.without_verdict,
report.focused,
"mode {mode}: judged and without_verdict must be complements over \
the focus set, or the dashboard silently under- or over-reports: \
{report:?}"
);
assert_eq!(
report.judged.len(),
1,
"mode {mode} reached a verdict but was not recorded as judged: \
{report:?}"
);
}
assert!(
broken.would_remove > 0,
"a contract that provably breaks commutativity produced no prospective \
removal: {broken:?}"
);
assert_eq!(
honest.would_remove, 0,
"a conforming contract produced a prospective removal, which is the false \
positive the whole deployment plan is gated on not happening: {honest:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_contract_whose_every_case_is_inconclusive_is_not_judged()
-> Result<(), Box<dyn std::error::Error>> {
use freenet_stdlib::prelude::{
ContractCode, ContractContainer, ContractWasmAPIVersion, Parameters, WrappedContract,
};
use std::sync::Arc;
const CONFORMING: u8 = 0;
const REQUIRES_RELATED: u8 = 7;
let wasm = crate::wasm_runtime::tests::get_test_module("test_contract_conformance")?;
let code_hash = *blake3::hash(&wasm).as_bytes();
let store_dir = crate::util::tests::get_temp_dir();
std::fs::create_dir_all(store_dir.path())?;
let db = crate::contract::storages::Storage::new(store_dir.path()).await?;
let mut store =
crate::wasm_runtime::ContractStore::new(store_dir.path().into(), 10_000_000, db)?;
let transitions = &[(vec![1u8], vec![1u8, 2]), (vec![2u8], vec![2u8, 3])];
let unjudged_parameters: Parameters<'static> = Parameters::from(vec![REQUIRES_RELATED]);
let unjudged_contract = WrappedContract::new(
Arc::new(ContractCode::from(wasm.clone())),
unjudged_parameters.clone(),
);
let unjudged_id = *unjudged_contract.key().id();
store.store_contract(ContractContainer::Wasm(ContractWasmAPIVersion::V1(
unjudged_contract,
)))?;
let mut samplers =
samplers_for(unjudged_id, vec![REQUIRES_RELATED], code_hash, transitions);
let judged_parameters: Parameters<'static> = Parameters::from(vec![CONFORMING]);
let judged_contract = WrappedContract::new(
Arc::new(ContractCode::from(wasm)),
judged_parameters.clone(),
);
let judged_id = *judged_contract.key().id();
store.store_contract(ContractContainer::Wasm(ContractWasmAPIVersion::V1(
judged_contract,
)))?;
samplers.extend(samplers_for(
judged_id,
vec![CONFORMING],
code_hash,
transitions,
));
assert_eq!(
samplers.len(),
2,
"test setup did not produce two distinct contracts"
);
let dir = tempfile::TempDir::new()?;
let mut runner = ShadowRunner::new(dir.path(), EnforcementMode::Shadow);
let report = tick(&mut runner, &samplers, Some(store_dir.path())).await;
assert_eq!(
report.probed, 2,
"both contracts were not probed, so this proves nothing: {report:?}"
);
assert!(
report.cases > 0,
"no case ran, so this proves nothing: {report:?}"
);
assert!(
report.inconclusive > 0,
"expected the REQUIRES_RELATED contract's cases to be inconclusive, or \
the fixture no longer exercises this failure mode: {report:?}"
);
assert!(
!report.judged_contains(&unjudged_id),
"a contract whose every case was inconclusive was recorded as judged: \
{report:?}"
);
assert!(
report.judged_contains(&judged_id),
"a contract that genuinely reached a verdict on every case was NOT \
recorded as judged: {report:?}"
);
let judged_record = report
.judged
.iter()
.find(|j| j.contract == judged_id)
.expect("the conforming sibling is judged");
assert!(
judged_record.verdicts > 0,
"a judged contract carries no verdict count, so its page can only report \
the node-wide number and cannot say how much looking IT got: {report:?}"
);
assert_eq!(
judged_record.verdicts,
report.cases - report.inconclusive,
"the only contract to reach a verdict this tick must own every one of the \
tick's verdicts; its per-contract count disagrees with the totals, so \
the page and the log would report different things: {report:?}"
);
assert_eq!(
report.judged.len(),
1,
"expected exactly the conforming sibling to be judged: {report:?}"
);
assert_eq!(
report.without_verdict, 1,
"a probed contract that never reached a verdict did not count toward \
without_verdict: {report:?}"
);
assert_eq!(
report.judged.len() + report.without_verdict,
report.focused,
"judged and without_verdict must be complements over the focus set, or \
the dashboard silently under- or over-reports: {report:?}"
);
Ok(())
}
}