#![cfg(all(feature = "lineage-walk", feature = "testing"))]
use std::cell::Cell;
use anyhow::Result;
use chia_bls::{PublicKey, SecretKey};
use chia_protocol::{Bytes32, Coin, CoinSpend, Program};
use chia_puzzle_types::singleton::{SingletonArgs, SingletonSolution};
use chia_puzzle_types::{EveProof, LineageProof, Memos, Proof};
use chia_sdk_driver::{
Launcher, Layer, SingletonLayer, Spend, SpendContext, SpendWithConditions, StandardLayer,
};
use chia_sdk_test::Simulator;
use chia_sdk_types::{Condition, Conditions};
use clvm_utils::{tree_hash, TreeHash};
use clvmr::serde::{node_from_bytes, node_to_bytes, node_to_bytes_backrefs};
use clvmr::{Allocator, NodePtr};
use dig_chainsource_interface::{
walk_singleton_lineage, walk_singleton_lineage_bounded, ChainSource, ChainSourceError,
CoinRecord, LineageWalkError, MockChainSource, SingletonLineage,
};
struct SimSource<'a> {
sim: &'a Simulator,
children_reads: Cell<usize>,
withheld_spend: Option<Bytes32>,
withheld_record: Option<Bytes32>,
backref_reveals: bool,
}
impl SimSource<'_> {
fn serialized_as_configured(&self, spend: CoinSpend) -> CoinSpend {
if !self.backref_reveals {
return spend;
}
CoinSpend::new(
spend.coin,
backref_serialized(&spend.puzzle_reveal),
spend.solution,
)
}
}
fn backref_serialized(program: &Program) -> Program {
let mut allocator = Allocator::new();
let node = node_from_bytes(&mut allocator, program.as_ref()).expect("the program deserializes");
Program::from(node_to_bytes_backrefs(&allocator, node).expect("the program re-serializes"))
}
impl ChainSource for SimSource<'_> {
type Error = ChainSourceError;
fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
if self.withheld_record == Some(coin_id) {
return Ok(None);
}
Ok(self.sim.coin_state(coin_id).map(CoinRecord::from))
}
fn coin_records_by_puzzle_hash(
&self,
puzzle_hash: Bytes32,
include_spent: bool,
) -> Result<Vec<CoinRecord>, Self::Error> {
Ok(self
.sim
.unspent_coins(puzzle_hash, false)
.into_iter()
.filter_map(|coin| self.sim.coin_state(coin.coin_id()))
.map(CoinRecord::from)
.filter(|record| include_spent || !record.is_spent())
.collect())
}
fn coin_records_by_parent(
&self,
parent_coin_id: Bytes32,
) -> Result<Vec<CoinRecord>, Self::Error> {
self.children_reads.set(self.children_reads.get() + 1);
Ok(self
.sim
.children(parent_coin_id)
.into_iter()
.map(CoinRecord::from)
.collect())
}
fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
if self.withheld_spend == Some(coin_id) {
return Ok(None);
}
Ok(self
.sim
.coin_spend(coin_id)
.map(|spend| self.serialized_as_configured(spend)))
}
fn resolve_singleton_lineage(
&self,
launcher_id: Bytes32,
) -> Result<Option<SingletonLineage>, Self::Error> {
dig_chainsource_interface::resolve_singleton_lineage_via_walk(self, launcher_id)
}
fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
Ok(None)
}
fn block_timestamp(&self, _height: u32) -> Result<Option<u64>, Self::Error> {
Ok(None)
}
}
struct Singleton {
launcher_id: Bytes32,
trail: Vec<Coin>,
proof: Proof,
inner_puzzle_hash: Bytes32,
pk: PublicKey,
sk: SecretKey,
}
impl Singleton {
fn tip(&self) -> Coin {
*self.trail.last().expect("a launched singleton has a tip")
}
fn outer_puzzle_hash(&self) -> Bytes32 {
SingletonArgs::curry_tree_hash(self.launcher_id, TreeHash::from(self.inner_puzzle_hash))
.into()
}
}
fn launch(sim: &mut Simulator, ctx: &mut SpendContext) -> Result<Singleton> {
launch_with_amount(sim, ctx, 1)
}
fn launch_with_amount(
sim: &mut Simulator,
ctx: &mut SpendContext,
amount: u64,
) -> Result<Singleton> {
let owner = sim.bls(amount);
let launcher = Launcher::new(owner.coin.coin_id(), amount);
let launcher_coin = launcher.coin();
let (conditions, eve) = launcher.spend(ctx, owner.puzzle_hash, ())?;
StandardLayer::new(owner.pk).spend(ctx, owner.coin, conditions)?;
sim.spend_coins(ctx.take(), std::slice::from_ref(&owner.sk))?;
Ok(Singleton {
launcher_id: launcher_coin.coin_id(),
trail: vec![launcher_coin, eve],
proof: Proof::Eve(EveProof {
parent_parent_coin_info: launcher_coin.parent_coin_info,
parent_amount: launcher_coin.amount,
}),
inner_puzzle_hash: owner.puzzle_hash,
pk: owner.pk,
sk: owner.sk,
})
}
fn advance(sim: &mut Simulator, ctx: &mut SpendContext, singleton: &mut Singleton) -> Result<()> {
advance_paying(sim, ctx, singleton, singleton.tip().amount, None)
}
fn advance_paying(
sim: &mut Simulator,
ctx: &mut SpendContext,
singleton: &mut Singleton,
recreate_amount: u64,
decoy: Option<(Bytes32, u64)>,
) -> Result<()> {
let tip = singleton.tip();
let sk = singleton.sk.clone();
let mut conditions =
Conditions::new().create_coin(singleton.inner_puzzle_hash, recreate_amount, Memos::None);
if let Some((puzzle_hash, amount)) = decoy {
conditions = conditions.create_coin(puzzle_hash, amount, Memos::None);
}
let inner = StandardLayer::new(singleton.pk).spend_with_conditions(ctx, conditions)?;
let layer = SingletonLayer::new(singleton.launcher_id, StandardLayer::new(singleton.pk));
let solution = SingletonSolution {
lineage_proof: singleton.proof,
amount: tip.amount,
inner_solution: inner.solution,
};
let puzzle = layer.construct_puzzle(ctx)?;
let solution = ctx.alloc(&solution)?;
ctx.spend(tip, Spend::new(puzzle, solution))?;
sim.spend_coins(ctx.take(), std::slice::from_ref(&sk))?;
singleton.proof = Proof::Lineage(LineageProof {
parent_parent_coin_info: tip.parent_coin_info,
parent_inner_puzzle_hash: singleton.inner_puzzle_hash,
parent_amount: tip.amount,
});
singleton.trail.push(Coin::new(
tip.coin_id(),
singleton.outer_puzzle_hash(),
recreate_amount,
));
Ok(())
}
fn source(sim: &Simulator) -> SimSource<'_> {
SimSource {
sim,
children_reads: Cell::new(0),
withheld_spend: None,
withheld_record: None,
backref_reveals: false,
}
}
#[test]
fn walk_returns_every_coin_from_the_launcher_to_the_tip() -> Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let mut singleton = launch(&mut sim, ctx)?;
advance(&mut sim, ctx, &mut singleton)?;
advance(&mut sim, ctx, &mut singleton)?;
let src = source(&sim);
let lineage = walk_singleton_lineage(&src, singleton.launcher_id)?
.expect("a live singleton has a lineage");
assert_eq!(lineage.tip(), singleton.tip().coin_id());
assert_eq!(lineage.len(), singleton.trail.len());
for coin in &singleton.trail {
assert!(
lineage.contains(coin.coin_id()),
"genuine lineage coin {} is missing",
coin.coin_id()
);
}
Ok(())
}
#[test]
fn a_backref_serialized_puzzle_reveal_resolves_exactly_as_the_plain_one() -> Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let mut singleton = launch(&mut sim, ctx)?;
advance(&mut sim, ctx, &mut singleton)?;
advance(&mut sim, ctx, &mut singleton)?;
let eve_reveal = sim
.coin_spend(singleton.trail[1].coin_id())
.expect("the eve is spent")
.puzzle_reveal;
let compressed = backref_serialized(&eve_reveal);
assert_ne!(
compressed.as_ref(),
eve_reveal.as_ref(),
"the reveal must actually use back-references for this fixture to bite"
);
let mut allocator = Allocator::new();
assert!(
node_from_bytes(&mut allocator, compressed.as_ref()).is_err(),
"the non-backref reader must be unable to read the compressed reveal"
);
let mut src = source(&sim);
src.backref_reveals = true;
let lineage = walk_singleton_lineage(&src, singleton.launcher_id)?
.expect("a compressed reveal is still a genuine singleton");
assert_eq!(lineage.tip(), singleton.tip().coin_id());
assert_eq!(lineage.len(), singleton.trail.len());
Ok(())
}
#[test]
fn a_lookalike_coin_wearing_the_singleton_puzzle_hash_is_not_a_member() -> Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let mut singleton = launch(&mut sim, ctx)?;
advance(&mut sim, ctx, &mut singleton)?;
let attacker = sim.bls(1);
let spoofed_puzzle_hash = singleton.outer_puzzle_hash();
StandardLayer::new(attacker.pk).spend(
ctx,
attacker.coin,
Conditions::new().create_coin(spoofed_puzzle_hash, 1, Memos::None),
)?;
sim.spend_coins(ctx.take(), std::slice::from_ref(&attacker.sk))?;
let spoof = Coin::new(attacker.coin.coin_id(), spoofed_puzzle_hash, 1);
assert!(sim.coin_state(spoof.coin_id()).is_some());
assert_eq!(spoof.puzzle_hash, singleton.tip().puzzle_hash);
assert_ne!(spoof.coin_id(), singleton.tip().coin_id());
let src = source(&sim);
let lineage = walk_singleton_lineage(&src, singleton.launcher_id)?.expect("live singleton");
assert!(
!lineage.contains(spoof.coin_id()),
"a look-alike coin with no genuine recreation parent-spend was admitted"
);
assert_eq!(lineage.tip(), singleton.tip().coin_id());
Ok(())
}
#[test]
fn a_genuine_sibling_of_the_successor_is_not_selected_as_the_successor() -> Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let mut singleton = launch_with_amount(&mut sim, ctx, 5)?;
let successor_puzzle_hash = singleton.outer_puzzle_hash();
let eve = singleton.tip();
advance_paying(
&mut sim,
ctx,
&mut singleton,
3,
Some((successor_puzzle_hash, 2)),
)?;
let decoy = Coin::new(eve.coin_id(), successor_puzzle_hash, 2);
let src = source(&sim);
let children = src.coin_records_by_parent(eve.coin_id())?;
assert_eq!(children.len(), 2, "the successor must have a real sibling");
assert!(children
.iter()
.all(|child| child.coin.puzzle_hash == successor_puzzle_hash));
assert!(sim.coin_state(decoy.coin_id()).is_some());
src.children_reads.set(0);
let lineage = walk_singleton_lineage(&src, singleton.launcher_id)?.expect("live singleton");
assert_eq!(lineage.tip(), singleton.tip().coin_id());
assert!(
!lineage.contains(decoy.coin_id()),
"an even-amount sibling wearing the successor's puzzle hash was admitted"
);
assert_eq!(
src.children_reads.get(),
0,
"the walk consulted the child list, so a source could choose its successor"
);
Ok(())
}
#[test]
fn an_unknown_launcher_is_a_genuine_absence() -> Result<()> {
let sim = Simulator::new();
let src = source(&sim);
assert_eq!(
walk_singleton_lineage(&src, Bytes32::new([0x11; 32]))?,
None
);
Ok(())
}
#[test]
fn a_coin_that_is_not_a_launcher_is_a_genuine_absence() -> Result<()> {
let mut sim = Simulator::new();
let ordinary = sim.bls(1);
let src = source(&sim);
assert_eq!(
walk_singleton_lineage(&src, ordinary.coin.coin_id())?,
None,
"an ordinary coin's id names no singleton"
);
Ok(())
}
#[test]
fn a_transport_failure_is_never_reported_as_an_absent_lineage() {
let source = MockChainSource::new().fail_with(ChainSourceError::Transport("socket".into()));
let error = walk_singleton_lineage(&source, Bytes32::new([0x22; 32]))
.expect_err("a read failure must not resolve");
assert_eq!(
error,
LineageWalkError::Source(ChainSourceError::Transport("socket".into())),
"the source's own error must survive the walk verbatim"
);
}
#[test]
fn an_unsupported_read_stays_distinguishable_from_unreadable_and_from_absent() {
let source = MockChainSource::new().fail_with(ChainSourceError::Unsupported("coin_record"));
let projected: ChainSourceError = walk_singleton_lineage(&source, Bytes32::new([0x33; 32]))
.expect_err("unsupported is not an absence")
.into();
assert_eq!(projected, ChainSourceError::Unsupported("coin_record"));
assert_ne!(projected, ChainSourceError::Malformed("coin_record".into()));
}
fn melt_with(
sim: &mut Simulator,
ctx: &mut SpendContext,
singleton: &Singleton,
melt: Conditions,
) -> Result<()> {
let tip = singleton.tip();
let sk = singleton.sk.clone();
let inner = StandardLayer::new(singleton.pk).spend_with_conditions(ctx, melt)?;
let layer = SingletonLayer::new(singleton.launcher_id, StandardLayer::new(singleton.pk));
let puzzle = layer.construct_puzzle(ctx)?;
let solution = ctx.alloc(&SingletonSolution {
lineage_proof: singleton.proof,
amount: tip.amount,
inner_solution: inner.solution,
})?;
ctx.spend(tip, Spend::new(puzzle, solution))?;
sim.spend_coins(ctx.take(), std::slice::from_ref(&sk))?;
Ok(())
}
#[test]
fn a_melted_singleton_has_no_lineage() -> Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let singleton = launch(&mut sim, ctx)?;
let melt = ctx.alloc(&(51, (singleton.inner_puzzle_hash, (-113, ()))))?;
melt_with(
&mut sim,
ctx,
&singleton,
Conditions::new().with(Condition::Other(melt)),
)?;
let src = source(&sim);
assert_eq!(walk_singleton_lineage(&src, singleton.launcher_id)?, None);
Ok(())
}
#[test]
fn a_singleton_melted_with_standard_tooling_has_no_lineage() -> Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let singleton = launch(&mut sim, ctx)?;
melt_with(
&mut sim,
ctx,
&singleton,
Conditions::new().melt_singleton(),
)?;
const CANONICAL_MELT: [u8; 8] = [0xff, 0x33, 0xff, 0x80, 0xff, 0x81, 0x8f, 0x80];
let spend = sim
.coin_spend(singleton.tip().coin_id())
.expect("the melt spend is on chain");
assert!(
spend
.solution
.as_ref()
.windows(CANONICAL_MELT.len())
.any(|window| window == CANONICAL_MELT),
"the standard melt must serialize its CREATE_COIN with a nil puzzle hash"
);
assert_eq!(
walk_singleton_lineage(&source(&sim), singleton.launcher_id)?,
None,
"a singleton melted with standard tooling is a genuine absence, not a refusal"
);
Ok(())
}
#[test]
fn exceeding_the_hop_bound_refuses_rather_than_truncating() -> Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let mut singleton = launch(&mut sim, ctx)?;
advance(&mut sim, ctx, &mut singleton)?;
advance(&mut sim, ctx, &mut singleton)?;
let src = source(&sim);
assert_eq!(singleton.trail.len(), 4);
let error = walk_singleton_lineage_bounded(&src, singleton.launcher_id, 2)
.expect_err("an over-deep walk must not resolve");
assert_eq!(error, LineageWalkError::TooDeep { limit: 2 });
assert_eq!(
ChainSourceError::from(error),
ChainSourceError::LineageTooDeep { limit: 2 },
"the over-deep refusal must stay distinguishable from every other failure"
);
let lineage = walk_singleton_lineage_bounded(&src, singleton.launcher_id, 3)?
.expect("the walk completes at exactly the bound");
assert_eq!(lineage.tip(), singleton.tip().coin_id());
Ok(())
}
#[test]
fn a_spend_of_the_wrong_coin_fails_closed() {
let launcher_ph = Bytes32::new(chia_puzzles::SINGLETON_LAUNCHER_HASH);
let asked_for = Coin::new(Bytes32::new([0x01; 32]), launcher_ph, 1);
let other = Coin::new(Bytes32::new([0x02; 32]), launcher_ph, 1);
let eve_puzzle_hash = Bytes32::new([0x0B; 32]);
let eve = Coin::new(asked_for.coin_id(), eve_puzzle_hash, 1);
let source = MockChainSource::new()
.with_coin(asked_for.coin_id(), record(asked_for))
.with_coin(eve.coin_id(), record(eve))
.with_spend(
asked_for.coin_id(),
CoinSpend::new(
other,
launcher_reveal(),
launcher_solution(eve_puzzle_hash, 1),
),
);
let error = walk_singleton_lineage(&source, asked_for.coin_id())
.expect_err("a mismatched spend must fail closed");
assert_eq!(
error,
LineageWalkError::Malformed(format!(
"source returned a spend of coin {} when asked for {}",
other.coin_id(),
asked_for.coin_id()
)),
"the refusal must be the coin-identity check, not some other guard that also says Malformed"
);
let honest = MockChainSource::new()
.with_coin(asked_for.coin_id(), record(asked_for))
.with_coin(eve.coin_id(), record(eve))
.with_spend(
asked_for.coin_id(),
CoinSpend::new(
asked_for,
launcher_reveal(),
launcher_solution(eve_puzzle_hash, 1),
),
);
assert_eq!(
walk_singleton_lineage(&honest, asked_for.coin_id())
.expect("the honest launcher resolves")
.expect("a launched singleton")
.tip(),
eve.coin_id()
);
}
#[test]
fn a_spent_coin_that_is_not_a_launcher_is_still_a_genuine_absence() {
let eve_puzzle_hash = Bytes32::new([0x0A; 32]);
let reveal = quoting_puzzle(&vec![(51, (eve_puzzle_hash, (1, ())))]);
let ordinary = Coin::new(Bytes32::new([0x09; 32]), tree_hash_of(&reveal), 1);
let eve = Coin::new(ordinary.coin_id(), eve_puzzle_hash, 1);
assert_ne!(
ordinary.puzzle_hash,
Bytes32::new(chia_puzzles::SINGLETON_LAUNCHER_HASH)
);
let source = MockChainSource::new()
.with_coin(ordinary.coin_id(), spent_record(ordinary, 7))
.with_coin(eve.coin_id(), record(eve))
.with_spend(
ordinary.coin_id(),
CoinSpend::new(ordinary, reveal, Program::from(vec![0x80])),
);
assert_eq!(
walk_singleton_lineage(&source, ordinary.coin_id()),
Ok(None),
"an ordinary spent coin names no singleton, however well its spend reads"
);
}
#[test]
fn a_reveal_that_does_not_hash_to_the_coin_fails_closed() {
let launcher_ph = Bytes32::new(chia_puzzles::SINGLETON_LAUNCHER_HASH);
let launcher = Coin::new(Bytes32::new([0x03; 32]), launcher_ph, 1);
let source = MockChainSource::new()
.with_coin(launcher.coin_id(), record(launcher))
.with_spend(
launcher.coin_id(),
CoinSpend::new(
launcher,
Program::from(vec![0x01, 0x80]),
Program::from(vec![0x80]),
),
);
let error = walk_singleton_lineage(&source, launcher.coin_id())
.expect_err("a foreign reveal must fail closed");
assert!(matches!(error, LineageWalkError::Malformed(_)));
}
#[test]
fn an_unspent_launcher_has_no_singleton_state() {
let launcher_ph = Bytes32::new(chia_puzzles::SINGLETON_LAUNCHER_HASH);
let launcher = Coin::new(Bytes32::new([0x04; 32]), launcher_ph, 1);
let source = MockChainSource::new().with_coin(launcher.coin_id(), record(launcher));
assert_eq!(
walk_singleton_lineage(&source, launcher.coin_id()),
Ok(None)
);
}
#[test]
fn an_eve_that_is_not_a_singleton_fails_closed() -> Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let owner = sim.bls(1);
let launcher_coin = Coin::new(
owner.coin.coin_id(),
Bytes32::new(chia_puzzles::SINGLETON_LAUNCHER_HASH),
1,
);
StandardLayer::new(owner.pk).spend(
ctx,
owner.coin,
Conditions::new().create_coin(launcher_coin.puzzle_hash, 1, Memos::None),
)?;
let launcher_puzzle = ctx.alloc(&Program::from(chia_puzzles::SINGLETON_LAUNCHER.to_vec()))?;
let launcher_solution = ctx.alloc(&(owner.puzzle_hash, (1, (Vec::<Bytes32>::new(), ()))))?;
ctx.spend(
launcher_coin,
Spend::new(launcher_puzzle, launcher_solution),
)?;
sim.spend_coins(ctx.take(), std::slice::from_ref(&owner.sk))?;
let fake_eve = Coin::new(launcher_coin.coin_id(), owner.puzzle_hash, 1);
StandardLayer::new(owner.pk).spend(ctx, fake_eve, Conditions::new())?;
sim.spend_coins(ctx.take(), std::slice::from_ref(&owner.sk))?;
let src = source(&sim);
let error = walk_singleton_lineage(&src, launcher_coin.coin_id())
.expect_err("a non-singleton eve must not resolve to a lineage");
assert_eq!(
error,
LineageWalkError::NotASingleton {
coin_id: fake_eve.coin_id()
}
);
Ok(())
}
#[test]
fn a_launcher_record_for_the_wrong_coin_fails_closed() {
let launcher_ph = Bytes32::new(chia_puzzles::SINGLETON_LAUNCHER_HASH);
let asked_for = Coin::new(Bytes32::new([0x05; 32]), launcher_ph, 1);
let returned = Coin::new(Bytes32::new([0x06; 32]), launcher_ph, 1);
let source = MockChainSource::new().with_coin(asked_for.coin_id(), record(returned));
let error = walk_singleton_lineage(&source, asked_for.coin_id())
.expect_err("a record for a different coin must fail closed");
assert!(matches!(error, LineageWalkError::Malformed(_)));
}
#[test]
fn every_failure_reports_itself_distinguishably() {
let coin_id = Bytes32::new([0x07; 32]);
let messages = [
LineageWalkError::Source(ChainSourceError::Timeout).to_string(),
LineageWalkError::<ChainSourceError>::Malformed("bad".into()).to_string(),
LineageWalkError::<ChainSourceError>::NotASingleton { coin_id }.to_string(),
LineageWalkError::<ChainSourceError>::TooDeep { limit: 9 }.to_string(),
];
assert!(messages.iter().all(|message| !message.is_empty()));
assert_eq!(
messages
.iter()
.collect::<std::collections::BTreeSet<_>>()
.len(),
messages.len(),
"each failure must read differently in a log"
);
assert_eq!(
ChainSourceError::from(LineageWalkError::<ChainSourceError>::NotASingleton { coin_id }),
ChainSourceError::Malformed(format!(
"coin {coin_id} is not a genuine singleton of this launcher"
))
);
assert_eq!(
ChainSourceError::from(LineageWalkError::<ChainSourceError>::Malformed("x".into())),
ChainSourceError::Malformed("x".into())
);
}
#[test]
fn a_spent_coin_whose_spend_the_source_cannot_serve_is_never_reported_as_the_tip() -> Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let mut singleton = launch(&mut sim, ctx)?;
advance(&mut sim, ctx, &mut singleton)?;
advance(&mut sim, ctx, &mut singleton)?;
let stale = singleton.trail[2];
let tip = singleton.tip();
let spent_state = sim.coin_state(stale.coin_id()).expect("C2 is on chain");
assert!(
spent_state.spent_height.is_some(),
"C2 must be spent for the ambiguity to exist"
);
assert_ne!(stale.coin_id(), tip.coin_id());
let mut src = source(&sim);
src.withheld_spend = Some(stale.coin_id());
let error = walk_singleton_lineage(&src, singleton.launcher_id)
.expect_err("a spend the source cannot serve is an unknown, not a tip");
assert!(
matches!(error, LineageWalkError::Malformed(_)),
"expected a refusal, got {error:?}"
);
let honest = source(&sim);
assert_eq!(
walk_singleton_lineage(&honest, singleton.launcher_id)?
.expect("the honest chain resolves")
.tip(),
tip.coin_id(),
);
Ok(())
}
#[test]
fn a_spent_launcher_whose_spend_the_source_cannot_serve_is_not_an_absence() {
let launcher_ph = Bytes32::new(chia_puzzles::SINGLETON_LAUNCHER_HASH);
let launcher = Coin::new(Bytes32::new([0x08; 32]), launcher_ph, 1);
let source = MockChainSource::new().with_coin(launcher.coin_id(), spent_record(launcher, 12));
let error = walk_singleton_lineage(&source, launcher.coin_id())
.expect_err("a spent launcher with no readable spend is unknown, not unlaunched");
assert!(
matches!(error, LineageWalkError::Malformed(_)),
"expected a refusal, got {error:?}"
);
let unspent = MockChainSource::new().with_coin(launcher.coin_id(), record(launcher));
assert_eq!(
walk_singleton_lineage(&unspent, launcher.coin_id()),
Ok(None)
);
}
#[test]
fn a_derived_successor_the_source_does_not_know_fails_closed() -> Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let mut singleton = launch(&mut sim, ctx)?;
advance(&mut sim, ctx, &mut singleton)?;
advance(&mut sim, ctx, &mut singleton)?;
let unknown = singleton.trail[2];
let mut src = source(&sim);
src.withheld_record = Some(unknown.coin_id());
let error = walk_singleton_lineage(&src, singleton.launcher_id)
.expect_err("a successor the source does not know must fail closed");
match error {
LineageWalkError::Malformed(detail) => assert!(
detail.contains(&unknown.coin_id().to_string()),
"the refusal must name the unknown coin: {detail}"
),
other => panic!("expected a refusal, got {other:?}"),
}
let honest = source(&sim);
assert_eq!(
walk_singleton_lineage(&honest, singleton.launcher_id)?
.expect("the honest chain resolves")
.tip(),
singleton.tip().coin_id(),
);
Ok(())
}
#[test]
fn an_unspent_eve_is_the_tip_of_a_two_coin_lineage() -> Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let singleton = launch(&mut sim, ctx)?;
let eve = singleton.tip();
assert!(
sim.coin_spend(eve.coin_id()).is_none(),
"the eve must be unspent for this to be the documented case"
);
let src = source(&sim);
let lineage =
walk_singleton_lineage(&src, singleton.launcher_id)?.expect("a freshly launched singleton");
assert_eq!(lineage.tip(), eve.coin_id());
assert_eq!(lineage.len(), 2);
assert!(lineage.contains(singleton.launcher_id));
Ok(())
}
#[test]
fn a_singleton_curried_to_a_different_launcher_is_not_a_member() -> Result<()> {
let ctx = &mut SpendContext::new();
let victim = Coin::new(
Bytes32::new([0x0E; 32]),
Bytes32::new(chia_puzzles::SINGLETON_LAUNCHER_HASH),
1,
);
let foreign_launcher_id = Bytes32::new([0xFE; 32]);
let inner = ctx.alloc(&vec![(51, (Bytes32::new([0x0F; 32]), (1, ())))])?;
let quoted_inner = ctx.alloc(&(1, inner))?;
let foreign = ctx.curry(SingletonArgs::new(foreign_launcher_id, quoted_inner))?;
let eve = Coin::new(victim.coin_id(), Bytes32::from(tree_hash(ctx, foreign)), 1);
let eve_solution = ctx.alloc(&SingletonSolution {
lineage_proof: Proof::Eve(EveProof {
parent_parent_coin_info: victim.parent_coin_info,
parent_amount: 1,
}),
amount: 1,
inner_solution: NodePtr::NIL,
})?;
let source = MockChainSource::new()
.with_coin(victim.coin_id(), record(victim))
.with_coin(eve.coin_id(), spent_record(eve, 8))
.with_spend(
victim.coin_id(),
CoinSpend::new(
victim,
launcher_reveal(),
launcher_solution(eve.puzzle_hash, 1),
),
)
.with_spend(
eve.coin_id(),
CoinSpend::new(eve, ctx.serialize(&foreign)?, ctx.serialize(&eve_solution)?),
);
assert!(
SingletonLayer::<chia_sdk_driver::Puzzle>::parse_puzzle(
ctx,
chia_sdk_driver::Puzzle::parse(ctx, foreign)
)?
.is_some_and(|layer| layer.launcher_id == foreign_launcher_id),
"the reveal must be a well-formed singleton of the OTHER launcher"
);
assert_eq!(
walk_singleton_lineage(&source, victim.coin_id()),
Err(LineageWalkError::NotASingleton {
coin_id: eve.coin_id()
}),
"a singleton of another launcher must be refused as such, not derived from"
);
Ok(())
}
fn launcher_reveal() -> Program {
Program::from(chia_puzzles::SINGLETON_LAUNCHER.to_vec())
}
fn launcher_solution(puzzle_hash: Bytes32, amount: u64) -> Program {
serialized(&(puzzle_hash, (amount, (Vec::<Bytes32>::new(), ()))))
}
fn quoting_puzzle<T: clvm_traits::ToClvm<Allocator>>(conditions: &T) -> Program {
serialized(&(1, conditions))
}
fn tree_hash_of(program: &Program) -> Bytes32 {
let mut allocator = Allocator::new();
let node = node_from_bytes(&mut allocator, program.as_ref()).expect("the program deserializes");
Bytes32::from(tree_hash(&allocator, node))
}
fn serialized<T: clvm_traits::ToClvm<Allocator>>(value: &T) -> Program {
let mut allocator = Allocator::new();
let node =
clvm_traits::ToClvm::to_clvm(value, &mut allocator).expect("the value always allocates");
Program::from(node_to_bytes(&allocator, node).expect("the value always serializes"))
}
fn record(coin: Coin) -> CoinRecord {
CoinRecord {
coin,
confirmed_height: Some(1),
spent_height: None,
timestamp: None,
coinbase: false,
}
}
fn spent_record(coin: Coin, spent_height: u32) -> CoinRecord {
CoinRecord {
spent_height: Some(spent_height),
..record(coin)
}
}