use chia_protocol::Bytes32;
use chia_puzzles::SINGLETON_LAUNCHER_HASH;
use dig_chainsource_interface::ChainSource;
use dig_merkle::hydrate;
use crate::error::{EvidenceError, EvidenceResult};
use crate::evidence::{chain_err, Evidence};
pub const MAX_LINEAGE_DEPTH: usize = 100_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RootAnchorClaim {
pub store_id: Bytes32,
pub generation_root: Bytes32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RootAnchorEvidence {
store_id: Bytes32,
generation_root: Bytes32,
committing_coin: Bytes32,
lineage_tip: Bytes32,
}
impl RootAnchorEvidence {
pub fn store_id(&self) -> Bytes32 {
self.store_id
}
pub fn generation_root(&self) -> Bytes32 {
self.generation_root
}
pub fn committing_coin(&self) -> Bytes32 {
self.committing_coin
}
pub fn lineage_tip(&self) -> Bytes32 {
self.lineage_tip
}
}
impl Evidence for RootAnchorEvidence {
type Claim = RootAnchorClaim;
fn gather<S: ChainSource>(claim: &Self::Claim, chain: &S) -> EvidenceResult<Self> {
let launcher = chain
.coin_record(claim.store_id)
.map_err(chain_err)?
.ok_or(EvidenceError::LauncherNotFound)?;
if launcher.coin.puzzle_hash != Bytes32::from(SINGLETON_LAUNCHER_HASH) {
return Err(EvidenceError::NotALauncher);
}
let lineage = chain
.resolve_singleton_lineage(claim.store_id)
.map_err(chain_err)?
.ok_or(EvidenceError::NoLineage)?;
let mut current = lineage.tip();
for _ in 0..MAX_LINEAGE_DEPTH {
if !lineage.contains(current) {
return Err(EvidenceError::RootNotCommitted);
}
let record = chain
.coin_record(current)
.map_err(chain_err)?
.ok_or(EvidenceError::RootNotCommitted)?;
if let Some(creating_spend) = chain
.coin_spend(record.coin.parent_coin_info)
.map_err(chain_err)?
{
if let Ok(store) = hydrate(&creating_spend) {
if store.info.launcher_id == claim.store_id
&& store.info.metadata.root_hash == claim.generation_root
{
return Ok(Self {
store_id: claim.store_id,
generation_root: claim.generation_root,
committing_coin: current,
lineage_tip: lineage.tip(),
});
}
}
}
let parent = record.coin.parent_coin_info;
if parent == claim.store_id {
return Err(EvidenceError::RootNotCommitted);
}
current = parent;
}
Err(EvidenceError::LineageTooDeep)
}
fn verify(&self) -> EvidenceResult<()> {
if self.committing_coin == Bytes32::default() || self.store_id == Bytes32::default() {
return Err(EvidenceError::RootNotCommitted);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use chia_protocol::Coin;
use chia_puzzle_types::standard::StandardArgs;
use chia_wallet_sdk::test::Simulator;
use dig_chainsource_interface::MockChainSource;
use dig_chainsource_interface::{CoinRecord, SingletonLineage};
use dig_merkle::{mint_datastore, Owner};
struct MintedStore {
launcher: Coin,
eve: Coin,
launcher_spend: chia_protocol::CoinSpend,
store_id: Bytes32,
root: Bytes32,
}
fn mint(root: Bytes32) -> anyhow::Result<MintedStore> {
let mut sim = Simulator::new();
let owner = sim.bls(1_000_000);
let owner_ph: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
let built = mint_datastore(
owner.coin,
Owner::Standard(owner.pk),
root,
None,
None,
None,
None,
None,
owner_ph,
vec![],
0,
)?;
sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
let minted = built.child.expect("mint yields a child");
let store_id = minted.info.launcher_id;
let launcher_spend = built
.coin_spends
.iter()
.find(|s| s.coin.coin_id() == store_id)
.expect("launcher spend present")
.clone();
Ok(MintedStore {
launcher: launcher_spend.coin,
eve: minted.coin,
launcher_spend,
store_id,
root,
})
}
fn record(coin: Coin, spent_height: Option<u32>) -> CoinRecord {
CoinRecord {
coin,
confirmed_height: Some(1),
spent_height,
timestamp: None,
coinbase: false,
}
}
fn authentic_source(m: &MintedStore) -> MockChainSource {
MockChainSource::new()
.with_coin(m.store_id, record(m.launcher, Some(2)))
.with_coin(m.eve.coin_id(), record(m.eve, None))
.with_spend(m.store_id, m.launcher_spend.clone())
.with_lineage(
m.store_id,
SingletonLineage::new(m.eve.coin_id(), [m.store_id, m.eve.coin_id()]),
)
}
#[test]
fn a_genuine_root_anchor_gathers_and_verifies() -> anyhow::Result<()> {
let m = mint(Bytes32::new([0x5a; 32]))?;
let source = authentic_source(&m);
let claim = RootAnchorClaim {
store_id: m.store_id,
generation_root: m.root,
};
let evidence = RootAnchorEvidence::gather(&claim, &source).expect("genuine anchor");
assert_eq!(evidence.store_id(), m.store_id);
assert_eq!(evidence.generation_root(), m.root);
assert_eq!(evidence.committing_coin(), m.eve.coin_id());
assert!(evidence.verify().is_ok());
Ok(())
}
#[test]
fn a_root_never_committed_is_rejected() -> anyhow::Result<()> {
let m = mint(Bytes32::new([0x5a; 32]))?;
let source = authentic_source(&m);
let claim = RootAnchorClaim {
store_id: m.store_id,
generation_root: Bytes32::new([0xAA; 32]), };
assert_eq!(
RootAnchorEvidence::gather(&claim, &source).unwrap_err(),
EvidenceError::RootNotCommitted
);
Ok(())
}
#[test]
fn an_impostor_whose_store_id_coin_is_not_a_launcher_is_rejected() {
let store_id = Bytes32::new([0x11; 32]);
let impostor = Coin::new(Bytes32::new([0x99; 32]), Bytes32::new([0x22; 32]), 1);
let source = MockChainSource::new()
.with_coin(store_id, record(impostor, Some(2)))
.with_lineage(store_id, SingletonLineage::single(store_id));
let claim = RootAnchorClaim {
store_id,
generation_root: Bytes32::new([0x33; 32]),
};
assert_eq!(
RootAnchorEvidence::gather(&claim, &source).unwrap_err(),
EvidenceError::NotALauncher
);
}
#[test]
fn an_absent_launcher_is_rejected() {
let claim = RootAnchorClaim {
store_id: Bytes32::new([0x44; 32]),
generation_root: Bytes32::new([0x55; 32]),
};
assert_eq!(
RootAnchorEvidence::gather(&claim, &MockChainSource::new()).unwrap_err(),
EvidenceError::LauncherNotFound
);
}
#[test]
fn an_unreadable_chain_fails_closed() {
use dig_chainsource_interface::ChainSourceError;
let source = MockChainSource::new().fail_with(ChainSourceError::Timeout);
let claim = RootAnchorClaim {
store_id: Bytes32::new([0x44; 32]),
generation_root: Bytes32::new([0x55; 32]),
};
assert!(matches!(
RootAnchorEvidence::gather(&claim, &source).unwrap_err(),
EvidenceError::Chain(_)
));
}
#[test]
fn a_launcher_with_no_lineage_is_rejected() -> anyhow::Result<()> {
let m = mint(Bytes32::new([0x5a; 32]))?;
let source = MockChainSource::new().with_coin(m.store_id, record(m.launcher, Some(2)));
let claim = RootAnchorClaim {
store_id: m.store_id,
generation_root: m.root,
};
assert_eq!(
RootAnchorEvidence::gather(&claim, &source).unwrap_err(),
EvidenceError::NoLineage
);
Ok(())
}
}