use clvm_traits::ToClvm;
use chia_wallet_sdk::driver::{Datastore, DriverError, Puzzle, SpendContext};
use chia_wallet_sdk::prelude::Allocator;
use crate::metadata::DigDataStoreMetadata;
use crate::types::{Bytes32, CoinSpend};
use crate::{MerkleError, MerkleResult};
fn require_reveal_matches_coin(spend: &CoinSpend) -> MerkleResult<()> {
let mut allocator = Allocator::new();
let puzzle_ptr = spend
.puzzle_reveal
.to_clvm(&mut allocator)
.map_err(|error| MerkleError::Parse(format!("puzzle reveal: {error}")))?;
let puzzle = Puzzle::parse(&allocator, puzzle_ptr);
if Bytes32::from(puzzle.curried_puzzle_hash()) != spend.coin.puzzle_hash {
return Err(MerkleError::Chain(format!(
"the puzzle reveal for coin {} does not hash to the coin's puzzle hash — the source \
returned a puzzle the coin never committed to",
spend.coin.coin_id()
)));
}
Ok(())
}
pub fn hydrate(parent_spend: &CoinSpend) -> MerkleResult<Datastore<DigDataStoreMetadata>> {
require_reveal_matches_coin(parent_spend)?;
let mut ctx = SpendContext::new();
match Datastore::<DigDataStoreMetadata>::from_spend(&mut ctx, parent_spend, &[]) {
Ok(Some(store)) => Ok(store),
Ok(None) => Err(MerkleError::NotDataStore),
Err(DriverError::MissingChild) => Err(MerkleError::MissingLineage),
Err(DriverError::MissingHint | DriverError::MissingMemo) => Err(MerkleError::MissingHint),
Err(other) => Err(MerkleError::Driver(other)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::melt::melt;
use crate::metadata::DigDataStoreMetadata;
use crate::mint::mint_datastore;
use crate::types::{Bytes32, Owner};
use chia_protocol::Bytes;
use chia_puzzle_types::singleton::LauncherSolution;
use chia_puzzle_types::standard::StandardArgs;
use chia_wallet_sdk::driver::{DelegatedPuzzle, DlLauncherKvList, StandardLayer};
use chia_wallet_sdk::test::Simulator;
#[test]
fn hydrate_reconstructs_a_spendable_store() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let owner = sim.bls(1_000_000);
let owner_ph: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
let root = Bytes32::new([0x5a; 32]);
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 launcher_spend = built
.coin_spends
.iter()
.find(|s| s.coin.coin_id() == minted.info.launcher_id)
.expect("launcher-coin spend present");
let store = hydrate(launcher_spend)?;
assert_eq!(store.info.metadata.root_hash, root);
assert_eq!(store.info.launcher_id, minted.info.launcher_id);
let updated = crate::update::update_root(
&store,
Owner::Standard(owner.pk),
DigDataStoreMetadata {
root_hash: Bytes32::new([0x77; 32]),
..Default::default()
},
)?;
sim.spend_coins(updated.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
Ok(())
}
#[test]
fn hydrate_refuses_a_reveal_the_coin_never_committed_to() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let mut settled_store_with_recreation_spend =
|root: Bytes32| -> anyhow::Result<(CoinSpend, Bytes32)> {
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 store = built.child.expect("mint yields a child");
let launcher_id = store.info.launcher_id;
let updated = crate::update::update_root(
&store,
Owner::Standard(owner.pk),
DigDataStoreMetadata {
root_hash: root,
..Default::default()
},
)?;
sim.spend_coins(updated.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
Ok((updated.coin_spends[0].clone(), launcher_id))
};
let victim_root = Bytes32::new([0x11; 32]);
let attacker_root = Bytes32::new([0xee; 32]);
let (victim_spend, victim_launcher_id) = settled_store_with_recreation_spend(victim_root)?;
let (attacker_spend, _) = settled_store_with_recreation_spend(attacker_root)?;
assert_ne!(
victim_spend.coin.puzzle_hash, attacker_spend.coin.puzzle_hash,
"fixture precondition: the forged reveal must differ from the coin's committed puzzle"
);
let forged = CoinSpend::new(
victim_spend.coin,
attacker_spend.puzzle_reveal.clone(),
attacker_spend.solution.clone(),
);
match hydrate(&forged) {
Err(MerkleError::Chain(message)) => {
assert!(
message.contains("does not hash to"),
"refusal must name the unbound reveal, got: {message}"
);
}
other => panic!(
"a reveal the coin never committed to must be REFUSED, not parsed; got {:?}",
other.map(|store| (store.info.launcher_id, store.info.metadata.root_hash))
),
}
let honest = hydrate(&victim_spend)?;
assert_eq!(honest.info.launcher_id, victim_launcher_id);
assert_eq!(honest.info.metadata.root_hash, victim_root);
Ok(())
}
#[test]
fn hydrate_fails_closed_on_a_non_datastore_spend() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let mut ctx = SpendContext::new();
let alice = sim.bls(1);
let alice_p2 = StandardLayer::new(alice.pk);
let memos = ctx.hint(alice.puzzle_hash)?;
alice_p2.spend(
&mut ctx,
alice.coin,
chia_wallet_sdk::types::Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
)?;
let spends = ctx.take();
let standard_spend = spends
.iter()
.find(|s| s.coin.coin_id() == alice.coin.coin_id())
.expect("standard spend present");
assert!(
matches!(hydrate(standard_spend), Err(MerkleError::NotDataStore)),
"a plain standard spend is not a DataLayer coin"
);
Ok(())
}
#[test]
fn hydrate_fails_closed_on_a_terminal_melt() -> anyhow::Result<()> {
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),
Bytes32::new([0x5a; 32]),
None,
None,
None,
None,
None,
owner_ph,
vec![],
0,
)?;
sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
let store = built.child.expect("mint yields a child");
let melted = melt(&store, Owner::Standard(owner.pk))?;
sim.spend_coins(melted.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
let melt_spend = &melted.coin_spends[0];
assert!(
matches!(hydrate(melt_spend), Err(MerkleError::MissingLineage)),
"a terminal melt has no child to hydrate"
);
Ok(())
}
#[test]
fn hydrate_fails_closed_on_a_missing_oracle_fee_hint() -> anyhow::Result<()> {
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),
Bytes32::new([0x5a; 32]),
None,
None,
None,
None,
None,
owner_ph,
vec![],
0,
)?;
let minted = built.child.expect("mint yields a child");
let launcher_spend = built
.coin_spends
.iter()
.find(|s| s.coin.coin_id() == minted.info.launcher_id)
.expect("launcher-coin spend present");
let oracle_ph = Bytes32::new([0x33; 32]);
let kv = DlLauncherKvList {
metadata: DigDataStoreMetadata {
root_hash: Bytes32::new([0x5a; 32]),
..Default::default()
},
state_layer_inner_puzzle_hash: owner_ph,
memos: vec![
Bytes::from(owner_ph.to_vec()),
Bytes::new(vec![3u8]), Bytes::from(oracle_ph.to_vec()),
],
};
let solution = LauncherSolution {
singleton_puzzle_hash: Bytes32::new([0x44; 32]),
amount: 1,
key_value_list: kv,
};
let mut ctx = SpendContext::new();
let solution_ptr = ctx.alloc(&solution)?;
let malformed_solution = ctx.serialize(&solution_ptr)?;
let crafted = CoinSpend::new(
launcher_spend.coin,
launcher_spend.puzzle_reveal.clone(),
malformed_solution,
);
assert!(
matches!(hydrate(&crafted), Err(MerkleError::MissingHint)),
"a launcher hint declaring an oracle puzzle without its fee fails closed to MissingHint"
);
Ok(())
}
#[test]
fn hydrate_accepts_an_empty_oracle_fee_memo_as_a_zero_fee() -> anyhow::Result<()> {
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),
Bytes32::new([0x5a; 32]),
None,
None,
None,
None,
None,
owner_ph,
vec![],
0,
)?;
let minted = built.child.expect("mint yields a child");
let launcher_spend = built
.coin_spends
.iter()
.find(|s| s.coin.coin_id() == minted.info.launcher_id)
.expect("launcher-coin spend present");
let oracle_ph = Bytes32::new([0x33; 32]);
let kv = DlLauncherKvList {
metadata: DigDataStoreMetadata {
root_hash: Bytes32::new([0x5a; 32]),
..Default::default()
},
state_layer_inner_puzzle_hash: owner_ph,
memos: vec![
Bytes::from(owner_ph.to_vec()),
Bytes::new(vec![3u8]), Bytes::from(oracle_ph.to_vec()),
Bytes::new(Vec::new()), ],
};
let solution = LauncherSolution {
singleton_puzzle_hash: Bytes32::new([0x44; 32]),
amount: 1,
key_value_list: kv,
};
let mut ctx = SpendContext::new();
let solution_ptr = ctx.alloc(&solution)?;
let malformed_solution = ctx.serialize(&solution_ptr)?;
let crafted = CoinSpend::new(
launcher_spend.coin,
launcher_spend.puzzle_reveal.clone(),
malformed_solution,
);
let store = hydrate(&crafted).expect("an empty fee memo decodes rather than panicking");
assert!(
store
.info
.delegated_puzzles
.iter()
.any(|p| matches!(p, DelegatedPuzzle::Oracle(ph, fee) if *ph == oracle_ph && *fee == 0)),
"the empty fee memo decodes to an oracle puzzle carrying a zero fee, not to some other delegated-puzzle shape"
);
Ok(())
}
}