use chia_protocol::{Bytes32, Coin, CoinSpend, Program};
use chia_puzzle_types::singleton::SingletonArgs;
use chia_puzzle_types::Proof;
use chia_puzzles::SINGLETON_LAUNCHER_HASH;
use chia_sdk_utils::Address;
use chia_wallet_sdk::driver::{Did, DidInfo, Layer, Puzzle, SingletonLayer};
use chia_wallet_sdk::prelude::{Allocator, NodePtr};
use chia_wallet_sdk::types::{run_puzzle, Condition};
use clvm_traits::{FromClvm, ToClvm};
use clvm_utils::TreeHash;
use crate::error::{DidError, DidResult};
pub use dig_chainsource_interface::{ChainSource, SingletonLineage};
pub const MAX_LINEAGE_DEPTH: usize = 100_000;
#[derive(Debug, Clone)]
pub struct DidTip {
pub coin: Coin,
pub info: DidInfo,
pub proof: Proof,
}
impl DidTip {
pub fn did(&self) -> Did {
Did::new(self.coin, self.proof, self.info)
}
}
#[derive(Debug)]
pub(crate) struct AuthenticatedLineage {
pub(crate) launcher_id: Bytes32,
pub(crate) launcher_coin: Coin,
pub(crate) trail: Vec<Bytes32>,
}
pub(crate) fn authenticate_singleton<S: ChainSource>(
coin_id: Bytes32,
source: &S,
) -> DidResult<AuthenticatedLineage> {
authenticate_singleton_bounded(coin_id, source, MAX_LINEAGE_DEPTH)
}
pub(crate) fn authenticate_singleton_bounded<S: ChainSource>(
coin_id: Bytes32,
source: &S,
max_depth: usize,
) -> DidResult<AuthenticatedLineage> {
let mut allocator = Allocator::new();
let mut trail = vec![coin_id];
let mut current = coin_id;
let mut expected_launcher: Option<Bytes32> = None;
for _hop in 0..max_depth {
let spend = source
.parent_spend(current)
.map_err(chain_error)?
.ok_or(DidError::NotASingleton)?;
let parent = spend.coin;
let (parent_puzzle, parent_solution) = parse_spend(&mut allocator, &spend)?;
if parent.puzzle_hash == SINGLETON_LAUNCHER_HASH.into() {
let launcher_id = parent.coin_id();
if let Some(expected) = expected_launcher {
require(expected == launcher_id)?;
}
require(launcher_creates(
&mut allocator,
parent,
parent_puzzle,
parent_solution,
current,
)?)?;
return Ok(AuthenticatedLineage {
launcher_id,
launcher_coin: parent,
trail,
});
}
let layer = SingletonLayer::<Puzzle>::parse_puzzle(&allocator, parent_puzzle)
.map_err(DidError::Driver)?
.ok_or(DidError::NotASingleton)?;
if let Some(expected) = expected_launcher {
require(expected == layer.launcher_id)?;
}
expected_launcher = Some(layer.launcher_id);
let successor = singleton_successor(&mut allocator, parent, &layer, parent_solution)?
.ok_or(DidError::NotASingleton)?;
require(successor.coin_id() == current)?;
trail.push(parent.coin_id());
current = parent.coin_id();
}
Err(DidError::LineageTooDeep)
}
fn singleton_successor(
allocator: &mut Allocator,
parent: Coin,
layer: &SingletonLayer<Puzzle>,
parent_solution: NodePtr,
) -> DidResult<Option<Coin>> {
let solution = SingletonLayer::<Puzzle>::parse_solution(allocator, parent_solution)
.map_err(DidError::Driver)?;
let output = run_puzzle(allocator, layer.inner_puzzle.ptr(), solution.inner_solution)
.map_err(|error| DidError::Parse(error.to_string()))?;
let conditions = Vec::<Condition>::from_clvm(allocator, output)
.map_err(|e| DidError::Parse(e.to_string()))?;
let Some(create_coin) = conditions
.into_iter()
.filter_map(Condition::into_create_coin)
.find(|create_coin| create_coin.amount % 2 == 1)
else {
return Ok(None);
};
let inner_hash: TreeHash = create_coin.puzzle_hash.into();
let full_puzzle_hash = SingletonArgs::curry_tree_hash(layer.launcher_id, inner_hash);
Ok(Some(Coin::new(
parent.coin_id(),
full_puzzle_hash.into(),
create_coin.amount,
)))
}
fn launcher_creates(
allocator: &mut Allocator,
launcher: Coin,
launcher_puzzle: Puzzle,
launcher_solution: NodePtr,
eve_id: Bytes32,
) -> DidResult<bool> {
let output = run_puzzle(allocator, launcher_puzzle.ptr(), launcher_solution)
.map_err(|error| DidError::Parse(error.to_string()))?;
let conditions = Vec::<Condition>::from_clvm(allocator, output)
.map_err(|e| DidError::Parse(e.to_string()))?;
Ok(conditions
.into_iter()
.filter_map(Condition::into_create_coin)
.any(|create_coin| {
Coin::new(
launcher.coin_id(),
create_coin.puzzle_hash,
create_coin.amount,
)
.coin_id()
== eve_id
}))
}
pub fn walk_did_lineage_to_tip<S: ChainSource>(
source: &S,
launcher_id: Bytes32,
) -> DidResult<Option<DidTip>> {
let Some(lineage) = source
.resolve_singleton_lineage(launcher_id)
.map_err(chain_error)?
else {
return Ok(None);
};
let tip_id = lineage.tip();
let spend = source
.parent_spend(tip_id)
.map_err(chain_error)?
.ok_or(DidError::NoIdentitySingleton)?;
let parent = spend.coin;
let mut allocator = Allocator::new();
let (parent_puzzle, parent_solution) = parse_spend(&mut allocator, &spend)?;
let layer = SingletonLayer::<Puzzle>::parse_puzzle(&allocator, parent_puzzle)
.map_err(DidError::Driver)?
.ok_or(DidError::NotDid)?;
let tip_coin = singleton_successor(&mut allocator, parent, &layer, parent_solution)?
.filter(|coin| coin.coin_id() == tip_id)
.ok_or(DidError::NotDid)?;
let did = Did::parse_child(
&mut allocator,
parent,
parent_puzzle,
parent_solution,
tip_coin,
)
.map_err(DidError::Driver)?
.ok_or(DidError::NotDid)?;
Ok(Some(DidTip {
coin: did.coin,
info: did.info,
proof: did.proof,
}))
}
pub fn resolve_xch_address<S: ChainSource>(
launcher_id: Bytes32,
prefix: &str,
source: &S,
) -> DidResult<Option<Address>> {
let Some(tip) = walk_did_lineage_to_tip(source, launcher_id)? else {
return Ok(None);
};
let authenticated = authenticate_singleton(tip.coin.coin_id(), source)?;
if authenticated.launcher_id != launcher_id {
return Err(DidError::LauncherMismatch);
}
let address = Address::new(tip.info.p2_puzzle_hash, prefix.to_string());
address
.encode()
.map_err(|error| DidError::Parse(error.to_string()))?;
Ok(Some(address))
}
pub fn resolve_xch_address_from_did_string<S: ChainSource>(
did: &str,
prefix: &str,
source: &S,
) -> DidResult<Option<Address>> {
let launcher_id = crate::launcher_id_from_did_string(did)?;
resolve_xch_address(launcher_id, prefix, source)
}
fn parse_spend(allocator: &mut Allocator, spend: &CoinSpend) -> DidResult<(Puzzle, NodePtr)> {
let puzzle_ptr = alloc_program(allocator, &spend.puzzle_reveal)?;
let solution_ptr = alloc_program(allocator, &spend.solution)?;
Ok((Puzzle::parse(allocator, puzzle_ptr), solution_ptr))
}
fn alloc_program(allocator: &mut Allocator, program: &Program) -> DidResult<NodePtr> {
program
.to_clvm(allocator)
.map_err(|error| DidError::Parse(error.to_string()))
}
fn require(condition: bool) -> DidResult<()> {
condition.then_some(()).ok_or(DidError::NotASingleton)
}
fn chain_error<E: core::fmt::Display>(error: E) -> DidError {
DidError::Chain(error.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use chia_puzzle_types::Memos;
use chia_wallet_sdk::driver::{SingletonInfo, SpendContext, StandardLayer};
use chia_wallet_sdk::test::Simulator;
use chia_wallet_sdk::types::Conditions;
use dig_chainsource_interface::{CoinRecord, MockChainSource};
use crate::create::create_simple_did;
use crate::did_string::did_string_from_launcher_id;
use crate::types::Owner;
const XCH: &str = "xch";
struct SettledDid {
did: Did,
launcher_id: Bytes32,
}
struct SimSource<'a> {
sim: &'a Simulator,
lineages: HashMap<Bytes32, SingletonLineage>,
}
impl ChainSource for SimSource<'_> {
type Error = String;
fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error> {
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(Vec::new())
}
fn coin_records_by_parent(
&self,
_parent_coin_id: Bytes32,
) -> Result<Vec<CoinRecord>, Self::Error> {
Ok(Vec::new())
}
fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error> {
let Some(state) = self.sim.coin_state(coin_id) else {
return Ok(None);
};
let (Some(reveal), Some(solution)) =
(self.sim.puzzle_reveal(coin_id), self.sim.solution(coin_id))
else {
return Ok(None);
};
Ok(Some(CoinSpend::new(state.coin, reveal, solution)))
}
fn resolve_singleton_lineage(
&self,
launcher_id: Bytes32,
) -> Result<Option<SingletonLineage>, Self::Error> {
Ok(self.lineages.get(&launcher_id).cloned())
}
fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
Ok(None)
}
fn block_timestamp(&self, _height: u32) -> Result<Option<u64>, Self::Error> {
Ok(None)
}
}
fn settle_did(sim: &mut Simulator, ctx: &mut SpendContext) -> anyhow::Result<SettledDid> {
let owner = sim.bls(1);
let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
let did = spend.child.expect("create returns a child DID");
sim.spend_coins(spend.coin_spends, std::slice::from_ref(&owner.sk))?;
let launcher_id = did.info.launcher_id();
Ok(SettledDid { did, launcher_id })
}
fn did_lineage(did: &Did) -> SingletonLineage {
SingletonLineage::new(
did.coin.coin_id(),
[
did.info.launcher_id(),
did.coin.parent_coin_info,
did.coin.coin_id(),
],
)
}
fn honest_source<'a>(sim: &'a Simulator, did: &Did) -> SimSource<'a> {
SimSource {
sim,
lineages: HashMap::from([(did.info.launcher_id(), did_lineage(did))]),
}
}
#[test]
fn resolve_happy_path_matches_the_owner_address() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let SettledDid { did, launcher_id } = settle_did(&mut sim, ctx)?;
let source = honest_source(&sim, &did);
let address = resolve_xch_address(launcher_id, XCH, &source)?
.expect("a launched, authenticated DID resolves to an address");
assert_eq!(address.puzzle_hash, did.info.p2_puzzle_hash);
assert_eq!(address.prefix, XCH);
let expected = Address::new(did.info.p2_puzzle_hash, XCH.to_string()).encode()?;
assert_eq!(address.encode()?, expected);
Ok(())
}
#[test]
fn resolved_address_roundtrips_through_decode() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let SettledDid { did, launcher_id } = settle_did(&mut sim, ctx)?;
let source = honest_source(&sim, &did);
let address = resolve_xch_address(launcher_id, XCH, &source)?.expect("resolves");
let decoded = Address::decode(&address.encode()?)?;
assert_eq!(decoded.puzzle_hash, did.info.p2_puzzle_hash);
assert_eq!(decoded.prefix, XCH);
Ok(())
}
#[test]
fn resolve_rejects_an_echoed_different_dids_tip() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let victim = settle_did(&mut sim, ctx)?;
let attacker = settle_did(&mut sim, ctx)?;
let source = SimSource {
sim: &sim,
lineages: HashMap::from([(victim.launcher_id, did_lineage(&attacker.did))]),
};
let result = resolve_xch_address(victim.launcher_id, XCH, &source);
assert!(matches!(result, Err(DidError::LauncherMismatch)));
let attacker_address =
Address::new(attacker.did.info.p2_puzzle_hash, XCH.to_string()).encode()?;
assert!(
!matches!(result, Ok(Some(address)) if address.encode().ok() == Some(attacker_address))
);
Ok(())
}
#[test]
fn resolve_rejects_a_spoofed_curry_singleton() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let victim = settle_did(&mut sim, ctx)?;
let alice = sim.bls(1);
let alice_p2 = StandardLayer::new(alice.pk);
let fake_singleton_puzzle_hash: Bytes32 =
SingletonArgs::curry_tree_hash(victim.launcher_id, alice.puzzle_hash.into()).into();
alice_p2.spend(
ctx,
alice.coin,
Conditions::new().create_coin(fake_singleton_puzzle_hash, 1, Memos::None),
)?;
sim.spend_coins(ctx.take(), std::slice::from_ref(&alice.sk))?;
let fake_coin = Coin::new(alice.coin.coin_id(), fake_singleton_puzzle_hash, 1);
let source = SimSource {
sim: &sim,
lineages: HashMap::from([(
victim.launcher_id,
SingletonLineage::single(fake_coin.coin_id()),
)]),
};
let result = resolve_xch_address(victim.launcher_id, XCH, &source);
assert!(matches!(
result,
Err(DidError::NotDid | DidError::NotASingleton)
));
Ok(())
}
#[test]
fn resolve_returns_none_for_unlaunched_or_melted() -> anyhow::Result<()> {
let source = MockChainSource::new();
let launcher_id: Bytes32 =
clvm_utils::tree_hash_atom(b"dig-did::resolve::unlaunched-launcher").into();
let resolved = resolve_xch_address(launcher_id, XCH, &source)?;
assert!(resolved.is_none());
Ok(())
}
#[test]
fn resolve_from_did_string_rejects_malformed() {
let source = MockChainSource::new();
let error = resolve_xch_address_from_did_string("not-a-valid-did", XCH, &source)
.expect_err("a malformed did:chia string must fail closed");
assert!(matches!(error, DidError::InvalidDidString(_)));
}
#[test]
fn resolve_from_did_string_happy_path_matches_direct_resolution() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let ctx = &mut SpendContext::new();
let SettledDid { did, launcher_id } = settle_did(&mut sim, ctx)?;
let source = honest_source(&sim, &did);
let did_string = did_string_from_launcher_id(launcher_id);
let via_string = resolve_xch_address_from_did_string(&did_string, XCH, &source)?
.expect("resolves via the did:chia string");
let via_launcher = resolve_xch_address(launcher_id, XCH, &source)?.expect("resolves");
assert_eq!(via_string.encode()?, via_launcher.encode()?);
Ok(())
}
}