diff --git a/Cargo.lock b/Cargo.lock index 2e6c76ff..be19020f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -539,6 +539,7 @@ dependencies = [ "hex", "hex-literal", "once_cell", + "rstest", ] [[package]] diff --git a/crates/chia-sdk-driver/src/driver_error.rs b/crates/chia-sdk-driver/src/driver_error.rs index bcdcbe85..90c91c79 100644 --- a/crates/chia-sdk-driver/src/driver_error.rs +++ b/crates/chia-sdk-driver/src/driver_error.rs @@ -47,4 +47,7 @@ pub enum DriverError { #[error("custom driver error: {0}")] Custom(String), + + #[error("invalid merkle proof")] + InvalidMerkleProof, } diff --git a/crates/chia-sdk-driver/src/layers.rs b/crates/chia-sdk-driver/src/layers.rs index af737f2e..6115ef0a 100644 --- a/crates/chia-sdk-driver/src/layers.rs +++ b/crates/chia-sdk-driver/src/layers.rs @@ -1,24 +1,28 @@ +mod augmented_condition_layer; mod cat_layer; mod did_layer; mod nft_ownership_layer; mod nft_state_layer; +mod p2_curried_layer; mod p2_delegated_conditions_layer; mod p2_delegated_singleton_layer; -mod p2_one_of_many; -mod p2_singleton; +mod p2_one_of_many_layer; +mod p2_singleton_layer; mod royalty_transfer_layer; mod settlement_layer; mod singleton_layer; mod standard_layer; +pub use augmented_condition_layer::*; pub use cat_layer::*; pub use did_layer::*; pub use nft_ownership_layer::*; pub use nft_state_layer::*; +pub use p2_curried_layer::*; pub use p2_delegated_conditions_layer::*; pub use p2_delegated_singleton_layer::*; -pub use p2_one_of_many::*; -pub use p2_singleton::*; +pub use p2_one_of_many_layer::*; +pub use p2_singleton_layer::*; pub use royalty_transfer_layer::*; pub use settlement_layer::*; pub use singleton_layer::*; diff --git a/crates/chia-sdk-driver/src/layers/augmented_condition_layer.rs b/crates/chia-sdk-driver/src/layers/augmented_condition_layer.rs new file mode 100644 index 00000000..901c591f --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/augmented_condition_layer.rs @@ -0,0 +1,68 @@ +use chia_sdk_types::{ + AugmentedConditionArgs, AugmentedConditionSolution, Condition, AUGMENTED_CONDITION_PUZZLE_HASH, +}; +use clvm_traits::{FromClvm, ToClvm}; +use clvmr::{Allocator, NodePtr}; + +use crate::{DriverError, Layer, Puzzle, SpendContext}; + +/// The augmented condition [`Layer`] allows for adding a condition to a puzzle's output. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AugmentedConditionLayer { + pub condition: Condition, + pub inner_puzzle: I, +} + +impl Layer for AugmentedConditionLayer +where + T: ToClvm + FromClvm + Clone, + I: Layer, +{ + type Solution = AugmentedConditionSolution; + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != AUGMENTED_CONDITION_PUZZLE_HASH { + return Ok(None); + } + + let args = AugmentedConditionArgs::::from_clvm(allocator, puzzle.args)?; + let Some(inner_layer) = I::parse_puzzle(allocator, args.inner_puzzle)? else { + return Ok(None); + }; + + Ok(Some(Self { + condition: args.condition, + inner_puzzle: inner_layer, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + Ok(AugmentedConditionSolution::::from_clvm( + allocator, solution, + )?) + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let inner_puzzle = self.inner_puzzle.construct_puzzle(ctx)?; + let curried = ctx.curry(AugmentedConditionArgs { + condition: self.condition.clone(), + inner_puzzle, + })?; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + ctx.alloc(&solution) + } +} diff --git a/crates/chia-sdk-driver/src/layers/p2_curried_layer.rs b/crates/chia-sdk-driver/src/layers/p2_curried_layer.rs new file mode 100644 index 00000000..4776c4af --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/p2_curried_layer.rs @@ -0,0 +1,56 @@ +use chia_protocol::Bytes32; +use chia_sdk_types::{P2CurriedArgs, P2CurriedSolution, P2_CURRIED_PUZZLE_HASH}; +use clvm_traits::FromClvm; +use clvmr::{Allocator, NodePtr}; + +use crate::{DriverError, Layer, Puzzle, SpendContext}; + +/// The p2 curried [`Layer`] allows for . +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct P2CurriedLayer { + pub puzzle_hash: Bytes32, +} + +impl Layer for P2CurriedLayer { + type Solution = P2CurriedSolution; + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != P2_CURRIED_PUZZLE_HASH { + return Ok(None); + } + + let args = P2CurriedArgs::from_clvm(allocator, puzzle.args)?; + + Ok(Some(Self { + puzzle_hash: args.puzzle_hash, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + Ok(P2CurriedSolution::::from_clvm( + allocator, solution, + )?) + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = ctx.curry(P2CurriedArgs { + puzzle_hash: self.puzzle_hash, + })?; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + ctx.alloc(&solution) + } +} diff --git a/crates/chia-sdk-driver/src/layers/p2_one_of_many.rs b/crates/chia-sdk-driver/src/layers/p2_one_of_many_layer.rs similarity index 77% rename from crates/chia-sdk-driver/src/layers/p2_one_of_many.rs rename to crates/chia-sdk-driver/src/layers/p2_one_of_many_layer.rs index 16f6bdaf..c065ab24 100644 --- a/crates/chia-sdk-driver/src/layers/p2_one_of_many.rs +++ b/crates/chia-sdk-driver/src/layers/p2_one_of_many_layer.rs @@ -1,18 +1,25 @@ use chia_protocol::Bytes32; use chia_sdk_types::{P2OneOfManyArgs, P2OneOfManySolution, P2_ONE_OF_MANY_PUZZLE_HASH}; use clvm_traits::FromClvm; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; use clvmr::{Allocator, NodePtr}; use crate::{DriverError, Layer, Puzzle, SpendContext}; /// The p2 1 of n [`Layer`] allows for picking from several delegated puzzles at runtime without revealing up front. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct P2OneOfMany { +pub struct P2OneOfManyLayer { /// The merkle root used to lookup the delegated puzzle as part of the solution. pub merkle_root: Bytes32, } -impl Layer for P2OneOfMany { +impl P2OneOfManyLayer { + pub fn new(merkle_root: Bytes32) -> Self { + Self { merkle_root } + } +} + +impl Layer for P2OneOfManyLayer { type Solution = P2OneOfManySolution; fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { @@ -54,3 +61,13 @@ impl Layer for P2OneOfMany { ctx.alloc(&solution) } } + +impl ToTreeHash for P2OneOfManyLayer { + fn tree_hash(&self) -> TreeHash { + CurriedProgram { + program: P2_ONE_OF_MANY_PUZZLE_HASH, + args: P2OneOfManyArgs::new(self.merkle_root), + } + .tree_hash() + } +} diff --git a/crates/chia-sdk-driver/src/layers/p2_singleton.rs b/crates/chia-sdk-driver/src/layers/p2_singleton_layer.rs similarity index 96% rename from crates/chia-sdk-driver/src/layers/p2_singleton.rs rename to crates/chia-sdk-driver/src/layers/p2_singleton_layer.rs index b91696f2..13c91895 100644 --- a/crates/chia-sdk-driver/src/layers/p2_singleton.rs +++ b/crates/chia-sdk-driver/src/layers/p2_singleton_layer.rs @@ -10,11 +10,11 @@ use crate::{DriverError, Layer, Puzzle, Spend, SpendContext}; /// The p2 singleton [`Layer`] allows for requiring that a /// singleton be spent alongside this coin to authorize it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct P2Singleton { +pub struct P2SingletonLayer { pub launcher_id: Bytes32, } -impl P2Singleton { +impl P2SingletonLayer { pub fn new(launcher_id: Bytes32) -> Self { Self { launcher_id } } @@ -55,7 +55,7 @@ impl P2Singleton { } } -impl Layer for P2Singleton { +impl Layer for P2SingletonLayer { type Solution = P2SingletonSolution; fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { @@ -100,7 +100,7 @@ impl Layer for P2Singleton { } } -impl ToTreeHash for P2Singleton { +impl ToTreeHash for P2SingletonLayer { fn tree_hash(&self) -> TreeHash { P2SingletonArgs::curry_tree_hash(self.launcher_id) } @@ -129,7 +129,7 @@ mod tests { let launcher_id = launcher.coin().coin_id(); let (create_singleton, singleton) = launcher.spend(ctx, puzzle_hash, ())?; - let p2_singleton = P2Singleton::new(launcher_id); + let p2_singleton = P2SingletonLayer::new(launcher_id); let p2_singleton_hash = p2_singleton.tree_hash().into(); p2.spend( diff --git a/crates/chia-sdk-driver/src/lib.rs b/crates/chia-sdk-driver/src/lib.rs index 6a360974..0cb7ec5f 100644 --- a/crates/chia-sdk-driver/src/lib.rs +++ b/crates/chia-sdk-driver/src/lib.rs @@ -4,7 +4,6 @@ mod driver_error; mod hashed_ptr; mod layer; mod layers; -mod merkle_tree; mod primitives; mod puzzle; mod spend; @@ -15,7 +14,6 @@ pub use driver_error::*; pub use hashed_ptr::*; pub use layer::*; pub use layers::*; -pub use merkle_tree::*; pub use primitives::*; pub use puzzle::*; pub use spend::*; diff --git a/crates/chia-sdk-driver/src/primitives.rs b/crates/chia-sdk-driver/src/primitives.rs index 84593409..481857b5 100644 --- a/crates/chia-sdk-driver/src/primitives.rs +++ b/crates/chia-sdk-driver/src/primitives.rs @@ -1,10 +1,12 @@ mod cat; +mod clawback; mod did; mod intermediate_launcher; mod launcher; mod nft; pub use cat::*; +pub use clawback::*; pub use did::*; pub use intermediate_launcher::*; pub use launcher::*; diff --git a/crates/chia-sdk-driver/src/primitives/clawback.rs b/crates/chia-sdk-driver/src/primitives/clawback.rs new file mode 100644 index 00000000..d12190cb --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/clawback.rs @@ -0,0 +1,183 @@ +use std::num::NonZeroU64; + +use chia_protocol::Bytes32; +use chia_sdk_types::{ + AugmentedConditionArgs, AugmentedConditionSolution, Condition, MerkleTree, P2CurriedArgs, + P2CurriedSolution, P2OneOfManySolution, AUGMENTED_CONDITION_PUZZLE_HASH, + P2_CURRIED_PUZZLE_HASH, +}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::NodePtr; + +use crate::{DriverError, Layer, P2OneOfManyLayer, Spend, SpendContext}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Clawback { + /// The number of seconds until this clawback can be claimed by the recipient. + pub timelock: NonZeroU64, + /// The original sender of the coin, who can claw it back until claimed. + pub sender_puzzle_hash: Bytes32, + /// The intended recipient who can claim after the timelock period is up. + pub recipient_puzzle_hash: Bytes32, +} + +impl Clawback { + pub fn claim_path_puzzle_hash(&self) -> TreeHash { + CurriedProgram { + program: AUGMENTED_CONDITION_PUZZLE_HASH, + args: AugmentedConditionArgs::new( + Condition::::assert_seconds_relative(self.timelock.into()), + TreeHash::from(self.recipient_puzzle_hash), + ), + } + .tree_hash() + } + + pub fn claim_path_puzzle( + &self, + ctx: &mut SpendContext, + inner_puzzle: NodePtr, + ) -> Result { + ctx.curry(AugmentedConditionArgs::new( + Condition::::assert_seconds_relative(self.timelock.into()), + inner_puzzle, + )) + } + + pub fn clawback_path_puzzle_hash(&self) -> TreeHash { + CurriedProgram { + program: P2_CURRIED_PUZZLE_HASH, + args: P2CurriedArgs::new(self.sender_puzzle_hash), + } + .tree_hash() + } + + pub fn clawback_path_puzzle(&self, ctx: &mut SpendContext) -> Result { + ctx.curry(P2CurriedArgs::new(self.sender_puzzle_hash)) + } + + pub fn merkle_tree(&self) -> MerkleTree { + MerkleTree::new(&[ + self.claim_path_puzzle_hash().into(), + self.clawback_path_puzzle_hash().into(), + ]) + } + + pub fn to_layer(&self) -> P2OneOfManyLayer { + P2OneOfManyLayer::new(self.merkle_tree().root()) + } + + pub fn claim_spend(&self, ctx: &mut SpendContext, spend: Spend) -> Result { + let merkle_tree = self.merkle_tree(); + + let puzzle = self.claim_path_puzzle(ctx, spend.puzzle)?; + let solution = ctx.alloc(&AugmentedConditionSolution::new(spend.solution))?; + + let proof = merkle_tree + .proof(ctx.tree_hash(puzzle).into()) + .ok_or(DriverError::InvalidMerkleProof)?; + + P2OneOfManyLayer::new(merkle_tree.root()) + .construct_spend(ctx, P2OneOfManySolution::new(proof, puzzle, solution)) + } + + pub fn clawback_spend( + &self, + ctx: &mut SpendContext, + spend: Spend, + ) -> Result { + let merkle_tree = self.merkle_tree(); + + let puzzle = self.clawback_path_puzzle(ctx)?; + let solution = ctx.alloc(&P2CurriedSolution::new(spend.puzzle, spend.solution))?; + + let proof = merkle_tree + .proof(ctx.tree_hash(puzzle).into()) + .ok_or(DriverError::InvalidMerkleProof)?; + + P2OneOfManyLayer::new(merkle_tree.root()) + .construct_spend(ctx, P2OneOfManySolution::new(proof, puzzle, solution)) + } +} + +#[cfg(test)] +mod tests { + use chia_protocol::Coin; + use chia_sdk_test::Simulator; + use chia_sdk_types::Conditions; + + use crate::{SpendWithConditions, StandardLayer}; + + use super::*; + + #[test] + #[allow(clippy::similar_names)] + fn test_clawback_coin_claim() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + let (alice_sk, alice_pk, alice_puzzle_hash, alice_coin) = sim.child_p2(1, 0)?; + let alice = StandardLayer::new(alice_pk); + + let (bob_sk, bob_pk, bob_puzzle_hash, _) = sim.child_p2(1, 1)?; + let bob = StandardLayer::new(bob_pk); + + let clawback = Clawback { + timelock: NonZeroU64::MIN, + sender_puzzle_hash: alice_puzzle_hash, + recipient_puzzle_hash: bob_puzzle_hash, + }; + let clawback_puzzle_hash = clawback.to_layer().tree_hash().into(); + + alice.spend( + ctx, + alice_coin, + Conditions::new().create_coin(clawback_puzzle_hash, 1, Vec::new()), + )?; + let clawback_coin = Coin::new(alice_coin.coin_id(), clawback_puzzle_hash, 1); + + sim.spend_coins(ctx.take(), &[alice_sk])?; + + let bob_inner = bob.spend_with_conditions(ctx, Conditions::new().reserve_fee(1))?; + let claim_spend = clawback.claim_spend(ctx, bob_inner)?; + ctx.spend(clawback_coin, claim_spend)?; + + sim.spend_coins(ctx.take(), &[bob_sk])?; + + Ok(()) + } + + #[test] + #[allow(clippy::similar_names)] + fn test_clawback_coin_clawback() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + let (sk, pk, puzzle_hash, coin) = sim.new_p2(1)?; + let p2 = StandardLayer::new(pk); + + let clawback = Clawback { + timelock: NonZeroU64::MAX, + sender_puzzle_hash: puzzle_hash, + recipient_puzzle_hash: Bytes32::default(), + }; + let clawback_puzzle_hash = clawback.to_layer().tree_hash().into(); + + p2.spend( + ctx, + coin, + Conditions::new().create_coin(clawback_puzzle_hash, 1, Vec::new()), + )?; + let clawback_coin = Coin::new(coin.coin_id(), clawback_puzzle_hash, 1); + + sim.spend_coins(ctx.take(), &[sk.clone()])?; + + let inner = p2.spend_with_conditions(ctx, Conditions::new().reserve_fee(1))?; + let clawback_spend = clawback.clawback_spend(ctx, inner)?; + ctx.spend(clawback_coin, clawback_spend)?; + + sim.spend_coins(ctx.take(), &[sk])?; + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/datalayer/datastore.rs b/crates/chia-sdk-driver/src/primitives/datalayer/datastore.rs index d9f0ec71..6be20360 100644 --- a/crates/chia-sdk-driver/src/primitives/datalayer/datastore.rs +++ b/crates/chia-sdk-driver/src/primitives/datalayer/datastore.rs @@ -75,7 +75,7 @@ where let inner_solution = DelegationLayerSolution { // if running owner puzzle, the line below will return 'None', thus ensuring correct puzzle behavior - merkle_proof: tree.get_proof(delegated_puzzle_hash.into()), + merkle_proof: tree.proof(delegated_puzzle_hash.into()), puzzle_reveal: inner_spend.puzzle, puzzle_solution: inner_spend.solution, }; @@ -533,7 +533,7 @@ impl DataStore { let new_puzzle_hash = if new_delegated_puzzles.is_empty() { new_inner_puzzle_hash } else { - let new_merkle_root = get_merkle_tree(ctx, new_delegated_puzzles.clone())?.root; + let new_merkle_root = get_merkle_tree(ctx, new_delegated_puzzles.clone())?.root(); DelegationLayerArgs::curry_tree_hash( launcher_id, new_inner_puzzle_hash, @@ -812,7 +812,7 @@ pub mod tests { // admin: remove writer from delegated puzzles let delegated_puzzles = vec![admin_delegated_puzzle, oracle_delegated_puzzle]; let new_merkle_tree = get_merkle_tree(ctx, delegated_puzzles.clone())?; - let new_merkle_root = new_merkle_tree.root; + let new_merkle_root = new_merkle_tree.root(); let new_merkle_root_condition = UpdateDataStoreMerkleRoot { new_merkle_root, @@ -1029,7 +1029,7 @@ pub mod tests { let new_merkle_tree = get_merkle_tree(ctx, dst_delegated_puzzles.clone())?; let new_merkle_root_condition = UpdateDataStoreMerkleRoot { - new_merkle_root: new_merkle_tree.root, + new_merkle_root: new_merkle_tree.root(), memos: DataStore::::get_recreation_memos( src_datastore.info.launcher_id, owner_puzzle_hash.into(), @@ -1939,7 +1939,7 @@ pub mod tests { let merkle_tree = get_merkle_tree(ctx, delegated_puzzles.clone())?; let delegation_layer = - DelegationLayer::new(Bytes32::default(), Bytes32::default(), merkle_tree.root); + DelegationLayer::new(Bytes32::default(), Bytes32::default(), merkle_tree.root()); let puzzle_ptr = delegation_layer.construct_puzzle(ctx)?; @@ -1947,7 +1947,7 @@ pub mod tests { let solution_ptr = delegation_layer.construct_solution( ctx, DelegationLayerSolution { - merkle_proof: merkle_tree.get_proof(delegated_puzzle_hash.into()), + merkle_proof: merkle_tree.proof(delegated_puzzle_hash.into()), puzzle_reveal: inner_spend.puzzle, puzzle_solution: inner_spend.solution, }, diff --git a/crates/chia-sdk-driver/src/primitives/datalayer/datastore_info.rs b/crates/chia-sdk-driver/src/primitives/datalayer/datastore_info.rs index dfed1446..185ac4be 100644 --- a/crates/chia-sdk-driver/src/primitives/datalayer/datastore_info.rs +++ b/crates/chia-sdk-driver/src/primitives/datalayer/datastore_info.rs @@ -1,11 +1,10 @@ use crate::{ - DelegationLayer, DriverError, Layer, MerkleTree, NftStateLayer, OracleLayer, SingletonLayer, - SpendContext, + DelegationLayer, DriverError, Layer, NftStateLayer, OracleLayer, SingletonLayer, SpendContext, }; use chia_protocol::{Bytes, Bytes32}; use chia_puzzles::nft::NftStateLayerArgs; use chia_sdk_types::{ - DelegationLayerArgs, WriterLayerArgs, DELEGATION_LAYER_PUZZLE_HASH, + DelegationLayerArgs, MerkleTree, WriterLayerArgs, DELEGATION_LAYER_PUZZLE_HASH, DL_METADATA_UPDATER_PUZZLE_HASH, }; use clvm_traits::{ClvmDecoder, ClvmEncoder, FromClvm, FromClvmError, Raw, ToClvm, ToClvmError}; @@ -211,7 +210,7 @@ impl DataStoreInfo { DelegationLayer::new( self.launcher_id, self.owner_puzzle_hash, - get_merkle_tree(ctx, self.delegated_puzzles)?.root, + get_merkle_tree(ctx, self.delegated_puzzles)?.root(), ), ), )) @@ -247,7 +246,7 @@ impl DataStoreInfo { mod_hash: DELEGATION_LAYER_PUZZLE_HASH.into(), launcher_id: self.launcher_id, owner_puzzle_hash: self.owner_puzzle_hash, - merkle_root: get_merkle_tree(ctx, self.delegated_puzzles.clone())?.root, + merkle_root: get_merkle_tree(ctx, self.delegated_puzzles.clone())?.root(), }, } .tree_hash(), diff --git a/crates/chia-sdk-driver/src/primitives/datalayer/datastore_launcher.rs b/crates/chia-sdk-driver/src/primitives/datalayer/datastore_launcher.rs index 2c7c1a6c..64842fb9 100644 --- a/crates/chia-sdk-driver/src/primitives/datalayer/datastore_launcher.rs +++ b/crates/chia-sdk-driver/src/primitives/datalayer/datastore_launcher.rs @@ -32,7 +32,7 @@ impl Launcher { DelegationLayerArgs::curry_tree_hash( launcher_id, owner_puzzle_hash.into(), - get_merkle_tree(ctx, delegated_puzzles.clone())?.root, + get_merkle_tree(ctx, delegated_puzzles.clone())?.root(), ) }; diff --git a/crates/chia-sdk-driver/src/puzzle.rs b/crates/chia-sdk-driver/src/puzzle.rs index 5a464d90..ee1ba305 100644 --- a/crates/chia-sdk-driver/src/puzzle.rs +++ b/crates/chia-sdk-driver/src/puzzle.rs @@ -1,4 +1,4 @@ -use clvm_traits::FromClvm; +use clvm_traits::{FromClvm, FromClvmError}; use clvm_utils::{tree_hash, CurriedProgram, ToTreeHash, TreeHash}; use clvmr::{Allocator, NodePtr}; @@ -75,6 +75,12 @@ impl PartialEq for Puzzle { impl Eq for Puzzle {} +impl FromClvm for Puzzle { + fn from_clvm(allocator: &Allocator, puzzle: NodePtr) -> Result { + Ok(Self::parse(allocator, puzzle)) + } +} + #[derive(Debug, Clone, Copy)] pub struct CurriedPuzzle { pub curried_puzzle_hash: TreeHash, diff --git a/crates/chia-sdk-test/src/simulator.rs b/crates/chia-sdk-test/src/simulator.rs index 96da6e1b..80576a8e 100644 --- a/crates/chia-sdk-test/src/simulator.rs +++ b/crates/chia-sdk-test/src/simulator.rs @@ -178,6 +178,36 @@ impl Simulator { let mut added_hints = IndexMap::new(); let mut puzzle_solutions = IndexMap::new(); + if self.height < conds.height_absolute { + return Err(SimulatorError::Validation( + ErrorCode::AssertHeightAbsoluteFailed, + )); + } + + // TODO: Tick time differently? + if u64::from(self.height) < conds.seconds_absolute { + return Err(SimulatorError::Validation( + ErrorCode::AssertSecondsAbsoluteFailed, + )); + } + + if let Some(height) = conds.before_height_absolute { + if height < self.height { + return Err(SimulatorError::Validation( + ErrorCode::AssertBeforeHeightAbsoluteFailed, + )); + } + } + + // TODO: Tick time differently? + if let Some(seconds) = conds.before_seconds_absolute { + if seconds < self.height.into() { + return Err(SimulatorError::Validation( + ErrorCode::AssertBeforeSecondsAbsoluteFailed, + )); + } + } + for coin_spend in spend_bundle.coin_spends { puzzle_solutions.insert( coin_spend.coin.coin_id(), @@ -217,6 +247,64 @@ impl Simulator { .copied() .unwrap_or(CoinState::new(coin, None, Some(self.height))); + if let Some(relative_height) = spend.height_relative { + let Some(created_height) = coin_state.created_height else { + return Err(SimulatorError::Validation( + ErrorCode::EphemeralRelativeCondition, + )); + }; + + if self.height < created_height + relative_height { + return Err(SimulatorError::Validation( + ErrorCode::AssertHeightRelativeFailed, + )); + } + } + + // TODO: Tick time differently? + if let Some(relative_seconds) = spend.seconds_relative { + let Some(created_height) = coin_state.created_height else { + return Err(SimulatorError::Validation( + ErrorCode::EphemeralRelativeCondition, + )); + }; + + if u64::from(self.height) < u64::from(created_height) + relative_seconds { + return Err(SimulatorError::Validation( + ErrorCode::AssertSecondsRelativeFailed, + )); + } + } + + if let Some(relative_height) = spend.before_height_relative { + let Some(created_height) = coin_state.created_height else { + return Err(SimulatorError::Validation( + ErrorCode::EphemeralRelativeCondition, + )); + }; + + if created_height + relative_height < self.height { + return Err(SimulatorError::Validation( + ErrorCode::AssertBeforeHeightRelativeFailed, + )); + } + } + + // TODO: Tick time differently? + if let Some(relative_seconds) = spend.before_seconds_relative { + let Some(created_height) = coin_state.created_height else { + return Err(SimulatorError::Validation( + ErrorCode::EphemeralRelativeCondition, + )); + }; + + if u64::from(created_height) + relative_seconds < u64::from(self.height) { + return Err(SimulatorError::Validation( + ErrorCode::AssertBeforeSecondsRelativeFailed, + )); + } + } + removed_coins.insert(spend.coin_id, coin_state); } diff --git a/crates/chia-sdk-types/Cargo.toml b/crates/chia-sdk-types/Cargo.toml index 8302e208..3e2324ac 100644 --- a/crates/chia-sdk-types/Cargo.toml +++ b/crates/chia-sdk-types/Cargo.toml @@ -32,3 +32,4 @@ once_cell = { workspace = true } [dev-dependencies] hex = { workspace = true } anyhow = { workspace = true } +rstest = { workspace = true } diff --git a/crates/chia-sdk-types/src/lib.rs b/crates/chia-sdk-types/src/lib.rs index 337ded29..9f81ac23 100644 --- a/crates/chia-sdk-types/src/lib.rs +++ b/crates/chia-sdk-types/src/lib.rs @@ -1,6 +1,7 @@ mod condition; mod conditions; mod constants; +mod merkle_tree; mod puzzle_mod; mod puzzles; mod run_puzzle; @@ -8,6 +9,7 @@ mod run_puzzle; pub use condition::*; pub use conditions::*; pub use constants::*; +pub use merkle_tree::*; pub use puzzle_mod::*; pub use puzzles::*; pub use run_puzzle::*; diff --git a/crates/chia-sdk-driver/src/merkle_tree.rs b/crates/chia-sdk-types/src/merkle_tree.rs similarity index 89% rename from crates/chia-sdk-driver/src/merkle_tree.rs rename to crates/chia-sdk-types/src/merkle_tree.rs index 951fadd0..6d700bdf 100644 --- a/crates/chia-sdk-driver/src/merkle_tree.rs +++ b/crates/chia-sdk-types/src/merkle_tree.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; +use std::fmt::Debug; use chia_protocol::Bytes32; +use clvm_traits::{FromClvm, ToClvm}; use clvmr::sha2::Sha256; const HASH_TREE_PREFIX: &[u8] = &[2]; @@ -8,11 +10,23 @@ const HASH_LEAF_PREFIX: &[u8] = &[1]; #[derive(Debug, Clone)] pub struct MerkleTree { - pub root: Bytes32, - pub proofs: HashMap)>, + root: Bytes32, + proofs: HashMap, } -use std::fmt::Debug; +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct MerkleProof { + pub path: u32, + #[clvm(rest)] + pub proof: Vec, +} + +impl MerkleProof { + pub fn new(path: u32, proof: Vec) -> Self { + Self { path, proof } + } +} #[derive(Debug, Clone)] pub enum BinaryTree { @@ -33,7 +47,15 @@ impl MerkleTree { Self { root, proofs } } - fn build_merkle_tree(leaves: &[Bytes32]) -> (Bytes32, HashMap)>) { + pub fn proof(&self, leaf: Bytes32) -> Option { + self.proofs.get(&leaf).cloned() + } + + pub fn root(&self) -> Bytes32 { + self.root + } + + fn build_merkle_tree(leaves: &[Bytes32]) -> (Bytes32, HashMap) { let binary_tree = MerkleTree::list_to_binary_tree(leaves); MerkleTree::build_merkle_tree_from_binary_tree(&binary_tree) } @@ -63,12 +85,12 @@ impl MerkleTree { fn build_merkle_tree_from_binary_tree( tuples: &BinaryTree, - ) -> (Bytes32, HashMap)>) { + ) -> (Bytes32, HashMap) { match tuples { BinaryTree::Leaf(t) => { let hash = MerkleTree::sha256(&[HASH_LEAF_PREFIX, t]); let mut proof = HashMap::new(); - proof.insert(*t, (0, vec![])); + proof.insert(*t, MerkleProof::new(0, vec![])); (hash, proof) } BinaryTree::Node(left, right) => { @@ -79,25 +101,21 @@ impl MerkleTree { let new_root = MerkleTree::sha256(&[HASH_TREE_PREFIX, &left_root, &right_root]); let mut new_proofs = HashMap::new(); - for (name, (path, mut proof)) in left_proofs { + for (name, MerkleProof { path, mut proof }) in left_proofs { proof.push(right_root); - new_proofs.insert(name, (path, proof)); + new_proofs.insert(name, MerkleProof::new(path, proof)); } - for (name, (path, mut proof)) in right_proofs { + for (name, MerkleProof { path, mut proof }) in right_proofs { let path = path | (1 << proof.len()); proof.push(left_root); - new_proofs.insert(name, (path, proof)); + new_proofs.insert(name, MerkleProof::new(path, proof)); } (new_root, new_proofs) } } } - - pub fn get_proof(&self, leaf: Bytes32) -> Option<(u32, Vec)> { - self.proofs.get(&leaf).cloned() - } } #[cfg(test)] @@ -162,10 +180,10 @@ mod tests { ) { let merkle_tree = MerkleTree::new(leaves); - assert_eq!(merkle_tree.root, expected_root); + assert_eq!(merkle_tree.root(), expected_root); for (leaf, path, proof) in expected_proofs { - assert_eq!(merkle_tree.get_proof(leaf), Some((path, proof))); + assert_eq!(merkle_tree.proof(leaf), Some(MerkleProof::new(path, proof))); } } } diff --git a/crates/chia-sdk-types/src/puzzles.rs b/crates/chia-sdk-types/src/puzzles.rs index ce97569f..276e7b7c 100644 --- a/crates/chia-sdk-types/src/puzzles.rs +++ b/crates/chia-sdk-types/src/puzzles.rs @@ -1,8 +1,12 @@ +mod augmented_condition; +mod p2_curried; mod p2_delegated_conditions; mod p2_delegated_singleton; mod p2_one_of_many; mod p2_singleton; +pub use augmented_condition::*; +pub use p2_curried::*; pub use p2_delegated_conditions::*; pub use p2_delegated_singleton::*; pub use p2_one_of_many::*; diff --git a/crates/chia-sdk-types/src/puzzles/augmented_condition.rs b/crates/chia-sdk-types/src/puzzles/augmented_condition.rs new file mode 100644 index 00000000..a335af7f --- /dev/null +++ b/crates/chia-sdk-types/src/puzzles/augmented_condition.rs @@ -0,0 +1,57 @@ +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::TreeHash; +use hex_literal::hex; + +use crate::{Condition, Mod}; + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(curry)] +pub struct AugmentedConditionArgs { + pub condition: Condition, + pub inner_puzzle: I, +} + +impl AugmentedConditionArgs { + pub fn new(condition: Condition, inner_puzzle: I) -> Self { + Self { + condition, + inner_puzzle, + } + } +} + +impl Mod for AugmentedConditionArgs { + const MOD_REVEAL: &[u8] = &AUGMENTED_CONDITION_PUZZLE; + const MOD_HASH: TreeHash = AUGMENTED_CONDITION_PUZZLE_HASH; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct AugmentedConditionSolution { + pub inner_solution: I, +} + +impl AugmentedConditionSolution { + pub fn new(inner_solution: I) -> Self { + Self { inner_solution } + } +} + +pub const AUGMENTED_CONDITION_PUZZLE: [u8; 13] = hex!("ff04ff02ffff02ff05ff0b8080"); + +pub const AUGMENTED_CONDITION_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + "d303eafa617bedf0bc05850dd014e10fbddf622187dc07891a2aacba9d8a93f6" +)); + +#[cfg(test)] +mod tests { + use super::*; + + use crate::assert_puzzle_hash; + + #[test] + fn test_puzzle_hash() -> anyhow::Result<()> { + assert_puzzle_hash!(AUGMENTED_CONDITION_PUZZLE => AUGMENTED_CONDITION_PUZZLE_HASH); + Ok(()) + } +} diff --git a/crates/chia-sdk-types/src/puzzles/datalayer/delegation_layer.rs b/crates/chia-sdk-types/src/puzzles/datalayer/delegation_layer.rs index 398008a8..5ee243fc 100644 --- a/crates/chia-sdk-types/src/puzzles/datalayer/delegation_layer.rs +++ b/crates/chia-sdk-types/src/puzzles/datalayer/delegation_layer.rs @@ -3,7 +3,7 @@ use clvm_traits::{FromClvm, ToClvm}; use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; use hex_literal::hex; -use crate::Mod; +use crate::{MerkleProof, Mod}; pub const DELEGATION_LAYER_PUZZLE: [u8; 1027] = hex!( " @@ -84,7 +84,7 @@ impl DelegationLayerArgs { #[derive(ToClvm, FromClvm, Debug, Clone, PartialEq, Eq)] #[clvm(list)] pub struct DelegationLayerSolution { - pub merkle_proof: Option<(u32, Vec)>, + pub merkle_proof: Option, pub puzzle_reveal: P, pub puzzle_solution: S, } diff --git a/crates/chia-sdk-types/src/puzzles/p2_curried.rs b/crates/chia-sdk-types/src/puzzles/p2_curried.rs new file mode 100644 index 00000000..531bf2ee --- /dev/null +++ b/crates/chia-sdk-types/src/puzzles/p2_curried.rs @@ -0,0 +1,63 @@ +use chia_protocol::Bytes32; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::TreeHash; +use hex_literal::hex; + +use crate::Mod; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(curry)] +pub struct P2CurriedArgs { + pub puzzle_hash: Bytes32, +} + +impl P2CurriedArgs { + pub fn new(puzzle_hash: Bytes32) -> Self { + Self { puzzle_hash } + } +} + +impl Mod for P2CurriedArgs { + const MOD_REVEAL: &[u8] = &P2_CURRIED_PUZZLE; + const MOD_HASH: TreeHash = P2_CURRIED_PUZZLE_HASH; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct P2CurriedSolution { + pub puzzle: P, + pub solution: S, +} + +impl P2CurriedSolution { + pub fn new(puzzle: P, solution: S) -> Self { + Self { puzzle, solution } + } +} + +pub const P2_CURRIED_PUZZLE: [u8; 143] = hex!( + " + ff02ffff01ff02ffff03ffff09ff05ffff02ff02ffff04ff02ffff04ff0bff80 + 80808080ffff01ff02ff0bff1780ffff01ff088080ff0180ffff04ffff01ff02 + ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ff + 09ff80808080ffff02ff02ffff04ff02ffff04ff0dff8080808080ffff01ff0b + ffff0101ff058080ff0180ff018080 + " +); + +pub const P2_CURRIED_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + "13e29a62b42cd2ef72a79e4bacdc59733ca6310d65af83d349360d36ec622363" +)); + +#[cfg(test)] +mod tests { + use super::*; + + use crate::assert_puzzle_hash; + + #[test] + fn test_puzzle_hash() -> anyhow::Result<()> { + assert_puzzle_hash!(P2_CURRIED_PUZZLE => P2_CURRIED_PUZZLE_HASH); + Ok(()) + } +} diff --git a/crates/chia-sdk-types/src/puzzles/p2_one_of_many.rs b/crates/chia-sdk-types/src/puzzles/p2_one_of_many.rs index 86c399b5..54c7c3ca 100644 --- a/crates/chia-sdk-types/src/puzzles/p2_one_of_many.rs +++ b/crates/chia-sdk-types/src/puzzles/p2_one_of_many.rs @@ -3,7 +3,7 @@ use clvm_traits::{FromClvm, ToClvm}; use clvm_utils::TreeHash; use hex_literal::hex; -use crate::Mod; +use crate::{MerkleProof, Mod}; #[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] #[clvm(curry)] @@ -11,19 +11,35 @@ pub struct P2OneOfManyArgs { pub merkle_root: Bytes32, } +impl P2OneOfManyArgs { + pub fn new(merkle_root: Bytes32) -> Self { + Self { merkle_root } + } +} + impl Mod for P2OneOfManyArgs { const MOD_REVEAL: &[u8] = &P2_ONE_OF_MANY_PUZZLE; const MOD_HASH: TreeHash = P2_ONE_OF_MANY_PUZZLE_HASH; } -#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] #[clvm(list)] pub struct P2OneOfManySolution { - pub merkle_proof: Bytes32, + pub merkle_proof: MerkleProof, pub puzzle: P, pub solution: S, } +impl P2OneOfManySolution { + pub fn new(merkle_proof: MerkleProof, puzzle: P, solution: S) -> Self { + Self { + merkle_proof, + puzzle, + solution, + } + } +} + pub const P2_ONE_OF_MANY_PUZZLE: [u8; 280] = hex!( " ff02ffff01ff02ffff03ffff09ff05ffff02ff06ffff04ff02ffff04ffff0bff