use std::collections::{BTreeSet, HashMap};
use std::fmt;
use std::time::{Duration, Instant};
use chia_protocol::{Bytes32, Coin, CoinSpend, Program};
use chia_puzzle_types::singleton::SingletonArgs;
use chia_puzzles::SINGLETON_LAUNCHER_HASH;
use chia_sdk_driver::{Layer, Puzzle, SingletonLayer};
use chia_sdk_types::run_puzzle_with_cost;
use clvm_traits::FromClvm;
use clvm_utils::{tree_hash_from_bytes, TreeHash};
use clvmr::serde::node_from_bytes_backrefs;
use clvmr::{Allocator, NodePtr, SExp};
use crate::error::ChainSourceError;
use crate::lineage::SingletonLineage;
use crate::record::CoinRecord;
use crate::source::ChainSource;
pub const MAX_LINEAGE_DEPTH: usize = 100_000;
pub const DEFAULT_WALK_BUDGET: Duration = Duration::from_secs(45);
pub const MAX_HOP_CLVM_COST: u64 = 100_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct WalkBounds {
max_hops: usize,
budget: Duration,
}
impl Default for WalkBounds {
fn default() -> Self {
Self {
max_hops: MAX_LINEAGE_DEPTH,
budget: DEFAULT_WALK_BUDGET,
}
}
}
impl WalkBounds {
#[must_use]
pub fn hops(max_hops: usize) -> Self {
Self {
max_hops: max_hops.min(MAX_LINEAGE_DEPTH),
..Self::default()
}
}
#[must_use]
pub fn within(self, budget: Duration) -> Self {
Self { budget, ..self }
}
#[must_use]
pub fn max_hops(self) -> usize {
self.max_hops
}
#[must_use]
pub fn budget(self) -> Duration {
self.budget
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LineageWalkError<E> {
Source(E),
Malformed(String),
NotASingleton {
coin_id: Bytes32,
},
RevealTooLarge {
coin_id: Bytes32,
limit: usize,
},
TooDeep {
limit: usize,
},
DeadlineExceeded {
budget: Duration,
},
}
impl<E: fmt::Display> fmt::Display for LineageWalkError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Source(error) => write!(f, "chain source read failed: {error}"),
Self::Malformed(detail) => write!(f, "inconsistent chain data: {detail}"),
Self::NotASingleton { coin_id } => {
write!(
f,
"coin {coin_id} is not a genuine singleton of this launcher"
)
}
Self::RevealTooLarge { coin_id, limit } => {
write!(
f,
"the puzzle reveal of coin {coin_id} expands beyond the {limit}-byte bound"
)
}
Self::TooDeep { limit } => {
write!(f, "singleton lineage walk exceeded its {limit}-hop bound")
}
Self::DeadlineExceeded { budget } => {
write!(
f,
"singleton lineage walk exceeded its {budget:?} wall-clock budget"
)
}
}
}
}
impl<E: fmt::Display + fmt::Debug> std::error::Error for LineageWalkError<E> {}
impl From<LineageWalkError<ChainSourceError>> for ChainSourceError {
fn from(error: LineageWalkError<ChainSourceError>) -> Self {
match error {
LineageWalkError::Source(error) => error,
LineageWalkError::Malformed(detail) => ChainSourceError::Malformed(detail),
LineageWalkError::NotASingleton { coin_id } => ChainSourceError::Malformed(format!(
"coin {coin_id} is not a genuine singleton of this launcher"
)),
LineageWalkError::RevealTooLarge { limit, .. } => {
ChainSourceError::RevealTooLarge { limit }
}
LineageWalkError::TooDeep { limit } => ChainSourceError::LineageTooDeep { limit },
LineageWalkError::DeadlineExceeded { .. } => ChainSourceError::Timeout,
}
}
}
pub fn resolve_singleton_lineage_via_walk<S>(
source: &S,
launcher_id: Bytes32,
) -> Result<Option<SingletonLineage>, ChainSourceError>
where
S: ChainSource<Error = ChainSourceError>,
{
walk_singleton_lineage(source, launcher_id).map_err(Into::into)
}
pub fn walk_singleton_lineage<S: ChainSource>(
source: &S,
launcher_id: Bytes32,
) -> Result<Option<SingletonLineage>, LineageWalkError<S::Error>> {
walk_singleton_lineage_within(source, launcher_id, WalkBounds::default())
}
pub fn walk_singleton_lineage_bounded<S: ChainSource>(
source: &S,
launcher_id: Bytes32,
max_hops: usize,
) -> Result<Option<SingletonLineage>, LineageWalkError<S::Error>> {
walk_singleton_lineage_within(source, launcher_id, WalkBounds::hops(max_hops))
}
pub fn walk_singleton_lineage_within<S: ChainSource>(
source: &S,
launcher_id: Bytes32,
bounds: WalkBounds,
) -> Result<Option<SingletonLineage>, LineageWalkError<S::Error>> {
let started = Instant::now();
let Some(launcher) = read_launcher_coin(source, launcher_id)? else {
return Ok(None);
};
let mut members = BTreeSet::from([launcher_id]);
let mut current = launcher.coin;
let mut current_spent_height = launcher.spent_height;
let mut rule = HopRule::Launch;
for _hop in 0..=bounds.max_hops {
if started.elapsed() > bounds.budget {
return Err(LineageWalkError::DeadlineExceeded {
budget: bounds.budget,
});
}
let Some(spend) = read_spend_of(source, current, current_spent_height)? else {
let at_launcher = matches!(rule, HopRule::Launch);
return Ok((!at_launcher).then(|| SingletonLineage::new(current.coin_id(), members)));
};
let Some(successor) = successor_of(current, &spend, rule)? else {
return Ok(None);
};
let record = require_coin_exists(source, successor)?;
admit_member(&mut members, successor.coin_id())?;
current = successor;
current_spent_height = record.spent_height;
rule = HopRule::Recreate { launcher_id };
}
Err(LineageWalkError::TooDeep {
limit: bounds.max_hops,
})
}
#[derive(Debug, Clone, Copy)]
enum HopRule {
Launch,
Recreate { launcher_id: Bytes32 },
}
fn successor_of<E>(
coin: Coin,
spend: &CoinSpend,
rule: HopRule,
) -> Result<Option<Coin>, LineageWalkError<E>> {
let allocator = &mut Allocator::new();
let (puzzle, solution) = parse_spend(allocator, spend)?;
match rule {
HopRule::Launch => eve_created_by_launcher(allocator, coin, puzzle, solution),
HopRule::Recreate { launcher_id } => {
singleton_successor(allocator, coin, launcher_id, puzzle, solution)
}
}
}
fn admit_member<E>(
members: &mut BTreeSet<Bytes32>,
coin_id: Bytes32,
) -> Result<(), LineageWalkError<E>> {
if members.insert(coin_id) {
return Ok(());
}
Err(LineageWalkError::Malformed(format!(
"coin {coin_id} repeats in the lineage (a cycle)"
)))
}
fn read_launcher_coin<S: ChainSource>(
source: &S,
launcher_id: Bytes32,
) -> Result<Option<CoinRecord>, LineageWalkError<S::Error>> {
let Some(record) = source
.coin_record(launcher_id)
.map_err(LineageWalkError::Source)?
else {
return Ok(None);
};
if record.coin.coin_id() != launcher_id {
return Err(LineageWalkError::Malformed(format!(
"source returned coin {} for id {launcher_id}",
record.coin.coin_id()
)));
}
if record.coin.puzzle_hash != Bytes32::new(SINGLETON_LAUNCHER_HASH) {
return Ok(None);
}
Ok(Some(record))
}
fn read_spend_of<S: ChainSource>(
source: &S,
coin: Coin,
spent_height: Option<u32>,
) -> Result<Option<CoinSpend>, LineageWalkError<S::Error>> {
let Some(spend) = source
.coin_spend(coin.coin_id())
.map_err(LineageWalkError::Source)?
else {
return match spent_height {
Some(height) => Err(LineageWalkError::Malformed(format!(
"coin {} is recorded as spent at height {height}, but the source served no spend \
for it",
coin.coin_id()
))),
None => Ok(None),
};
};
if spend.coin != coin {
return Err(LineageWalkError::Malformed(format!(
"source returned a spend of coin {} when asked for {}",
spend.coin.coin_id(),
coin.coin_id()
)));
}
require_expandable_reveal(coin.coin_id(), &spend.puzzle_reveal)?;
let revealed = program_tree_hash(&spend.puzzle_reveal)?;
if Bytes32::from(revealed) != coin.puzzle_hash {
return Err(LineageWalkError::Malformed(format!(
"puzzle reveal does not hash to the puzzle hash of coin {}",
coin.coin_id()
)));
}
Ok(Some(spend))
}
fn require_coin_exists<S: ChainSource>(
source: &S,
coin: Coin,
) -> Result<CoinRecord, LineageWalkError<S::Error>> {
source
.coin_record(coin.coin_id())
.map_err(LineageWalkError::Source)?
.filter(|record| record.coin == coin)
.ok_or_else(|| {
LineageWalkError::Malformed(format!(
"the spend claims to create coin {}, which the source does not know",
coin.coin_id()
))
})
}
fn eve_created_by_launcher<E>(
allocator: &mut Allocator,
launcher: Coin,
puzzle: Puzzle,
solution: NodePtr,
) -> Result<Option<Coin>, LineageWalkError<E>> {
Ok(
match run_for_continuation(allocator, puzzle.ptr(), solution)? {
Continuation::Ends => None,
Continuation::Recreates(puzzle_hash, amount) => {
Some(Coin::new(launcher.coin_id(), puzzle_hash, amount))
}
},
)
}
fn singleton_successor<E>(
allocator: &mut Allocator,
parent: Coin,
launcher_id: Bytes32,
puzzle: Puzzle,
solution: NodePtr,
) -> Result<Option<Coin>, LineageWalkError<E>> {
let layer = SingletonLayer::<Puzzle>::parse_puzzle(allocator, puzzle)
.map_err(|error| LineageWalkError::Malformed(format!("undecodable puzzle: {error}")))?
.filter(|layer| layer.launcher_id == launcher_id)
.ok_or(LineageWalkError::NotASingleton {
coin_id: parent.coin_id(),
})?;
let solution = SingletonLayer::<Puzzle>::parse_solution(allocator, solution)
.map_err(|error| LineageWalkError::Malformed(format!("undecodable solution: {error}")))?;
Ok(
match run_for_continuation(allocator, layer.inner_puzzle.ptr(), solution.inner_solution)? {
Continuation::Ends => None,
Continuation::Recreates(inner_puzzle_hash, amount) => {
let full =
SingletonArgs::curry_tree_hash(launcher_id, TreeHash::from(inner_puzzle_hash));
Some(Coin::new(parent.coin_id(), full.into(), amount))
}
},
)
}
const SINGLETON_MELT_AMOUNT: i64 = -113;
#[derive(Debug)]
enum Continuation {
Recreates(Bytes32, u64),
Ends,
}
fn run_for_continuation<E>(
allocator: &mut Allocator,
puzzle: NodePtr,
solution: NodePtr,
) -> Result<Continuation, LineageWalkError<E>> {
let output = run_puzzle_with_cost(allocator, puzzle, solution, MAX_HOP_CLVM_COST, false)
.map_err(|error| LineageWalkError::Malformed(format!("puzzle did not run: {error}")))?
.1;
let conditions = Vec::<NodePtr>::from_clvm(allocator, output).map_err(|error| {
LineageWalkError::Malformed(format!("undecodable condition list: {error}"))
})?;
let mut recreation: Option<(Bytes32, u64)> = None;
for condition in conditions {
let Ok((opcode, arguments)) = ConditionHead::from_clvm(allocator, condition) else {
continue;
};
if opcode != CREATE_COIN {
continue;
}
let (puzzle_hash, (signed_amount, _memos)) =
CreateCoinArguments::from_clvm(allocator, arguments).map_err(|error| {
LineageWalkError::Malformed(format!("undecodable CREATE_COIN condition: {error}"))
})?;
if signed_amount == SINGLETON_MELT_AMOUNT {
return Ok(Continuation::Ends);
}
let amount = u64::try_from(signed_amount).map_err(|_| {
LineageWalkError::Malformed(format!(
"CREATE_COIN with the negative amount {signed_amount}, which is not the singleton \
melt marker {SINGLETON_MELT_AMOUNT}"
))
})?;
let puzzle_hash = Bytes32::from_clvm(allocator, puzzle_hash).map_err(|error| {
LineageWalkError::Malformed(format!(
"undecodable CREATE_COIN condition: recreation puzzle hash: {error}"
))
})?;
if amount % 2 == 0 {
continue;
}
if recreation.is_some() {
return Err(LineageWalkError::Malformed(
"a singleton spend emitted more than one odd-amount child".to_string(),
));
}
recreation = Some((puzzle_hash, amount));
}
Ok(match recreation {
Some((puzzle_hash, amount)) => Continuation::Recreates(puzzle_hash, amount),
None => Continuation::Ends,
})
}
const CREATE_COIN: i64 = 51;
type ConditionHead = (i64, NodePtr);
type CreateCoinArguments = (NodePtr, (i64, NodePtr));
fn parse_spend<E>(
allocator: &mut Allocator,
spend: &CoinSpend,
) -> Result<(Puzzle, NodePtr), LineageWalkError<E>> {
let puzzle = alloc(allocator, &spend.puzzle_reveal)?;
let solution = alloc(allocator, &spend.solution)?;
Ok((Puzzle::parse(allocator, puzzle), solution))
}
fn alloc<E>(allocator: &mut Allocator, program: &Program) -> Result<NodePtr, LineageWalkError<E>> {
node_from_bytes_backrefs(allocator, program.as_ref())
.map_err(|error| LineageWalkError::Malformed(format!("undecodable program: {error}")))
}
pub const MAX_REVEAL_EXPANDED_BYTES: usize = 4 * 1024 * 1024;
fn expanded_hash_input_bytes(allocator: &Allocator, root: NodePtr, limit: usize) -> usize {
const ATOM_PREFIX: usize = 1;
const PAIR_COST: usize = 1 + 32 + 32;
let ceiling = limit.saturating_add(1);
enum Step {
Cost(NodePtr),
Combine(NodePtr),
}
let mut sizes: HashMap<NodePtr, usize> = HashMap::new();
let mut steps = vec![Step::Cost(root)];
let mut costed: Vec<usize> = Vec::new();
while let Some(step) = steps.pop() {
match step {
Step::Cost(node) => {
if let Some(&known) = sizes.get(&node) {
costed.push(known);
continue;
}
match allocator.sexp(node) {
SExp::Atom => {
let size = ATOM_PREFIX
.saturating_add(allocator.atom_len(node))
.min(ceiling);
sizes.insert(node, size);
costed.push(size);
}
SExp::Pair(left, right) => {
steps.push(Step::Combine(node));
steps.push(Step::Cost(right));
steps.push(Step::Cost(left));
}
}
}
Step::Combine(node) => {
let (right, left) = (
costed.pop().expect("a pair's right child was costed first"),
costed.pop().expect("a pair's left child was costed first"),
);
let size = PAIR_COST
.saturating_add(left)
.saturating_add(right)
.min(ceiling);
sizes.insert(node, size);
costed.push(size);
}
}
}
costed
.pop()
.expect("the traversal leaves the root's cost on the stack")
}
fn require_expandable_reveal<E>(
coin_id: Bytes32,
reveal: &Program,
) -> Result<(), LineageWalkError<E>> {
let allocator = &mut Allocator::new();
let node = alloc(allocator, reveal)?;
if expanded_hash_input_bytes(allocator, node, MAX_REVEAL_EXPANDED_BYTES)
> MAX_REVEAL_EXPANDED_BYTES
{
return Err(LineageWalkError::RevealTooLarge {
coin_id,
limit: MAX_REVEAL_EXPANDED_BYTES,
});
}
Ok(())
}
fn program_tree_hash<E>(program: &Program) -> Result<TreeHash, LineageWalkError<E>> {
tree_hash_from_bytes(program.as_ref())
.map_err(|error| LineageWalkError::Malformed(format!("undecodable program: {error}")))
}
#[cfg(test)]
mod tests {
use clvm_traits::ToClvm;
use super::*;
#[test]
fn admitting_the_same_coin_twice_is_refused_as_a_cycle() {
let coin_id = Bytes32::new([0x5A; 32]);
let mut members = BTreeSet::new();
assert_eq!(
admit_member::<ChainSourceError>(&mut members, coin_id),
Ok(())
);
let repeat = admit_member::<ChainSourceError>(&mut members, coin_id)
.expect_err("a repeated coin is a cycle");
assert_eq!(
repeat,
LineageWalkError::Malformed(format!("coin {coin_id} repeats in the lineage (a cycle)"))
);
assert_eq!(members.len(), 1);
}
fn quoting(allocator: &mut Allocator, value: NodePtr) -> NodePtr {
let quote = allocator.one();
allocator
.new_pair(quote, value)
.expect("a two-node pair always allocates")
}
fn condition_list(allocator: &mut Allocator, conditions: Vec<NodePtr>) -> NodePtr {
conditions
.to_clvm(allocator)
.expect("a condition list always allocates")
}
fn continuation_of(
allocator: &mut Allocator,
conditions: Vec<NodePtr>,
) -> Result<Continuation, LineageWalkError<ChainSourceError>> {
let list = condition_list(allocator, conditions);
let puzzle = quoting(allocator, list);
run_for_continuation(allocator, puzzle, NodePtr::NIL)
}
#[test]
fn an_undecodable_create_coin_refuses_rather_than_reading_as_a_melt() {
let allocator = &mut Allocator::new();
let opcode = CREATE_COIN
.to_clvm(allocator)
.expect("the opcode allocates");
let truncated = allocator
.new_pair(opcode, NodePtr::NIL)
.expect("the condition allocates");
let error = continuation_of(allocator, vec![truncated])
.expect_err("an undecodable CREATE_COIN is not a melt");
assert!(
matches!(error, LineageWalkError::Malformed(detail) if detail.contains("CREATE_COIN")),
"the refusal must name the condition it could not read"
);
}
#[test]
fn a_negative_non_melt_amount_refuses() {
let allocator = &mut Allocator::new();
let condition = (CREATE_COIN, (Bytes32::new([0x0C; 32]), (-7i64, ())))
.to_clvm(allocator)
.expect("the condition allocates");
let error = continuation_of(allocator, vec![condition])
.expect_err("a negative non-melt amount is not a melt");
assert!(matches!(error, LineageWalkError::Malformed(_)));
}
#[test]
fn well_formed_conditions_still_decode_as_melt_and_as_recreation() {
let allocator = &mut Allocator::new();
let puzzle_hash = Bytes32::new([0x0D; 32]);
let melt = (CREATE_COIN, (puzzle_hash, (SINGLETON_MELT_AMOUNT, ())))
.to_clvm(allocator)
.expect("the condition allocates");
assert!(matches!(
continuation_of(allocator, vec![melt]),
Ok(Continuation::Ends)
));
let payment = (CREATE_COIN, (puzzle_hash, (2i64, ())))
.to_clvm(allocator)
.expect("the condition allocates");
let recreate = (CREATE_COIN, (puzzle_hash, (3i64, ())))
.to_clvm(allocator)
.expect("the condition allocates");
assert!(matches!(
continuation_of(allocator, vec![payment, recreate]),
Ok(Continuation::Recreates(hash, 3)) if hash == puzzle_hash
));
}
#[test]
fn the_canonical_nil_puzzle_hash_melt_still_decodes_as_a_melt() {
let allocator = &mut Allocator::new();
let canonical_melt = (CREATE_COIN, ((), (SINGLETON_MELT_AMOUNT, ())))
.to_clvm(allocator)
.expect("the condition allocates");
assert!(
matches!(
continuation_of(allocator, vec![canonical_melt]),
Ok(Continuation::Ends)
),
"the canonical `(51 () -113)` melt must end the lineage, not refuse"
);
}
#[test]
fn a_recreation_whose_puzzle_hash_is_not_32_bytes_still_refuses() {
let allocator = &mut Allocator::new();
let short_hash = (CREATE_COIN, ([0x0Eu8; 31], (3i64, ())))
.to_clvm(allocator)
.expect("the condition allocates");
let error = continuation_of(allocator, vec![short_hash])
.expect_err("a recreation with an unreadable puzzle hash is not a melt");
assert!(
matches!(error, LineageWalkError::Malformed(detail) if detail.contains("CREATE_COIN")),
"the refusal must name the condition it could not read"
);
}
#[test]
fn an_even_amount_create_coin_with_an_unreadable_puzzle_hash_still_refuses() {
let allocator = &mut Allocator::new();
let short_hash = (CREATE_COIN, ([0x0Eu8; 31], (2i64, ())))
.to_clvm(allocator)
.expect("the condition allocates");
let error = continuation_of(allocator, vec![short_hash])
.expect_err("an unreadable puzzle hash is refused whatever the amount's parity");
assert!(
matches!(error, LineageWalkError::Malformed(detail) if detail.contains("CREATE_COIN")),
"the refusal must name the condition it could not read"
);
}
#[test]
fn two_odd_amount_children_refuse_rather_than_choosing_one() {
let allocator = &mut Allocator::new();
let first = (CREATE_COIN, (Bytes32::new([0x1A; 32]), (1i64, ())))
.to_clvm(allocator)
.expect("the condition allocates");
let second = (CREATE_COIN, (Bytes32::new([0x1B; 32]), (3i64, ())))
.to_clvm(allocator)
.expect("the condition allocates");
let error = continuation_of(allocator, vec![first, second])
.expect_err("two odd-amount children are ambiguous");
assert_eq!(
error,
LineageWalkError::Malformed(
"a singleton spend emitted more than one odd-amount child".to_string()
)
);
}
#[test]
fn the_expanded_bound_admits_a_node_at_the_limit_and_refuses_one_byte_over() {
let allocator = &mut Allocator::new();
let inner = allocator
.new_pair(NodePtr::NIL, NodePtr::NIL)
.expect("a pair allocates");
let node = allocator
.new_pair(inner, NodePtr::NIL)
.expect("a pair allocates");
assert_eq!(
expanded_hash_input_bytes(allocator, node, 133),
133,
"the cost model must be exactly 65 per pair and 1 + len per atom"
);
assert!(
expanded_hash_input_bytes(allocator, node, 132) > 132,
"one byte over the bound must be refused"
);
}
#[test]
fn a_self_referential_dag_saturates_instead_of_being_counted() {
let allocator = &mut Allocator::new();
let mut node = allocator.new_atom(&[1]).expect("a one-byte atom allocates");
for _ in 0..40 {
node = allocator
.new_pair(node, node)
.expect("a self-cons adds one pair");
}
assert_eq!(
expanded_hash_input_bytes(allocator, node, MAX_REVEAL_EXPANDED_BYTES),
MAX_REVEAL_EXPANDED_BYTES + 1,
"the traversal must stop one byte past the bound, never compute 2^40"
);
}
#[test]
fn a_tree_the_size_of_a_heavy_honest_reveal_is_admitted() {
let allocator = &mut Allocator::new();
let mut node = NodePtr::NIL;
for _ in 0..2_100 {
node = allocator
.new_pair(NodePtr::NIL, node)
.expect("a pair allocates");
}
let cost = expanded_hash_input_bytes(allocator, node, MAX_REVEAL_EXPANDED_BYTES);
assert!(
cost > 100_000,
"the control is only load-bearing if it is genuinely reveal-sized; it cost {cost}"
);
assert!(
cost <= MAX_REVEAL_EXPANDED_BYTES,
"a reveal the size of a heavy honest one must be admitted; it cost {cost}"
);
}
#[test]
fn successor_of_takes_no_allocator_parameter() {
let source = include_str!("walk.rs");
let signature = source
.split_once("fn successor_of<E>(")
.expect("successor_of is declared in this file")
.1
.split_once(") ->")
.expect("its parameter list is closed")
.0;
assert!(
!signature.contains("Allocator"),
"successor_of must create its own allocator per hop, not accept a hoisted one: \
({signature})"
);
}
#[test]
fn a_hop_cap_beyond_the_canonical_bound_is_clamped() {
assert_eq!(WalkBounds::hops(usize::MAX).max_hops(), MAX_LINEAGE_DEPTH);
assert_eq!(WalkBounds::hops(7).max_hops(), 7);
}
}