diff --git a/Cargo.lock b/Cargo.lock index e76ecd4a..c5803b8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -332,7 +332,7 @@ dependencies = [ [[package]] name = "chia-wallet-sdk" -version = "0.4.0" +version = "0.5.0" dependencies = [ "bech32", "bip39", diff --git a/src/signer.rs b/src/signer.rs index b8f6dae6..b79530e3 100644 --- a/src/signer.rs +++ b/src/signer.rs @@ -77,3 +77,119 @@ pub async fn sign_spend_bundle( } Ok(aggregate_signature) } + +#[cfg(test)] +mod tests { + use chia_bls::SecretKey; + use chia_protocol::{Bytes, Bytes32, Coin, Program}; + use clvm_traits::{clvm_list, FromNodePtr, ToClvm}; + use hex_literal::hex; + + use crate::{testing::SEED, PublicKeyStore, SkDerivationStore}; + + use super::*; + + const AGG_SIG_ME: [u8; 32] = + hex!("ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb"); + + fn coin1() -> Coin { + Coin::new(Bytes32::from([0; 32]), Bytes32::from([1; 32]), 2) + } + + fn serialize(value: T) -> Program + where + T: ToClvm, + { + let mut a = Allocator::new(); + let ptr = value.to_clvm(&mut a).unwrap(); + Program::from_node_ptr(&a, ptr).unwrap() + } + + macro_rules! condition { + ($name:ident, $pk:expr, $msg:expr) => { + Condition::::$name { + public_key: $pk, + message: $msg, + } + }; + } + + #[tokio::test] + async fn test_sign_spend() { + let root_sk = SecretKey::from_seed(SEED.as_ref()); + let sk_store = SkDerivationStore::new(&root_sk); + sk_store.derive_to_index(1).await; + + let sk = sk_store.secret_key(0).await.unwrap(); + let pk = sk.public_key(); + let msg = Bytes::new(vec![42; 42]); + + let mut a = Allocator::new(); + + macro_rules! test_conditions { + ( $( $name:ident: $hex:expr ),* ) => { $( + let coin_spend = CoinSpend::new( + coin1(), + serialize(1), + serialize(clvm_list!(condition!($name, pk.clone(), msg.clone()))), + ); + + let signature = sign_coin_spend(&sk_store, &mut a, &coin_spend, AGG_SIG_ME).await.unwrap(); + + assert_eq!(hex::encode(signature.to_bytes()), hex::encode($hex)); + )* }; + } + + test_conditions!( + AggSigParent: hex!( + " + 8b9a388289f71fe24b5ed21a7169e2046df4e2291e509e6b89b57d03fb51b137 + 6c41211a9289050e066419908242365912a909094977dbd9ac209d44db1f1a2b + 340cee3ce572727308911fcfc81d3a7a94711c31ad618bb7f7b4c08367887334 + " + ), + AggSigPuzzle: hex!( + " + 97bc452a7a6439ea281f0ae33cf418173b98fbe782ab2928dbd6097ef5dc58ab + dd5ef34fe4b4e13d4f5ce4397e5cad6f0072283b53ff03ea4a2d37c3a16c861e + a1f289e29d7d6fd4552e762347e5bb4590eda4b486974c3e208e0e27138846b5 + " + ), + AggSigAmount: hex!( + " + b2b7bb2dbe8617b22001d57b5e934378bd98ede324ae464376108aa00c703938 + 1d97e39dcc2c96680f8b28c159eb09b50c54a65a7ad981168c98c7b790557e4e + 1d6bfe2881f695a150838d72608586ae93a0311739f361c6bf811518405504d7 + " + ), + AggSigPuzzleAmount: hex!( + " + b828185c2572a6fc408934c426c11d6452096532d19483bb4c299eaa04bd45a9 + a231ee7b0293c211b0056d7e9862478b0f4e451229ce1432141642e1a2d04708 + 2873f77699406ab353d9fa04d11fac2e22420ebd8fd3917793bbb9642f29ef27 + " + ), + AggSigParentAmount: hex!( + " + a8942f9b1bd2b5ce9a624b3652734a4a8318f774f212bff5f46b4967edf1e3b4 + ec1a759a060f33ec62a15d5d7b162c71172a41f675f19574f28f65cfd6bd73de + 3f6aa0cd73b3fc7889e188f258d554c690b33c9099a39e14c72c293e04118afb + " + ), + AggSigUnsafe: hex!( + " + b34d5f4c969d7b0290b1af7de8f71903bd71d6875744b5263bf00b60ab4da6dd + 3c671a0a3b765cb6e5a7b8b9305a59e20274bf6c53b6891e27543b77948edcaa + 5270a4db63e70b8e13d8b1624b3ef5149466c9e99d21959254687e5f7a36de42 + " + ), + AggSigMe: hex!( + " + 89266426a248adee3a699196f00e89a509a177cffb0d2df7fa405bb6a42ffe90 + 0450f4788ac34f505991054a0ee0b036013e594a5738199bfe33a37b61496acd + 28d63ac7b3de741b57e822f75ebb3c69df03aa5ef241094386967d6c7805cb63 + " + ) + ); + } +} diff --git a/src/spends/cat.rs b/src/spends/cat.rs index f36f087a..12d9472a 100644 --- a/src/spends/cat.rs +++ b/src/spends/cat.rs @@ -1,146 +1,59 @@ -use chia_bls::PublicKey; use chia_client::Peer; -use chia_protocol::{Bytes32, Coin, CoinSpend, Program}; +use chia_protocol::{Coin, CoinSpend, RejectPuzzleSolution}; use chia_wallet::{ - cat::{CatArgs, CatSolution, CoinProof, EverythingWithSignatureTailArgs, CAT_PUZZLE_HASH}, - standard::{standard_puzzle_hash, StandardArgs, StandardSolution}, + cat::{CatArgs, CAT_PUZZLE_HASH}, + standard::standard_puzzle_hash, LineageProof, }; -use clvm_traits::{clvm_quote, FromNodePtr, ToClvmError, ToNodePtr}; -use clvm_utils::{curry_tree_hash, tree_hash, tree_hash_atom, CurriedProgram}; -use clvmr::{allocator::NodePtr, Allocator}; +use clvm_traits::{FromClvm, FromClvmError, ToClvmError}; +use clvm_utils::{tree_hash, CurriedProgram}; +use clvmr::{allocator::NodePtr, serde::node_from_bytes, Allocator}; +use thiserror::Error; -use crate::{ - utils::request_puzzle_args, CatCondition, Condition, CreateCoin, DerivationStore, RunTail, -}; +use crate::{CatCondition, DerivationStore}; -/// The information required to spend a CAT coin. -/// This assumes that the inner puzzle is a standard transaction. -pub struct CatSpend { - /// The CAT coin that is being spent. - pub coin: Coin, - /// The public key used for the inner puzzle. - pub synthetic_key: PublicKey, - /// The desired output conditions for the coin spend. - pub conditions: Vec>, - /// The extra delta produced as part of this spend. - pub extra_delta: i64, - /// The inner puzzle hash. - pub p2_puzzle_hash: [u8; 32], - /// The lineage proof of the CAT. - pub lineage_proof: LineageProof, -} +mod issuance; +mod raw_spend; -/// The information required to create and spend an eve CAT coin. -pub struct EveSpendInfo { - /// The full puzzle hash of the eve CAT coin. - pub puzzle_hash: [u8; 32], - /// The coin spend for the eve CAT. - pub coin_spend: CoinSpend, -} +pub use issuance::*; +pub use raw_spend::*; -/// Constructs a coin spend to issue more of an `EverythingWithSignature` CAT. -pub fn issue_cat_with_public_key( - a: &mut Allocator, - cat_puzzle_ptr: NodePtr, - tail_puzzle_ptr: NodePtr, - public_key: PublicKey, - parent_coin_id: Bytes32, - amount: u64, - conditions: &[Condition], -) -> Result { - let mut cat_conditions: Vec> = Vec::with_capacity(conditions.len() + 1); - cat_conditions.extend( - conditions - .iter() - .map(|condition| CatCondition::Normal(condition.clone())), - ); +/// An error that occurs while trying to spend a CAT. +#[derive(Debug, Error)] +pub enum CatSpendError { + /// When the mod hash of a parent coin is not a CAT. + #[error("wrong mod hash")] + WrongModHash([u8; 32]), - let tail = CurriedProgram { - program: tail_puzzle_ptr, - args: EverythingWithSignatureTailArgs { public_key }, - } - .to_node_ptr(a)?; + /// When conversion to a CLVM NodePtr fails. + #[error("to clvm error: {0}")] + ToClvm(#[from] ToClvmError), - cat_conditions.push(CatCondition::RunTail(RunTail { - program: tail, - solution: NodePtr::NIL, - })); + /// When conversion from a CLVM NodePtr fails. + #[error("from clvm error: {0}")] + FromClvm(#[from] FromClvmError), - spend_new_eve_cat( - a, - cat_puzzle_ptr, - parent_coin_id, - tree_hash(a, tail), - amount, - &cat_conditions, - ) -} + /// When conversion to or from bytes fails. + #[error("io error: {0}")] + Io(#[from] std::io::Error), -/// Creates an eve CAT coin and spends it. -pub fn spend_new_eve_cat( - a: &mut Allocator, - cat_puzzle_ptr: NodePtr, - parent_coin_id: Bytes32, - tail_program_hash: [u8; 32], - amount: u64, - conditions: &[CatCondition], -) -> Result { - let inner_puzzle = clvm_quote!(conditions).to_node_ptr(a)?; - let inner_puzzle_hash = tree_hash(a, inner_puzzle); - - let puzzle = CurriedProgram { - program: cat_puzzle_ptr, - args: CatArgs { - mod_hash: CAT_PUZZLE_HASH.into(), - tail_program_hash: tail_program_hash.into(), - inner_puzzle, - }, - } - .to_node_ptr(a)?; - - let puzzle_hash = tree_hash(a, puzzle); - let coin = Coin::new(parent_coin_id, puzzle_hash.into(), amount); - - let solution = CatSolution { - inner_puzzle_solution: (), - lineage_proof: None, - prev_coin_id: coin.coin_id().into(), - this_coin_info: coin.clone(), - next_coin_proof: CoinProof { - parent_coin_info: parent_coin_id, - inner_puzzle_hash: inner_puzzle_hash.into(), - amount, - }, - prev_subtotal: 0, - extra_delta: 0, - } - .to_node_ptr(a)?; - - let coin_spend = CoinSpend::new( - coin, - Program::from_node_ptr(a, puzzle).unwrap(), - Program::from_node_ptr(a, solution).unwrap(), - ); - - Ok(EveSpendInfo { - puzzle_hash, - coin_spend, - }) + /// When a request to the peer fails. + #[error("peer error: {0}")] + Peer(#[from] chia_client::Error), } /// Creates spend for a list of CAT coins. -#[allow(clippy::too_many_arguments)] // TODO: fix -pub async fn spend_cat_coins( +#[allow(clippy::too_many_arguments)] +pub async fn construct_cat_spends( a: &mut Allocator, standard_puzzle_ptr: NodePtr, cat_puzzle_ptr: NodePtr, - derivation_store: &impl DerivationStore, peer: &Peer, + derivation_store: &impl DerivationStore, coins: Vec, conditions: Vec>, asset_id: [u8; 32], -) -> Vec { +) -> Result, CatSpendError> { let mut spends = Vec::new(); let mut conditions = Some(conditions); @@ -170,15 +83,18 @@ pub async fn spend_cat_coins( .cloned() .unwrap(); - let cat_args: CatArgs = request_puzzle_args( - a, - peer, - &coin, - CAT_PUZZLE_HASH, - parent.spent_height.unwrap(), - ) - .await - .unwrap(); + let puzzle = peer + .request_puzzle_and_solution(coin.parent_coin_info, parent.spent_height.unwrap()) + .await? + .puzzle; + + let ptr = node_from_bytes(a, puzzle.as_slice())?; + let puzzle: CurriedProgram> = FromClvm::from_clvm(a, ptr)?; + + let mod_hash = tree_hash(a, puzzle.program); + if mod_hash != CAT_PUZZLE_HASH { + return Err(CatSpendError::WrongModHash(mod_hash)); + } // Spend information. let spend = CatSpend { @@ -193,104 +109,18 @@ pub async fn spend_cat_coins( p2_puzzle_hash, lineage_proof: LineageProof { parent_coin_info: parent.coin.parent_coin_info, - inner_puzzle_hash: tree_hash(a, cat_args.inner_puzzle).into(), + inner_puzzle_hash: tree_hash(a, puzzle.args.inner_puzzle).into(), amount: parent.coin.amount, }, }; spends.push(spend); } - create_raw_cat_spends(a, standard_puzzle_ptr, cat_puzzle_ptr, asset_id, &spends).unwrap() -} - -/// Creates a set of CAT coin spends for a given asset id. -pub fn create_raw_cat_spends( - a: &mut Allocator, - standard_puzzle_ptr: NodePtr, - cat_puzzle_ptr: NodePtr, - asset_id: [u8; 32], - cat_spends: &[CatSpend], -) -> Result, ToClvmError> { - let mut total_delta = 0; - - cat_spends - .iter() - .enumerate() - .map(|(index, cat_spend)| { - // Calculate the delta and add it to the subtotal. - let delta = cat_spend.conditions.iter().fold( - cat_spend.coin.amount as i64 - cat_spend.extra_delta, - |delta, condition| { - if let CatCondition::Normal(Condition::CreateCoin( - CreateCoin::Normal { amount, .. } | CreateCoin::Memos { amount, .. }, - )) = condition - { - return delta - *amount as i64; - } - delta - }, - ); - - let prev_subtotal = total_delta; - - total_delta += delta; - - // Find information of neighboring coins on the ring. - let prev_cat = &cat_spends[index.wrapping_sub(1) % cat_spends.len()]; - let next_cat = &cat_spends[index.wrapping_add(1) % cat_spends.len()]; - - // Construct the puzzle. - let puzzle = CurriedProgram { - program: cat_puzzle_ptr, - args: CatArgs { - mod_hash: CAT_PUZZLE_HASH.into(), - tail_program_hash: asset_id.into(), - inner_puzzle: CurriedProgram { - program: standard_puzzle_ptr, - args: StandardArgs { - synthetic_key: cat_spend.synthetic_key.clone(), - }, - }, - }, - } - .to_node_ptr(a)?; - - // Construct the solution. - let solution = CatSolution { - inner_puzzle_solution: StandardSolution { - original_public_key: None, - delegated_puzzle: clvm_quote!(&cat_spend.conditions), - solution: (), - }, - lineage_proof: Some(cat_spend.lineage_proof.clone()), - prev_coin_id: prev_cat.coin.coin_id().into(), - this_coin_info: cat_spend.coin.clone(), - next_coin_proof: CoinProof { - parent_coin_info: next_cat.coin.parent_coin_info, - inner_puzzle_hash: next_cat.p2_puzzle_hash.into(), - amount: next_cat.coin.amount, - }, - prev_subtotal, - extra_delta: cat_spend.extra_delta, - } - .to_node_ptr(a)?; - - // Create the coin spend. - Ok(CoinSpend::new( - cat_spend.coin.clone(), - Program::from_node_ptr(a, puzzle).unwrap(), - Program::from_node_ptr(a, solution).unwrap(), - )) - }) - .collect() -} - -/// Calculates the puzzle hash of a CAT without generating the full puzzle. -pub fn cat_puzzle_hash(asset_id: [u8; 32], inner_puzzle_hash: [u8; 32]) -> [u8; 32] { - let mod_hash = tree_hash_atom(&CAT_PUZZLE_HASH); - let asset_id_hash = tree_hash_atom(&asset_id); - curry_tree_hash( - CAT_PUZZLE_HASH, - &[mod_hash, asset_id_hash, inner_puzzle_hash], - ) + Ok(spend_cat_coins( + a, + standard_puzzle_ptr, + cat_puzzle_ptr, + asset_id, + &spends, + )?) } diff --git a/src/spends/cat/issuance.rs b/src/spends/cat/issuance.rs new file mode 100644 index 00000000..6994d7c3 --- /dev/null +++ b/src/spends/cat/issuance.rs @@ -0,0 +1,108 @@ +use chia_bls::PublicKey; +use chia_protocol::{Bytes32, Coin, CoinSpend, Program}; +use chia_wallet::cat::{ + CatArgs, CatSolution, CoinProof, EverythingWithSignatureTailArgs, CAT_PUZZLE_HASH, +}; +use clvm_traits::{clvm_quote, FromNodePtr, ToClvmError, ToNodePtr}; +use clvm_utils::{tree_hash, CurriedProgram}; +use clvmr::{Allocator, NodePtr}; + +use crate::{CatCondition, Condition, RunTail}; + +/// The information required to create and spend an eve CAT coin. +pub struct EveSpendInfo { + /// The full puzzle hash of the eve CAT coin. + pub puzzle_hash: [u8; 32], + /// The coin spend for the eve CAT. + pub coin_spend: CoinSpend, +} + +/// Constructs a coin spend to issue more of an `EverythingWithSignature` CAT. +pub fn issue_cat_with_signature( + a: &mut Allocator, + cat_puzzle_ptr: NodePtr, + tail_puzzle_ptr: NodePtr, + public_key: PublicKey, + parent_coin_id: Bytes32, + amount: u64, + conditions: &[Condition], +) -> Result { + let mut cat_conditions: Vec> = Vec::with_capacity(conditions.len() + 1); + cat_conditions.extend( + conditions + .iter() + .map(|condition| CatCondition::Normal(condition.clone())), + ); + + let tail = CurriedProgram { + program: tail_puzzle_ptr, + args: EverythingWithSignatureTailArgs { public_key }, + } + .to_node_ptr(a)?; + + cat_conditions.push(CatCondition::RunTail(RunTail { + program: tail, + solution: NodePtr::NIL, + })); + + issue_cat_eve( + a, + cat_puzzle_ptr, + parent_coin_id, + tree_hash(a, tail), + amount, + &cat_conditions, + ) +} + +/// Creates an eve CAT coin and spends it. +pub fn issue_cat_eve( + a: &mut Allocator, + cat_puzzle_ptr: NodePtr, + parent_coin_id: Bytes32, + tail_program_hash: [u8; 32], + amount: u64, + conditions: &[CatCondition], +) -> Result { + let inner_puzzle = clvm_quote!(conditions).to_node_ptr(a)?; + let inner_puzzle_hash = tree_hash(a, inner_puzzle); + + let puzzle = CurriedProgram { + program: cat_puzzle_ptr, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: tail_program_hash.into(), + inner_puzzle, + }, + } + .to_node_ptr(a)?; + + let puzzle_hash = tree_hash(a, puzzle); + let coin = Coin::new(parent_coin_id, puzzle_hash.into(), amount); + + let solution = CatSolution { + inner_puzzle_solution: (), + lineage_proof: None, + prev_coin_id: coin.coin_id().into(), + this_coin_info: coin.clone(), + next_coin_proof: CoinProof { + parent_coin_info: parent_coin_id, + inner_puzzle_hash: inner_puzzle_hash.into(), + amount, + }, + prev_subtotal: 0, + extra_delta: 0, + } + .to_node_ptr(a)?; + + let coin_spend = CoinSpend::new( + coin, + Program::from_node_ptr(a, puzzle).unwrap(), + Program::from_node_ptr(a, solution).unwrap(), + ); + + Ok(EveSpendInfo { + puzzle_hash, + coin_spend, + }) +} diff --git a/src/spends/cat/raw_spend.rs b/src/spends/cat/raw_spend.rs new file mode 100644 index 00000000..59ccc05a --- /dev/null +++ b/src/spends/cat/raw_spend.rs @@ -0,0 +1,200 @@ +use chia_bls::PublicKey; +use chia_protocol::{Coin, CoinSpend, Program}; +use chia_wallet::{ + cat::{CatArgs, CatSolution, CoinProof, CAT_PUZZLE_HASH}, + standard::{StandardArgs, StandardSolution}, + LineageProof, +}; +use clvm_traits::{clvm_quote, FromNodePtr, ToClvmError, ToNodePtr}; +use clvm_utils::CurriedProgram; +use clvmr::{Allocator, NodePtr}; + +use crate::{CatCondition, Condition, CreateCoin}; + +/// The information required to spend a CAT coin. +/// This assumes that the inner puzzle is a standard transaction. +pub struct CatSpend { + /// The CAT coin that is being spent. + pub coin: Coin, + /// The public key used for the inner puzzle. + pub synthetic_key: PublicKey, + /// The desired output conditions for the coin spend. + pub conditions: Vec>, + /// The extra delta produced as part of this spend. + pub extra_delta: i64, + /// The inner puzzle hash. + pub p2_puzzle_hash: [u8; 32], + /// The lineage proof of the CAT. + pub lineage_proof: LineageProof, +} + +/// Creates a set of CAT coin spends for a given asset id. +pub fn spend_cat_coins( + a: &mut Allocator, + standard_puzzle_ptr: NodePtr, + cat_puzzle_ptr: NodePtr, + asset_id: [u8; 32], + cat_spends: &[CatSpend], +) -> Result, ToClvmError> { + let mut total_delta = 0; + + cat_spends + .iter() + .enumerate() + .map(|(index, cat_spend)| { + // Calculate the delta and add it to the subtotal. + let delta = cat_spend.conditions.iter().fold( + cat_spend.coin.amount as i64 - cat_spend.extra_delta, + |delta, condition| { + if let CatCondition::Normal(Condition::CreateCoin( + CreateCoin::Normal { amount, .. } | CreateCoin::Memos { amount, .. }, + )) = condition + { + return delta - *amount as i64; + } + delta + }, + ); + + let prev_subtotal = total_delta; + + total_delta += delta; + + // Find information of neighboring coins on the ring. + let prev_cat = &cat_spends[index.wrapping_sub(1) % cat_spends.len()]; + let next_cat = &cat_spends[index.wrapping_add(1) % cat_spends.len()]; + + // Construct the puzzle. + let puzzle = CurriedProgram { + program: cat_puzzle_ptr, + args: CatArgs { + mod_hash: CAT_PUZZLE_HASH.into(), + tail_program_hash: asset_id.into(), + inner_puzzle: CurriedProgram { + program: standard_puzzle_ptr, + args: StandardArgs { + synthetic_key: cat_spend.synthetic_key.clone(), + }, + }, + }, + } + .to_node_ptr(a)?; + + // Construct the solution. + let solution = CatSolution { + inner_puzzle_solution: StandardSolution { + original_public_key: None, + delegated_puzzle: clvm_quote!(&cat_spend.conditions), + solution: (), + }, + lineage_proof: Some(cat_spend.lineage_proof.clone()), + prev_coin_id: prev_cat.coin.coin_id().into(), + this_coin_info: cat_spend.coin.clone(), + next_coin_proof: CoinProof { + parent_coin_info: next_cat.coin.parent_coin_info, + inner_puzzle_hash: next_cat.p2_puzzle_hash.into(), + amount: next_cat.coin.amount, + }, + prev_subtotal, + extra_delta: cat_spend.extra_delta, + } + .to_node_ptr(a)?; + + // Create the coin spend. + Ok(CoinSpend::new( + cat_spend.coin.clone(), + Program::from_node_ptr(a, puzzle).unwrap(), + Program::from_node_ptr(a, solution).unwrap(), + )) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use chia_bls::{derive_keys::master_to_wallet_unhardened, SecretKey}; + use chia_protocol::Bytes32; + use chia_wallet::{ + cat::{cat_puzzle_hash, CAT_PUZZLE}, + standard::{standard_puzzle_hash, DEFAULT_HIDDEN_PUZZLE_HASH, STANDARD_PUZZLE}, + DeriveSynthetic, + }; + use clvmr::serde::{node_from_bytes, node_to_bytes}; + use hex_literal::hex; + + use crate::testing::SEED; + + use super::*; + + #[test] + fn test_cat_spend() { + let synthetic_key = + master_to_wallet_unhardened(&SecretKey::from_seed(SEED.as_ref()).public_key(), 0) + .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH); + + let a = &mut Allocator::new(); + let standard_puzzle_ptr = node_from_bytes(a, &STANDARD_PUZZLE).unwrap(); + let cat_puzzle_ptr = node_from_bytes(a, &CAT_PUZZLE).unwrap(); + + let asset_id = [42; 32]; + + let p2_puzzle_hash = standard_puzzle_hash(&synthetic_key); + let cat_puzzle_hash = cat_puzzle_hash(asset_id, p2_puzzle_hash); + + let parent_coin = Coin::new(Bytes32::new([0; 32]), Bytes32::new(cat_puzzle_hash), 69); + let coin = Coin::new( + Bytes32::from(parent_coin.coin_id()), + Bytes32::new(cat_puzzle_hash), + 42, + ); + + let conditions = vec![CatCondition::Normal(Condition::CreateCoin( + CreateCoin::Normal { + puzzle_hash: coin.puzzle_hash, + amount: coin.amount, + }, + ))]; + + let coin_spend = spend_cat_coins( + a, + standard_puzzle_ptr, + cat_puzzle_ptr, + asset_id, + &[CatSpend { + coin, + synthetic_key, + conditions, + extra_delta: 0, + lineage_proof: LineageProof { + parent_coin_info: parent_coin.parent_coin_info, + inner_puzzle_hash: p2_puzzle_hash.into(), + amount: parent_coin.amount, + }, + p2_puzzle_hash, + }], + ) + .unwrap() + .remove(0); + + let output_ptr = coin_spend + .puzzle_reveal + .run(a, 0, u64::MAX, &coin_spend.solution) + .unwrap() + .1; + let actual = node_to_bytes(a, output_ptr).unwrap(); + + let expected = hex!( + " + ffff46ffa06438c882c2db9f5c2a8b4cbda9258c40a6583b2d7c6becc1678607 + 4d558c834980ffff3cffa1cb9c4d253a0e1a091d620a55616e104f3329f58ee8 + 6e708d0527b1cc58a73b649e80ffff3dffa0c3bb7f0a7e1bd2cae332bbd0d1a7 + e275c1e6c643b2659e22c24f513886d3874e80ffff32ffb08584adae5630842a + 1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dc + c9183fe61e48d8bfffa0e5924c23faf33c9a1bf18c70d40cb09e4b194f521b9f + 6fceb2685c0612ac34a980ffff33ffa0f9f2d59294f2aae8f9833db876d1bf43 + 95d46af18c17312041c6f4a4d73fa041ff2a8080 + " + ); + assert_eq!(hex::encode(actual), hex::encode(expected)); + } +} diff --git a/src/spends/standard.rs b/src/spends/standard.rs index e30353e2..cb24711b 100644 --- a/src/spends/standard.rs +++ b/src/spends/standard.rs @@ -84,20 +84,15 @@ mod tests { use super::*; - // Calculates a synthetic key at the given derivation index, using the test seed. - fn synthetic_key(index: u32) -> PublicKey { - let sk = SecretKey::from_seed(SEED.as_ref()); - let pk = sk.public_key(); - let child_key = master_to_wallet_unhardened(&pk, index); - child_key.derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH) - } - #[test] fn test_standard_spend() { + let synthetic_key = + master_to_wallet_unhardened(&SecretKey::from_seed(SEED.as_ref()).public_key(), 0) + .derive_synthetic(&DEFAULT_HIDDEN_PUZZLE_HASH); + let a = &mut Allocator::new(); let standard_puzzle_ptr = node_from_bytes(a, &STANDARD_PUZZLE).unwrap(); let coin = Coin::new(Bytes32::from([0; 32]), Bytes32::from([1; 32]), 42); - let synthetic_key = synthetic_key(0); let conditions = vec![Condition::CreateCoin(CreateCoin::Normal { puzzle_hash: coin.puzzle_hash, diff --git a/src/stores/derivation_store.rs b/src/stores/derivation_store.rs index fa882a30..f2f91c9e 100644 --- a/src/stores/derivation_store.rs +++ b/src/stores/derivation_store.rs @@ -1,16 +1,12 @@ use std::future::Future; -use chia_bls::{ - derive_keys::master_to_wallet_unhardened_intermediate, DerivableKey, PublicKey, SecretKey, -}; -use chia_wallet::{ - standard::{standard_puzzle_hash, DEFAULT_HIDDEN_PUZZLE_HASH}, - DeriveSynthetic, -}; -use indexmap::IndexMap; -use parking_lot::Mutex; +use crate::PublicKeyStore; -use crate::{PublicKeyStore, SecretKeyStore}; +mod pk_derivation_store; +mod sk_derivation_store; + +pub use pk_derivation_store::*; +pub use sk_derivation_store::*; /// Keeps track of derived puzzle hashes in a wallet, based on its public keys. pub trait DerivationStore: PublicKeyStore { @@ -23,217 +19,3 @@ pub trait DerivationStore: PublicKeyStore { /// Gets all of the puzzle hashes. fn puzzle_hashes(&self) -> impl Future> + Send; } - -/// An in-memory derivation store implementation. -pub struct SimpleDerivationStore { - intermediate_sk: SecretKey, - hidden_puzzle_hash: [u8; 32], - derivations: Mutex>, -} - -#[derive(Clone)] -struct SkDerivation { - secret_key: SecretKey, - puzzle_hash: [u8; 32], -} - -impl SimpleDerivationStore { - /// Creates a new key store with the default hidden puzzle hash. - /// An intermediate secret key is derived from the root key. - pub fn new(root_key: &SecretKey) -> Self { - Self { - intermediate_sk: master_to_wallet_unhardened_intermediate(root_key), - hidden_puzzle_hash: DEFAULT_HIDDEN_PUZZLE_HASH, - derivations: Mutex::new(IndexMap::new()), - } - } - - /// Creates a new key store with a custom hidden puzzle hash. - /// An intermediate secret key is derived from the root key. - pub fn new_with_hidden_puzzle(root_key: &SecretKey, hidden_puzzle_hash: [u8; 32]) -> Self { - let mut key_store = Self::new(root_key); - key_store.hidden_puzzle_hash = hidden_puzzle_hash; - key_store - } -} - -impl DerivationStore for SimpleDerivationStore { - async fn index_of_ph(&self, puzzle_hash: [u8; 32]) -> Option { - self.derivations - .lock() - .iter() - .position(|derivation| derivation.1.puzzle_hash == puzzle_hash) - .map(|index| index as u32) - } - - async fn puzzle_hash(&self, index: u32) -> Option<[u8; 32]> { - self.derivations - .lock() - .get_index(index as usize) - .map(|derivation| derivation.1.puzzle_hash) - } - - async fn puzzle_hashes(&self) -> Vec<[u8; 32]> { - self.derivations - .lock() - .values() - .map(|item| item.puzzle_hash) - .collect() - } -} - -impl PublicKeyStore for SimpleDerivationStore { - async fn count(&self) -> u32 { - self.derivations.lock().len() as u32 - } - - async fn public_key(&self, index: u32) -> Option { - self.derivations - .lock() - .get_index(index as usize) - .map(|derivation| derivation.0.clone()) - } - - async fn index_of_pk(&self, public_key: &PublicKey) -> Option { - self.derivations - .lock() - .get_index_of(public_key) - .map(|index| index as u32) - } - - async fn derive_to_index(&self, index: u32) { - let mut derivations = self.derivations.lock(); - let current = derivations.len() as u32; - for index in current..index { - let secret_key = self - .intermediate_sk - .derive_unhardened(index) - .derive_synthetic(&self.hidden_puzzle_hash); - let public_key = secret_key.public_key(); - let puzzle_hash = standard_puzzle_hash(&public_key); - derivations.insert( - public_key, - SkDerivation { - secret_key, - puzzle_hash, - }, - ); - } - } -} - -impl SecretKeyStore for SimpleDerivationStore { - async fn secret_key(&self, index: u32) -> Option { - self.derivations - .lock() - .get_index(index as usize) - .map(|item| item.1.secret_key.clone()) - } -} - -#[cfg(test)] -mod tests { - use chia_bls::sign; - use hex::ToHex; - use hex_literal::hex; - - use crate::testing::SEED; - - use super::*; - - #[tokio::test] - async fn test_key_pairs() { - let root_sk = SecretKey::from_seed(SEED.as_ref()); - let store = SimpleDerivationStore::new(&root_sk); - - // Derive the first 10 keys. - store.derive_to_index(10).await; - - let sks: Vec = store - .derivations - .lock() - .values() - .map(|derivation| derivation.secret_key.clone()) - .collect(); - let pks: Vec = store.derivations.lock().keys().cloned().collect(); - - let sks_hex: Vec = sks.iter().map(|sk| sk.to_bytes().encode_hex()).collect(); - let pks_hex: Vec = pks.iter().map(|pk| pk.to_bytes().encode_hex()).collect(); - - let manual_pks_hex: Vec = sks - .iter() - .map(|sk| sk.public_key().to_bytes().encode_hex()) - .collect(); - - assert_eq!(&pks_hex, &manual_pks_hex); - - let expected_sks_hex = vec![ - "125e0b72383dfc25e125613331f1d0b3d011e4e66e06e851cdfbbcf4d32dfb46", - "3d6e7e99226e190bc495938ac5be8d8689445bfaa6a6396f021473c2adc8ef7d", - "36f3ac4d23877d2e90086864d28b9e0aa88e5fdc2ac08115bf11bacd8c1c4ccc", - "01711de0ebf04952c7bc53ba9ff4463f063c60d4c507dd8ed4a6448d9d2ced08", - "501aaeb127c2480976badc5601165b84d7906ed9c8754e05b2f65d5a6fdbc20b", - "5093bccdb9936b10c6f330b10abbf7c2937e7ccc76a35704a2f1cee96c23e173", - "013ec1bc4a37bea42cbc792ad23102f0759d2f941627b70dff039571e062301c", - "1b9abaeb853ef0102ce5bb07f804e628fd846e2b7e67036b109dfd4b06414e81", - "50394a6e095bd279b1ff1a095a15ecd561a66a5c3c5b9ab51214f97a3e68017e", - "41a5f2ebd31a3e338aa7af91fb1235dbb02b053fbf38073e0de9b448b2d1fdb0", - ]; - assert_eq!(sks_hex, expected_sks_hex); - - let expected_pks_hex = vec![ - "8584adae5630842a1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dcc9183fe61e48d8bf", - "b07c0a00a30501d18418df3ece3335d2c7339e0589e61b9230cffc9573d0df739726e84e55e91d68744b0f3791285b96", - "963eea603ce281d63daca66f0926421f51d6d24027e498cb9d02f6477e3e01c4c4fda666fc3ea4199fdf566244ba74e0", - "b33bbccea1926947b7a83080c8b6a193121bf3480411abeb5fb31fa70002c150ba1d40a5c6a53b36cdd51ea468f0c2e4", - "a7bf25f67541a4e292a06282d714bbbc203a8bd6b0d0b804d097a071388f84665659a1a1f220130d97bcd2c4775f1077", - "a8fa6e4e7732e36d6e4e537c172a2c1e7fd926a43abd191c5aa82974a54e9de1addb32ea404724722dedc78407bbb098", - "b40b3c77251cea8e4c9cbbecbaa7fe40e9ad5e1298c83696d879cffd0c28f9ed61d5f3aec34eb44593861b8d8aba796e", - "94e949fd1ea33ac4886511c39ee3b98d2580a6fd66d2bb8517de0a1cd0afefea29702b1f6a3e88e74ce0686c7d53bde8", - "b042fccde247d98b363c6edb1d921da2b099493e00713ba8d44b3d777901f33b41dd496f58baff1c1fc725e3f16f4b13", - "a67d7a1f2c0754f97f9db696fb95c9f5462eb0a3fcb60dc072aebfad1ff3faabb6dd8f769f37c2e4df01af81863e410c", - ]; - assert_eq!(pks_hex, expected_pks_hex); - } - - #[tokio::test] - async fn test_sign_message() { - let root_sk = SecretKey::from_seed(SEED.as_ref()); - let store = SimpleDerivationStore::new(&root_sk); - - // Derive the first key. - store.derive_to_index(1).await; - - let message = b"Hello, Chia blockchain!"; - - let pk = store.public_key(0).await.unwrap(); - let sk = store.to_secret_key(&pk).await.unwrap(); - - let sk_hex: String = sk.to_bytes().encode_hex(); - let pk_hex: String = pk.to_bytes().encode_hex(); - let manual_pk_hex: String = sk.public_key().to_bytes().encode_hex(); - - assert_eq!(pk_hex, manual_pk_hex); - assert_eq!( - sk_hex, - "125e0b72383dfc25e125613331f1d0b3d011e4e66e06e851cdfbbcf4d32dfb46" - ); - assert_eq!( - pk_hex, - "8584adae5630842a1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dcc9183fe61e48d8bf" - ); - - let sig_hex: String = sign(&sk, message).to_bytes().encode_hex(); - - assert_eq!( - sig_hex, - hex::encode(hex!( - " - a8cdf5167335be076807e285ed64e6ec649f560ee9f361265d918395fda3d583 - 76fbe22967cea973a61495a50755716c1951d7f3429faebea09b968c8347fe7d - 1effa1285d944ed26d17481b01689c2c4c9c7ab2435388267a40f7355ed79dc2 - " - )) - ); - } -} diff --git a/src/stores/derivation_store/pk_derivation_store.rs b/src/stores/derivation_store/pk_derivation_store.rs new file mode 100644 index 00000000..72a647c3 --- /dev/null +++ b/src/stores/derivation_store/pk_derivation_store.rs @@ -0,0 +1,126 @@ +use chia_bls::{derive_keys::master_to_wallet_unhardened_intermediate, DerivableKey, PublicKey}; +use chia_wallet::{ + standard::{standard_puzzle_hash, DEFAULT_HIDDEN_PUZZLE_HASH}, + DeriveSynthetic, +}; +use indexmap::IndexMap; +use parking_lot::Mutex; + +use crate::{DerivationStore, PublicKeyStore}; + +/// An in-memory derivation store implementation. +pub struct PkDerivationStore { + intermediate_pk: PublicKey, + hidden_puzzle_hash: [u8; 32], + derivations: Mutex>, +} + +impl PkDerivationStore { + /// Creates a new key store with the default hidden puzzle hash. + /// An intermediate secret key is derived from the root key. + pub fn new(root_key: &PublicKey) -> Self { + Self { + intermediate_pk: master_to_wallet_unhardened_intermediate(root_key), + hidden_puzzle_hash: DEFAULT_HIDDEN_PUZZLE_HASH, + derivations: Mutex::new(IndexMap::new()), + } + } + + /// Creates a new key store with a custom hidden puzzle hash. + /// An intermediate secret key is derived from the root key. + pub fn new_with_hidden_puzzle(root_key: &PublicKey, hidden_puzzle_hash: [u8; 32]) -> Self { + let mut key_store = Self::new(root_key); + key_store.hidden_puzzle_hash = hidden_puzzle_hash; + key_store + } +} + +impl DerivationStore for PkDerivationStore { + async fn index_of_ph(&self, puzzle_hash: [u8; 32]) -> Option { + self.derivations + .lock() + .iter() + .position(|derivation| *derivation.1 == puzzle_hash) + .map(|index| index as u32) + } + + async fn puzzle_hash(&self, index: u32) -> Option<[u8; 32]> { + self.derivations + .lock() + .get_index(index as usize) + .map(|derivation| *derivation.1) + } + + async fn puzzle_hashes(&self) -> Vec<[u8; 32]> { + self.derivations.lock().values().copied().collect() + } +} + +impl PublicKeyStore for PkDerivationStore { + async fn count(&self) -> u32 { + self.derivations.lock().len() as u32 + } + + async fn public_key(&self, index: u32) -> Option { + self.derivations + .lock() + .get_index(index as usize) + .map(|derivation| derivation.0.clone()) + } + + async fn index_of_pk(&self, public_key: &PublicKey) -> Option { + self.derivations + .lock() + .get_index_of(public_key) + .map(|index| index as u32) + } + + async fn derive_to_index(&self, index: u32) { + let mut derivations = self.derivations.lock(); + let current = derivations.len() as u32; + for index in current..index { + let public_key = self + .intermediate_pk + .derive_unhardened(index) + .derive_synthetic(&self.hidden_puzzle_hash); + let puzzle_hash = standard_puzzle_hash(&public_key); + derivations.insert(public_key, puzzle_hash); + } + } +} + +#[cfg(test)] +mod tests { + use chia_bls::SecretKey; + use hex::ToHex; + + use crate::testing::SEED; + + use super::*; + + #[tokio::test] + async fn test_key_pairs() { + let root_pk = SecretKey::from_seed(SEED.as_ref()).public_key(); + let store = PkDerivationStore::new(&root_pk); + + // Derive the first 10 keys. + store.derive_to_index(10).await; + + let pks: Vec = store.derivations.lock().keys().cloned().collect(); + let pks_hex: Vec = pks.iter().map(|pk| pk.to_bytes().encode_hex()).collect(); + + let expected_pks_hex = vec![ + "8584adae5630842a1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dcc9183fe61e48d8bf", + "b07c0a00a30501d18418df3ece3335d2c7339e0589e61b9230cffc9573d0df739726e84e55e91d68744b0f3791285b96", + "963eea603ce281d63daca66f0926421f51d6d24027e498cb9d02f6477e3e01c4c4fda666fc3ea4199fdf566244ba74e0", + "b33bbccea1926947b7a83080c8b6a193121bf3480411abeb5fb31fa70002c150ba1d40a5c6a53b36cdd51ea468f0c2e4", + "a7bf25f67541a4e292a06282d714bbbc203a8bd6b0d0b804d097a071388f84665659a1a1f220130d97bcd2c4775f1077", + "a8fa6e4e7732e36d6e4e537c172a2c1e7fd926a43abd191c5aa82974a54e9de1addb32ea404724722dedc78407bbb098", + "b40b3c77251cea8e4c9cbbecbaa7fe40e9ad5e1298c83696d879cffd0c28f9ed61d5f3aec34eb44593861b8d8aba796e", + "94e949fd1ea33ac4886511c39ee3b98d2580a6fd66d2bb8517de0a1cd0afefea29702b1f6a3e88e74ce0686c7d53bde8", + "b042fccde247d98b363c6edb1d921da2b099493e00713ba8d44b3d777901f33b41dd496f58baff1c1fc725e3f16f4b13", + "a67d7a1f2c0754f97f9db696fb95c9f5462eb0a3fcb60dc072aebfad1ff3faabb6dd8f769f37c2e4df01af81863e410c", + ]; + assert_eq!(pks_hex, expected_pks_hex); + } +} diff --git a/src/stores/derivation_store/sk_derivation_store.rs b/src/stores/derivation_store/sk_derivation_store.rs new file mode 100644 index 00000000..99b0caeb --- /dev/null +++ b/src/stores/derivation_store/sk_derivation_store.rs @@ -0,0 +1,225 @@ +use chia_bls::{ + derive_keys::master_to_wallet_unhardened_intermediate, DerivableKey, PublicKey, SecretKey, +}; +use chia_wallet::{ + standard::{standard_puzzle_hash, DEFAULT_HIDDEN_PUZZLE_HASH}, + DeriveSynthetic, +}; +use indexmap::IndexMap; +use parking_lot::Mutex; + +use crate::{DerivationStore, PublicKeyStore, SecretKeyStore}; + +/// An in-memory derivation store implementation. +pub struct SkDerivationStore { + intermediate_sk: SecretKey, + hidden_puzzle_hash: [u8; 32], + derivations: Mutex>, +} + +#[derive(Clone)] +struct SkDerivation { + secret_key: SecretKey, + puzzle_hash: [u8; 32], +} + +impl SkDerivationStore { + /// Creates a new key store with the default hidden puzzle hash. + /// An intermediate secret key is derived from the root key. + pub fn new(root_key: &SecretKey) -> Self { + Self { + intermediate_sk: master_to_wallet_unhardened_intermediate(root_key), + hidden_puzzle_hash: DEFAULT_HIDDEN_PUZZLE_HASH, + derivations: Mutex::new(IndexMap::new()), + } + } + + /// Creates a new key store with a custom hidden puzzle hash. + /// An intermediate secret key is derived from the root key. + pub fn new_with_hidden_puzzle(root_key: &SecretKey, hidden_puzzle_hash: [u8; 32]) -> Self { + let mut key_store = Self::new(root_key); + key_store.hidden_puzzle_hash = hidden_puzzle_hash; + key_store + } +} + +impl DerivationStore for SkDerivationStore { + async fn index_of_ph(&self, puzzle_hash: [u8; 32]) -> Option { + self.derivations + .lock() + .iter() + .position(|derivation| derivation.1.puzzle_hash == puzzle_hash) + .map(|index| index as u32) + } + + async fn puzzle_hash(&self, index: u32) -> Option<[u8; 32]> { + self.derivations + .lock() + .get_index(index as usize) + .map(|derivation| derivation.1.puzzle_hash) + } + + async fn puzzle_hashes(&self) -> Vec<[u8; 32]> { + self.derivations + .lock() + .values() + .map(|item| item.puzzle_hash) + .collect() + } +} + +impl PublicKeyStore for SkDerivationStore { + async fn count(&self) -> u32 { + self.derivations.lock().len() as u32 + } + + async fn public_key(&self, index: u32) -> Option { + self.derivations + .lock() + .get_index(index as usize) + .map(|derivation| derivation.0.clone()) + } + + async fn index_of_pk(&self, public_key: &PublicKey) -> Option { + self.derivations + .lock() + .get_index_of(public_key) + .map(|index| index as u32) + } + + async fn derive_to_index(&self, index: u32) { + let mut derivations = self.derivations.lock(); + let current = derivations.len() as u32; + for index in current..index { + let secret_key = self + .intermediate_sk + .derive_unhardened(index) + .derive_synthetic(&self.hidden_puzzle_hash); + let public_key = secret_key.public_key(); + let puzzle_hash = standard_puzzle_hash(&public_key); + derivations.insert( + public_key, + SkDerivation { + secret_key, + puzzle_hash, + }, + ); + } + } +} + +impl SecretKeyStore for SkDerivationStore { + async fn secret_key(&self, index: u32) -> Option { + self.derivations + .lock() + .get_index(index as usize) + .map(|item| item.1.secret_key.clone()) + } +} + +#[cfg(test)] +mod tests { + use chia_bls::sign; + use hex::ToHex; + use hex_literal::hex; + + use crate::testing::SEED; + + use super::*; + + #[tokio::test] + async fn test_key_pairs() { + let root_sk = SecretKey::from_seed(SEED.as_ref()); + let store = SkDerivationStore::new(&root_sk); + + // Derive the first 10 keys. + store.derive_to_index(10).await; + + let sks: Vec = store + .derivations + .lock() + .values() + .map(|derivation| derivation.secret_key.clone()) + .collect(); + let pks: Vec = store.derivations.lock().keys().cloned().collect(); + + let sks_hex: Vec = sks.iter().map(|sk| sk.to_bytes().encode_hex()).collect(); + let pks_hex: Vec = pks.iter().map(|pk| pk.to_bytes().encode_hex()).collect(); + + let manual_pks_hex: Vec = sks + .iter() + .map(|sk| sk.public_key().to_bytes().encode_hex()) + .collect(); + + assert_eq!(&pks_hex, &manual_pks_hex); + + let expected_sks_hex = vec![ + "125e0b72383dfc25e125613331f1d0b3d011e4e66e06e851cdfbbcf4d32dfb46", + "3d6e7e99226e190bc495938ac5be8d8689445bfaa6a6396f021473c2adc8ef7d", + "36f3ac4d23877d2e90086864d28b9e0aa88e5fdc2ac08115bf11bacd8c1c4ccc", + "01711de0ebf04952c7bc53ba9ff4463f063c60d4c507dd8ed4a6448d9d2ced08", + "501aaeb127c2480976badc5601165b84d7906ed9c8754e05b2f65d5a6fdbc20b", + "5093bccdb9936b10c6f330b10abbf7c2937e7ccc76a35704a2f1cee96c23e173", + "013ec1bc4a37bea42cbc792ad23102f0759d2f941627b70dff039571e062301c", + "1b9abaeb853ef0102ce5bb07f804e628fd846e2b7e67036b109dfd4b06414e81", + "50394a6e095bd279b1ff1a095a15ecd561a66a5c3c5b9ab51214f97a3e68017e", + "41a5f2ebd31a3e338aa7af91fb1235dbb02b053fbf38073e0de9b448b2d1fdb0", + ]; + assert_eq!(sks_hex, expected_sks_hex); + + let expected_pks_hex = vec![ + "8584adae5630842a1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dcc9183fe61e48d8bf", + "b07c0a00a30501d18418df3ece3335d2c7339e0589e61b9230cffc9573d0df739726e84e55e91d68744b0f3791285b96", + "963eea603ce281d63daca66f0926421f51d6d24027e498cb9d02f6477e3e01c4c4fda666fc3ea4199fdf566244ba74e0", + "b33bbccea1926947b7a83080c8b6a193121bf3480411abeb5fb31fa70002c150ba1d40a5c6a53b36cdd51ea468f0c2e4", + "a7bf25f67541a4e292a06282d714bbbc203a8bd6b0d0b804d097a071388f84665659a1a1f220130d97bcd2c4775f1077", + "a8fa6e4e7732e36d6e4e537c172a2c1e7fd926a43abd191c5aa82974a54e9de1addb32ea404724722dedc78407bbb098", + "b40b3c77251cea8e4c9cbbecbaa7fe40e9ad5e1298c83696d879cffd0c28f9ed61d5f3aec34eb44593861b8d8aba796e", + "94e949fd1ea33ac4886511c39ee3b98d2580a6fd66d2bb8517de0a1cd0afefea29702b1f6a3e88e74ce0686c7d53bde8", + "b042fccde247d98b363c6edb1d921da2b099493e00713ba8d44b3d777901f33b41dd496f58baff1c1fc725e3f16f4b13", + "a67d7a1f2c0754f97f9db696fb95c9f5462eb0a3fcb60dc072aebfad1ff3faabb6dd8f769f37c2e4df01af81863e410c", + ]; + assert_eq!(pks_hex, expected_pks_hex); + } + + #[tokio::test] + async fn test_sign_message() { + let root_sk = SecretKey::from_seed(SEED.as_ref()); + let store = SkDerivationStore::new(&root_sk); + + // Derive the first key. + store.derive_to_index(1).await; + + let message = b"Hello, Chia blockchain!"; + + let pk = store.public_key(0).await.unwrap(); + let sk = store.to_secret_key(&pk).await.unwrap(); + + let sk_hex: String = sk.to_bytes().encode_hex(); + let pk_hex: String = pk.to_bytes().encode_hex(); + let manual_pk_hex: String = sk.public_key().to_bytes().encode_hex(); + + assert_eq!(pk_hex, manual_pk_hex); + assert_eq!( + sk_hex, + "125e0b72383dfc25e125613331f1d0b3d011e4e66e06e851cdfbbcf4d32dfb46" + ); + assert_eq!( + pk_hex, + "8584adae5630842a1766bc444d2b872dd3080f4e5daaecf6f762a4be7dc148f37868149d4217f3dcc9183fe61e48d8bf" + ); + + let sig_hex: String = sign(&sk, message).to_bytes().encode_hex(); + + assert_eq!( + sig_hex, + hex::encode(hex!( + " + a8cdf5167335be076807e285ed64e6ec649f560ee9f361265d918395fda3d583 + 76fbe22967cea973a61495a50755716c1951d7f3429faebea09b968c8347fe7d + 1effa1285d944ed26d17481b01689c2c4c9c7ab2435388267a40f7355ed79dc2 + " + )) + ); + } +} diff --git a/src/utils.rs b/src/utils.rs index a0fdd44c..4e9d0ffd 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,27 +1,3 @@ -use std::io; - -use chia_client::Peer; -use chia_protocol::{Coin, RejectPuzzleSolution}; -use clvm_traits::{FromClvm, FromClvmError}; -use clvm_utils::{tree_hash, CurriedProgram}; -use clvmr::{allocator::NodePtr, serde::node_from_bytes, Allocator}; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum RequestPuzzleError { - #[error("peer error: {0}")] - Peer(#[from] chia_client::Error), - - #[error("clvm error: {0}")] - Clvm(#[from] FromClvmError), - - #[error("io error: {0}")] - Io(#[from] io::Error), - - #[error("wrong mod hash")] - WrongModHash([u8; 32]), -} - pub fn u64_to_bytes(amount: u64) -> Vec { let bytes: Vec = amount.to_be_bytes().into(); let mut slice = bytes.as_slice(); @@ -37,28 +13,29 @@ pub fn u64_to_bytes(amount: u64) -> Vec { slice.into() } -pub async fn request_puzzle_args( - a: &mut Allocator, - peer: &Peer, - coin: &Coin, - expected_mod_hash: [u8; 32], - height: u32, -) -> Result -where - T: FromClvm, -{ - let puzzle = peer - .request_puzzle_and_solution(coin.parent_coin_info, height) - .await? - .puzzle; - - let ptr = node_from_bytes(a, puzzle.as_slice())?; - let puzzle: CurriedProgram = FromClvm::from_clvm(a, ptr)?; - - let mod_hash = tree_hash(a, puzzle.program); - if mod_hash != expected_mod_hash { - return Err(RequestPuzzleError::WrongModHash(mod_hash)); +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bytes() { + assert_eq!(u64_to_bytes(0), &[]); + assert_eq!(u64_to_bytes(1), &[1]); + assert_eq!(u64_to_bytes(45213), &[0, 176, 157]); + assert_eq!( + u64_to_bytes(u64::MAX), + &[255, 255, 255, 255, 255, 255, 255, 255] + ); + assert_eq!(u64_to_bytes(1721349832147), &[1, 144, 200, 113, 253, 211]); + assert_eq!(u64_to_bytes(10000), &[39, 16]); + assert_eq!(u64_to_bytes(1000), &[3, 232]); + assert_eq!( + u64_to_bytes(u64::MAX - 1), + &[255, 255, 255, 255, 255, 255, 255, 254] + ); + assert_eq!( + u64_to_bytes(u64::MAX / 2), + &[127, 255, 255, 255, 255, 255, 255, 255] + ); } - - Ok(puzzle.args) } diff --git a/src/wallet.rs b/src/wallet.rs index f40a9870..c64c398c 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,5 +1,5 @@ mod coin_selection; -mod sync; +mod sync_manager; pub use coin_selection::*; -pub use sync::*; +pub use sync_manager::*; diff --git a/src/wallet/sync.rs b/src/wallet/sync.rs deleted file mode 100644 index ab926e3c..00000000 --- a/src/wallet/sync.rs +++ /dev/null @@ -1,185 +0,0 @@ -use std::sync::Arc; - -use chia_client::{Error, Peer, PeerEvent}; -use tokio::sync::mpsc; - -use crate::{CoinStore, DerivationStore}; - -/// Settings used while syncing a derivation wallet. -#[derive(Debug, Clone)] -pub struct SyncConfig { - /// The minimum number of unused derivation indices. - pub minimum_unused_derivations: u32, -} - -impl Default for SyncConfig { - fn default() -> Self { - Self { - minimum_unused_derivations: 100, - } - } -} - -/// Syncs a derivation wallet. -pub async fn incremental_sync( - peer: Arc, - derivation_store: Arc, - coin_store: Arc, - config: SyncConfig, - synced_sender: mpsc::Sender<()>, -) -> Result<(), Error<()>> { - let mut event_receiver = peer.receiver().resubscribe(); - - let derivations = derivation_store.count().await; - - if derivations > 0 { - let mut puzzle_hashes = Vec::new(); - for index in 0..derivations { - puzzle_hashes.push(derivation_store.puzzle_hash(index).await.unwrap().into()); - } - let coin_states = peer.register_for_ph_updates(puzzle_hashes, 0).await?; - coin_store.update_coin_state(coin_states).await; - } - - sync_to_unused_index( - peer.as_ref(), - derivation_store.as_ref(), - coin_store.as_ref(), - &config, - ) - .await?; - - synced_sender.send(()).await.ok(); - - while let Ok(event) = event_receiver.recv().await { - if let PeerEvent::CoinStateUpdate(update) = event { - coin_store.update_coin_state(update.items).await; - sync_to_unused_index( - peer.as_ref(), - derivation_store.as_ref(), - coin_store.as_ref(), - &config, - ) - .await?; - - synced_sender.send(()).await.ok(); - } - } - - Ok(()) -} - -/// Subscribe to another set of puzzle hashes. -pub async fn subscribe( - peer: &Peer, - coin_store: &impl CoinStore, - puzzle_hashes: Vec<[u8; 32]>, -) -> Result<(), Error<()>> { - let mut i = 0; - while i < puzzle_hashes.len() { - let coin_states = peer - .register_for_ph_updates( - puzzle_hashes[i..i + 100] - .iter() - .map(|ph| ph.into()) - .collect(), - 0, - ) - .await?; - coin_store.update_coin_state(coin_states).await; - // TODO: Remove this hardcoded value? - i += 100; - } - Ok(()) -} - -/// Create more derivations for a wallet. -pub async fn derive_more( - peer: &Peer, - derivation_store: &impl DerivationStore, - coin_store: &impl CoinStore, - amount: u32, -) -> Result<(), Error<()>> { - let start = derivation_store.count().await; - derivation_store.derive_to_index(start + amount).await; - - let mut puzzle_hashes: Vec<[u8; 32]> = Vec::new(); - - for index in start..(start + amount) { - puzzle_hashes.push(derivation_store.puzzle_hash(index).await.unwrap()); - } - - subscribe(peer, coin_store, puzzle_hashes).await -} - -/// Gets the last unused derivation index for a wallet. -pub async fn unused_index( - derivation_store: &impl DerivationStore, - coin_store: &impl CoinStore, -) -> Option { - let derivations = derivation_store.count().await; - let mut unused_index = None; - for index in (0..derivations).rev() { - let puzzle_hash = derivation_store.puzzle_hash(index).await.unwrap(); - if !coin_store.is_used(puzzle_hash).await { - unused_index = Some(index); - } else { - break; - } - } - unused_index -} - -/// Syncs a wallet such that there are enough unused derivations. -pub async fn sync_to_unused_index( - peer: &Peer, - derivation_store: &impl DerivationStore, - coin_store: &impl CoinStore, - config: &SyncConfig, -) -> Result> { - // If there aren't any derivations, generate the first batch. - let derivations = derivation_store.count().await; - - if derivations == 0 { - derive_more( - peer, - derivation_store, - coin_store, - config.minimum_unused_derivations, - ) - .await?; - } - - loop { - let derivations = derivation_store.count().await; - let result = unused_index(derivation_store, coin_store).await; - - if let Some(unused_index) = result { - // Calculate the extra unused derivations after that index. - let extra_indices = derivations - unused_index; - - // Make sure at least `gap` indices are available if needed. - if extra_indices < config.minimum_unused_derivations { - derive_more( - peer, - derivation_store, - coin_store, - config.minimum_unused_derivations, - ) - .await?; - } - - // Return the unused derivation index. - return Ok(unused_index); - } else { - // Generate more puzzle hashes and check again. - derive_more( - peer, - derivation_store, - coin_store, - config.minimum_unused_derivations, - ) - .await?; - } - } -} diff --git a/src/wallet/sync_manager.rs b/src/wallet/sync_manager.rs new file mode 100644 index 00000000..aee64755 --- /dev/null +++ b/src/wallet/sync_manager.rs @@ -0,0 +1,438 @@ +use std::{future::Future, sync::Arc}; + +use chia_client::{Peer, PeerEvent}; +use chia_protocol::CoinState; +use tokio::sync::{broadcast, mpsc, Mutex}; + +use crate::{CoinStore, DerivationStore}; + +/// Settings used while syncing a derivation wallet. +#[derive(Debug, Clone)] +pub struct SyncConfig { + /// The minimum number of unused derivation indices. + pub minimum_unused_derivations: u32, +} + +impl Default for SyncConfig { + fn default() -> Self { + Self { + minimum_unused_derivations: 100, + } + } +} + +/// An interface for everything needed to sync a wallet. +pub trait SyncManager { + /// The error that may be returned from methods. + type Error; + + /// Receives the next batch of coin state updates. + fn receive_updates(&self) -> impl Future>> + Send; + + /// Subscribes to a set of puzzle hashes and returns the initial coin states. + fn subscribe( + &self, + puzzle_hashes: Vec<[u8; 32]>, + min_height: u32, + ) -> impl Future, Self::Error>> + Send; + + /// Whether or not a given puzzle hash has been used. + fn is_used(&self, puzzle_hash: [u8; 32]) -> impl Future + Send; + + /// Sent whenever the wallet has been caught up. + fn handle_synced(&self) -> impl Future> + Send; + + /// Sent whenever a coin which does not match a puzzle hash directly is received. + fn apply_updates( + &self, + coin_states: Vec, + ) -> impl Future> + Send; +} + +/// A simple implementation of a sync manager, that syncs against a single peer and coin store. +pub struct SimpleSyncManager { + peer: Arc, + receiver: Mutex>, + sender: mpsc::Sender<()>, + coin_store: Arc, +} + +impl SimpleSyncManager { + /// Creates a new sync manager for a given peer and coin store. + /// The sender is for whenever the wallet is synced. + pub fn new(peer: Arc, coin_store: Arc, sender: mpsc::Sender<()>) -> Self { + let receiver = peer.receiver().resubscribe(); + + Self { + peer, + receiver: Mutex::new(receiver), + sender, + coin_store, + } + } +} + +impl SyncManager for SimpleSyncManager +where + C: CoinStore + Send + Sync, +{ + type Error = chia_client::Error<()>; + + async fn receive_updates(&self) -> Option> { + loop { + if let PeerEvent::CoinStateUpdate(update) = + self.receiver.lock().await.recv().await.ok()? + { + return Some(update.items); + } + } + } + + async fn subscribe( + &self, + puzzle_hashes: Vec<[u8; 32]>, + min_height: u32, + ) -> Result, Self::Error> { + self.peer + .register_for_ph_updates( + puzzle_hashes.into_iter().map(Into::into).collect(), + min_height, + ) + .await + } + + async fn is_used(&self, puzzle_hash: [u8; 32]) -> bool { + self.coin_store.is_used(puzzle_hash).await + } + + async fn handle_synced(&self) -> Result<(), Self::Error> { + self.sender.send(()).await.unwrap(); + Ok(()) + } + + async fn apply_updates(&self, coin_states: Vec) -> Result<(), Self::Error> { + self.coin_store.update_coin_state(coin_states).await; + Ok(()) + } +} + +/// Syncs a derivation wallet. +pub async fn incremental_sync( + sync_manager: Arc>, + derivation_store: Arc, + config: SyncConfig, +) -> Result<(), Err> { + let derivations = derivation_store.count().await; + + if derivations > 0 { + let mut puzzle_hashes = Vec::new(); + for index in 0..derivations { + puzzle_hashes.push(derivation_store.puzzle_hash(index).await.unwrap()); + } + let coin_states = sync_manager.subscribe(puzzle_hashes, 0).await?; + sync_manager.apply_updates(coin_states).await?; + } + + sync_to_unused_index(sync_manager.as_ref(), derivation_store.as_ref(), &config).await?; + + sync_manager.handle_synced().await?; + + while let Some(updates) = sync_manager.receive_updates().await { + sync_manager.apply_updates(updates).await?; + sync_to_unused_index(sync_manager.as_ref(), derivation_store.as_ref(), &config).await?; + + sync_manager.handle_synced().await?; + } + + Ok(()) +} + +/// Subscribe to another set of puzzle hashes. +pub async fn subscribe( + sync_manager: &impl SyncManager, + puzzle_hashes: Vec<[u8; 32]>, +) -> Result<(), Err> { + let mut i = 0; + while i < puzzle_hashes.len() { + let coin_states = sync_manager + .subscribe( + puzzle_hashes[i..(i + 100).min(puzzle_hashes.len())].to_vec(), + 0, + ) + .await?; + sync_manager.apply_updates(coin_states).await?; + i += 100; + } + Ok(()) +} + +/// Create more derivations for a wallet. +pub async fn derive_more( + sync_manager: &impl SyncManager, + derivation_store: &impl DerivationStore, + amount: u32, +) -> Result<(), Err> { + let start = derivation_store.count().await; + derivation_store.derive_to_index(start + amount).await; + + let mut puzzle_hashes: Vec<[u8; 32]> = Vec::new(); + + for index in start..(start + amount) { + puzzle_hashes.push(derivation_store.puzzle_hash(index).await.unwrap()); + } + + subscribe(sync_manager, puzzle_hashes).await +} + +/// Gets the last unused derivation index for a wallet. +pub async fn unused_index( + sync_manager: &impl SyncManager, + derivation_store: &impl DerivationStore, +) -> Result, Err> { + let derivations = derivation_store.count().await; + let mut unused_index = None; + for index in (0..derivations).rev() { + if !sync_manager + .is_used(derivation_store.puzzle_hash(index).await.unwrap()) + .await + { + unused_index = Some(index); + } else { + break; + } + } + Ok(unused_index) +} + +/// Syncs a wallet such that there are enough unused derivations. +pub async fn sync_to_unused_index( + sync_manager: &impl SyncManager, + derivation_store: &impl DerivationStore, + config: &SyncConfig, +) -> Result { + // If there aren't any derivations, generate the first batch. + let derivations = derivation_store.count().await; + + if derivations == 0 { + derive_more( + sync_manager, + derivation_store, + config.minimum_unused_derivations, + ) + .await?; + } + + loop { + let derivations = derivation_store.count().await; + let result = unused_index(sync_manager, derivation_store).await?; + + if let Some(unused_index) = result { + // Calculate the extra unused derivations after that index. + let extra_indices = derivations - unused_index; + + // Make sure at least `gap` indices are available if needed. + if extra_indices < config.minimum_unused_derivations { + derive_more( + sync_manager, + derivation_store, + config.minimum_unused_derivations, + ) + .await?; + } + + // Return the unused derivation index. + return Ok(unused_index); + } + + // Generate more puzzle hashes and check again. + derive_more( + sync_manager, + derivation_store, + config.minimum_unused_derivations, + ) + .await?; + } +} + +#[cfg(test)] +mod tests { + use std::collections::{HashMap, HashSet}; + + use chia_bls::SecretKey; + use chia_protocol::{Bytes32, Coin}; + + use crate::{testing::SEED, MemoryCoinStore, PublicKeyStore, SkDerivationStore}; + + use super::*; + + #[derive(Default)] + struct TestSyncManager { + // Coin id to hints. + hints: Mutex>>, + subscriptions: Mutex>, + coin_states: Mutex>, + coin_store: Arc, + synced_sender: Option>, + coin_state_receiver: Mutex>>>, + } + + impl TestSyncManager { + fn new( + coin_state_receiver: mpsc::Receiver>, + synced_sender: mpsc::Sender<()>, + ) -> Self { + Self { + synced_sender: Some(synced_sender), + coin_state_receiver: Mutex::new(Some(coin_state_receiver)), + ..Default::default() + } + } + } + + impl SyncManager for TestSyncManager { + type Error = (); + + async fn receive_updates(&self) -> Option> { + if let Some(receiver) = &mut *self.coin_state_receiver.lock().await { + return receiver.recv().await; + } + None + } + + async fn subscribe( + &self, + puzzle_hashes: Vec<[u8; 32]>, + min_height: u32, + ) -> Result, Self::Error> { + self.subscriptions.lock().await.extend(&puzzle_hashes); + + let hints = self.hints.lock().await; + + Ok(self + .coin_states + .lock() + .await + .iter() + .filter(|coin_state| { + let height = coin_state + .spent_height + .unwrap_or(0) + .max(coin_state.created_height.unwrap_or(0)); + + // If below min height, skip. + if height < min_height { + return false; + } + + let puzzle_hash = &coin_state.coin.puzzle_hash.into(); + + // If puzzle hash doesn't match, + if !puzzle_hashes.contains(puzzle_hash) { + // Check if the coin is hinted to one of the puzzle hashes. + if let Some(hints) = hints.get(&coin_state.coin.coin_id()) { + return puzzle_hashes.iter().any(|ph| hints.contains(ph)); + } + + return false; + } + + true + }) + .cloned() + .collect()) + } + + async fn is_used(&self, puzzle_hash: [u8; 32]) -> bool { + self.coin_store.is_used(puzzle_hash).await + } + + async fn handle_synced(&self) -> Result<(), Self::Error> { + if let Some(sender) = &self.synced_sender { + sender.send(()).await.unwrap(); + } + Ok(()) + } + + async fn apply_updates(&self, coin_states: Vec) -> Result<(), Self::Error> { + self.coin_store.update_coin_state(coin_states).await; + Ok(()) + } + } + + #[tokio::test] + async fn test_sync_nothing() { + let root_sk = SecretKey::from_seed(SEED.as_ref()); + let derivation_store = Arc::new(SkDerivationStore::new(&root_sk)); + + let (coin_state_sender, coin_state_receiver) = mpsc::channel(32); + let (synced_sender, mut synced_receiver) = mpsc::channel(32); + let sync = Arc::new(TestSyncManager::new(coin_state_receiver, synced_sender)); + + tokio::spawn(incremental_sync( + sync, + derivation_store, + SyncConfig::default(), + )); + + drop(coin_state_sender); + + synced_receiver.recv().await.unwrap(); + } + + #[tokio::test] + async fn test_sync_one_by_one_and_update() { + let root_sk = SecretKey::from_seed(SEED.as_ref()); + let derivation_store = Arc::new(SkDerivationStore::new(&root_sk)); + + derivation_store.derive_to_index(10).await; + let puzzle_hashes = derivation_store.puzzle_hashes().await; + + let (coin_state_sender, coin_state_receiver) = mpsc::channel(32); + let (synced_sender, mut synced_receiver) = mpsc::channel(32); + let sync = Arc::new(TestSyncManager::new(coin_state_receiver, synced_sender)); + + let coin_states: Vec = puzzle_hashes + .into_iter() + .map(|ph| { + CoinState::new( + Coin::new(Bytes32::new([0; 32]), ph.into(), 1), + None, + Some(123), + ) + }) + .collect(); + sync.coin_states.lock().await.extend(coin_states.clone()); + + tokio::spawn(incremental_sync( + sync.clone(), + derivation_store.clone(), + SyncConfig { + minimum_unused_derivations: 1, + }, + )); + + synced_receiver.recv().await.unwrap(); + + let coins: HashSet = sync.coin_store.unspent_coins().await.into_iter().collect(); + let expected_coins: HashSet = coin_states + .into_iter() + .map(|coin_state| coin_state.coin) + .collect(); + assert_eq!(coins, expected_coins); + + assert_eq!(derivation_store.count().await, 11); + + let next_ph = derivation_store.puzzle_hash(10).await.unwrap(); + let coin_state = CoinState::new( + Coin::new(Bytes32::new([1; 32]), next_ph.into(), 1), + Some(1000), + Some(999), + ); + coin_state_sender.send(vec![coin_state]).await.unwrap(); + + synced_receiver.recv().await.unwrap(); + + assert_eq!(sync.coin_store.unspent_coins().await.len(), 10); + assert_eq!(derivation_store.count().await, 12); + } +}