diff --git a/Cargo.lock b/Cargo.lock index be19020f..d5249df6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -538,7 +538,11 @@ dependencies = [ "clvmr", "hex", "hex-literal", + "k256", "once_cell", + "p256", + "rand", + "rand_chacha", "rstest", ] diff --git a/Cargo.toml b/Cargo.toml index 928b2f5f..a05c4fcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ workspace = true [features] chip-0035 = ["chia-sdk-driver/chip-0035", "chia-sdk-types/chip-0035"] +vault = ["chia-sdk-driver/vault", "chia-sdk-types/vault"] offers = ["chia-sdk-driver/offers"] native-tls = ["chia-sdk-client/native-tls"] rustls = ["chia-sdk-client/rustls"] @@ -135,6 +136,7 @@ napi = { version = "2.12.2", default-features = false } paste = "1.0.15" bigdecimal = "0.4.6" k256 = "0.13.4" +p256 = "0.13.2" [profile.release] lto = true diff --git a/crates/chia-sdk-driver/Cargo.toml b/crates/chia-sdk-driver/Cargo.toml index 41082db6..327eeadc 100644 --- a/crates/chia-sdk-driver/Cargo.toml +++ b/crates/chia-sdk-driver/Cargo.toml @@ -19,6 +19,7 @@ workspace = true [features] chip-0035 = ["chia-sdk-types/chip-0035"] +vault = ["chia-sdk-types/vault"] offers = [ "dep:bech32", "dep:chia-traits", diff --git a/crates/chia-sdk-driver/src/driver_error.rs b/crates/chia-sdk-driver/src/driver_error.rs index 90c91c79..7c90ac95 100644 --- a/crates/chia-sdk-driver/src/driver_error.rs +++ b/crates/chia-sdk-driver/src/driver_error.rs @@ -50,4 +50,13 @@ pub enum DriverError { #[error("invalid merkle proof")] InvalidMerkleProof, + + #[error("unknown puzzle")] + UnknownPuzzle, + + #[error("wrong number of spends")] + WrongSpendCount, + + #[error("missing member spend")] + MissingMemberSpend, } diff --git a/crates/chia-sdk-driver/src/layer.rs b/crates/chia-sdk-driver/src/layer.rs index 09eb93c2..a0b3b38d 100644 --- a/crates/chia-sdk-driver/src/layer.rs +++ b/crates/chia-sdk-driver/src/layer.rs @@ -81,7 +81,7 @@ where } fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { - ctx.alloc(self) + ctx.alloc(&self) } fn construct_solution( diff --git a/crates/chia-sdk-driver/src/layers/datalayer/oracle_layer.rs b/crates/chia-sdk-driver/src/layers/datalayer/oracle_layer.rs index ac519750..f7175482 100644 --- a/crates/chia-sdk-driver/src/layers/datalayer/oracle_layer.rs +++ b/crates/chia-sdk-driver/src/layers/datalayer/oracle_layer.rs @@ -61,7 +61,7 @@ impl Layer for OracleLayer { } let conditions: Vec> = vec![ - Condition::create_coin(self.oracle_puzzle_hash, self.oracle_fee, vec![]), + Condition::create_coin(self.oracle_puzzle_hash, self.oracle_fee, None), Condition::create_puzzle_announcement(Bytes::new("$".into())), ]; diff --git a/crates/chia-sdk-driver/src/layers/p2_singleton_layer.rs b/crates/chia-sdk-driver/src/layers/p2_singleton_layer.rs index 13c91895..11d2f534 100644 --- a/crates/chia-sdk-driver/src/layers/p2_singleton_layer.rs +++ b/crates/chia-sdk-driver/src/layers/p2_singleton_layer.rs @@ -132,20 +132,22 @@ mod tests { let p2_singleton = P2SingletonLayer::new(launcher_id); let p2_singleton_hash = p2_singleton.tree_hash().into(); + let memos = ctx.hint(launcher_id)?; p2.spend( ctx, coin, - create_singleton.create_coin(p2_singleton_hash, 1, vec![launcher_id.into()]), + create_singleton.create_coin(p2_singleton_hash, 1, Some(memos)), )?; let p2_coin = Coin::new(coin.coin_id(), p2_singleton_hash, 1); p2_singleton.spend_coin(ctx, p2_coin, puzzle_hash)?; + let memos = ctx.hint(launcher_id)?; let inner_solution = p2 .spend_with_conditions( ctx, Conditions::new() - .create_coin(puzzle_hash, 1, vec![launcher_id.into()]) + .create_coin(puzzle_hash, 1, Some(memos)) .create_puzzle_announcement(p2_coin.coin_id().into()), )? .solution; diff --git a/crates/chia-sdk-driver/src/layers/standard_layer.rs b/crates/chia-sdk-driver/src/layers/standard_layer.rs index 7e9edc56..a14fb84f 100644 --- a/crates/chia-sdk-driver/src/layers/standard_layer.rs +++ b/crates/chia-sdk-driver/src/layers/standard_layer.rs @@ -134,13 +134,13 @@ mod tests { p2.spend( ctx, coin, - Conditions::new().create_coin(puzzle_hash, u64::MAX, Vec::new()), + Conditions::new().create_coin(puzzle_hash, u64::MAX, None), )?; p2.spend( ctx, Coin::new(coin.coin_id(), puzzle_hash, u64::MAX), - Conditions::new().create_coin(puzzle_hash, 1, Vec::new()), + Conditions::new().create_coin(puzzle_hash, 1, None), )?; sim.spend_coins(ctx.take(), &[sk])?; diff --git a/crates/chia-sdk-driver/src/primitives.rs b/crates/chia-sdk-driver/src/primitives.rs index 481857b5..8f6e283b 100644 --- a/crates/chia-sdk-driver/src/primitives.rs +++ b/crates/chia-sdk-driver/src/primitives.rs @@ -17,3 +17,9 @@ mod datalayer; #[cfg(feature = "chip-0035")] pub use datalayer::*; + +#[cfg(feature = "vault")] +mod vault; + +#[cfg(feature = "vault")] +pub use vault::*; diff --git a/crates/chia-sdk-driver/src/primitives/cat.rs b/crates/chia-sdk-driver/src/primitives/cat.rs index ae2a3338..e01a6d99 100644 --- a/crates/chia-sdk-driver/src/primitives/cat.rs +++ b/crates/chia-sdk-driver/src/primitives/cat.rs @@ -107,7 +107,7 @@ impl Cat { )?; Ok(( - Conditions::new().create_coin(puzzle_hash, amount, Vec::new()), + Conditions::new().create_coin(puzzle_hash, amount, None), eve, )) } @@ -134,7 +134,7 @@ impl Cat { let create_coins = conditions .into_iter() - .filter_map(|ptr| ctx.extract::(ptr).ok()); + .filter_map(|ptr| ctx.extract::>(ptr).ok()); let delta = create_coins.fold( i128::from(cat.coin.amount) - i128::from(*extra_delta), @@ -283,11 +283,12 @@ mod tests { let (sk, pk, puzzle_hash, coin) = sim.new_p2(1)?; let p2 = StandardLayer::new(pk); + let memos = ctx.hint(puzzle_hash)?; let (issue_cat, cat) = Cat::single_issuance_eve( ctx, coin.coin_id(), 1, - Conditions::new().create_coin(puzzle_hash, 1, vec![puzzle_hash.into()]), + Conditions::new().create_coin(puzzle_hash, 1, Some(memos)), )?; p2.spend(ctx, coin, issue_cat)?; @@ -311,12 +312,13 @@ mod tests { let (sk, pk, puzzle_hash, coin) = sim.new_p2(1)?; let p2 = StandardLayer::new(pk); + let memos = ctx.hint(puzzle_hash)?; let (issue_cat, cat) = Cat::multi_issuance_eve( ctx, coin.coin_id(), pk, 1, - Conditions::new().create_coin(puzzle_hash, 1, vec![puzzle_hash.into()]), + Conditions::new().create_coin(puzzle_hash, 1, Some(memos)), )?; p2.spend(ctx, coin, issue_cat)?; sim.spend_coins(ctx.take(), &[sk])?; @@ -358,11 +360,12 @@ mod tests { let (sk, pk, puzzle_hash, coin) = sim.new_p2(2)?; let p2 = StandardLayer::new(pk); + let memos = ctx.hint(puzzle_hash)?; let (issue_cat, _cat) = Cat::single_issuance_eve( ctx, coin.coin_id(), 1, - Conditions::new().create_coin(puzzle_hash, 2, vec![puzzle_hash.into()]), + Conditions::new().create_coin(puzzle_hash, 2, Some(memos)), )?; p2.spend(ctx, coin, issue_cat)?; @@ -398,8 +401,9 @@ mod tests { // Issue the CAT coins with those amounts. let mut conditions = Conditions::new(); + let memos = ctx.hint(puzzle_hash)?; for &amount in &amounts { - conditions = conditions.create_coin(puzzle_hash, amount, vec![puzzle_hash.into()]); + conditions = conditions.create_coin(puzzle_hash, amount, Some(memos)); } let (issue_cat, cat) = Cat::single_issuance_eve(ctx, coin.coin_id(), sum, conditions)?; @@ -424,7 +428,7 @@ mod tests { Conditions::new().create_coin( puzzle_hash, cat.coin.amount, - vec![puzzle_hash.into()], + Some(memos), ), )?, )) @@ -455,13 +459,15 @@ mod tests { let custom_p2 = ctx.alloc(&1)?; let custom_p2_puzzle_hash = ctx.tree_hash(custom_p2).into(); + let memos = ctx.hint(puzzle_hash)?; + let custom_memos = ctx.hint(custom_p2_puzzle_hash)?; let (issue_cat, cat) = Cat::single_issuance_eve( ctx, coin.coin_id(), 2, Conditions::new() - .create_coin(puzzle_hash, 1, vec![puzzle_hash.into()]) - .create_coin(custom_p2_puzzle_hash, 1, vec![custom_p2_puzzle_hash.into()]), + .create_coin(puzzle_hash, 1, Some(memos)) + .create_coin(custom_p2_puzzle_hash, 1, Some(custom_memos)), )?; p2.spend(ctx, coin, issue_cat)?; sim.spend_coins(ctx.take(), &[sk.clone()])?; @@ -471,7 +477,7 @@ mod tests { cat.wrapped_child(puzzle_hash, 1), p2.spend_with_conditions( ctx, - Conditions::new().create_coin(puzzle_hash, 1, vec![puzzle_hash.into()]), + Conditions::new().create_coin(puzzle_hash, 1, Some(memos)), )?, ), CatSpend::new( @@ -481,7 +487,7 @@ mod tests { ctx.alloc(&[CreateCoin::new( custom_p2_puzzle_hash, 1, - vec![custom_p2_puzzle_hash.into()], + Some(custom_memos), )])?, ), ), @@ -500,8 +506,8 @@ mod tests { let (sk, pk, puzzle_hash, coin) = sim.new_p2(10000)?; let p2 = StandardLayer::new(pk); - let conditions = - Conditions::new().create_coin(puzzle_hash, 10000, vec![puzzle_hash.into()]); + let memos = ctx.hint(puzzle_hash)?; + let conditions = Conditions::new().create_coin(puzzle_hash, 10000, Some(memos)); let (issue_cat, cat) = Cat::multi_issuance_eve(ctx, coin.coin_id(), pk, 10000, conditions)?; p2.spend(ctx, coin, issue_cat)?; @@ -512,7 +518,7 @@ mod tests { p2.spend_with_conditions( ctx, Conditions::new() - .create_coin(puzzle_hash, 7000, vec![puzzle_hash.into()]) + .create_coin(puzzle_hash, 7000, Some(memos)) .run_cat_tail(tail, NodePtr::NIL), )?, -3000, diff --git a/crates/chia-sdk-driver/src/primitives/clawback.rs b/crates/chia-sdk-driver/src/primitives/clawback.rs index d12190cb..1c3517e4 100644 --- a/crates/chia-sdk-driver/src/primitives/clawback.rs +++ b/crates/chia-sdk-driver/src/primitives/clawback.rs @@ -132,7 +132,7 @@ mod tests { alice.spend( ctx, alice_coin, - Conditions::new().create_coin(clawback_puzzle_hash, 1, Vec::new()), + Conditions::new().create_coin(clawback_puzzle_hash, 1, None), )?; let clawback_coin = Coin::new(alice_coin.coin_id(), clawback_puzzle_hash, 1); @@ -166,7 +166,7 @@ mod tests { p2.spend( ctx, coin, - Conditions::new().create_coin(clawback_puzzle_hash, 1, Vec::new()), + Conditions::new().create_coin(clawback_puzzle_hash, 1, None), )?; let clawback_coin = Coin::new(coin.coin_id(), clawback_puzzle_hash, 1); diff --git a/crates/chia-sdk-driver/src/primitives/datalayer/datastore.rs b/crates/chia-sdk-driver/src/primitives/datalayer/datastore.rs index 6be20360..53078b1e 100644 --- a/crates/chia-sdk-driver/src/primitives/datalayer/datastore.rs +++ b/crates/chia-sdk-driver/src/primitives/datalayer/datastore.rs @@ -375,7 +375,13 @@ where // if the coin was re-created with memos, there is a delegation layer // and delegated puzzles have been updated (we can rebuild the list from memos) - if inner_create_coin_condition.memos.len() > 1 { + + let inner_memos = Vec::::from_clvm( + allocator, + inner_create_coin_condition.memos.unwrap_or_default().value, + )?; + + if inner_memos.len() > 1 { // keep in mind that there's always the launcher id memo being added return Ok(Some(Self::build_datastore( new_coin, @@ -383,7 +389,7 @@ where Proof::Lineage(singleton_layer.lineage_proof(cs.coin)), new_metadata, state_layer.inner_puzzle.tree_hash().into(), - inner_create_coin_condition.memos, + inner_memos, )?)); } @@ -545,7 +551,7 @@ impl DataStore { Ok(Condition::CreateCoin(CreateCoin { amount: 1, puzzle_hash: new_puzzle_hash, - memos: if hint_delegated_puzzles { + memos: Some(ctx.memos(&if hint_delegated_puzzles { Self::get_recreation_memos( launcher_id, new_inner_puzzle_hash.into(), @@ -553,7 +559,7 @@ impl DataStore { ) } else { vec![launcher_id.into()] - }, + })?), })) } @@ -590,7 +596,7 @@ pub mod tests { use chia_bls::{PublicKey, SecretKey}; use chia_puzzles::standard::StandardArgs; use chia_sdk_test::{test_secret_keys, Simulator}; - use chia_sdk_types::{Conditions, MeltSingleton, UpdateDataStoreMerkleRoot}; + use chia_sdk_types::{Conditions, MeltSingleton, Memos, UpdateDataStoreMerkleRoot}; use clvmr::sha2::Sha256; use rstest::rstest; @@ -709,7 +715,7 @@ pub mod tests { } let datastore_inner_spend = StandardLayer::new(pk) - .spend_with_conditions(ctx, Conditions::new().create_coin(puzzle_hash, 1, vec![]))?; + .spend_with_conditions(ctx, Conditions::new().create_coin(puzzle_hash, 1, None))?; let old_datastore_coin = datastore.coin; let new_spend = datastore.spend(ctx, datastore_inner_spend)?; @@ -1757,7 +1763,7 @@ pub mod tests { Conditions::new().with(Condition::CreateCoin(CreateCoin { puzzle_hash: attacker_puzzle_hash.into(), amount: 1, - memos: vec![], + memos: None, })), )?; @@ -1907,10 +1913,10 @@ pub mod tests { new_updater_puzzle_hash: new_updater_ph.into(), }, conditions: if output_conditions { - vec![CreateCoin { + vec![CreateCoin:: { puzzle_hash: [0; 32].into(), amount: 1, - memos: vec![], + memos: None, }] } else { vec![] @@ -2100,11 +2106,11 @@ pub mod tests { inner_spend_conditions = inner_spend_conditions.with(Condition::CreateCoin(CreateCoin { puzzle_hash: new_inner_ph, amount: 1, - memos: vec![ - launcher_coin.coin_id().into(), - second_root_hash.value().into(), - new_inner_ph.into(), - ], + memos: Memos::some(ctx.alloc(&[ + launcher_coin.coin_id(), + second_root_hash.value(), + new_inner_ph, + ])?), })); let inner_spend = diff --git a/crates/chia-sdk-driver/src/primitives/did.rs b/crates/chia-sdk-driver/src/primitives/did.rs index b1125942..6dd8fbb2 100644 --- a/crates/chia-sdk-driver/src/primitives/did.rs +++ b/crates/chia-sdk-driver/src/primitives/did.rs @@ -136,13 +136,15 @@ where .with_p2_puzzle_hash(p2_puzzle_hash) .inner_puzzle_hash(); + let memos = ctx.hint(p2_puzzle_hash)?; + self.spend_with( ctx, inner, extra_conditions.create_coin( new_inner_puzzle_hash.into(), self.coin.amount, - vec![p2_puzzle_hash.into()], + Some(memos), ), )?; @@ -170,13 +172,15 @@ where .with_metadata(metadata.clone()) .inner_puzzle_hash(); + let memos = ctx.hint(self.info.p2_puzzle_hash)?; + self.spend_with( ctx, inner, extra_conditions.create_coin( new_inner_puzzle_hash.into(), self.coin.amount, - vec![self.info.p2_puzzle_hash.into()], + Some(memos), ), )?; @@ -230,7 +234,7 @@ where } let singleton_solution = - SingletonLayer::::parse_solution(allocator, parent_solution)?; + SingletonLayer::::parse_solution(allocator, parent_solution)?; let output = run_puzzle( allocator, @@ -247,14 +251,12 @@ where return Err(DriverError::MissingChild); }; - let Some(hint) = create_coin - .memos - .into_iter() - .find_map(|memo| memo.try_into().ok()) - else { + let Some(memos) = create_coin.memos else { return Err(DriverError::MissingHint); }; + let (hint, _) = <(Bytes32, NodePtr)>::from_clvm(allocator, memos.value)?; + let metadata_ptr = did_layer.metadata.to_clvm(allocator)?; let metadata_hash = tree_hash(allocator, metadata_ptr); let did_layer_hashed = did_layer.clone().with_metadata(metadata_hash); diff --git a/crates/chia-sdk-driver/src/primitives/intermediate_launcher.rs b/crates/chia-sdk-driver/src/primitives/intermediate_launcher.rs index 25f89382..f5539eae 100644 --- a/crates/chia-sdk-driver/src/primitives/intermediate_launcher.rs +++ b/crates/chia-sdk-driver/src/primitives/intermediate_launcher.rs @@ -62,7 +62,7 @@ impl IntermediateLauncher { self.mint_total, ))?; - parent = parent.create_coin(self.intermediate_coin.puzzle_hash, 0, Vec::new()); + parent = parent.create_coin(self.intermediate_coin.puzzle_hash, 0, None); let puzzle_reveal = ctx.serialize(&puzzle)?; let solution = ctx.serialize(&())?; diff --git a/crates/chia-sdk-driver/src/primitives/launcher.rs b/crates/chia-sdk-driver/src/primitives/launcher.rs index 63175576..e846bf83 100644 --- a/crates/chia-sdk-driver/src/primitives/launcher.rs +++ b/crates/chia-sdk-driver/src/primitives/launcher.rs @@ -4,9 +4,9 @@ use chia_protocol::{Bytes32, Coin, CoinSpend, Program}; use chia_puzzles::singleton::{ LauncherSolution, SingletonArgs, SINGLETON_LAUNCHER_PUZZLE, SINGLETON_LAUNCHER_PUZZLE_HASH, }; -use chia_sdk_types::{announcement_id, Conditions}; +use chia_sdk_types::{announcement_id, Conditions, Memos}; use clvm_traits::ToClvm; -use clvmr::Allocator; +use clvmr::{Allocator, NodePtr}; use crate::{DriverError, SpendContext}; @@ -40,17 +40,13 @@ impl Launcher { SINGLETON_LAUNCHER_PUZZLE_HASH.into(), amount, ), - Conditions::new().create_coin( - SINGLETON_LAUNCHER_PUZZLE_HASH.into(), - amount, - Vec::new(), - ), + Conditions::new().create_coin(SINGLETON_LAUNCHER_PUZZLE_HASH.into(), amount, None), ) } /// The parent coin specified when constructing the [`Launcher`] will create the launcher coin. /// The created launcher coin will be hinted to make identifying it easier later. - pub fn hinted(parent_coin_id: Bytes32, amount: u64, hint: Bytes32) -> Self { + pub fn with_memos(parent_coin_id: Bytes32, amount: u64, memos: Memos) -> Self { Self::from_coin( Coin::new( parent_coin_id, @@ -60,7 +56,7 @@ impl Launcher { Conditions::new().create_coin( SINGLETON_LAUNCHER_PUZZLE_HASH.into(), amount, - vec![hint.into()], + Some(memos), ), ) } @@ -72,11 +68,7 @@ impl Launcher { /// For example, this is useful for minting NFTs from intermediate coins created with an earlier instance of a DID. pub fn create_early(parent_coin_id: Bytes32, amount: u64) -> (Conditions, Self) { ( - Conditions::new().create_coin( - SINGLETON_LAUNCHER_PUZZLE_HASH.into(), - amount, - Vec::new(), - ), + Conditions::new().create_coin(SINGLETON_LAUNCHER_PUZZLE_HASH.into(), amount, None), Self::from_coin( Coin::new( parent_coin_id, @@ -93,16 +85,16 @@ impl Launcher { /// /// This method is used to create the launcher coin immediately from the parent, then spend it later attached to any coin spend. /// For example, this is useful for minting NFTs from intermediate coins created with an earlier instance of a DID. - pub fn create_early_hinted( + pub fn create_early_with_memos( parent_coin_id: Bytes32, amount: u64, - hint: Bytes32, + memos: Memos, ) -> (Conditions, Self) { ( Conditions::new().create_coin( SINGLETON_LAUNCHER_PUZZLE_HASH.into(), amount, - vec![hint.into()], + Some(memos), ), Self::from_coin( Coin::new( diff --git a/crates/chia-sdk-driver/src/primitives/nft.rs b/crates/chia-sdk-driver/src/primitives/nft.rs index aed7c39d..02273f6d 100644 --- a/crates/chia-sdk-driver/src/primitives/nft.rs +++ b/crates/chia-sdk-driver/src/primitives/nft.rs @@ -155,15 +155,13 @@ where N: ToClvm + FromClvm + ToTreeHash, M: ToTreeHash, { + let memos = ctx.hint(p2_puzzle_hash)?; + self.spend_with( ctx, inner, extra_conditions - .create_coin( - p2_puzzle_hash, - self.coin.amount, - vec![p2_puzzle_hash.into()], - ) + .create_coin(p2_puzzle_hash, self.coin.amount, Some(memos)) .update_nft_metadata(metadata_update.puzzle, metadata_update.solution), )?; @@ -204,14 +202,12 @@ where M: ToTreeHash, I: SpendWithConditions, { + let memos = ctx.hint(p2_puzzle_hash)?; + self.spend_with( ctx, inner, - extra_conditions.create_coin( - p2_puzzle_hash, - self.coin.amount, - vec![p2_puzzle_hash.into()], - ), + extra_conditions.create_coin(p2_puzzle_hash, self.coin.amount, Some(memos)), )?; let metadata = self.info.metadata.clone(); @@ -337,15 +333,13 @@ where Conditions::new() }; + let memos = ctx.hint(p2_puzzle_hash)?; + self.spend_with( ctx, inner, extra_conditions - .create_coin( - p2_puzzle_hash, - self.coin.amount, - vec![p2_puzzle_hash.into()], - ) + .create_coin(p2_puzzle_hash, self.coin.amount, Some(memos)) .with(transfer_condition), )?; diff --git a/crates/chia-sdk-driver/src/primitives/nft/nft_launcher.rs b/crates/chia-sdk-driver/src/primitives/nft/nft_launcher.rs index fdf43fe2..0a6cfa2d 100644 --- a/crates/chia-sdk-driver/src/primitives/nft/nft_launcher.rs +++ b/crates/chia-sdk-driver/src/primitives/nft/nft_launcher.rs @@ -64,8 +64,9 @@ impl Launcher { ) }); + let memos = ctx.hint(mint.p2_puzzle_hash)?; let conditions = Conditions::new() - .create_coin(mint.p2_puzzle_hash, 1, vec![mint.p2_puzzle_hash.into()]) + .create_coin(mint.p2_puzzle_hash, 1, Some(memos)) .extend(transfer_condition.clone()); let inner_puzzle = ctx.alloc(&clvm_quote!(conditions))?; @@ -237,7 +238,7 @@ mod tests { let (mint_nft, _nft) = launcher.mint_nft(ctx, mint)?; - let _ = did.update(ctx, &p2, mint_nft.create_coin(puzzle_hash, 0, Vec::new()))?; + let _ = did.update(ctx, &p2, mint_nft.create_coin(puzzle_hash, 0, None))?; p2.spend(ctx, intermediate_coin, create_launcher)?; sim.spend_coins(ctx.take(), &[sk])?; @@ -277,7 +278,7 @@ mod tests { let did = did.update( ctx, &p2, - Conditions::new().create_coin(puzzle_hash, 0, Vec::new()), + Conditions::new().create_coin(puzzle_hash, 0, None), )?; let _ = did.update(ctx, &p2, mint_nft)?; diff --git a/crates/chia-sdk-driver/src/primitives/vault.rs b/crates/chia-sdk-driver/src/primitives/vault.rs new file mode 100644 index 00000000..0415b91b --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/vault.rs @@ -0,0 +1,16 @@ +mod known_puzzles; +mod m_of_n; +mod member; +mod puzzle_with_restrictions; +mod restriction; +mod vault_launcher; +mod vault_layer; +mod vault_primitive; + +pub use known_puzzles::*; +pub use m_of_n::*; +pub use member::*; +pub use puzzle_with_restrictions::*; +pub use restriction::*; +pub use vault_layer::*; +pub use vault_primitive::*; diff --git a/crates/chia-sdk-driver/src/primitives/vault/known_puzzles.rs b/crates/chia-sdk-driver/src/primitives/vault/known_puzzles.rs new file mode 100644 index 00000000..aa95ce1a --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/vault/known_puzzles.rs @@ -0,0 +1,17 @@ +use std::collections::HashMap; + +use clvm_utils::TreeHash; + +use super::{MemberKind, RestrictionKind}; + +#[derive(Debug, Default, Clone)] +pub struct KnownPuzzles { + pub restrictions: HashMap, + pub members: HashMap, +} + +impl KnownPuzzles { + pub fn new() -> Self { + Self::default() + } +} diff --git a/crates/chia-sdk-driver/src/primitives/vault/m_of_n.rs b/crates/chia-sdk-driver/src/primitives/vault/m_of_n.rs new file mode 100644 index 00000000..bf65e42d --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/vault/m_of_n.rs @@ -0,0 +1,198 @@ +use std::collections::HashMap; + +use chia_protocol::Bytes32; +use chia_sdk_types::{ + MerkleTree, Mod, Vault1ofNArgs, Vault1ofNSolution, VaultMofNArgs, VaultMofNSolution, + VaultNofNArgs, VaultNofNSolution, +}; +use clvm_traits::clvm_tuple; +use clvm_utils::{tree_hash_atom, tree_hash_pair, TreeHash}; +use clvmr::NodePtr; + +use crate::{DriverError, Spend, SpendContext}; + +use super::{KnownPuzzles, Member, PuzzleWithRestrictions, VaultLayer}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MofNOptimization { + One, + All, +} + +#[derive(Debug, Clone)] +pub struct MofN { + required: usize, + members: Vec>, +} + +impl MofN { + pub fn new(required: usize, members: Vec>) -> Option { + if members.len() < required { + return None; + } + Some(Self { required, members }) + } + + pub fn required(&self) -> usize { + self.required + } + + pub fn members(&self) -> &[PuzzleWithRestrictions] { + &self.members + } + + pub fn solve( + &self, + ctx: &mut SpendContext, + member_spends: HashMap, + ) -> Result { + if member_spends.len() != self.required { + return Err(DriverError::WrongSpendCount); + } + + match self.optimization() { + Some(MofNOptimization::One) => { + let (member_puzzle_hash, member_spend) = member_spends + .into_iter() + .next() + .expect("missing single spend"); + + let merkle_tree = self.merkle_tree(); + let merkle_proof = merkle_tree + .proof(member_puzzle_hash.into()) + .ok_or(DriverError::InvalidMerkleProof)?; + + ctx.alloc(&Vault1ofNSolution::new( + merkle_proof, + member_spend.puzzle, + member_spend.solution, + )) + } + Some(MofNOptimization::All) => { + let mut member_solutions = Vec::with_capacity(self.required); + + for member in &self.members { + let spend = member_spends + .get(&member.puzzle_hash()) + .ok_or(DriverError::MissingMemberSpend)?; + + member_solutions.push(spend.solution); + } + + ctx.alloc(&VaultNofNSolution::new(member_solutions)) + } + None => { + let puzzle_hashes: Vec = self + .members + .iter() + .map(|member| member.puzzle_hash().into()) + .collect(); + + let proof = m_of_n_proof(ctx, &puzzle_hashes, &member_spends)?; + + ctx.alloc(&VaultMofNSolution::new(proof)) + } + } + } + + fn optimization(&self) -> Option { + if self.required == 1 { + Some(MofNOptimization::One) + } else if self.required == self.members.len() { + Some(MofNOptimization::All) + } else { + None + } + } + + fn merkle_tree(&self) -> MerkleTree { + let leaves: Vec = self + .members + .iter() + .map(|member| member.puzzle_hash().into()) + .collect(); + MerkleTree::new(&leaves) + } +} + +impl VaultLayer for MofN { + fn puzzle_hash(&self) -> TreeHash { + match self.optimization() { + Some(MofNOptimization::One) => { + let merkle_tree = self.merkle_tree(); + Vault1ofNArgs::new(merkle_tree.root()).curry_tree_hash() + } + Some(MofNOptimization::All) => { + let members = self.members.iter().map(VaultLayer::puzzle_hash).collect(); + VaultNofNArgs::new(members).curry_tree_hash() + } + None => { + let merkle_tree = self.merkle_tree(); + VaultMofNArgs::new(self.required, merkle_tree.root()).curry_tree_hash() + } + } + } + + fn puzzle(&self, ctx: &mut SpendContext) -> Result { + match self.optimization() { + Some(MofNOptimization::One) => { + let merkle_tree = self.merkle_tree(); + ctx.curry(Vault1ofNArgs::new(merkle_tree.root())) + } + Some(MofNOptimization::All) => { + let members = self + .members + .iter() + .map(|member| member.puzzle(ctx)) + .collect::>()?; + ctx.curry(VaultNofNArgs::new(members)) + } + None => { + let merkle_tree = self.merkle_tree(); + ctx.curry(VaultMofNArgs::new(self.required, merkle_tree.root())) + } + } + } + + fn replace(self, known_puzzles: &KnownPuzzles) -> Self { + let required = self.required; + let members = self + .members + .into_iter() + .map(|member| member.replace(known_puzzles)) + .collect(); + Self { required, members } + } +} + +fn m_of_n_proof( + ctx: &mut SpendContext, + puzzle_hashes: &[Bytes32], + member_spends: &HashMap, +) -> Result { + if puzzle_hashes.len() == 1 { + let puzzle_hash = puzzle_hashes[0]; + + return if let Some(spend) = member_spends.get(&puzzle_hash.into()) { + ctx.alloc(&clvm_tuple!((), spend.puzzle, spend.solution)) + } else { + ctx.alloc(&Bytes32::from(tree_hash_atom(&puzzle_hash))) + }; + } + + let mid_index = puzzle_hashes.len().div_ceil(2); + let first = &puzzle_hashes[..mid_index]; + let rest = &puzzle_hashes[mid_index..]; + + let first_proof = m_of_n_proof(ctx, first, member_spends)?; + let rest_proof = m_of_n_proof(ctx, rest, member_spends)?; + + if first_proof.is_pair() || rest_proof.is_pair() { + ctx.alloc(&(first_proof, rest_proof)) + } else { + let first_hash = ctx.extract::(first_proof)?; + let rest_hash = ctx.extract::(rest_proof)?; + let pair_hash = Bytes32::from(tree_hash_pair(first_hash.into(), rest_hash.into())); + ctx.alloc(&pair_hash) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/vault/member.rs b/crates/chia-sdk-driver/src/primitives/vault/member.rs new file mode 100644 index 00000000..a8e5a1f4 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/vault/member.rs @@ -0,0 +1,82 @@ +use chia_bls::PublicKey; +use chia_sdk_types::{BlsMember, Mod}; +use clvm_utils::TreeHash; +use clvmr::NodePtr; + +use crate::{DriverError, SpendContext}; + +use super::{KnownPuzzles, MofN, PuzzleWithRestrictions, VaultLayer}; + +#[derive(Debug, Clone)] +pub struct Member { + puzzle_hash: TreeHash, + kind: MemberKind, +} + +#[derive(Debug, Clone)] +pub enum MemberKind { + Bls(BlsMember), + MofN(MofN), + Unknown, +} + +impl Member { + pub fn bls(public_key: PublicKey) -> Self { + let member = BlsMember::new(public_key); + Self { + puzzle_hash: member.curry_tree_hash(), + kind: MemberKind::Bls(member), + } + } + + pub fn m_of_n(required: usize, members: Vec>) -> Self { + let m_of_n = MofN::new(required, members).expect("invalid m_of_n"); + Self { + puzzle_hash: m_of_n.puzzle_hash(), + kind: MemberKind::MofN(m_of_n), + } + } + + pub fn unknown(puzzle_hash: TreeHash) -> Self { + Self { + puzzle_hash, + kind: MemberKind::Unknown, + } + } + + pub fn kind(&self) -> &MemberKind { + &self.kind + } +} + +impl VaultLayer for Member { + fn puzzle_hash(&self) -> TreeHash { + self.puzzle_hash + } + + fn puzzle(&self, ctx: &mut SpendContext) -> Result { + match &self.kind { + MemberKind::Bls(bls) => ctx.curry(bls), + MemberKind::MofN(m_of_n) => m_of_n.puzzle(ctx), + MemberKind::Unknown => Err(DriverError::UnknownPuzzle), + } + } + + fn replace(self, known_puzzles: &KnownPuzzles) -> Self { + let kind = known_puzzles + .members + .get(&self.puzzle_hash) + .cloned() + .unwrap_or(self.kind); + + let kind = match kind { + MemberKind::Bls(..) | MemberKind::Unknown => kind, + MemberKind::MofN(m_of_n) => MemberKind::MofN(m_of_n.replace(known_puzzles)), + }; + + Self { + puzzle_hash: self.puzzle_hash, + kind, + } + } +} diff --git a/crates/chia-sdk-driver/src/primitives/vault/memos.rs b/crates/chia-sdk-driver/src/primitives/vault/memos.rs new file mode 100644 index 00000000..07a2d7c1 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/vault/memos.rs @@ -0,0 +1,52 @@ +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct RestrictionMemo { + pub is_morpher: bool, + pub curried_puzzle_hash: Bytes32, + pub restriction: T, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct TimelockMemo { + pub seconds: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct MemberMemo { + pub curried_puzzle_hash: Bytes32, + pub member: T, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct BlsMemberMemo { + pub public_key: PublicKey, +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct MofNMemo { + pub required: usize, + pub members: Vec, +} + +pub fn from_memo(allocator: &Allocator, memo: NodePtr) -> Result { + let memo = MofNMemo::from_clvm(allocator, memo)?; + + if memo.members.len() < memo.required { + return Err(DriverError::InvalidMemo); + } + + let mut members = Vec::with_capacity(memo.members.len()); + + for member_memo in memo.members { + members.push(Member::from_memo(allocator, member_memo)?); + } + + Ok(Self { + required: memo.required, + members, + }) +} diff --git a/crates/chia-sdk-driver/src/primitives/vault/puzzle_with_restrictions.rs b/crates/chia-sdk-driver/src/primitives/vault/puzzle_with_restrictions.rs new file mode 100644 index 00000000..5d728b33 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/vault/puzzle_with_restrictions.rs @@ -0,0 +1,150 @@ +use chia_sdk_types::{ + DelegatedFeederArgs, DelegatedFeederSolution, IndexWrapperArgs, Mod, RestrictionsArgs, + RestrictionsSolution, +}; +use clvm_utils::TreeHash; +use clvmr::NodePtr; + +use crate::{DriverError, Spend, SpendContext}; + +use super::{KnownPuzzles, Restriction, VaultLayer}; + +#[derive(Debug, Clone)] +pub struct PuzzleWithRestrictions { + nonce: usize, + restrictions: Vec, + puzzle: T, + has_delegated_feeder: bool, +} + +impl PuzzleWithRestrictions { + pub fn top_level(nonce: usize, restrictions: Vec, puzzle: T) -> Self { + Self { + nonce, + restrictions, + puzzle, + has_delegated_feeder: true, + } + } + + pub fn inner(nonce: usize, restrictions: Vec, puzzle: T) -> Self { + Self { + nonce, + restrictions, + puzzle, + has_delegated_feeder: false, + } + } + + pub fn nonce(&self) -> usize { + self.nonce + } + + pub fn restrictions(&self) -> &[Restriction] { + &self.restrictions + } + + pub fn inner_puzzle(&self) -> &T { + &self.puzzle + } + + pub fn solve( + &self, + ctx: &mut SpendContext, + member_validator_solutions: Vec, + delegated_puzzle_validator_solutions: Vec, + inner_solution: NodePtr, + delegated_spend: Option, + ) -> Result { + let mut solution = inner_solution; + + if !self.restrictions.is_empty() { + solution = ctx.alloc(&RestrictionsSolution::new( + member_validator_solutions, + delegated_puzzle_validator_solutions, + solution, + ))?; + } + + if let Some(delegated_spend) = delegated_spend { + solution = ctx.alloc(&DelegatedFeederSolution::new( + delegated_spend.puzzle, + delegated_spend.solution, + solution, + ))?; + } + + Ok(solution) + } +} + +impl VaultLayer for PuzzleWithRestrictions +where + T: VaultLayer, +{ + fn puzzle_hash(&self) -> TreeHash { + let mut puzzle_hash = self.puzzle.puzzle_hash(); + + if !self.restrictions.is_empty() { + let mut member_validators = Vec::new(); + let mut delegated_puzzle_validators = Vec::new(); + + for restriction in &self.restrictions { + if restriction.is_member_condition_validator() { + member_validators.push(restriction.puzzle_hash()); + } else { + delegated_puzzle_validators.push(restriction.puzzle_hash()); + } + } + + puzzle_hash = + RestrictionsArgs::new(member_validators, delegated_puzzle_validators, puzzle_hash) + .curry_tree_hash(); + } + + if self.has_delegated_feeder { + puzzle_hash = DelegatedFeederArgs::new(puzzle_hash).curry_tree_hash(); + } + + IndexWrapperArgs::new(self.nonce, puzzle_hash).curry_tree_hash() + } + + fn puzzle(&self, ctx: &mut SpendContext) -> Result { + let mut puzzle = self.puzzle.puzzle(ctx)?; + + if !self.restrictions.is_empty() { + let mut member_validators = Vec::new(); + let mut delegated_puzzle_validators = Vec::new(); + + for restriction in &self.restrictions { + if restriction.is_member_condition_validator() { + member_validators.push(restriction.puzzle(ctx)?); + } else { + delegated_puzzle_validators.push(restriction.puzzle(ctx)?); + } + } + + puzzle = ctx.curry(RestrictionsArgs::new( + member_validators, + delegated_puzzle_validators, + puzzle, + ))?; + } + + if self.has_delegated_feeder { + puzzle = ctx.curry(DelegatedFeederArgs::new(puzzle))?; + } + + ctx.curry(IndexWrapperArgs::new(self.nonce, puzzle)) + } + + fn replace(mut self, known_puzzles: &KnownPuzzles) -> Self { + let mut restrictions = Vec::with_capacity(self.restrictions.len()); + for restriction in self.restrictions { + restrictions.push(restriction.replace(known_puzzles)); + } + self.restrictions = restrictions; + self.puzzle = self.puzzle.replace(known_puzzles); + self + } +} diff --git a/crates/chia-sdk-driver/src/primitives/vault/restriction.rs b/crates/chia-sdk-driver/src/primitives/vault/restriction.rs new file mode 100644 index 00000000..a2b76593 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/vault/restriction.rs @@ -0,0 +1,69 @@ +use chia_sdk_types::Timelock; +use clvm_utils::TreeHash; +use clvmr::NodePtr; + +use crate::{DriverError, SpendContext}; + +use super::{KnownPuzzles, VaultLayer}; + +#[derive(Debug, Clone, Copy)] +pub struct Restriction { + puzzle_hash: TreeHash, + is_member_condition_validator: bool, + kind: RestrictionKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RestrictionKind { + Timelock(Timelock), + Unknown, +} + +impl Restriction { + pub fn new( + puzzle_hash: TreeHash, + is_member_condition_validator: bool, + kind: RestrictionKind, + ) -> Self { + Self { + puzzle_hash, + is_member_condition_validator, + kind, + } + } + + pub fn is_member_condition_validator(&self) -> bool { + self.is_member_condition_validator + } + + pub fn kind(&self) -> RestrictionKind { + self.kind + } +} + +impl VaultLayer for Restriction { + fn puzzle_hash(&self) -> TreeHash { + self.puzzle_hash + } + + fn puzzle(&self, ctx: &mut SpendContext) -> Result { + match &self.kind { + RestrictionKind::Timelock(timelock) => ctx.curry(timelock), + RestrictionKind::Unknown => Err(DriverError::UnknownPuzzle), + } + } + + fn replace(self, known_puzzles: &KnownPuzzles) -> Self { + let kind = known_puzzles + .restrictions + .get(&self.puzzle_hash) + .copied() + .unwrap_or(self.kind); + + Self { + puzzle_hash: self.puzzle_hash, + is_member_condition_validator: self.is_member_condition_validator, + kind, + } + } +} diff --git a/crates/chia-sdk-driver/src/primitives/vault/vault_launcher.rs b/crates/chia-sdk-driver/src/primitives/vault/vault_launcher.rs new file mode 100644 index 00000000..1e9621a8 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/vault/vault_launcher.rs @@ -0,0 +1,34 @@ +use chia_puzzles::{EveProof, Proof}; +use chia_sdk_types::Conditions; +use clvm_traits::ToClvm; +use clvmr::Allocator; + +use crate::{DriverError, Launcher, SpendContext}; + +use super::{Member, PuzzleWithRestrictions, Vault, VaultLayer}; + +impl Launcher { + pub fn mint_vault( + self, + ctx: &mut SpendContext, + custody: PuzzleWithRestrictions, + memos: M, + ) -> Result<(Conditions, Vault), DriverError> + where + M: ToClvm, + { + let launcher_coin = self.coin(); + let custody_hash = custody.puzzle_hash(); + let (conditions, coin) = self.spend(ctx, custody_hash.into(), memos)?; + let vault = Vault { + coin, + launcher_id: launcher_coin.coin_id(), + proof: Proof::Eve(EveProof { + parent_parent_coin_info: launcher_coin.parent_coin_info, + parent_amount: launcher_coin.amount, + }), + custody, + }; + Ok((conditions, vault)) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/vault/vault_layer.rs b/crates/chia-sdk-driver/src/primitives/vault/vault_layer.rs new file mode 100644 index 00000000..57d72525 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/vault/vault_layer.rs @@ -0,0 +1,13 @@ +use clvm_utils::TreeHash; +use clvmr::NodePtr; + +use crate::{DriverError, SpendContext}; + +use super::KnownPuzzles; + +pub trait VaultLayer { + #[must_use] + fn replace(self, known_puzzles: &KnownPuzzles) -> Self; + fn puzzle_hash(&self) -> TreeHash; + fn puzzle(&self, ctx: &mut SpendContext) -> Result; +} diff --git a/crates/chia-sdk-driver/src/primitives/vault/vault_primitive.rs b/crates/chia-sdk-driver/src/primitives/vault/vault_primitive.rs new file mode 100644 index 00000000..731bdf3a --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/vault/vault_primitive.rs @@ -0,0 +1,182 @@ +use chia_protocol::{Bytes32, Coin}; +use chia_puzzles::{singleton::SingletonArgs, Proof}; +use chia_sdk_types::Mod; +use clvm_utils::TreeHash; +use clvmr::NodePtr; + +use crate::{DriverError, SpendContext}; + +use super::{KnownPuzzles, Member, PuzzleWithRestrictions, VaultLayer}; + +#[derive(Debug, Clone)] +pub struct Vault { + pub coin: Coin, + pub launcher_id: Bytes32, + pub proof: Proof, + pub custody: PuzzleWithRestrictions, +} + +impl VaultLayer for Vault { + fn puzzle_hash(&self) -> TreeHash { + SingletonArgs::new(self.launcher_id, self.custody.puzzle_hash()).curry_tree_hash() + } + + fn puzzle(&self, ctx: &mut SpendContext) -> Result { + let puzzle = self.custody.puzzle(ctx)?; + ctx.curry(SingletonArgs::new(self.launcher_id, puzzle)) + } + + fn replace(self, known_puzzles: &KnownPuzzles) -> Self { + Self { + coin: self.coin, + launcher_id: self.launcher_id, + proof: self.proof, + custody: self.custody.replace(known_puzzles), + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use chia_bls::DerivableKey; + use chia_puzzles::singleton::SingletonSolution; + use chia_sdk_test::Simulator; + use chia_sdk_types::{BlsMember, Conditions}; + use clvm_traits::clvm_quote; + + use crate::{Launcher, MemberKind, Spend, SpendContext, StandardLayer}; + + use super::*; + + #[test] + fn test_single_sig() -> 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 hidden_member = BlsMember::new(pk); + + let custody = PuzzleWithRestrictions::top_level( + 0, + Vec::new(), + Member::unknown(hidden_member.curry_tree_hash()), + ); + let (mint_vault, vault) = Launcher::new(coin.coin_id(), 1).mint_vault(ctx, custody, ())?; + p2.spend(ctx, coin, mint_vault)?; + sim.spend_coins(ctx.take(), &[sk.clone()])?; + + let mut known_members = HashMap::new(); + known_members.insert( + hidden_member.curry_tree_hash(), + MemberKind::Bls(hidden_member), + ); + let vault = vault.replace(&KnownPuzzles { + members: known_members, + ..Default::default() + }); + + let delegated_puzzle = ctx.alloc(&clvm_quote!(Conditions::new().create_coin( + vault.custody.puzzle_hash().into(), + vault.coin.amount, + None + )))?; + + let puzzle = vault.puzzle(ctx)?; + let inner_solution = vault.custody.solve( + ctx, + Vec::new(), + Vec::new(), + NodePtr::NIL, + Some(Spend { + puzzle: delegated_puzzle, + solution: NodePtr::NIL, + }), + )?; + let solution = ctx.alloc(&SingletonSolution { + lineage_proof: vault.proof, + amount: vault.coin.amount, + inner_solution, + })?; + + ctx.spend(vault.coin, Spend::new(puzzle, solution))?; + sim.spend_coins(ctx.take(), &[sk])?; + + Ok(()) + } + + #[test] + fn test_multi_sig() -> 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 (sk1, sk2) = (sk.derive_unhardened(1), sk.derive_unhardened(1)); + let (pk1, pk2) = (sk1.public_key(), sk2.public_key()); + + let custody = PuzzleWithRestrictions::top_level( + 0, + Vec::new(), + Member::m_of_n( + 1, + vec![ + PuzzleWithRestrictions::inner(0, Vec::new(), Member::bls(pk1)), + PuzzleWithRestrictions::inner(0, Vec::new(), Member::bls(pk2)), + ], + ), + ); + let (mint_vault, vault) = Launcher::new(coin.coin_id(), 1).mint_vault(ctx, custody, ())?; + p2.spend(ctx, coin, mint_vault)?; + sim.spend_coins(ctx.take(), &[sk.clone()])?; + + let delegated_puzzle = ctx.alloc(&clvm_quote!(Conditions::new().create_coin( + vault.custody.puzzle_hash().into(), + vault.coin.amount, + None + )))?; + + let puzzle = vault.puzzle(ctx)?; + + let MemberKind::MofN(m_of_n) = vault.custody.inner_puzzle().kind() else { + unreachable!(); + }; + let member_outer = PuzzleWithRestrictions::inner(0, Vec::new(), Member::bls(pk1)); + let member_puzzle = member_outer.puzzle(ctx)?; + let mut member_spends = HashMap::new(); + member_spends.insert( + member_outer.puzzle_hash(), + Spend::new( + member_puzzle, + member_outer.solve(ctx, Vec::new(), Vec::new(), NodePtr::NIL, None)?, + ), + ); + let m_of_n_solution = m_of_n.solve(ctx, member_spends)?; + + let inner_solution = vault.custody.solve( + ctx, + Vec::new(), + Vec::new(), + m_of_n_solution, + Some(Spend { + puzzle: delegated_puzzle, + solution: NodePtr::NIL, + }), + )?; + + let solution = ctx.alloc(&SingletonSolution { + lineage_proof: vault.proof, + amount: vault.coin.amount, + inner_solution, + })?; + + ctx.spend(vault.coin, Spend::new(puzzle, solution))?; + sim.spend_coins(ctx.take(), &[sk1])?; + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/spend_context.rs b/crates/chia-sdk-driver/src/spend_context.rs index de051cc2..f1b85938 100644 --- a/crates/chia-sdk-driver/src/spend_context.rs +++ b/crates/chia-sdk-driver/src/spend_context.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; -use chia_protocol::{Coin, CoinSpend, Program}; +use chia_protocol::{Bytes32, Coin, CoinSpend, Program}; use chia_puzzles::{ nft::{NFT_METADATA_UPDATER_PUZZLE, NFT_METADATA_UPDATER_PUZZLE_HASH}, offer::{SETTLEMENT_PAYMENTS_PUZZLE, SETTLEMENT_PAYMENTS_PUZZLE_HASH}, singleton::{SINGLETON_LAUNCHER_PUZZLE, SINGLETON_LAUNCHER_PUZZLE_HASH}, }; -use chia_sdk_types::{run_puzzle, Mod}; +use chia_sdk_types::{run_puzzle, Memos, Mod}; use clvm_traits::{FromClvm, ToClvm}; use clvm_utils::{tree_hash, CurriedProgram, TreeHash}; use clvmr::{serde::node_from_bytes, Allocator, NodePtr}; @@ -84,6 +84,17 @@ impl SpendContext { Ok(Program::from_clvm(&self.allocator, ptr)?) } + pub fn memos(&mut self, value: &T) -> Result, DriverError> + where + T: ToClvm, + { + Ok(Memos::new(self.alloc(value)?)) + } + + pub fn hint(&mut self, hint: Bytes32) -> Result, DriverError> { + Ok(Memos::hint(&mut self.allocator, hint)?) + } + pub fn alloc_mod(&mut self) -> Result where T: Mod, diff --git a/crates/chia-sdk-signer/Cargo.toml b/crates/chia-sdk-signer/Cargo.toml index 8b18d0a4..9fa42089 100644 --- a/crates/chia-sdk-signer/Cargo.toml +++ b/crates/chia-sdk-signer/Cargo.toml @@ -3,7 +3,7 @@ name = "chia-sdk-signer" version = "0.19.1" edition = "2021" license = "Apache-2.0" -description = "Calculates the BLS signatures required for coin spends in a transaction." +description = "Calculates the signatures required for coin spends in a transaction." authors = ["Brandon Haggstrom "] homepage = "https://github.com/Rigidity/chia-wallet-sdk" repository = "https://github.com/Rigidity/chia-wallet-sdk" diff --git a/crates/chia-sdk-signer/src/secp.rs b/crates/chia-sdk-signer/src/secp.rs index a81864f1..d8f53173 100644 --- a/crates/chia-sdk-signer/src/secp.rs +++ b/crates/chia-sdk-signer/src/secp.rs @@ -1,47 +1,5 @@ mod required_secp_signature; mod secp_dialect; -mod secp_public_key; -mod secp_secret_key; -mod secp_signature; pub use required_secp_signature::*; pub use secp_dialect::*; -pub use secp_public_key::*; -pub use secp_secret_key::*; -pub use secp_signature::*; - -#[cfg(test)] -mod tests { - use rand::{Rng, SeedableRng}; - use rand_chacha::ChaCha8Rng; - - use super::*; - - #[test] - fn test_secp_key() -> anyhow::Result<()> { - let mut rng = ChaCha8Rng::seed_from_u64(1337); - - let sk = SecpSecretKey::from_bytes(rng.gen())?; - assert_eq!( - hex::encode(sk.to_bytes()), - "ae491886341a539a1ccfaffcc9c78650ad1adc6270620c882b8d29bf6b9bc4cd" - ); - - let pk = sk.public_key(); - assert_eq!( - hex::encode(pk.to_bytes()), - "02827cdbbed87e45683d448be2ea15fb72ba3732247bda18474868cf5456123fb4" - ); - - let message_hash: [u8; 32] = rng.gen(); - let sig = sk.sign_prehashed(message_hash)?; - assert_eq!( - hex::encode(sig.to_bytes()), - "6f07897d1d28b8698af5dec5ca06907b1304b227dc9f740b8c4065cf04d5e8653ae66aa17063e7120ee7f22fae54373b35230e259244b90400b65cf00d86c591" - ); - - assert!(pk.verify_prehashed(message_hash, sig)); - - Ok(()) - } -} diff --git a/crates/chia-sdk-signer/src/secp/required_secp_signature.rs b/crates/chia-sdk-signer/src/secp/required_secp_signature.rs index 95c14acd..8a906e9f 100644 --- a/crates/chia-sdk-signer/src/secp/required_secp_signature.rs +++ b/crates/chia-sdk-signer/src/secp/required_secp_signature.rs @@ -1,16 +1,14 @@ +use chia_sdk_types::{Secp256k1PublicKey, Secp256r1PublicKey}; use clvmr::NodePtr; -use super::SecpPublicKey; - #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SecpOp { - K1, - R1, +pub enum SecpPublicKey { + K1(Secp256k1PublicKey), + R1(Secp256r1PublicKey), } #[derive(Debug, Clone, Copy)] pub struct RequiredSecpSignature { - pub op: SecpOp, pub public_key: SecpPublicKey, pub message_hash: [u8; 32], pub placeholder_ptr: NodePtr, diff --git a/crates/chia-sdk-signer/src/secp/secp_dialect.rs b/crates/chia-sdk-signer/src/secp/secp_dialect.rs index 994aafcd..105192fe 100644 --- a/crates/chia-sdk-signer/src/secp/secp_dialect.rs +++ b/crates/chia-sdk-signer/src/secp/secp_dialect.rs @@ -1,6 +1,7 @@ use std::cell::RefCell; use chia_protocol::Bytes32; +use chia_sdk_types::{Secp256k1PublicKey, Secp256r1PublicKey}; use clvm_traits::FromClvm; use clvmr::{ cost::Cost, @@ -10,12 +11,10 @@ use clvmr::{ Allocator, NodePtr, }; -use crate::SecpOp; - use super::{RequiredSecpSignature, SecpPublicKey}; -const SECP256R1_VERIFY_COST: Cost = 1850000; -const SECP256K1_VERIFY_COST: Cost = 1300000; +const SECP256R1_VERIFY_COST: Cost = 1_850_000; +const SECP256K1_VERIFY_COST: Cost = 1_300_000; #[derive(Debug, Default, Clone)] pub struct SecpDialect { @@ -78,18 +77,22 @@ where let atom = allocator.atom(op); let opcode = u32::from_be_bytes(atom.as_ref().try_into().unwrap()); - let (op, name, cost) = match opcode { + let (r1, name, cost) = match opcode { // We special case these opcodes and allow the response to pass through otherwise. // If new operators are added to the main dialect, they likely shouldn't be included here. // We're using the same cost to ensure that softfork conditions behave the same. - 0x13d61f00 => (SecpOp::K1, "secp256k1_verify", SECP256K1_VERIFY_COST), - 0x1c3a8f00 => (SecpOp::R1, "secp256r1_verify", SECP256R1_VERIFY_COST), + 0x13d6_1f00 => (false, "secp256k1_verify", SECP256K1_VERIFY_COST), + 0x1c3a_8f00 => (true, "secp256r1_verify", SECP256R1_VERIFY_COST), _ => return response, }; let [pubkey, msg, sig] = get_args::<3>(allocator, args, name)?; - let Ok(public_key) = SecpPublicKey::from_clvm(allocator, pubkey) else { + let Ok(public_key) = (if r1 { + Secp256r1PublicKey::from_clvm(allocator, pubkey).map(SecpPublicKey::R1) + } else { + Secp256k1PublicKey::from_clvm(allocator, pubkey).map(SecpPublicKey::K1) + }) else { return response; }; @@ -98,7 +101,6 @@ where }; self.collected_ops.borrow_mut().push(RequiredSecpSignature { - op, public_key, message_hash: message_hash.to_bytes(), placeholder_ptr: sig, @@ -111,13 +113,12 @@ where #[cfg(test)] mod tests { use chia_protocol::Bytes; + use chia_sdk_types::Secp256k1SecretKey; use clvm_traits::{clvm_list, clvm_quote, ToClvm}; use clvmr::{run_program, ChiaDialect}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; - use crate::SecpSecretKey; - use super::*; #[test] @@ -126,7 +127,7 @@ mod tests { let mut rng = ChaCha8Rng::seed_from_u64(1337); let op = Bytes::from(vec![0x13, 0xd6, 0x1f, 0x00]); - let public_key = SecpSecretKey::from_bytes(rng.gen())?.public_key(); + let public_key = Secp256k1SecretKey::from_bytes(rng.gen())?.public_key(); let fake_sig = a.new_atom(&[1, 2, 3])?; let message = a.new_atom(&[42; 32])?; let program = clvm_list!( @@ -146,8 +147,7 @@ mod tests { assert_eq!(collected.len(), 1); let item = collected.remove(0); - assert_eq!(item.op, SecpOp::K1); - assert_eq!(item.public_key, public_key); + assert_eq!(item.public_key, SecpPublicKey::K1(public_key)); assert_eq!(item.placeholder_ptr, fake_sig); Ok(()) diff --git a/crates/chia-sdk-signer/src/secp/secp_secret_key.rs b/crates/chia-sdk-signer/src/secp/secp_secret_key.rs deleted file mode 100644 index fabc0ff9..00000000 --- a/crates/chia-sdk-signer/src/secp/secp_secret_key.rs +++ /dev/null @@ -1,28 +0,0 @@ -use k256::ecdsa::SigningKey; - -use crate::SignerError; - -use super::{SecpPublicKey, SecpSignature}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SecpSecretKey(SigningKey); - -impl SecpSecretKey { - pub fn to_bytes(&self) -> [u8; 32] { - self.0.to_bytes().into() - } - - pub fn from_bytes(bytes: [u8; 32]) -> Result { - Ok(Self(SigningKey::from_bytes((&bytes).into())?)) - } - - pub fn public_key(&self) -> SecpPublicKey { - SecpPublicKey(*self.0.verifying_key()) - } - - pub fn sign_prehashed(&self, message_hash: [u8; 32]) -> Result { - Ok(SecpSignature( - self.0.sign_prehash_recoverable(&message_hash)?.0, - )) - } -} diff --git a/crates/chia-sdk-test/src/peer_simulator.rs b/crates/chia-sdk-test/src/peer_simulator.rs index b7022dbf..851c84cd 100644 --- a/crates/chia-sdk-test/src/peer_simulator.rs +++ b/crates/chia-sdk-test/src/peer_simulator.rs @@ -166,7 +166,8 @@ mod tests { Bytes, CoinSpend, CoinStateFilters, CoinStateUpdate, RespondCoinState, RespondPuzzleState, SpendBundle, }; - use chia_sdk_types::{AggSigMe, CreateCoin, Remark}; + use chia_sdk_types::{AggSigMe, CreateCoin, Memos, Remark}; + use clvmr::NodePtr; use crate::{coin_state_updates, test_secret_key, test_transaction, to_program, to_puzzle}; @@ -363,7 +364,7 @@ mod tests { vec![CoinSpend::new( coin, puzzle_reveal, - to_program([CreateCoin::new(puzzle_hash, 1, Vec::new())])?, + to_program([CreateCoin::::new(puzzle_hash, 1, None)])?, )], Signature::default(), ); @@ -388,7 +389,11 @@ mod tests { vec![CoinSpend::new( coin, puzzle_reveal.clone(), - to_program([CreateCoin::new(puzzle_hash, coin.amount - 1, Vec::new())])?, + to_program([CreateCoin::::new( + puzzle_hash, + coin.amount - 1, + None, + )])?, )], Signature::default(), ); @@ -450,8 +455,8 @@ mod tests { coin, puzzle_reveal, to_program([ - CreateCoin::new(puzzle_hash, 1, Vec::new()), - CreateCoin::new(puzzle_hash, 2, Vec::new()), + CreateCoin::::new(puzzle_hash, 1, None), + CreateCoin::::new(puzzle_hash, 2, None), ])?, )], Signature::default(), @@ -578,7 +583,7 @@ mod tests { vec![CoinSpend::new( coin, puzzle_reveal, - to_program([CreateCoin::new(puzzle_hash, 1, Vec::new())])?, + to_program([CreateCoin::::new(puzzle_hash, 1, None)])?, )], Signature::default(), ); @@ -660,7 +665,7 @@ mod tests { vec![CoinSpend::new( coin, puzzle_reveal, - to_program([CreateCoin::new(child_coin.puzzle_hash, 1, Vec::new())])?, + to_program([CreateCoin::::new(child_coin.puzzle_hash, 1, None)])?, )], Signature::default(), ); @@ -745,7 +750,7 @@ mod tests { vec![CoinSpend::new( coin, puzzle_reveal, - to_program([CreateCoin::new(puzzle_hash, 0, vec![hint.into()])])?, + to_program([CreateCoin::new(puzzle_hash, 0, Some(Memos::new([hint])))])?, )], Signature::default(), ); diff --git a/crates/chia-sdk-types/Cargo.toml b/crates/chia-sdk-types/Cargo.toml index 3e2324ac..c30569d3 100644 --- a/crates/chia-sdk-types/Cargo.toml +++ b/crates/chia-sdk-types/Cargo.toml @@ -16,6 +16,7 @@ workspace = true [features] chip-0035 = [] +vault = [] [dependencies] chia-sdk-derive = { workspace = true } @@ -28,8 +29,12 @@ clvm-utils = { workspace = true } clvmr = { workspace = true } hex-literal = { workspace = true } once_cell = { workspace = true } +k256 = { workspace = true } +p256 = { workspace = true } [dev-dependencies] hex = { workspace = true } anyhow = { workspace = true } rstest = { workspace = true } +rand = { workspace = true } +rand_chacha = { workspace = true } diff --git a/crates/chia-sdk-types/src/condition.rs b/crates/chia-sdk-types/src/condition.rs index dece1124..d3531afb 100644 --- a/crates/chia-sdk-types/src/condition.rs +++ b/crates/chia-sdk-types/src/condition.rs @@ -1,11 +1,12 @@ use chia_bls::PublicKey; use chia_protocol::{Bytes, Bytes32}; use chia_sdk_derive::conditions; -use clvm_traits::{FromClvm, ToClvm}; +use clvm_traits::{FromClvm, ToClvm, ToClvmError}; mod agg_sig; pub use agg_sig::*; +use clvmr::{Allocator, NodePtr}; conditions! { pub enum Condition { @@ -53,11 +54,11 @@ conditions! { public_key: PublicKey, message: Bytes, }, - CreateCoin { + CreateCoin as Copy { opcode: i8 if 51, puzzle_hash: Bytes32, amount: u64, - memos?: Vec, + ...memos: Option>, }, ReserveFee as Copy { opcode: i8 if 52, @@ -194,6 +195,30 @@ conditions! { } } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct Memos { + pub value: T, +} + +impl Memos { + pub fn new(value: T) -> Self { + Self { value } + } + + pub fn some(value: T) -> Option { + Some(Self { value }) + } +} + +impl Memos { + pub fn hint(allocator: &mut Allocator, hint: Bytes32) -> Result { + Ok(Self { + value: [hint].to_clvm(allocator)?, + }) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] #[clvm(list)] pub struct NewMetadataInfo { diff --git a/crates/chia-sdk-types/src/lib.rs b/crates/chia-sdk-types/src/lib.rs index 9f81ac23..2700947e 100644 --- a/crates/chia-sdk-types/src/lib.rs +++ b/crates/chia-sdk-types/src/lib.rs @@ -5,6 +5,7 @@ mod merkle_tree; mod puzzle_mod; mod puzzles; mod run_puzzle; +mod secp; pub use condition::*; pub use conditions::*; @@ -13,3 +14,4 @@ pub use merkle_tree::*; pub use puzzle_mod::*; pub use puzzles::*; pub use run_puzzle::*; +pub use secp::*; diff --git a/crates/chia-sdk-types/src/puzzle_mod.rs b/crates/chia-sdk-types/src/puzzle_mod.rs index 6d6c0822..8a4bd699 100644 --- a/crates/chia-sdk-types/src/puzzle_mod.rs +++ b/crates/chia-sdk-types/src/puzzle_mod.rs @@ -35,6 +35,14 @@ pub trait Mod { } } +impl Mod for &T +where + T: Mod, +{ + const MOD_REVEAL: &'static [u8] = T::MOD_REVEAL; + const MOD_HASH: TreeHash = T::MOD_HASH; +} + impl Mod for StandardArgs { const MOD_REVEAL: &[u8] = &STANDARD_PUZZLE; const MOD_HASH: TreeHash = STANDARD_PUZZLE_HASH; diff --git a/crates/chia-sdk-types/src/puzzles.rs b/crates/chia-sdk-types/src/puzzles.rs index 276e7b7c..ae321e27 100644 --- a/crates/chia-sdk-types/src/puzzles.rs +++ b/crates/chia-sdk-types/src/puzzles.rs @@ -18,6 +18,12 @@ mod datalayer; #[cfg(feature = "chip-0035")] pub use datalayer::*; +#[cfg(feature = "vault")] +mod vault; + +#[cfg(feature = "vault")] +pub use vault::*; + #[cfg(test)] mod tests { #[macro_export] diff --git a/crates/chia-sdk-types/src/puzzles/vault.rs b/crates/chia-sdk-types/src/puzzles/vault.rs new file mode 100644 index 00000000..4c16064c --- /dev/null +++ b/crates/chia-sdk-types/src/puzzles/vault.rs @@ -0,0 +1,15 @@ +mod delegated_feeder; +mod index_wrapper; +mod members; +mod restrictions; +mod vault_1_of_n; +mod vault_m_of_n; +mod vault_n_of_n; + +pub use delegated_feeder::*; +pub use index_wrapper::*; +pub use members::*; +pub use restrictions::*; +pub use vault_1_of_n::*; +pub use vault_m_of_n::*; +pub use vault_n_of_n::*; diff --git a/crates/chia-sdk-types/src/puzzles/vault/delegated_feeder.rs b/crates/chia-sdk-types/src/puzzles/vault/delegated_feeder.rs new file mode 100644 index 00000000..07324550 --- /dev/null +++ b/crates/chia-sdk-types/src/puzzles/vault/delegated_feeder.rs @@ -0,0 +1,57 @@ +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 DelegatedFeederArgs { + pub inner_puzzle: I, +} + +impl DelegatedFeederArgs { + pub fn new(inner_puzzle: I) -> Self { + Self { inner_puzzle } + } +} + +impl Mod for DelegatedFeederArgs { + const MOD_REVEAL: &[u8] = &DELEGATED_FEEDER_PUZZLE; + const MOD_HASH: TreeHash = DELEGATED_FEEDER_PUZZLE_HASH; +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(solution)] +pub struct DelegatedFeederSolution { + pub delegated_puzzle: P, + pub delegated_solution: S, + #[clvm(rest)] + pub inner_solution: I, +} + +impl DelegatedFeederSolution { + pub fn new(delegated_puzzle: P, delegated_solution: S, inner_solution: I) -> Self { + Self { + delegated_puzzle, + delegated_solution, + inner_solution, + } + } +} + +pub const DELEGATED_FEEDER_PUZZLE: [u8; 203] = hex!( + " + ff02ffff01ff02ff04ffff04ff02ffff04ffff02ff05ffff04ffff02ff06ffff + 04ff02ffff04ff0bff80808080ff1f8080ffff04ffff02ff0bff1780ff808080 + 8080ffff04ffff01ffff02ffff03ff05ffff01ff04ff09ffff02ff04ffff04ff + 02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff02ffff03ff + ff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff8080 + 8080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101 + ff058080ff0180ff018080 + " +); + +pub const DELEGATED_FEEDER_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + "9db33d93853179903d4dd272a00345ee6630dc94907dbcdd96368df6931060fd" +)); diff --git a/crates/chia-sdk-types/src/puzzles/vault/index_wrapper.rs b/crates/chia-sdk-types/src/puzzles/vault/index_wrapper.rs new file mode 100644 index 00000000..4a8d61ab --- /dev/null +++ b/crates/chia-sdk-types/src/puzzles/vault/index_wrapper.rs @@ -0,0 +1,32 @@ +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 IndexWrapperArgs { + pub index: usize, + pub inner_puzzle: I, +} + +impl IndexWrapperArgs { + pub fn new(index: usize, inner_puzzle: I) -> Self { + Self { + index, + inner_puzzle, + } + } +} + +impl Mod for IndexWrapperArgs { + const MOD_REVEAL: &[u8] = &INDEX_WRAPPER; + const MOD_HASH: TreeHash = INDEX_WRAPPER_HASH; +} + +pub const INDEX_WRAPPER: [u8; 7] = hex!("ff02ff05ff0780"); + +pub const INDEX_WRAPPER_HASH: TreeHash = TreeHash::new(hex!( + "847d971ef523417d555ea9854b1612837155d34d453298defcd310774305f657" +)); diff --git a/crates/chia-sdk-types/src/puzzles/vault/members.rs b/crates/chia-sdk-types/src/puzzles/vault/members.rs new file mode 100644 index 00000000..f0c02cf0 --- /dev/null +++ b/crates/chia-sdk-types/src/puzzles/vault/members.rs @@ -0,0 +1,3 @@ +mod bls_member; + +pub use bls_member::*; diff --git a/crates/chia-sdk-types/src/puzzles/vault/members/bls_member.rs b/crates/chia-sdk-types/src/puzzles/vault/members/bls_member.rs new file mode 100644 index 00000000..f1565c62 --- /dev/null +++ b/crates/chia-sdk-types/src/puzzles/vault/members/bls_member.rs @@ -0,0 +1,34 @@ +use chia_bls::PublicKey; +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 BlsMember { + pub public_key: PublicKey, +} + +impl BlsMember { + pub fn new(public_key: PublicKey) -> Self { + Self { public_key } + } +} + +impl Mod for BlsMember { + const MOD_REVEAL: &[u8] = &BLS_MEMBER; + const MOD_HASH: TreeHash = BLS_MEMBER_HASH; +} + +pub const BLS_MEMBER: [u8; 41] = hex!( + " + ff02ffff01ff04ffff04ff02ffff04ff05ffff04ff0bff80808080ff8080ffff + 04ffff0132ff018080 + " +); + +pub const BLS_MEMBER_HASH: TreeHash = TreeHash::new(hex!( + "21a3ae8b3ce64d41ca98d6d8df8f465c9e1bfb19ab40284a5da8479ba7fade78" +)); diff --git a/crates/chia-sdk-types/src/puzzles/vault/restrictions.rs b/crates/chia-sdk-types/src/puzzles/vault/restrictions.rs new file mode 100644 index 00000000..313a40df --- /dev/null +++ b/crates/chia-sdk-types/src/puzzles/vault/restrictions.rs @@ -0,0 +1,74 @@ +mod timelock; + +pub use timelock::*; + +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::TreeHash; +use hex_literal::hex; + +use crate::Mod; + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(curry)] +pub struct RestrictionsArgs { + pub member_validators: Vec, + pub delegated_puzzle_validators: Vec, + pub inner_puzzle: I, +} + +impl RestrictionsArgs { + pub fn new( + member_validators: Vec, + delegated_puzzle_validators: Vec, + inner_puzzle: I, + ) -> Self { + Self { + member_validators, + delegated_puzzle_validators, + inner_puzzle, + } + } +} + +impl Mod for RestrictionsArgs { + const MOD_REVEAL: &[u8] = &RESTRICTIONS_PUZZLE; + const MOD_HASH: TreeHash = RESTRICTIONS_PUZZLE_HASH; +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(solution)] +pub struct RestrictionsSolution { + pub member_validator_solutions: Vec, + pub delegated_puzzle_validator_solutions: Vec, + pub inner_solution: I, +} + +impl RestrictionsSolution { + pub fn new( + member_validator_solutions: Vec, + delegated_puzzle_validator_solutions: Vec, + inner_solution: I, + ) -> Self { + Self { + member_validator_solutions, + delegated_puzzle_validator_solutions, + inner_solution, + } + } +} + +pub const RESTRICTIONS_PUZZLE: [u8; 204] = hex!( + " + ff02ffff01ff02ff04ffff04ff02ffff04ff05ffff04ff5fffff04ffff02ff17 + ffff04ff2fff82017f8080ffff04ffff02ff06ffff04ff02ffff04ff0bffff04 + ff81bfffff04ff2fff808080808080ff80808080808080ffff04ffff01ffff03 + ff80ffff02ff06ffff04ff02ffff04ff05ffff04ff0bffff04ff17ff80808080 + 8080ff1780ff02ffff03ff05ffff01ff03ff80ffff02ff09ffff04ff17ff1380 + 80ffff02ff06ffff04ff02ffff04ff0dffff04ff1bffff04ff17ff8080808080 + 8080ff8080ff0180ff018080 + " +); + +pub const RESTRICTIONS_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + "a28d59d39f964a93159c986b1914694f6f2f1c9901178f91e8b0ba4045980eef" +)); diff --git a/crates/chia-sdk-types/src/puzzles/vault/restrictions/timelock.rs b/crates/chia-sdk-types/src/puzzles/vault/restrictions/timelock.rs new file mode 100644 index 00000000..d43ac381 --- /dev/null +++ b/crates/chia-sdk-types/src/puzzles/vault/restrictions/timelock.rs @@ -0,0 +1,36 @@ +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 Timelock { + pub seconds: u64, +} + +impl Timelock { + pub fn new(seconds: u64) -> Self { + Self { seconds } + } +} + +impl Mod for Timelock { + const MOD_REVEAL: &[u8] = &TIMELOCK_PUZZLE; + const MOD_HASH: TreeHash = TIMELOCK_PUZZLE_HASH; +} + +pub const TIMELOCK_PUZZLE: [u8; 137] = hex!( + " + ff02ffff01ff02ff06ffff04ff02ffff04ff05ffff04ff0bff8080808080ffff + 04ffff01ff50ff02ffff03ffff02ffff03ffff09ff23ff0480ffff01ff02ffff + 03ffff09ff53ff0580ffff01ff0101ff8080ff0180ff8080ff0180ffff010bff + ff01ff04ff13ffff02ff06ffff04ff02ffff04ff05ffff04ff1bff8080808080 + 8080ff0180ff018080 + " +); + +pub const TIMELOCK_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + "a6f96d8ecf9bd29e8c41822d231408823707b587bc0d372e5db4ac9733cbea3c" +)); diff --git a/crates/chia-sdk-types/src/puzzles/vault/vault_1_of_n.rs b/crates/chia-sdk-types/src/puzzles/vault/vault_1_of_n.rs new file mode 100644 index 00000000..8719976a --- /dev/null +++ b/crates/chia-sdk-types/src/puzzles/vault/vault_1_of_n.rs @@ -0,0 +1,59 @@ +use chia_protocol::Bytes32; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::TreeHash; +use hex_literal::hex; + +use crate::{MerkleProof, Mod}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(curry)] +pub struct Vault1ofNArgs { + pub merkle_root: Bytes32, +} + +impl Vault1ofNArgs { + pub fn new(merkle_root: Bytes32) -> Self { + Self { merkle_root } + } +} + +impl Mod for Vault1ofNArgs { + const MOD_REVEAL: &[u8] = &VAULT_1_OF_N_PUZZLE; + const MOD_HASH: TreeHash = VAULT_1_OF_N_PUZZLE_HASH; +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(solution)] +pub struct Vault1ofNSolution { + pub merkle_proof: MerkleProof, + pub member_puzzle: P, + pub member_solution: S, +} + +impl Vault1ofNSolution { + pub fn new(merkle_proof: MerkleProof, member_puzzle: P, member_solution: S) -> Self { + Self { + merkle_proof, + member_puzzle, + member_solution, + } + } +} + +pub const VAULT_1_OF_N_PUZZLE: [u8; 286] = hex!( + " + ff02ffff01ff02ffff03ffff09ff05ffff02ff06ffff04ff02ffff04ffff0bff + ff0101ffff02ff04ffff04ff02ffff04ff2fff8080808080ffff04ff17ff8080 + 80808080ffff01ff02ff2fffff04ff0bff5f8080ffff01ff088080ff0180ffff + 04ffff01ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff04ffff + 04ff02ffff04ff09ff80808080ffff02ff04ffff04ff02ffff04ff0dff808080 + 8080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff1bffff01ff02ff06 + ffff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff01 + 02ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff + 17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff018080 + " +); + +pub const VAULT_1_OF_N_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + "bcb9aa74893bebcfa2da87271b0330bf2773b6391144ae72262b6824d9c55939" +)); diff --git a/crates/chia-sdk-types/src/puzzles/vault/vault_m_of_n.rs b/crates/chia-sdk-types/src/puzzles/vault/vault_m_of_n.rs new file mode 100644 index 00000000..3a9e6911 --- /dev/null +++ b/crates/chia-sdk-types/src/puzzles/vault/vault_m_of_n.rs @@ -0,0 +1,68 @@ +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 VaultMofNArgs { + pub required: usize, + pub merkle_root: Bytes32, +} + +impl VaultMofNArgs { + pub fn new(required: usize, merkle_root: Bytes32) -> Self { + Self { + required, + merkle_root, + } + } +} + +impl Mod for VaultMofNArgs { + const MOD_REVEAL: &[u8] = &VAULT_M_OF_N_PUZZLE; + const MOD_HASH: TreeHash = VAULT_M_OF_N_PUZZLE_HASH; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(solution)] +pub struct VaultMofNSolution

{ + pub proofs: P, +} + +impl

VaultMofNSolution

{ + pub fn new(proofs: P) -> Self { + Self { proofs } + } +} + +pub const VAULT_M_OF_N_PUZZLE: [u8; 622] = hex!( + " + ff02ffff01ff02ff16ffff04ff02ffff04ff05ffff04ff0bffff04ffff02ff0c + ffff04ff02ffff04ff2fffff04ff17ff8080808080ff808080808080ffff04ff + ff01ffffff02ffff03ffff07ff0580ffff01ff02ff0cffff04ff02ffff04ff05 + ffff04ff0bff8080808080ffff01ff04ff05ffff01ff80ff80808080ff0180ff + 02ffff03ff09ffff01ff04ffff0bffff0102ffff05ffff02ff08ffff04ff02ff + ff04ff09ffff04ff0bff808080808080ffff05ffff02ff08ffff04ff02ffff04 + ff0dffff04ff0bff80808080808080ffff04ffff02ff0affff04ff02ffff04ff + ff05ffff06ffff02ff08ffff04ff02ffff04ff09ffff04ff0bff808080808080 + 80ffff04ffff05ffff06ffff02ff08ffff04ff02ffff04ff0dffff04ff0bff80 + 808080808080ff8080808080ffff04ffff10ffff05ffff06ffff06ffff02ff08 + ffff04ff02ffff04ff09ffff04ff0bff8080808080808080ffff05ffff06ffff + 06ffff02ff08ffff04ff02ffff04ff0dffff04ff0bff808080808080808080ff + 80808080ffff01ff04ffff0bffff0101ffff02ff1effff04ff02ffff04ff15ff + 8080808080ffff04ffff02ff15ffff04ff0bff1d8080ffff01ff0180808080ff + 0180ffff02ffff03ff05ffff01ff04ff09ffff02ff0affff04ff02ffff04ff0d + ffff04ff0bff808080808080ffff010b80ff0180ffff02ffff03ffff22ffff09 + ff05ff81b780ffff09ff0bff278080ffff0157ffff01ff088080ff0180ff02ff + ff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09 + ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bff + ff0101ff058080ff0180ff018080 + " +); + +pub const VAULT_M_OF_N_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + "de27deb2ebc7f1e1b77e1d38cc2f9d90fbd54d4b13dd4e6fa1f659177e36ed4f" +)); diff --git a/crates/chia-sdk-types/src/puzzles/vault/vault_n_of_n.rs b/crates/chia-sdk-types/src/puzzles/vault/vault_n_of_n.rs new file mode 100644 index 00000000..0ceccf1d --- /dev/null +++ b/crates/chia-sdk-types/src/puzzles/vault/vault_n_of_n.rs @@ -0,0 +1,51 @@ +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::TreeHash; +use hex_literal::hex; + +use crate::Mod; + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(curry)] +pub struct VaultNofNArgs { + pub members: Vec, +} + +impl VaultNofNArgs { + pub fn new(members: Vec) -> Self { + Self { members } + } +} + +impl Mod for VaultNofNArgs { + const MOD_REVEAL: &[u8] = &VAULT_N_OF_N_PUZZLE; + const MOD_HASH: TreeHash = VAULT_N_OF_N_PUZZLE_HASH; +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(solution)] +pub struct VaultNofNSolution { + pub member_solutions: Vec, +} + +impl VaultNofNSolution { + pub fn new(member_solutions: Vec) -> Self { + Self { member_solutions } + } +} + +pub const VAULT_N_OF_N_PUZZLE: [u8; 243] = hex!( + " + ff02ffff01ff02ff04ffff04ff02ffff04ff05ffff04ff17ffff04ff0bff8080 + 80808080ffff04ffff01ffff02ffff03ff0dffff01ff02ff0affff04ff02ffff + 04ffff02ff0effff04ff02ffff04ff09ffff04ff13ffff04ff17ff8080808080 + 80ffff04ffff02ff04ffff04ff02ffff04ff0dffff04ff1bffff04ff17ff8080 + 80808080ff8080808080ffff01ff02ff0effff04ff02ffff04ff09ffff04ff13 + ffff04ff17ff80808080808080ff0180ffff02ffff03ff05ffff01ff04ff09ff + ff02ff0affff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff + 0180ff02ff05ffff04ff17ff0b8080ff018080 + " +); + +pub const VAULT_N_OF_N_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + "d4394f50cb1d6ef130788db2e69ab0087ef79b0737179f201c1d1d2a52df1e59" +)); diff --git a/crates/chia-sdk-types/src/secp.rs b/crates/chia-sdk-types/src/secp.rs new file mode 100644 index 00000000..a0a2d83a --- /dev/null +++ b/crates/chia-sdk-types/src/secp.rs @@ -0,0 +1,77 @@ +mod secp256k1_public_key; +mod secp256k1_secret_key; +mod secp256k1_signature; +mod secp256r1_public_key; +mod secp256r1_secret_key; +mod secp256r1_signature; + +pub use secp256k1_public_key::*; +pub use secp256k1_secret_key::*; +pub use secp256k1_signature::*; +pub use secp256r1_public_key::*; +pub use secp256r1_secret_key::*; +pub use secp256r1_signature::*; + +#[cfg(test)] +mod tests { + use rand::{Rng, SeedableRng}; + use rand_chacha::ChaCha8Rng; + + use super::*; + + #[test] + fn test_secp256k1_key() -> anyhow::Result<()> { + let mut rng = ChaCha8Rng::seed_from_u64(1337); + + let sk = Secp256k1SecretKey::from_bytes(rng.gen())?; + assert_eq!( + hex::encode(sk.to_bytes()), + "ae491886341a539a1ccfaffcc9c78650ad1adc6270620c882b8d29bf6b9bc4cd" + ); + + let pk = sk.public_key(); + assert_eq!( + hex::encode(pk.to_bytes()), + "02827cdbbed87e45683d448be2ea15fb72ba3732247bda18474868cf5456123fb4" + ); + + let message_hash: [u8; 32] = rng.gen(); + let sig = sk.sign_prehashed(message_hash)?; + assert_eq!( + hex::encode(sig.to_bytes()), + "6f07897d1d28b8698af5dec5ca06907b1304b227dc9f740b8c4065cf04d5e8653ae66aa17063e7120ee7f22fae54373b35230e259244b90400b65cf00d86c591" + ); + + assert!(pk.verify_prehashed(message_hash, sig)); + + Ok(()) + } + + #[test] + fn test_secp256r1_key() -> anyhow::Result<()> { + let mut rng = ChaCha8Rng::seed_from_u64(1337); + + let sk = Secp256r1SecretKey::from_bytes(rng.gen())?; + assert_eq!( + hex::encode(sk.to_bytes()), + "ae491886341a539a1ccfaffcc9c78650ad1adc6270620c882b8d29bf6b9bc4cd" + ); + + let pk = sk.public_key(); + assert_eq!( + hex::encode(pk.to_bytes()), + "037dc85102f5eb7867b9580fea8b242c774173e1a47db320c798242d3a7a7579e4" + ); + + let message_hash: [u8; 32] = rng.gen(); + let sig = sk.sign_prehashed(message_hash)?; + assert_eq!( + hex::encode(sig.to_bytes()), + "550e83da8cf9b2d407ed093ae213869ebd7ceaea603920f87d535690e52b40537915d8fe3d5a96c87e700c56dc638c32f7a2954f2ba409367d1a132000cc2228" + ); + + assert!(pk.verify_prehashed(message_hash, sig)); + + Ok(()) + } +} diff --git a/crates/chia-sdk-signer/src/secp/secp_public_key.rs b/crates/chia-sdk-types/src/secp/secp256k1_public_key.rs similarity index 74% rename from crates/chia-sdk-signer/src/secp/secp_public_key.rs rename to crates/chia-sdk-types/src/secp/secp256k1_public_key.rs index 0749bd10..ff79f8f8 100644 --- a/crates/chia-sdk-signer/src/secp/secp_public_key.rs +++ b/crates/chia-sdk-types/src/secp/secp256k1_public_key.rs @@ -1,32 +1,30 @@ use clvm_traits::{ClvmDecoder, ClvmEncoder, FromClvm, FromClvmError, ToClvm, ToClvmError}; use clvmr::Atom; use k256::ecdsa::signature::hazmat::PrehashVerifier; -use k256::ecdsa::VerifyingKey; +use k256::ecdsa::{Error, VerifyingKey}; -use crate::SignerError; - -use super::SecpSignature; +use super::Secp256k1Signature; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SecpPublicKey(pub(crate) VerifyingKey); +pub struct Secp256k1PublicKey(pub(crate) VerifyingKey); -impl SecpPublicKey { +impl Secp256k1PublicKey { pub const SIZE: usize = 33; pub fn to_bytes(&self) -> [u8; Self::SIZE] { - self.0.to_sec1_bytes().as_ref().try_into().unwrap() + self.0.to_encoded_point(true).as_ref().try_into().unwrap() } - pub fn from_bytes(bytes: [u8; Self::SIZE]) -> Result { + pub fn from_bytes(bytes: [u8; Self::SIZE]) -> Result { Ok(Self(VerifyingKey::from_sec1_bytes(&bytes)?)) } - pub fn verify_prehashed(&self, message_hash: [u8; 32], signature: SecpSignature) -> bool { + pub fn verify_prehashed(&self, message_hash: [u8; 32], signature: Secp256k1Signature) -> bool { self.0.verify_prehash(&message_hash, &signature.0).is_ok() } } -impl ToClvm for SecpPublicKey +impl ToClvm for Secp256k1PublicKey where E: ClvmEncoder, { @@ -35,7 +33,7 @@ where } } -impl FromClvm for SecpPublicKey +impl FromClvm for Secp256k1PublicKey where D: ClvmDecoder, { diff --git a/crates/chia-sdk-types/src/secp/secp256k1_secret_key.rs b/crates/chia-sdk-types/src/secp/secp256k1_secret_key.rs new file mode 100644 index 00000000..27ace0b3 --- /dev/null +++ b/crates/chia-sdk-types/src/secp/secp256k1_secret_key.rs @@ -0,0 +1,26 @@ +use k256::ecdsa::{Error, SigningKey}; + +use super::{Secp256k1PublicKey, Secp256k1Signature}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Secp256k1SecretKey(SigningKey); + +impl Secp256k1SecretKey { + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes().into() + } + + pub fn from_bytes(bytes: [u8; 32]) -> Result { + Ok(Self(SigningKey::from_bytes((&bytes).into())?)) + } + + pub fn public_key(&self) -> Secp256k1PublicKey { + Secp256k1PublicKey(*self.0.verifying_key()) + } + + pub fn sign_prehashed(&self, message_hash: [u8; 32]) -> Result { + Ok(Secp256k1Signature( + self.0.sign_prehash_recoverable(&message_hash)?.0, + )) + } +} diff --git a/crates/chia-sdk-signer/src/secp/secp_signature.rs b/crates/chia-sdk-types/src/secp/secp256k1_signature.rs similarity index 78% rename from crates/chia-sdk-signer/src/secp/secp_signature.rs rename to crates/chia-sdk-types/src/secp/secp256k1_signature.rs index c1082bcd..955a5eed 100644 --- a/crates/chia-sdk-signer/src/secp/secp_signature.rs +++ b/crates/chia-sdk-types/src/secp/secp256k1_signature.rs @@ -1,25 +1,23 @@ use clvm_traits::{ClvmDecoder, ClvmEncoder, FromClvm, FromClvmError, ToClvm, ToClvmError}; use clvmr::Atom; -use k256::ecdsa::Signature; - -use crate::SignerError; +use k256::ecdsa::{Error, Signature}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SecpSignature(pub(crate) Signature); +pub struct Secp256k1Signature(pub(crate) Signature); -impl SecpSignature { +impl Secp256k1Signature { pub const SIZE: usize = 64; pub fn to_bytes(&self) -> [u8; Self::SIZE] { self.0.to_bytes().into() } - pub fn from_bytes(bytes: [u8; Self::SIZE]) -> Result { + pub fn from_bytes(bytes: [u8; Self::SIZE]) -> Result { Ok(Self(Signature::from_slice(&bytes)?)) } } -impl ToClvm for SecpSignature +impl ToClvm for Secp256k1Signature where E: ClvmEncoder, { @@ -28,7 +26,7 @@ where } } -impl FromClvm for SecpSignature +impl FromClvm for Secp256k1Signature where D: ClvmDecoder, { diff --git a/crates/chia-sdk-types/src/secp/secp256r1_public_key.rs b/crates/chia-sdk-types/src/secp/secp256r1_public_key.rs new file mode 100644 index 00000000..5a117f09 --- /dev/null +++ b/crates/chia-sdk-types/src/secp/secp256r1_public_key.rs @@ -0,0 +1,51 @@ +use clvm_traits::{ClvmDecoder, ClvmEncoder, FromClvm, FromClvmError, ToClvm, ToClvmError}; +use clvmr::Atom; +use p256::ecdsa::signature::hazmat::PrehashVerifier; +use p256::ecdsa::{Error, VerifyingKey}; + +use super::Secp256r1Signature; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Secp256r1PublicKey(pub(crate) VerifyingKey); + +impl Secp256r1PublicKey { + pub const SIZE: usize = 33; + + pub fn to_bytes(&self) -> [u8; Self::SIZE] { + self.0.to_encoded_point(true).as_ref().try_into().unwrap() + } + + pub fn from_bytes(bytes: [u8; Self::SIZE]) -> Result { + Ok(Self(VerifyingKey::from_sec1_bytes(&bytes)?)) + } + + pub fn verify_prehashed(&self, message_hash: [u8; 32], signature: Secp256r1Signature) -> bool { + self.0.verify_prehash(&message_hash, &signature.0).is_ok() + } +} + +impl ToClvm for Secp256r1PublicKey +where + E: ClvmEncoder, +{ + fn to_clvm(&self, encoder: &mut E) -> Result { + encoder.encode_atom(Atom::Borrowed(&self.to_bytes())) + } +} + +impl FromClvm for Secp256r1PublicKey +where + D: ClvmDecoder, +{ + fn from_clvm(decoder: &D, node: D::Node) -> Result { + let atom = decoder.decode_atom(&node)?; + let bytes: [u8; Self::SIZE] = + atom.as_ref() + .try_into() + .map_err(|_| FromClvmError::WrongAtomLength { + expected: Self::SIZE, + found: atom.len(), + })?; + Self::from_bytes(bytes).map_err(|error| FromClvmError::Custom(error.to_string())) + } +} diff --git a/crates/chia-sdk-types/src/secp/secp256r1_secret_key.rs b/crates/chia-sdk-types/src/secp/secp256r1_secret_key.rs new file mode 100644 index 00000000..ce90d19c --- /dev/null +++ b/crates/chia-sdk-types/src/secp/secp256r1_secret_key.rs @@ -0,0 +1,26 @@ +use p256::ecdsa::{Error, SigningKey}; + +use super::{Secp256r1PublicKey, Secp256r1Signature}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Secp256r1SecretKey(SigningKey); + +impl Secp256r1SecretKey { + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes().into() + } + + pub fn from_bytes(bytes: [u8; 32]) -> Result { + Ok(Self(SigningKey::from_bytes((&bytes).into())?)) + } + + pub fn public_key(&self) -> Secp256r1PublicKey { + Secp256r1PublicKey(*self.0.verifying_key()) + } + + pub fn sign_prehashed(&self, message_hash: [u8; 32]) -> Result { + Ok(Secp256r1Signature( + self.0.sign_prehash_recoverable(&message_hash)?.0, + )) + } +} diff --git a/crates/chia-sdk-types/src/secp/secp256r1_signature.rs b/crates/chia-sdk-types/src/secp/secp256r1_signature.rs new file mode 100644 index 00000000..2d54b818 --- /dev/null +++ b/crates/chia-sdk-types/src/secp/secp256r1_signature.rs @@ -0,0 +1,44 @@ +use clvm_traits::{ClvmDecoder, ClvmEncoder, FromClvm, FromClvmError, ToClvm, ToClvmError}; +use clvmr::Atom; +use p256::ecdsa::{Error, Signature}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Secp256r1Signature(pub(crate) Signature); + +impl Secp256r1Signature { + pub const SIZE: usize = 64; + + pub fn to_bytes(&self) -> [u8; Self::SIZE] { + self.0.to_bytes().into() + } + + pub fn from_bytes(bytes: [u8; Self::SIZE]) -> Result { + Ok(Self(Signature::from_slice(&bytes)?)) + } +} + +impl ToClvm for Secp256r1Signature +where + E: ClvmEncoder, +{ + fn to_clvm(&self, encoder: &mut E) -> Result { + encoder.encode_atom(Atom::Borrowed(&self.0.to_vec())) + } +} + +impl FromClvm for Secp256r1Signature +where + D: ClvmDecoder, +{ + fn from_clvm(decoder: &D, node: D::Node) -> Result { + let atom = decoder.decode_atom(&node)?; + let bytes: [u8; Self::SIZE] = + atom.as_ref() + .try_into() + .map_err(|_| FromClvmError::WrongAtomLength { + expected: Self::SIZE, + found: atom.len(), + })?; + Self::from_bytes(bytes).map_err(|error| FromClvmError::Custom(error.to_string())) + } +} diff --git a/crates/chia-sdk-utils/src/coin_selection.rs b/crates/chia-sdk-utils/src/coin_selection.rs index c975dca7..59a0a1ab 100644 --- a/crates/chia-sdk-utils/src/coin_selection.rs +++ b/crates/chia-sdk-utils/src/coin_selection.rs @@ -37,7 +37,7 @@ pub fn select_coins( // Checks to ensure the balance is sufficient before continuing. let spendable_amount = spendable_coins .iter() - .fold(0u128, |acc, coin| acc + coin.amount as u128); + .fold(0u128, |acc, coin| acc + u128::from(coin.amount)); if spendable_amount < amount { return Err(CoinSelectionError::InsufficientBalance(spendable_amount)); @@ -48,7 +48,7 @@ pub fn select_coins( // Exact coin match. for coin in &spendable_coins { - if coin.amount as u128 == amount { + if u128::from(coin.amount) == amount { return Ok(vec![*coin]); } } @@ -57,7 +57,7 @@ pub fn select_coins( let mut smaller_sum = 0; for coin in &spendable_coins { - let coin_amount = coin.amount as u128; + let coin_amount = u128::from(coin.amount); if coin_amount < amount { smaller_coins.insert(*coin); @@ -110,7 +110,7 @@ fn sum_largest_coins(coins: &[Coin], amount: u128) -> IndexSet { let mut selected_coins = IndexSet::new(); let mut selected_sum = 0; for coin in coins { - selected_sum += coin.amount as u128; + selected_sum += u128::from(coin.amount); selected_coins.insert(*coin); if selected_sum >= amount { @@ -121,11 +121,11 @@ fn sum_largest_coins(coins: &[Coin], amount: u128) -> IndexSet { } fn smallest_coin_above(coins: &[Coin], amount: u128) -> Option { - if (coins[0].amount as u128) < amount { + if u128::from(coins[0].amount) < amount { return None; } for coin in coins.iter().rev() { - if (coin.amount as u128) >= amount { + if u128::from(coin.amount) >= amount { return Some(*coin); } } @@ -165,7 +165,7 @@ pub fn knapsack_coin_algorithm( break; } - selected_sum += coin.amount as u128; + selected_sum += u128::from(coin.amount); selected_coins.insert(*coin); if selected_sum == amount { @@ -179,7 +179,7 @@ pub fn knapsack_coin_algorithm( best_sum = selected_sum; best_coins = Some(selected_coins.clone()); - selected_sum -= coin.amount as u128; + selected_sum -= u128::from(coin.amount); selected_coins.shift_remove(coin); } } diff --git a/examples/cat_spends.rs b/examples/cat_spends.rs index 4bdfc708..d1b54dad 100644 --- a/examples/cat_spends.rs +++ b/examples/cat_spends.rs @@ -14,9 +14,10 @@ fn main() -> anyhow::Result<()> { let p2_puzzle_hash = StandardArgs::curry_tree_hash(pk).into(); let coin = Coin::new(Bytes32::default(), p2_puzzle_hash, 1_000); + let memos = ctx.hint(p2_puzzle_hash)?; + // Issue the CAT using the single issuance (genesis by coin id) TAIL. - let conditions = - Conditions::new().create_coin(p2_puzzle_hash, coin.amount, vec![p2_puzzle_hash.into()]); + let conditions = Conditions::new().create_coin(p2_puzzle_hash, coin.amount, Some(memos)); let (issue_cat, cat) = Cat::single_issuance_eve(ctx, coin.coin_id(), coin.amount, conditions)?; p2.spend(ctx, coin, issue_cat)?; println!("Issued test CAT with asset id {}", cat.asset_id); @@ -27,7 +28,7 @@ fn main() -> anyhow::Result<()> { new_cat, p2.spend_with_conditions( ctx, - Conditions::new().create_coin(p2_puzzle_hash, coin.amount, vec![p2_puzzle_hash.into()]), + Conditions::new().create_coin(p2_puzzle_hash, coin.amount, Some(memos)), )?, )]; diff --git a/examples/custom_p2_puzzle.rs b/examples/custom_p2_puzzle.rs index a73ba321..feb98917 100644 --- a/examples/custom_p2_puzzle.rs +++ b/examples/custom_p2_puzzle.rs @@ -127,7 +127,7 @@ fn main() -> anyhow::Result<()> { let ctx = &mut SpendContext::new(); let conditions = Conditions::new() - .create_coin(puzzle_hash, 900, Vec::new()) + .create_coin(puzzle_hash, 900, None) .reserve_fee(100); ctx.spend_custom_coin(coin, pk, conditions)?; diff --git a/examples/spend_simulator.rs b/examples/spend_simulator.rs index 01d1996d..f032e902 100644 --- a/examples/spend_simulator.rs +++ b/examples/spend_simulator.rs @@ -22,7 +22,7 @@ fn main() -> anyhow::Result<()> { let ctx = &mut SpendContext::new(); let conditions = Conditions::new() - .create_coin(puzzle_hash, 900, Vec::new()) + .create_coin(puzzle_hash, 900, None) .reserve_fee(100); p2.spend(ctx, coin, conditions)?; diff --git a/napi/__test__/index.spec.ts b/napi/__test__/index.spec.ts index adff2ff9..fb671cb2 100644 --- a/napi/__test__/index.spec.ts +++ b/napi/__test__/index.spec.ts @@ -14,10 +14,10 @@ import { test("calculate coin id", (t) => { const coinId = toCoinId({ parentCoinInfo: fromHex( - "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a" + "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a", ), puzzleHash: fromHex( - "dbc1b4c900ffe48d575b5da5c638040125f65db0fe3e24494b76ea986457d986" + "dbc1b4c900ffe48d575b5da5c638040125f65db0fe3e24494b76ea986457d986", ), amount: 100n, }); @@ -26,9 +26,9 @@ test("calculate coin id", (t) => { compareBytes( coinId, fromHex( - "fd3e669c27be9d634fe79f1f7d7d8aaacc3597b855cffea1d708f4642f1d542a" - ) - ) + "fd3e669c27be9d634fe79f1f7d7d8aaacc3597b855cffea1d708f4642f1d542a", + ), + ), ); }); @@ -187,13 +187,13 @@ test("curry roundtrip", (t) => { const items = Array.from({ length: 10 }, (_, i) => i); const ptr = clvm.curry( clvm.nil(), - items.map((i) => clvm.alloc(i)) + items.map((i) => clvm.alloc(i)), ); const uncurry = ptr.uncurry()!; const args = uncurry.args.map((ptr) => ptr.toSmallNumber()); t.true( - compareBytes(clvm.treeHash(clvm.nil()), clvm.treeHash(uncurry.program)) + compareBytes(clvm.treeHash(clvm.nil()), clvm.treeHash(uncurry.program)), ); t.deepEqual(args, items); }); @@ -221,12 +221,12 @@ test("curry tree hash", (t) => { const items = Array.from({ length: 10 }, (_, i) => i); const ptr = clvm.curry( clvm.nil(), - items.map((i) => clvm.alloc(i)) + items.map((i) => clvm.alloc(i)), ); const treeHash = curryTreeHash( clvm.treeHash(clvm.nil()), - items.map((i) => clvm.treeHash(clvm.alloc(i))) + items.map((i) => clvm.treeHash(clvm.alloc(i))), ); const expected = clvm.treeHash(ptr); @@ -255,7 +255,7 @@ test("mint and spend nft", (t) => { const spend = clvm.spendP2Standard( p2.publicKey, - clvm.delegatedSpendForConditions(result.parentConditions) + clvm.delegatedSpendForConditions(result.parentConditions), ); simulator.spend( @@ -266,14 +266,14 @@ test("mint and spend nft", (t) => { solution: spend.solution.serialize(), }, ]), - [p2.secretKey] + [p2.secretKey], ); const innerSpend = clvm.spendP2Standard( p2.publicKey, clvm.delegatedSpendForConditions([ - clvm.createCoin(p2.puzzleHash, 1n, [p2.puzzleHash]), - ]) + clvm.createCoin(p2.puzzleHash, 1n, clvm.alloc([p2.puzzleHash])), + ]), ); const coinSpends = clvm.spendNft(result.nfts[0], innerSpend); @@ -284,11 +284,11 @@ test("mint and spend nft", (t) => { compareBytes( clvm .nftMetadata( - clvm.parseNftMetadata(clvm.deserialize(result.nfts[0].info.metadata)) + clvm.parseNftMetadata(clvm.deserialize(result.nfts[0].info.metadata)), ) .serialize(), - result.nfts[0].info.metadata - ) + result.nfts[0].info.metadata, + ), ); }); @@ -297,12 +297,17 @@ test("create and parse condition", (t) => { const puzzleHash = fromHex("ff".repeat(32)); - const condition = clvm.createCoin(puzzleHash, 1n, [puzzleHash]); + const condition = clvm.createCoin(puzzleHash, 1n, clvm.alloc([puzzleHash])); const parsed = clvm.parseCreateCoin(condition); - t.deepEqual(parsed, { - puzzleHash, - amount: 1n, - memos: [puzzleHash], - }); + t.true(parsed !== null && compareBytes(parsed.puzzleHash, puzzleHash)); + t.true(parsed !== null && parsed.amount === 1n); + + t.deepEqual( + parsed?.memos + ?.toList() + .map((memo) => memo.toAtom()) + .filter((memo) => memo !== null), + [puzzleHash], + ); }); diff --git a/napi/index.d.ts b/napi/index.d.ts index 6b96755c..be159bff 100644 --- a/napi/index.d.ts +++ b/napi/index.d.ts @@ -54,7 +54,7 @@ export interface AggSigMe { export interface CreateCoin { puzzleHash: Uint8Array amount: bigint - memos: Array + memos?: Program } export interface ReserveFee { amount: bigint @@ -294,7 +294,7 @@ export declare class ClvmAllocator { parseAggSigUnsafe(program: Program): AggSigUnsafe | null aggSigMe(publicKey: PublicKey, message: Uint8Array): Program parseAggSigMe(program: Program): AggSigMe | null - createCoin(puzzleHash: Uint8Array, amount: bigint, memos: Array): Program + createCoin(puzzleHash: Uint8Array, amount: bigint, memos: Program | null): Program parseCreateCoin(program: Program): CreateCoin | null reserveFee(amount: bigint): Program parseReserveFee(program: Program): ReserveFee | null diff --git a/napi/src/clvm.rs b/napi/src/clvm.rs index 28ec77f5..4dc5a761 100644 --- a/napi/src/clvm.rs +++ b/napi/src/clvm.rs @@ -5,7 +5,7 @@ use chia::{ protocol::{self, Bytes32}, puzzles::nft::{self, NFT_METADATA_UPDATER_PUZZLE_HASH}, }; -use chia_wallet_sdk::{self as sdk, HashedPtr, SpendContext}; +use chia_wallet_sdk::{self as sdk, HashedPtr, Memos, SpendContext}; use clvmr::{ run_program, serde::{node_from_bytes, node_from_bytes_backrefs}, @@ -417,7 +417,7 @@ macro_rules! conditions { }; Ok(Some($condition { - $( $name: condition.$name.into_js_contextual(env, this.clone(env)?)?, )* + $( $name: condition.$name.into_js_contextual(env, this.clone(env)?, self)?, )* })) } } @@ -463,8 +463,8 @@ conditions!( agg_sig_me(public_key: ClassInstance => bls::PublicKey, message: Uint8Array) }, CreateCoin { - "puzzleHash: Uint8Array, amount: bigint, memos: Array" - create_coin(puzzle_hash: Uint8Array, amount: BigInt, memos: Vec) + "puzzleHash: Uint8Array, amount: bigint, memos: Program | null" + create_coin(puzzle_hash: Uint8Array, amount: BigInt, memos: Option> => Option>) }, ReserveFee { "amount: bigint" diff --git a/napi/src/traits.rs b/napi/src/traits.rs index e8f8684d..35457b38 100644 --- a/napi/src/traits.rs +++ b/napi/src/traits.rs @@ -2,6 +2,7 @@ use chia::{ bls, protocol::{Bytes, BytesImpl}, }; +use chia_wallet_sdk::Memos; use clvmr::NodePtr; use napi::bindgen_prelude::*; @@ -28,7 +29,12 @@ pub(crate) trait FromRust { } pub(crate) trait IntoJsContextual { - fn into_js_contextual(self, env: Env, this: Reference) -> Result; + fn into_js_contextual( + self, + env: Env, + this: Reference, + clvm_allocator: &mut ClvmAllocator, + ) -> Result; } impl IntoRust for T @@ -53,7 +59,12 @@ impl IntoJsContextual for U where U: IntoJs, { - fn into_js_contextual(self, _env: Env, _this: Reference) -> Result { + fn into_js_contextual( + self, + _env: Env, + _this: Reference, + _clvm_allocator: &mut ClvmAllocator, + ) -> Result { self.into_js() } } @@ -63,6 +74,7 @@ impl IntoJsContextual> for NodePtr { self, env: Env, this: Reference, + _clvm_allocator: &mut ClvmAllocator, ) -> Result> { Program::new(this, self).into_instance(env) } @@ -73,6 +85,7 @@ impl IntoJsContextual>> for Vec { self, env: Env, this: Reference, + _clvm_allocator: &mut ClvmAllocator, ) -> Result>> { let mut result = Vec::with_capacity(self.len()); @@ -84,11 +97,32 @@ impl IntoJsContextual>> for Vec { } } +impl IntoJsContextual>> for Option> { + fn into_js_contextual( + self, + env: Env, + this: Reference, + clvm_allocator: &mut ClvmAllocator, + ) -> Result>> { + let Some(memos) = self else { + return Ok(None); + }; + + let ptr = clvm_allocator + .0 + .alloc(&memos.value) + .map_err(|error| Error::from_reason(format!("Failed to allocate CLVM: {error}")))?; + + Ok(Some(Program::new(this, ptr).into_instance(env)?)) + } +} + impl IntoJsContextual> for bls::PublicKey { fn into_js_contextual( self, env: Env, _this: Reference, + _clvm_allocator: &mut ClvmAllocator, ) -> Result> { PublicKey(self).into_instance(env) } @@ -276,6 +310,33 @@ impl IntoJs for num_bigint::BigInt { } } +impl FromJs> for Option +where + T: FromJs, +{ + fn from_js(js_value: Option) -> Result { + js_value.map(FromJs::from_js).transpose() + } +} + +impl FromJs for NodePtr { + fn from_js(program: Program) -> Result { + Ok(program.ptr) + } +} + +impl FromJs>> for Option> { + fn from_js(js_value: Option>) -> Result + where + Self: Sized, + { + let Some(program) = js_value else { + return Ok(None); + }; + Ok(Some(Memos::new(program.ptr))) + } +} + /// Helper function to convert Vec (words) into Vec (byte array) fn words_to_bytes(words: &[u64]) -> Vec { let mut bytes = Vec::with_capacity(words.len() * 8);