use dig_merkle::{hydrate, resolve_owner_did, MerkleError};
use crate::chain::ChainSource;
use crate::error::{DigStoreError, DigStoreResult};
use crate::size::SizeBucket;
use crate::types::{Bytes32, DataStore, DidRef, DigDataStoreMetadata, RootHistory};
pub fn get_store_urn(store_id: Bytes32) -> String {
crate::urn::store_urn(store_id)
}
pub fn get_latest_root_urn<C: ChainSource>(chain: &C, store_id: Bytes32) -> DigStoreResult<String> {
let root = get_latest_root(chain, store_id)?;
Ok(crate::urn::capsule_urn(store_id, root))
}
pub fn get_store_did_owner<C: ChainSource>(
chain: &C,
store_id: Bytes32,
) -> DigStoreResult<Option<DidRef>> {
resolve_owner_did(store_id, chain)
.map_err(|error| DigStoreError::Proof(format!("owner-DID discovery: {error}")))
}
pub fn get_store_singleton_tip<C: ChainSource>(
chain: &C,
store_id: Bytes32,
) -> DigStoreResult<DataStore<DigDataStoreMetadata>> {
walk_lineage(chain, store_id)?.tip.ok_or_else(|| {
DigStoreError::Proof(format!("store {store_id} has been melted (no live tip)"))
})
}
pub fn get_root_history<C: ChainSource>(
chain: &C,
store_id: Bytes32,
) -> DigStoreResult<RootHistory> {
Ok(RootHistory {
roots: walk_lineage(chain, store_id)?.roots,
})
}
pub fn get_latest_root<C: ChainSource>(chain: &C, store_id: Bytes32) -> DigStoreResult<Bytes32> {
Ok(get_store_singleton_tip(chain, store_id)?
.info
.metadata
.root_hash)
}
pub fn get_store_label<C: ChainSource>(
chain: &C,
store_id: Bytes32,
) -> DigStoreResult<Option<String>> {
Ok(get_store_singleton_tip(chain, store_id)?
.info
.metadata
.label)
}
pub fn get_store_description<C: ChainSource>(
chain: &C,
store_id: Bytes32,
) -> DigStoreResult<Option<String>> {
Ok(get_store_singleton_tip(chain, store_id)?
.info
.metadata
.description)
}
pub fn get_store_size_bucket<C: ChainSource>(
chain: &C,
store_id: Bytes32,
) -> DigStoreResult<Option<SizeBucket>> {
Ok(get_store_singleton_tip(chain, store_id)?
.info
.metadata
.size_bucket)
}
pub fn get_store_program_hash<C: ChainSource>(
chain: &C,
store_id: Bytes32,
) -> DigStoreResult<Option<Bytes32>> {
Ok(get_store_singleton_tip(chain, store_id)?
.info
.metadata
.program_hash)
}
struct Lineage {
roots: Vec<Bytes32>,
tip: Option<DataStore<DigDataStoreMetadata>>,
}
const MAX_LINEAGE_GENERATIONS: usize = 100_000;
fn walk_lineage<C: ChainSource>(chain: &C, store_id: Bytes32) -> DigStoreResult<Lineage> {
walk_lineage_bounded(chain, store_id, MAX_LINEAGE_GENERATIONS)
}
fn walk_lineage_bounded<C: ChainSource>(
chain: &C,
store_id: Bytes32,
max_generations: usize,
) -> DigStoreResult<Lineage> {
let launcher_spend = read_verified_spend(chain, store_id)?.ok_or_else(|| {
DigStoreError::Proof(format!("store {store_id} launcher not found on chain"))
})?;
let mut current = hydrate(&launcher_spend).map_err(|error| {
DigStoreError::Proof(format!("hydrate launcher of {store_id}: {error}"))
})?;
if current.info.launcher_id != store_id {
return Err(DigStoreError::Proof(format!(
"launcher mismatch: coin_spend({store_id}) hydrated store {}, not the requested store",
current.info.launcher_id
)));
}
let mut roots = Vec::new();
loop {
if roots.len() >= max_generations {
return Err(DigStoreError::Proof(format!(
"store {store_id} lineage exceeds the {max_generations}-generation cap; \
refusing to follow further (possible hostile chain source)"
)));
}
roots.push(current.info.metadata.root_hash);
match read_verified_spend(chain, current.coin.coin_id())? {
None => {
return Ok(Lineage {
roots,
tip: Some(current),
})
}
Some(spend) => match hydrate(&spend) {
Ok(child) => current = child,
Err(MerkleError::MissingLineage) => return Ok(Lineage { roots, tip: None }),
Err(error) => {
return Err(DigStoreError::Proof(format!(
"hydrate generation of {store_id}: {error}"
)))
}
},
}
}
}
fn read_verified_spend<C: ChainSource>(
chain: &C,
coin_id: Bytes32,
) -> DigStoreResult<Option<crate::types::CoinSpend>> {
let spend = chain
.coin_spend(coin_id)
.map_err(|error| DigStoreError::Proof(format!("chain read for {coin_id}: {error}")))?;
if let Some(spend) = &spend {
let returned = spend.coin.coin_id();
if returned != coin_id {
return Err(DigStoreError::Proof(format!(
"chain source returned a spend for coin {returned} when asked for {coin_id}"
)));
}
}
Ok(spend)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lifecycle::{create_store, modify_store, CreateStoreParams, StoreOwner};
use chia_puzzle_types::standard::StandardArgs;
use chia_wallet_sdk::test::Simulator;
use dig_chainsource_interface::MockChainSource;
fn id(b: u8) -> Bytes32 {
Bytes32::new([b; 32])
}
#[test]
fn get_store_urn_is_rootless_and_needs_no_chain() {
assert_eq!(
get_store_urn(id(0xaa)),
format!("urn:dig:chia:{}", "aa".repeat(32))
);
}
fn spend_of(
coin_spends: &[crate::types::CoinSpend],
coin_id: Bytes32,
) -> crate::types::CoinSpend {
coin_spends
.iter()
.find(|s| s.coin.coin_id() == coin_id)
.expect("spend present")
.clone()
}
fn two_generation_store() -> anyhow::Result<(Bytes32, MockChainSource)> {
let mut sim = Simulator::new();
let owner = sim.bls(1_000_000);
let owner_ph: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
let mint = create_store(
owner.coin,
StoreOwner::Standard(owner.pk),
owner_ph,
CreateStoreParams {
root_hash: id(0x5a),
size: SizeBucket::from_exponent(0).unwrap(),
label: None,
description: None,
program_hash: None,
fee: 0,
},
)?;
sim.spend_coins(mint.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
let eve = mint.child.clone().expect("mint yields a child");
let store_id = eve.info.launcher_id;
let modified = modify_store(&eve, StoreOwner::Standard(owner.pk), id(0x77))?;
sim.spend_coins(
modified.coin_spends.clone(),
std::slice::from_ref(&owner.sk),
)?;
let chain = MockChainSource::new()
.with_spend(store_id, spend_of(&mint.coin_spends, store_id))
.with_spend(
eve.coin.coin_id(),
spend_of(&modified.coin_spends, eve.coin.coin_id()),
);
Ok((store_id, chain))
}
#[test]
fn bounded_walk_follows_lineage_under_the_cap() -> anyhow::Result<()> {
let (store_id, chain) = two_generation_store()?;
let lineage = walk_lineage_bounded(&chain, store_id, 10)?;
assert_eq!(lineage.roots, vec![id(0x5a), id(0x77)]);
assert!(lineage.tip.is_some());
Ok(())
}
#[test]
fn bounded_walk_rejects_lineage_over_the_cap() -> anyhow::Result<()> {
let (store_id, chain) = two_generation_store()?;
assert!(
matches!(
walk_lineage_bounded(&chain, store_id, 1),
Err(DigStoreError::Proof(_))
),
"a lineage longer than the cap must fail closed"
);
Ok(())
}
}