use chia_protocol::{Bytes32, Coin, CoinSpend, Program};
use dig_chainsource_interface::{
ChainSource, ChainSourceError, ChainSourceProvider, MockChainSource, SingletonLineage,
};
const LAUNCHER_MARKER: [u8; 32] = [0xAAu8; 32];
const MAX_HOPS: usize = 8;
#[derive(Debug, PartialEq, Eq)]
enum Authenticated {
Launcher(Bytes32),
NotASingleton,
TooDeep,
}
fn authenticate<S: ChainSource>(source: &S, coin_id: Bytes32) -> Result<Authenticated, S::Error> {
let mut current = coin_id;
for _ in 0..MAX_HOPS {
let Some(spend) = source.parent_spend(current)? else {
return Ok(Authenticated::NotASingleton);
};
let parent = spend.coin;
if parent.puzzle_hash == Bytes32::new(LAUNCHER_MARKER) {
return Ok(Authenticated::Launcher(parent.coin_id()));
}
current = parent.coin_id();
}
Ok(Authenticated::TooDeep)
}
fn coin(parent: Bytes32, puzzle_hash: [u8; 32], amount: u64) -> Coin {
Coin::new(parent, Bytes32::new(puzzle_hash), amount)
}
fn record(coin: Coin) -> dig_chainsource_interface::CoinRecord {
dig_chainsource_interface::CoinRecord {
coin,
confirmed_height: Some(1),
spent_height: None,
timestamp: None,
coinbase: false,
}
}
fn spend_of(parent: Coin) -> CoinSpend {
CoinSpend::new(
parent,
Program::from(vec![0x01u8]),
Program::from(vec![0x80u8]),
)
}
#[test]
fn echoed_foreign_lineage_fails_membership() {
let launcher = Bytes32::new([0x01u8; 32]);
let genuine_tip = Bytes32::new([0x02u8; 32]);
let attacker_coin = Bytes32::new([0x66u8; 32]);
let source = MockChainSource::new()
.with_lineage(launcher, SingletonLineage::new(genuine_tip, [launcher]));
let lineage = source
.resolve_singleton_lineage(launcher)
.expect("read ok")
.expect("launcher exists");
assert!(lineage.contains(genuine_tip));
assert!(lineage.contains(launcher));
assert!(
!lineage.contains(attacker_coin),
"an attacker coin is never a lineage member, so it holds no authority",
);
}
#[test]
fn spoofed_curry_has_no_launcher_parent() {
let spoof = coin(Bytes32::new([0x33u8; 32]), [0x22u8; 32], 1);
let source = MockChainSource::new().with_coin(spoof.coin_id(), record(spoof));
assert_eq!(
authenticate(&source, spoof.coin_id()).expect("read ok"),
Authenticated::NotASingleton,
"a bare puzzle-hash match is not a singleton without a launcher parent-spend",
);
}
#[test]
fn melted_or_unlaunched_is_none() {
let source = MockChainSource::new();
let missing = Bytes32::new([0x44u8; 32]);
assert_eq!(source.resolve_singleton_lineage(missing), Ok(None));
assert_eq!(source.coin_record(missing), Ok(None));
}
#[test]
fn parent_gap_fails_closed() {
let parent = coin(Bytes32::new([0x55u8; 32]), [0x22u8; 32], 1);
let child = coin(parent.coin_id(), [0x22u8; 32], 1);
let source = MockChainSource::new()
.with_coin(child.coin_id(), record(child))
.with_coin(parent.coin_id(), record(parent));
assert_eq!(
authenticate(&source, child.coin_id()).expect("read ok"),
Authenticated::NotASingleton,
);
}
#[test]
fn transport_error_propagates() {
let source =
MockChainSource::new().fail_with(ChainSourceError::Transport("backend down".into()));
let any = Bytes32::new([0x77u8; 32]);
assert_eq!(
authenticate(&source, any),
Err(ChainSourceError::Transport("backend down".into())),
);
assert!(source.coin_record(any).is_err());
assert!(source.resolve_singleton_lineage(any).is_err());
}
#[test]
fn over_deep_walk_is_bounded() {
let ph = [0x22u8; 32];
let depth = MAX_HOPS + 4;
let mut chain = vec![coin(Bytes32::new([0xEEu8; 32]), ph, 1)];
for _ in 1..depth {
let child = coin(chain.last().unwrap().coin_id(), ph, 1);
chain.push(child);
}
let mut source = MockChainSource::new();
for link in &chain {
source = source
.with_coin(link.coin_id(), record(*link))
.with_spend(link.coin_id(), spend_of(*link));
}
let start = chain.last().unwrap().coin_id();
assert_eq!(
authenticate(&source, start).expect("read ok"),
Authenticated::TooDeep,
"an over-long parent chain is bounded, not looped",
);
}
#[test]
fn chain_source_is_object_safe() {
let boxed: Box<dyn ChainSource<Error = ChainSourceError>> = Box::new(MockChainSource::new());
let missing = Bytes32::new([0x00u8; 32]);
assert_eq!(boxed.coin_record(missing), Ok(None));
assert_eq!(boxed.peak_height(), Ok(None));
}
#[test]
fn provider_reports_info() {
let source = MockChainSource::new().with_peak(42);
let info = source.provider_info();
assert_eq!(info.id.0.as_ref(), "mock");
assert_eq!(source.peak_height(), Ok(Some(42)));
}