use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use super::connect::PeerOrigin;
use super::pool::PeerPool;
use super::test_support::loopback_peer;
use super::{OptAnswer, PeerBackend};
use crate::types::{ChainClaim, ChiaQueryError};
use crate::NetworkType;
impl ChainClaim for &'static str {
fn chain_claim(&self) -> String {
(*self).to_string()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Says {
Absent,
Present,
PresentOther,
Fails,
}
async fn backend_over(members: &[(Says, PeerOrigin)]) -> (PeerBackend, HashMap<SocketAddr, Says>) {
let pool = PeerPool::for_tests(members.len());
let mut script = HashMap::new();
for (says, origin) in members {
let peer = loopback_peer().await;
let addr = peer.socket_addr();
assert!(
pool.admit_for_tests(peer, addr, *origin).await,
"each scripted peer must be admitted under its own address"
);
script.insert(addr, *says);
}
let backend = PeerBackend {
pool,
network: NetworkType::Mainnet,
request_timeout: Duration::from_millis(50),
};
(backend, script)
}
async fn read_scripted(
backend: &PeerBackend,
script: &HashMap<SocketAddr, Says>,
) -> (Result<OptAnswer<&'static str>, ChiaQueryError>, usize) {
let asked = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&asked);
let result = backend
.read_opt_corroborated(move |peer| {
let counter = Arc::clone(&counter);
let says = *script
.get(&peer.socket_addr())
.expect("every peer in the pool is scripted");
async move {
counter.fetch_add(1, Ordering::SeqCst);
match says {
Says::Absent => Ok(None),
Says::Present => Ok(Some("the thing")),
Says::PresentOther => Ok(Some("a different thing")),
Says::Fails => Err(ChiaQueryError::PeerConnection("scripted failure".into())),
}
}
})
.await;
(result, asked.load(Ordering::SeqCst))
}
#[tokio::test]
async fn one_peer_saying_absent_is_not_an_absence() {
let (backend, script) = backend_over(&[(Says::Absent, PeerOrigin::Discovered)]).await;
let (answer, asked) = read_scripted(&backend, &script).await;
assert_eq!(
answer.expect("a lone absent answer is not an ERROR, it is an ungraded fact"),
OptAnswer::UncorroboratedAbsent,
"absence on one peer's word must be reported as uncorroborated"
);
assert_eq!(asked, 1, "there was only one peer to ask");
}
#[tokio::test]
async fn a_preferred_peer_agreeing_is_not_corroboration() {
let (backend, script) = backend_over(&[
(Says::Absent, PeerOrigin::Discovered),
(Says::Absent, PeerOrigin::Priority),
])
.await;
let (answer, asked) = read_scripted(&backend, &script).await;
assert_eq!(
answer.expect("no read failed"),
OptAnswer::UncorroboratedAbsent,
"a preferred peer is not an independent voice, however honestly it agrees"
);
assert_eq!(asked, 1, "the preferred peer must not even be consulted");
}
#[tokio::test]
async fn two_independent_peers_agreeing_is_an_absence() {
let (backend, script) = backend_over(&[
(Says::Absent, PeerOrigin::Discovered),
(Says::Absent, PeerOrigin::Discovered),
])
.await;
let (answer, asked) = read_scripted(&backend, &script).await;
assert_eq!(
answer.expect("no read failed"),
OptAnswer::CorroboratedAbsent
);
assert_eq!(
asked, 2,
"corroboration means a second peer was really asked"
);
}
#[tokio::test]
async fn a_contradicting_peer_is_refused_not_broken_in_either_direction() {
let (backend, script) = backend_over(&[
(Says::Absent, PeerOrigin::Discovered),
(Says::Present, PeerOrigin::Discovered),
])
.await;
let (answer, _) = read_scripted(&backend, &script).await;
assert!(
matches!(answer, Err(ChiaQueryError::SourcesDisagree(_))),
"a contradiction is evidence about the sources, not a tie to break: got {answer:?}"
);
}
#[tokio::test]
async fn a_corroborator_that_fails_leaves_the_absence_uncorroborated() {
let (backend, script) = backend_over(&[
(Says::Absent, PeerOrigin::Discovered),
(Says::Fails, PeerOrigin::Discovered),
])
.await;
let (answer, asked) = read_scripted(&backend, &script).await;
assert_eq!(
answer.expect("the FIRST peer answered, so the read did not fail"),
OptAnswer::UncorroboratedAbsent,
"silence from the second peer is not agreement"
);
assert_eq!(asked, 2, "the corroborator was asked and failed");
}
#[tokio::test]
async fn one_peer_saying_present_is_not_a_corroborated_presence() {
let (backend, script) = backend_over(&[(Says::Present, PeerOrigin::Discovered)]).await;
let (answer, asked) = read_scripted(&backend, &script).await;
assert_eq!(
answer.expect("a lone positive answer is not an ERROR, it is an ungraded fact"),
OptAnswer::UncorroboratedFound("the thing"),
"presence on one peer's word must be reported as uncorroborated"
);
assert_eq!(asked, 1, "there was only one peer to ask");
}
#[tokio::test]
async fn a_second_independent_peer_agreeing_makes_the_presence_corroborated() {
let (backend, script) = backend_over(&[
(Says::Present, PeerOrigin::Discovered),
(Says::Present, PeerOrigin::Discovered),
])
.await;
let (answer, asked) = read_scripted(&backend, &script).await;
assert_eq!(
answer.expect("no read failed"),
OptAnswer::Found("the thing")
);
assert_eq!(
asked, 2,
"corroboration means a second peer was really asked"
);
}
#[tokio::test]
async fn corroborators_that_claim_different_chain_state_disagree() {
let (backend, script) = backend_over(&[
(Says::Present, PeerOrigin::Discovered),
(Says::PresentOther, PeerOrigin::Discovered),
])
.await;
let (answer, _) = read_scripted(&backend, &script).await;
assert!(
matches!(answer, Err(ChiaQueryError::SourcesDisagree(_))),
"two different claims about the same coin is evidence, not a tie to break: got {answer:?}"
);
}
#[tokio::test]
async fn the_first_peers_answer_does_not_decide_against_the_corroborators() {
let (backend, script) = backend_over(&[
(Says::PresentOther, PeerOrigin::Discovered),
(Says::Present, PeerOrigin::Discovered),
(Says::Present, PeerOrigin::Discovered),
])
.await;
let (answer, asked) = read_scripted(&backend, &script).await;
assert!(
matches!(answer, Err(ChiaQueryError::SourcesDisagree(_))),
"a contradicted first answer is refused, in either direction: got {answer:?}"
);
assert_eq!(asked, 3, "every corroborator is asked, concurrently");
}
#[tokio::test]
async fn a_corroborator_reporting_absent_contradicts_the_presence() {
let (backend, script) = backend_over(&[
(Says::Present, PeerOrigin::Discovered),
(Says::Absent, PeerOrigin::Discovered),
])
.await;
let (answer, _) = read_scripted(&backend, &script).await;
assert!(
matches!(answer, Err(ChiaQueryError::SourcesDisagree(_))),
"present-then-absent is the same contradiction as absent-then-present: got {answer:?}"
);
}
#[tokio::test]
async fn a_corroborator_that_fails_leaves_the_presence_uncorroborated() {
let (backend, script) = backend_over(&[
(Says::Present, PeerOrigin::Discovered),
(Says::Fails, PeerOrigin::Discovered),
])
.await;
let (answer, asked) = read_scripted(&backend, &script).await;
assert_eq!(
answer.expect("the FIRST peer answered, so the read did not fail"),
OptAnswer::UncorroboratedFound("the thing"),
"silence from the corroborator is not agreement"
);
assert_eq!(asked, 2, "the corroborator was asked and failed");
}
#[tokio::test]
async fn a_preferred_peer_agreeing_is_not_corroboration_of_presence() {
let (backend, script) = backend_over(&[
(Says::Present, PeerOrigin::Discovered),
(Says::Present, PeerOrigin::Priority),
])
.await;
let (answer, asked) = read_scripted(&backend, &script).await;
assert_eq!(
answer.expect("no read failed"),
OptAnswer::UncorroboratedFound("the thing"),
"a preferred peer is not an independent voice, however honestly it agrees"
);
assert_eq!(asked, 1, "the preferred peer must not even be consulted");
}