From 4c361a7c5be03de3bef49311876c1a3b8b37165e Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Sun, 4 Aug 2024 19:45:57 +0900 Subject: [PATCH 01/16] Index tx output related with contract tx --- crates/chain/src/keychain.rs | 2 +- crates/wallet/src/wallet/mod.rs | 42 +++++-- crates/wallet/tests/common.rs | 208 +++++++++++++++++++++++++++++++- crates/wallet/tests/wallet.rs | 33 +++++ 4 files changed, 272 insertions(+), 13 deletions(-) diff --git a/crates/chain/src/keychain.rs b/crates/chain/src/keychain.rs index 2333f66b..55ac23af 100644 --- a/crates/chain/src/keychain.rs +++ b/crates/chain/src/keychain.rs @@ -11,7 +11,7 @@ //! [`SpkTxOutIndex`]: crate::SpkTxOutIndex #[cfg(feature = "miniscript")] -mod txout_index; +pub mod txout_index; use tapyrus::Amount; #[cfg(feature = "miniscript")] pub use txout_index::*; diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 3a026a43..45a76b08 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -24,7 +24,10 @@ use alloc::{ use core::fmt; use core::ops::Deref; use descriptor::error::Error as DescriptorError; -use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}; +use miniscript::{ + psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}, + Descriptor, DescriptorPublicKey, +}; use tapyrus::hashes::{Hash, HashEngine}; use tapyrus::sighash::{EcdsaSighashType, TapSighashType}; use tapyrus::{ @@ -39,21 +42,21 @@ use tapyrus::{ TxIn, }; pub use tdk_chain::keychain::Balance; +use tdk_chain::tx_graph::CalculateFeeError; use tdk_chain::{ - contract, indexed_tx_graph, - keychain::KeychainTxOutIndex, + contract, + indexed_tx_graph::{self}, + keychain::{self, KeychainTxOutIndex}, local_chain::{ self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain, }, spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, tx_graph::{CanonicalTx, TxGraph}, Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, Contract, - FullTxOut, IndexedTxGraph, + DescriptorExt, DescriptorId, FullTxOut, IndexedTxGraph, }; use tdk_persist::{Persist, PersistBackend}; -use tdk_chain::tx_graph::CalculateFeeError; - pub mod coin_selection; pub mod export; pub mod signer; @@ -2654,6 +2657,17 @@ impl Wallet { &self.indexed_graph.index } + /// Insert a descriptor with a keychain associated to it. + pub fn insert_descriptor( + &mut self, + keychain: KeychainKind, + descriptor: Descriptor, + ) -> tdk_chain::keychain::txout_index::ChangeSet { + self.indexed_graph + .index + .insert_descriptor(keychain, descriptor) + } + /// Get a reference to the inner [`LocalChain`]. pub fn local_chain(&self) -> &LocalChain { &self.chain @@ -2745,19 +2759,29 @@ impl Wallet { } else { let new_contract = Contract { contract_id: contract_id.clone(), - contract, + contract: contract.clone(), payment_base, spendable, }; changeset .contract .insert(contract_id.clone(), new_contract.clone()); + let p2c_public_key = + self.pay_to_contract_key(&payment_base, contract) + .map_err(|e| CreateContractError::Error { + e: anyhow::Error::new(e), + })?; + let descriptor_str = format!("pkh({})", p2c_public_key); + let (descriptor, _) = + Descriptor::::parse_descriptor(&self.secp, &descriptor_str) + .unwrap(); + let _ = self.insert_descriptor(KeychainKind::External, descriptor); self.contracts.insert(contract_id.clone(), new_contract); self.persist .stage_and_commit(changeset) .map_err(|e| CreateContractError::Error { e })?; } - return Ok(()); + Ok(()) } /// Update pay-to-contract information to the wallet. @@ -2786,7 +2810,7 @@ impl Wallet { } else { return Err(UpdateContractError::ContractNotFound { contract_id }); } - return Ok(()); + Ok(()) } } diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index 8b68c490..bd4f59a4 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -2,10 +2,10 @@ use std::str::FromStr; use tapyrus::hashes::Hash; -use tapyrus::script::color_identifier::ColorIdentifier; +use tapyrus::script::color_identifier::{self, ColorIdentifier}; use tapyrus::{ - transaction, Address, Amount, BlockHash, FeeRate, MalFixTxid, Network, OutPoint, Transaction, - TxIn, TxOut, + transaction, Address, Amount, BlockHash, FeeRate, MalFixTxid, Network, OutPoint, PublicKey, + Transaction, TxIn, TxOut, }; use tdk_chain::indexed_tx_graph::Indexer; use tdk_chain::{BlockId, ConfirmationTime}; @@ -388,6 +388,208 @@ pub fn get_funded_wallet_with_two_colored_coin_and_change( (wallet, tx1.malfix_txid(), color_id1, color_id2) } + +fn get_p2c_address(wallet: &mut Wallet, color_id: Option) -> Address { + let payment_base = + PublicKey::from_str("02046e89be90d26872e1318feb7d5ca7a6f588118e76f4906cf5b8ef262b63ab49") + .unwrap(); + let contract = "metadata".as_bytes().to_vec(); + wallet.store_contract( + "contract_id".to_string(), + contract.clone(), + payment_base, + true, + ); + wallet + .create_pay_to_contract_address(&payment_base, contract.clone(), color_id) + .unwrap() +} + +pub fn get_funded_wallet_with_p2c_and_change( + descriptor: &str, + change: &str, +) -> (Wallet, MalFixTxid, Address) { + let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Dev).unwrap(); + let fund_address = wallet.peek_address(KeychainKind::External, 0).address; + let sendto_address: Address = Address::from_str("msvWktzSViRZ5kiepVr6W8VrgE8a6mbiVu") + .expect("address") + .require_network(Network::Dev) + .unwrap(); + + let tx0 = Transaction { + version: transaction::Version::ONE, + lock_time: tapyrus::absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: MalFixTxid::all_zeros(), + vout: 0, + }, + script_sig: Default::default(), + sequence: Default::default(), + witness: Default::default(), + }], + output: vec![TxOut { + value: Amount::from_tap(76_000), + script_pubkey: fund_address.script_pubkey(), + }], + }; + + let out_point = OutPoint { + txid: tx0.malfix_txid(), + vout: 0, + }; + + let receive_address = get_p2c_address(&mut wallet, None); + + let tx1 = Transaction { + version: transaction::Version::ONE, + lock_time: tapyrus::absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: out_point, + script_sig: Default::default(), + sequence: Default::default(), + witness: Default::default(), + }], + output: vec![ + TxOut { + value: Amount::from_tap(50_000), + script_pubkey: receive_address.script_pubkey(), + }, + TxOut { + value: Amount::from_tap(25_000), + script_pubkey: sendto_address.script_pubkey(), + }, + ], + }; + + wallet + .insert_checkpoint(BlockId { + height: 1_000, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + wallet + .insert_checkpoint(BlockId { + height: 2_000, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + wallet + .insert_tx( + tx0, + ConfirmationTime::Confirmed { + height: 1_000, + time: 100, + }, + ) + .unwrap(); + wallet + .insert_tx( + tx1.clone(), + ConfirmationTime::Confirmed { + height: 2_000, + time: 200, + }, + ) + .unwrap(); + + (wallet, tx1.malfix_txid(), receive_address) +} + +pub fn get_funded_wallet_with_colored_p2c_and_change( + descriptor: &str, + change: &str, +) -> (Wallet, MalFixTxid, Address, ColorIdentifier) { + let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Dev).unwrap(); + let fund_address = wallet.peek_address(KeychainKind::External, 0).address; + let sendto_address: Address = Address::from_str("msvWktzSViRZ5kiepVr6W8VrgE8a6mbiVu") + .expect("address") + .require_network(Network::Dev) + .unwrap(); + + let tx0 = Transaction { + version: transaction::Version::ONE, + lock_time: tapyrus::absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: MalFixTxid::all_zeros(), + vout: 0, + }, + script_sig: Default::default(), + sequence: Default::default(), + witness: Default::default(), + }], + output: vec![TxOut { + value: Amount::from_tap(76_000), + script_pubkey: fund_address.script_pubkey(), + }], + }; + + let out_point = OutPoint { + txid: tx0.malfix_txid(), + vout: 0, + }; + let color_id = ColorIdentifier::reissuable(fund_address.script_pubkey().as_script()); + let receive_address = get_p2c_address(&mut wallet, Some(color_id)); + let tx1 = Transaction { + version: transaction::Version::ONE, + lock_time: tapyrus::absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: out_point, + script_sig: Default::default(), + sequence: Default::default(), + witness: Default::default(), + }], + output: vec![ + TxOut { + value: Amount::from_tap(100), + script_pubkey: receive_address.script_pubkey(), + }, + TxOut { + value: Amount::from_tap(50_000), + script_pubkey: fund_address.script_pubkey(), + }, + TxOut { + value: Amount::from_tap(25_000), + script_pubkey: sendto_address.script_pubkey(), + }, + ], + }; + + wallet + .insert_checkpoint(BlockId { + height: 1_000, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + wallet + .insert_checkpoint(BlockId { + height: 2_000, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + wallet + .insert_tx( + tx0, + ConfirmationTime::Confirmed { + height: 1_000, + time: 100, + }, + ) + .unwrap(); + wallet + .insert_tx( + tx1.clone(), + ConfirmationTime::Confirmed { + height: 2_000, + time: 200, + }, + ) + .unwrap(); + + (wallet, tx1.malfix_txid(), receive_address, color_id) +} + /// Return a fake wallet that appears to be funded for testing. /// /// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 61b9b8e0..3483b4ba 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -344,6 +344,27 @@ fn test_get_funded_wallet_colored_balance() { assert_eq!(wallet.balance(color_id).confirmed, Amount::ONE_TAP); } +#[test] +fn test_get_funded_wallet_p2c_balance() { + let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)"; + let (wallet, _, _) = + get_funded_wallet_with_p2c_and_change(get_test_pkh(), change_desc); + + assert_eq!( + wallet.balance(ColorIdentifier::default()).confirmed, + Amount::from_tap(50_000) + ); +} + +#[test] +fn test_get_funded_wallet_colored_p2c_balance() { + let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)"; + let (wallet, _, _, color_id) = + get_funded_wallet_with_colored_p2c_and_change(get_test_pkh(), change_desc); + + assert_eq!(wallet.balance(color_id).confirmed, Amount::from_tap(100)); +} + #[test] fn test_get_funded_wallet_sent_and_received() { let (wallet, txid) = get_funded_wallet_pkh(); @@ -393,6 +414,18 @@ fn test_get_funded_wallet_with_color_sent_and_received() { assert_eq!(received.to_tap(), 1); } +#[test] +fn test_get_funded_wallet_with_p2c_sent_and_received() { + let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)"; + let (wallet, txid, _, color_id) = + get_funded_wallet_with_colored_p2c_and_change(get_test_pkh(), change_desc); + + let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let (sent, received) = wallet.sent_and_received(&tx, &color_id); + assert_eq!(sent.to_tap(), 0); + assert_eq!(received.to_tap(), 100); +} + #[test] fn test_get_funded_wallet_tx_fees() { let (wallet, txid) = get_funded_wallet_pkh(); From 8e751032e2990bc454db73115b40657387f1c2ba Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Sun, 4 Aug 2024 19:55:17 +0900 Subject: [PATCH 02/16] Add documentation for error --- crates/wallet/src/wallet/mod.rs | 24 ++++++++++++++++++++---- crates/wallet/tests/wallet.rs | 5 ++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 45a76b08..a79cb16e 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -459,8 +459,16 @@ impl std::error::Error for GenerateContractError {} /// An error that may occur when registering contract data. #[derive(Debug)] pub enum CreateContractError { - ContractAlreadyExist { contract_id: String }, - Error { e: anyhow::Error }, + /// Contract with the specified contract_id already exists. + ContractAlreadyExist { + /// identifier of contract. + contract_id: String, + }, + /// Other error. + Error { + /// An error that caused this error. + e: anyhow::Error, + }, } impl fmt::Display for CreateContractError { @@ -480,8 +488,16 @@ impl std::error::Error for CreateContractError {} /// An error that may occur when updating contract. #[derive(Debug)] pub enum UpdateContractError { - ContractNotFound { contract_id: String }, - Error { e: anyhow::Error }, + /// No contract with the specified contract_id has been found. + ContractNotFound { + /// identifier of contract. + contract_id: String, + }, + /// Other error. + Error { + /// An error that caused this error. + e: anyhow::Error, + }, } impl fmt::Display for UpdateContractError { diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 3483b4ba..d197995a 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -347,8 +347,7 @@ fn test_get_funded_wallet_colored_balance() { #[test] fn test_get_funded_wallet_p2c_balance() { let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)"; - let (wallet, _, _) = - get_funded_wallet_with_p2c_and_change(get_test_pkh(), change_desc); + let (wallet, _, _) = get_funded_wallet_with_p2c_and_change(get_test_pkh(), change_desc); assert_eq!( wallet.balance(ColorIdentifier::default()).confirmed, @@ -393,7 +392,7 @@ fn test_get_funded_wallet_sent_and_received() { #[test] fn test_get_funded_wallet_with_color_sent_and_received() { let change_desc = "pkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"; - let (mut wallet, txid, color_id) = + let (wallet, txid, color_id) = get_funded_wallet_with_nft_and_change(get_test_pkh(), change_desc); let mut tx_amounts: Vec<(MalFixTxid, (Amount, Amount))> = wallet From 3ea18259dbac089214cc66a5834e9ad3ac76afb5 Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Mon, 5 Aug 2024 14:22:55 +0900 Subject: [PATCH 03/16] Fix CI Failure --- crates/chain/src/keychain.rs | 1 + crates/wallet/src/wallet/mod.rs | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/chain/src/keychain.rs b/crates/chain/src/keychain.rs index 55ac23af..ab57617f 100644 --- a/crates/chain/src/keychain.rs +++ b/crates/chain/src/keychain.rs @@ -10,6 +10,7 @@ //! //! [`SpkTxOutIndex`]: crate::SpkTxOutIndex +/// Indexer for TxOut #[cfg(feature = "miniscript")] pub mod txout_index; use tapyrus::Amount; diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index a79cb16e..f77607dc 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -467,7 +467,7 @@ pub enum CreateContractError { /// Other error. Error { /// An error that caused this error. - e: anyhow::Error, + reason: String, }, } @@ -477,7 +477,9 @@ impl fmt::Display for CreateContractError { CreateContractError::ContractAlreadyExist { contract_id } => { write!(f, "contract already exists (contract_id: {})", contract_id) } - CreateContractError::Error { e } => e.fmt(f), + CreateContractError::Error { reason } => { + write!(f, "can not create contract address (reason: {})", reason) + } } } } @@ -496,7 +498,7 @@ pub enum UpdateContractError { /// Other error. Error { /// An error that caused this error. - e: anyhow::Error, + reason: String, }, } @@ -506,7 +508,9 @@ impl fmt::Display for UpdateContractError { UpdateContractError::ContractNotFound { contract_id } => { write!(f, "contract does not found (contract_id: {})", contract_id) } - UpdateContractError::Error { e } => e.fmt(f), + UpdateContractError::Error { reason } => { + write!(f, "can not update contract address (reason: {})", reason) + } } } } @@ -2785,7 +2789,7 @@ impl Wallet { let p2c_public_key = self.pay_to_contract_key(&payment_base, contract) .map_err(|e| CreateContractError::Error { - e: anyhow::Error::new(e), + reason: e.to_string(), })?; let descriptor_str = format!("pkh({})", p2c_public_key); let (descriptor, _) = @@ -2795,7 +2799,9 @@ impl Wallet { self.contracts.insert(contract_id.clone(), new_contract); self.persist .stage_and_commit(changeset) - .map_err(|e| CreateContractError::Error { e })?; + .map_err(|e| CreateContractError::Error { + reason: e.to_string(), + })?; } Ok(()) } @@ -2822,7 +2828,9 @@ impl Wallet { self.contracts.insert(contract_id.clone(), new_contract); self.persist .stage_and_commit(changeset) - .map_err(|e| UpdateContractError::Error { e })?; + .map_err(|e| UpdateContractError::Error { + reason: e.to_string(), + })?; } else { return Err(UpdateContractError::ContractNotFound { contract_id }); } From 652d24ff22f623187358e2004e15bb59f9ca992d Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Fri, 9 Aug 2024 19:07:10 +0900 Subject: [PATCH 04/16] Fix for review comment --- crates/chain/src/keychain/txout_index.rs | 9 +- crates/chain/src/spk_txout_index.rs | 38 ++++-- crates/sqlite/src/store.rs | 22 +-- crates/wallet/src/wallet/mod.rs | 116 +++++++++++----- crates/wallet/tests/common.rs | 95 ++++++++++++- crates/wallet/tests/wallet.rs | 166 +++++++++++++++++++++-- 6 files changed, 371 insertions(+), 75 deletions(-) diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index 640508b3..31d0568c 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -10,8 +10,8 @@ use core::{ ops::{Bound, RangeBounds}, }; use tapyrus::{ - hashes::Hash, script::color_identifier::ColorIdentifier, Amount, MalFixTxid, OutPoint, Script, - SignedAmount, Transaction, TxOut, + hashes::Hash, script::color_identifier::ColorIdentifier, Amount, MalFixTxid, OutPoint, + PublicKey, Script, ScriptBuf, SignedAmount, Transaction, TxOut, }; use crate::Append; @@ -378,6 +378,11 @@ impl KeychainTxOutIndex { Some((keychain.clone(), *last_index)) } + /// Insert payment base key for pay-to-contract script pubkey + pub fn insert_p2c_spk(&mut self, spk: ScriptBuf, payment_base: PublicKey) { + self.inner.insert_p2c_spk(spk, payment_base); + } + /// Returns whether the spk under the `keychain`'s `index` has been used. /// /// Here, "unused" means that after the script pubkey was stored in the index, the index has diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index d76d86ac..e5855088 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -5,8 +5,8 @@ use crate::{ indexed_tx_graph::Indexer, }; use tapyrus::{ - script::color_identifier::ColorIdentifier, Amount, MalFixTxid, OutPoint, Script, ScriptBuf, - SignedAmount, Transaction, TxOut, + script::color_identifier::ColorIdentifier, Amount, MalFixTxid, OutPoint, PublicKey, Script, + ScriptBuf, SignedAmount, Transaction, TxOut, }; /// An index storing [`TxOut`]s that have a script pubkey that matches those in a list. @@ -41,6 +41,8 @@ pub struct SpkTxOutIndex { txouts: BTreeMap, /// Lookup from spk index to outpoints that had that spk spk_txouts: BTreeSet<(I, OutPoint)>, + /// Pay-to-contract payment_base lookup by p2c spk + p2c_spks: HashMap, } impl Default for SpkTxOutIndex { @@ -51,6 +53,7 @@ impl Default for SpkTxOutIndex { spk_indices: Default::default(), spk_txouts: Default::default(), unused: Default::default(), + p2c_spks: Default::default(), } } } @@ -103,12 +106,17 @@ impl SpkTxOutIndex { /// Scan a single `TxOut` for a matching script pubkey and returns the index that matches the /// script pubkey (if any). pub fn scan_txout(&mut self, op: OutPoint, txout: &TxOut) -> Option<&I> { - let spk_i = if txout.script_pubkey.is_colored() { - self.spk_indices.get(&ScriptBuf::from_bytes( - txout.script_pubkey.as_bytes()[35..].to_vec(), - )) + let script_pubkey = if txout.script_pubkey.is_colored() { + txout.script_pubkey.remove_color() } else { - self.spk_indices.get(&txout.script_pubkey) + txout.script_pubkey.clone() + }; + let payment_base = self.p2c_spks.get(&script_pubkey); + + let spk_i = if let Some(p) = payment_base { + self.spk_indices.get(p.as_script()) + } else { + self.spk_indices.get(&script_pubkey) }; if let Some(spk_i) = spk_i { self.txouts.insert(op, (spk_i.clone(), txout.clone())); @@ -192,6 +200,12 @@ impl SpkTxOutIndex { &self.spks } + /// Insert payment base key for pay-to-contract script pubkey + pub fn insert_p2c_spk(&mut self, spk: ScriptBuf, payment_base: PublicKey) { + let p2c_spk = ScriptBuf::new_p2pkh(&payment_base.pubkey_hash()); + self.p2c_spks.insert(spk, p2c_spk); + } + /// Adds a script pubkey to scan for. Returns `false` and does nothing if spk already exists in the map /// /// the index will look for outputs spending to this spk whenever it scans new data. @@ -304,11 +318,17 @@ impl SpkTxOutIndex { } for txout in &tx.output { let script_pubkey = if txout.script_pubkey.is_colored() { - ScriptBuf::from_bytes(txout.script_pubkey.as_bytes()[35..].to_vec()) + txout.script_pubkey.remove_color() } else { txout.script_pubkey.clone() }; - if let Some(index) = self.index_of_spk(&script_pubkey) { + let payment_base = self.p2c_spks.get(&script_pubkey); + let script_pubkey_ref = if let Some(p) = payment_base { + p + } else { + &script_pubkey + }; + if let Some(index) = self.index_of_spk(script_pubkey_ref) { if range.contains(index) && txout.script_pubkey.color_id().unwrap_or_default() == *color_id { diff --git a/crates/sqlite/src/store.rs b/crates/sqlite/src/store.rs index 6b6d1772..6a800da2 100644 --- a/crates/sqlite/src/store.rs +++ b/crates/sqlite/src/store.rs @@ -502,7 +502,7 @@ impl Store { let payment_base: Vec = c.payment_base.to_bytes(); let spendable: u32 = if c.spendable { 1 } else { 0 }; insert_contract_stmt.execute(named_params! { - ":contract_id": contract_id, ":contract": contract, ":payment_base": payment_base, ":spendable": spendable}) + ":contract_id": contract_id, ":contract": contract, ":payment_base": payment_base, ":spendable": spendable }) .map_err(Error::Sqlite)?; } Ok(()) @@ -663,17 +663,17 @@ mod test { agg_changeset.unwrap().contract.get("id").unwrap().spendable, true ); - + let payment_base = PublicKey::from_str( + "028bde91b10013e08949a318018fedbd896534a549a278e220169ee2a36517c7aa", + ) + .unwrap(); let mut contract: contract::ChangeSet = contract::ChangeSet::new(); contract.insert( "id".to_string(), Contract { contract_id: "id".to_string(), contract: vec![0x00, 0x01, 0x02], - payment_base: PublicKey::from_str( - "028bde91b10013e08949a318018fedbd896534a549a278e220169ee2a36517c7aa", - ) - .unwrap(), + payment_base, spendable: false, }, ); @@ -881,6 +881,11 @@ mod test { indexer: keychain::ChangeSet::default(), }; + let payment_base = PublicKey::from_str( + "028bde91b10013e08949a318018fedbd896534a549a278e220169ee2a36517c7aa", + ) + .unwrap(); + let mut contract: contract::ChangeSet = contract::ChangeSet::new(); contract.insert( "id".to_string(), @@ -890,10 +895,7 @@ mod test { 0x00, 0x00, 0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44, 0x00, 0x00, ], - payment_base: PublicKey::from_str( - "028bde91b10013e08949a318018fedbd896534a549a278e220169ee2a36517c7aa", - ) - .unwrap(), + payment_base, spendable: true, }, ); diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index f77607dc..c25437a9 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -437,6 +437,11 @@ pub enum GenerateContractError { }, /// Invalid address InvalidAddress(tapyrus::address::Error), + /// Payment base is invalid. + InvalidPublicKey { + /// public key(payment base). + public_key: PublicKey, + }, } impl fmt::Display for GenerateContractError { @@ -448,6 +453,9 @@ impl fmt::Display for GenerateContractError { GenerateContractError::ContractTooLarge { length } => { write!(f, "contract is too large (size: {})", length) } + GenerateContractError::InvalidPublicKey { public_key } => { + write!(f, "invalid payment base ({})", public_key) + } GenerateContractError::InvalidAddress(e) => e.fmt(f), } } @@ -464,6 +472,11 @@ pub enum CreateContractError { /// identifier of contract. contract_id: String, }, + /// Payment base is invalid. + InvalidPublicKey { + /// public key(payment base). + public_key: PublicKey, + }, /// Other error. Error { /// An error that caused this error. @@ -477,6 +490,9 @@ impl fmt::Display for CreateContractError { CreateContractError::ContractAlreadyExist { contract_id } => { write!(f, "contract already exists (contract_id: {})", contract_id) } + CreateContractError::InvalidPublicKey { public_key } => { + write!(f, "invalid payment base ({})", public_key) + } CreateContractError::Error { reason } => { write!(f, "can not create contract address (reason: {})", reason) } @@ -546,6 +562,18 @@ impl Wallet { ) } + fn build_p2c_spk_from_contract(wallet: &mut Wallet, contracts: contract::ChangeSet) { + for (_, contract) in contracts { + let payment_base = contract.payment_base; + match wallet.create_pay_to_contract_script(&payment_base, contract.contract, None) { + Ok(spk) => { + wallet.insert_p2c_spk(spk, payment_base); + } + _ => {} + } + } + } + /// Initialize an empty [`Wallet`] with a custom genesis hash. /// /// This is like [`Wallet::new`] with an additional `genesis_hash` parameter. This is useful @@ -572,7 +600,7 @@ impl Wallet { let indexed_graph = IndexedTxGraph::new(index); - let contracts = contract::ChangeSet::new(); + let contracts: BTreeMap = contract::ChangeSet::new(); let mut persist = Persist::new(db); persist.stage(ChangeSet { @@ -583,7 +611,7 @@ impl Wallet { }); persist.commit().map_err(NewError::Persist)?; - Ok(Wallet { + let mut wallet = Wallet { signers, change_signers, network, @@ -591,8 +619,10 @@ impl Wallet { indexed_graph, persist, secp, - contracts, - }) + contracts: contracts.clone(), + }; + + Ok(wallet) } /// Load [`Wallet`] from the given persistence backend. @@ -682,7 +712,7 @@ impl Wallet { let persist = Persist::new(db); - Ok(Wallet { + let mut wallet = Wallet { signers, change_signers, chain, @@ -690,8 +720,10 @@ impl Wallet { persist, network, secp, - contracts, - }) + contracts: contracts.clone(), + }; + Self::build_p2c_spk_from_contract(&mut wallet, contracts); + Ok(wallet) } /// Either loads [`Wallet`] from persistence, or initializes it if it does not exist. @@ -982,17 +1014,26 @@ impl Wallet { contract: Vec, color_id: Option, ) -> Result, GenerateContractError> { + let script = self.create_pay_to_contract_script(payment_base, contract, color_id)?; + let address = Address::from_script(script.as_script(), self.network) + .map_err(GenerateContractError::InvalidAddress)?; + Ok(address) + } + + fn create_pay_to_contract_script( + &self, + payment_base: &PublicKey, + contract: Vec, + color_id: Option, + ) -> Result { let p2c_public_key = self.pay_to_contract_key(payment_base, contract)?; let pubkey_hash = p2c_public_key.pubkey_hash(); let script: ScriptBuf = match color_id { Some(c) if c.is_colored() => ScriptBuf::new_cp2pkh(&color_id.unwrap(), &pubkey_hash), _ => ScriptBuf::new_p2pkh(&pubkey_hash), }; - let address = Address::from_script(script.as_script(), self.network) - .map_err(GenerateContractError::InvalidAddress)?; - Ok(address) + Ok(script) } - /// Generate pay-to-contract public key with the specified content hash. pub fn pay_to_contract_key( &self, @@ -1007,6 +1048,18 @@ impl Wallet { length: contract.len(), }); } + let index = match self + .indexed_graph + .index + .index_of_spk(ScriptBuf::new_p2pkh(&payment_base.pubkey_hash()).as_script()) + { + Some(i) => i, + None => { + return Err(GenerateContractError::InvalidPublicKey { + public_key: payment_base.clone(), + }) + } + }; let commitment: Scalar = self.create_pay_to_contract_commitment(payment_base, contract); let pubkey = payment_base .inner @@ -2677,15 +2730,8 @@ impl Wallet { &self.indexed_graph.index } - /// Insert a descriptor with a keychain associated to it. - pub fn insert_descriptor( - &mut self, - keychain: KeychainKind, - descriptor: Descriptor, - ) -> tdk_chain::keychain::txout_index::ChangeSet { - self.indexed_graph - .index - .insert_descriptor(keychain, descriptor) + fn insert_p2c_spk(&mut self, spk: ScriptBuf, payment_base: PublicKey) { + self.indexed_graph.index.insert_p2c_spk(spk, payment_base) } /// Get a reference to the inner [`LocalChain`]. @@ -2772,11 +2818,18 @@ impl Wallet { contract: Vec, payment_base: PublicKey, spendable: bool, - ) -> Result<(), CreateContractError> { + ) -> Result { let mut changeset = ChangeSet::default(); if let Some(c) = self.contracts.get(&contract_id) { return Err(CreateContractError::ContractAlreadyExist { contract_id }); - } else { + } + let new_contract = { + let spk = self + .create_pay_to_contract_script(&payment_base, contract.clone(), None) + .map_err(|e| CreateContractError::InvalidPublicKey { + public_key: payment_base, + })?; + self.insert_p2c_spk(spk.clone(), payment_base); let new_contract = Contract { contract_id: contract_id.clone(), contract: contract.clone(), @@ -2786,24 +2839,17 @@ impl Wallet { changeset .contract .insert(contract_id.clone(), new_contract.clone()); - let p2c_public_key = - self.pay_to_contract_key(&payment_base, contract) - .map_err(|e| CreateContractError::Error { - reason: e.to_string(), - })?; - let descriptor_str = format!("pkh({})", p2c_public_key); - let (descriptor, _) = - Descriptor::::parse_descriptor(&self.secp, &descriptor_str) - .unwrap(); - let _ = self.insert_descriptor(KeychainKind::External, descriptor); - self.contracts.insert(contract_id.clone(), new_contract); + + self.contracts + .insert(contract_id.clone(), new_contract.clone()); self.persist .stage_and_commit(changeset) .map_err(|e| CreateContractError::Error { reason: e.to_string(), })?; - } - Ok(()) + new_contract + }; + Ok(new_contract) } /// Update pay-to-contract information to the wallet. diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index bd4f59a4..d30c7cc7 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -1,5 +1,7 @@ #![allow(unused)] +use miniscript::ToPublicKey; +use miniscript::{descriptor, Descriptor, DescriptorPublicKey}; use std::str::FromStr; use tapyrus::hashes::Hash; use tapyrus::script::color_identifier::{self, ColorIdentifier}; @@ -8,7 +10,7 @@ use tapyrus::{ Transaction, TxIn, TxOut, }; use tdk_chain::indexed_tx_graph::Indexer; -use tdk_chain::{BlockId, ConfirmationTime}; +use tdk_chain::{BlockId, ConfirmationTime, Contract}; use tdk_wallet::{KeychainKind, LocalOutput, Wallet}; /// Return a fake wallet that appears to be funded for testing. @@ -390,9 +392,7 @@ pub fn get_funded_wallet_with_two_colored_coin_and_change( } fn get_p2c_address(wallet: &mut Wallet, color_id: Option) -> Address { - let payment_base = - PublicKey::from_str("02046e89be90d26872e1318feb7d5ca7a6f588118e76f4906cf5b8ef262b63ab49") - .unwrap(); + let payment_base = get_payment_base(wallet); let contract = "metadata".as_bytes().to_vec(); wallet.store_contract( "contract_id".to_string(), @@ -404,6 +404,31 @@ fn get_p2c_address(wallet: &mut Wallet, color_id: Option) -> Ad .create_pay_to_contract_address(&payment_base, contract.clone(), color_id) .unwrap() } +pub fn get_payment_base(wallet: &Wallet) -> PublicKey { + let descriptor = wallet.get_descriptor_for_keychain(KeychainKind::External); + let desc = descriptor_to_public_key(descriptor); + desc.unwrap() +} +pub fn descriptor_to_public_key(descriptor: &Descriptor) -> Option { + match descriptor { + Descriptor::Pkh(pk) => { + let inner = pk.as_inner(); + match inner { + DescriptorPublicKey::Single(single) => { + let single_pub_key = single.key.clone(); + match single_pub_key { + descriptor::SinglePubKey::FullKey(pk) => Some(pk), + descriptor::SinglePubKey::XOnly(_) => None, + } + } + DescriptorPublicKey::XPub(xpub) => Some(xpub.xkey.public_key.to_public_key()), + _ => None, + } + } + // 他のDescriptorの場合、サポートされていない + _ => None, + } +} pub fn get_funded_wallet_with_p2c_and_change( descriptor: &str, @@ -430,7 +455,7 @@ pub fn get_funded_wallet_with_p2c_and_change( }], output: vec![TxOut { value: Amount::from_tap(76_000), - script_pubkey: fund_address.script_pubkey(), + script_pubkey: sendto_address.script_pubkey(), }], }; @@ -590,6 +615,66 @@ pub fn get_funded_wallet_with_colored_p2c_and_change( (wallet, tx1.malfix_txid(), receive_address, color_id) } +pub fn get_p2c_tx(wallet: &mut Wallet, contract: &Contract) -> Vec { + let payment_base = get_payment_base(wallet); + let fund_address = wallet.peek_address(KeychainKind::External, 0).address; + let receive_address = wallet + .create_pay_to_contract_address(&payment_base, contract.clone().contract, None) + .unwrap(); + let sendto_address: Address = Address::from_str("msvWktzSViRZ5kiepVr6W8VrgE8a6mbiVu") + .expect("address") + .require_network(Network::Dev) + .unwrap(); + + let tx0 = Transaction { + version: transaction::Version::ONE, + lock_time: tapyrus::absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: MalFixTxid::all_zeros(), + vout: 0, + }, + script_sig: Default::default(), + sequence: Default::default(), + witness: Default::default(), + }], + output: vec![TxOut { + value: Amount::from_tap(70_000), + script_pubkey: fund_address.script_pubkey(), + }], + }; + + let out_point = OutPoint { + txid: tx0.malfix_txid(), + vout: 0, + }; + + let tx1 = Transaction { + version: transaction::Version::ONE, + lock_time: tapyrus::absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: out_point, + script_sig: Default::default(), + sequence: Default::default(), + witness: Default::default(), + }], + output: vec![ + TxOut { + value: Amount::from_tap(10_000), + script_pubkey: fund_address.script_pubkey(), + }, + TxOut { + value: Amount::from_tap(20_000), + script_pubkey: receive_address.script_pubkey(), + }, + TxOut { + value: Amount::from_tap(25_000), + script_pubkey: sendto_address.script_pubkey(), + }, + ], + }; + vec![tx0, tx1] +} /// Return a fake wallet that appears to be funded for testing. /// /// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index d197995a..472ea66d 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -4,7 +4,9 @@ use std::thread::sleep; use std::time::Duration; use assert_matches::assert_matches; +use miniscript::{descriptor, Descriptor, DescriptorPublicKey, ToPublicKey}; use rand::random; +use tapyrus::bip32::{ExtendendPrivKey, ExtendendPubKey}; use tapyrus::hashes::Hash; use tapyrus::hex::DisplayHex; use tapyrus::key::Secp256k1; @@ -22,12 +24,13 @@ use tdk_chain::{BlockId, ConfirmationTime}; use tdk_persist::PersistBackend; use tdk_sqlite::rusqlite::Connection; use tdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor}; +use tdk_wallet::keys::DescriptorSecretKey; use tdk_wallet::psbt::PsbtUtils; use tdk_wallet::signer::{SignOptions, SignerError}; use tdk_wallet::wallet::coin_selection::{self, LargestFirstCoinSelection}; use tdk_wallet::wallet::error::CreateTxError; use tdk_wallet::wallet::tx_builder::AddForeignUtxoError; -use tdk_wallet::wallet::{scalar_from, NewError}; +use tdk_wallet::wallet::{scalar_from, CreateContractError, NewError}; use tdk_wallet::wallet::{AddressInfo, Balance, Wallet}; use tdk_wallet::KeychainKind; @@ -362,6 +365,10 @@ fn test_get_funded_wallet_colored_p2c_balance() { get_funded_wallet_with_colored_p2c_and_change(get_test_pkh(), change_desc); assert_eq!(wallet.balance(color_id).confirmed, Amount::from_tap(100)); + assert_eq!( + wallet.balance(ColorIdentifier::default()).confirmed, + Amount::from_tap(50_000) + ); } #[test] @@ -3032,9 +3039,7 @@ fn test_create_pay_to_contract_address() { let change_desc = get_test_pkh(); let wallet = Wallet::new_no_persist(desc, change_desc, Network::Prod).unwrap(); - let payment_base = - PublicKey::from_str("02046e89be90d26872e1318feb7d5ca7a6f588118e76f4906cf5b8ef262b63ab49") - .unwrap(); + let payment_base = xprv_to_public_key("xprv9s21ZrQH143K4EXURwMHuLS469fFzZyXk7UUpdKfQwhoHcAiYTakpe8pMU2RiEdvrU9McyuE7YDoKcXkoAwEGoK53WBDnKKv2zZbb9BzttX"); let contract = vec![0x00, 0x01, 0x02, 0x03]; let address = wallet @@ -3067,10 +3072,7 @@ fn test_create_pay_to_contract_address_error() { let desc = "pkh(xprv9s21ZrQH143K4EXURwMHuLS469fFzZyXk7UUpdKfQwhoHcAiYTakpe8pMU2RiEdvrU9McyuE7YDoKcXkoAwEGoK53WBDnKKv2zZbb9BzttX)"; let change_desc = get_test_pkh(); let wallet = Wallet::new_no_persist(desc, change_desc, Network::Prod).unwrap(); - - let payment_base = - PublicKey::from_str("02046e89be90d26872e1318feb7d5ca7a6f588118e76f4906cf5b8ef262b63ab49") - .unwrap(); + let payment_base = xprv_to_public_key("xprv9s21ZrQH143K4EXURwMHuLS469fFzZyXk7UUpdKfQwhoHcAiYTakpe8pMU2RiEdvrU9McyuE7YDoKcXkoAwEGoK53WBDnKKv2zZbb9BzttX"); let result = wallet.create_pay_to_contract_address(&payment_base, Vec::new(), None); assert!(result.is_err()); @@ -3095,14 +3097,28 @@ fn test_pay_to_contract_key() { .unwrap(); let contract = "metadata".as_bytes().to_vec(); - let key = wallet.pay_to_contract_key(&payment_base, contract).unwrap(); + let key = wallet.pay_to_contract_key(&payment_base, contract.clone()); + assert!(key.is_err()); + + let payment_base = xprv_to_public_key("xprv9s21ZrQH143K4EXURwMHuLS469fFzZyXk7UUpdKfQwhoHcAiYTakpe8pMU2RiEdvrU9McyuE7YDoKcXkoAwEGoK53WBDnKKv2zZbb9BzttX"); + let key = wallet.pay_to_contract_key(&payment_base, contract.clone()); + assert!(key.is_ok()); assert_eq!( - key, - PublicKey::from_str("0248be1e77d5b063e555681faa3824ad32d738569faec75844d7c4ce5cd963d229") + key.unwrap(), + PublicKey::from_str("0393caf4389ac699d42284c5031b95e21c6648e8f3fed561ca04fe6289c9199c37") .unwrap() ) } +fn xprv_to_public_key(xprv_str: &str) -> PublicKey { + let secp = Secp256k1::new(); + + let xprv = ExtendendPrivKey::from_str(xprv_str).unwrap(); + + let xpub = ExtendendPubKey::from_priv(&secp, &xprv); + xpub.public_key.to_public_key() +} + #[test] fn test_create_pay_to_contract_commitment() { let desc = "pkh(xprv9s21ZrQH143K4EXURwMHuLS469fFzZyXk7UUpdKfQwhoHcAiYTakpe8pMU2RiEdvrU9McyuE7YDoKcXkoAwEGoK53WBDnKKv2zZbb9BzttX)"; @@ -3718,9 +3734,9 @@ fn test_store_contract() { let file_path = temp_dir.path().join("sqlite3.db"); let db = tdk_file_store::Store::create_new(DB_MAGIC, file_path).expect("must create db"); let mut wallet = Wallet::new(desc, change_desc, db, Network::Dev).expect("must init wallet"); - let payment_base = - PublicKey::from_str("02046e89be90d26872e1318feb7d5ca7a6f588118e76f4906cf5b8ef262b63ab49") - .unwrap(); + let descriptor = wallet.get_descriptor_for_keychain(KeychainKind::External); + let payment_base = descriptor_to_public_key(descriptor).unwrap(); + let result = wallet.store_contract( "contract_id".to_string(), vec![0x00, 0x01, 0x02, 0x03], @@ -3743,3 +3759,125 @@ fn test_store_contract() { let result = wallet.update_contract("invalid_id".to_string(), false); assert!(result.is_err()); } + +#[test] +fn test_store_contract_invalid_payment_base() { + let (desc, change_desc) = get_test_pkh_with_change_desc(); + let temp_dir = tempfile::tempdir().expect("must create tempdir"); + let file_path = temp_dir.path().join("sqlite3.db"); + let db = tdk_file_store::Store::create_new(DB_MAGIC, file_path).expect("must create db"); + let mut wallet = Wallet::new(desc, change_desc, db, Network::Dev).expect("must init wallet"); + + let payment_base = + PublicKey::from_str("02046e89be90d26872e1318feb7d5ca7a6f588118e76f4906cf5b8ef262b63ab49") + .unwrap(); + let result = wallet.store_contract( + "contract_id".to_string(), + vec![0x00, 0x01, 0x02, 0x03], + payment_base, + true, + ); + assert!(result.is_err()); +} + +#[test] +fn test_restore_contract_from_db_wallet() { + let temp_dir = tempfile::tempdir().expect("must create tempdir"); + let file_path = temp_dir.path().join("store.sqlite"); + let (desc, change_desc) = get_test_pkh_with_change_desc(); + + // create new wallet + let (wallet_spk_index, txs) = { + let db = + tdk_sqlite::Store::new(Connection::open(file_path.clone()).expect("connect error")) + .expect("must create db"); + let mut wallet = + Wallet::new(desc, change_desc, db, Network::Dev).expect("must init wallet"); + + let _ = wallet.next_unused_address(KeychainKind::External); + let payment_base = get_payment_base(&wallet); + let ret = wallet.store_contract( + "id".to_string(), + vec![0x00, 0x01, 0x02], + payment_base, + false, + ); + assert!(ret.is_ok()); + + // create p2c transaction + let txs = get_p2c_tx(&mut wallet, &ret.unwrap()); + (wallet.spk_index().clone(), txs) + }; + + // recover wallet + { + let db = tdk_sqlite::Store::new(Connection::open(file_path).expect("connect error")) + .expect("must recover db"); + let mut wallet = Wallet::load(db).expect("must recover wallet"); + + let payment_base = get_payment_base(&wallet); + + // error when storing contract agian + let ret = + wallet.store_contract("id".to_string(), vec![0x00, 0x01, 0x02], payment_base, true); + assert!(ret.is_err()); + + // can update + let ret = wallet.update_contract("id".to_string(), true); + assert!(ret.is_ok()); + + let balance = wallet.balance(ColorIdentifier::default()); + assert_eq!( + balance, + Balance { + immature: Amount::ZERO, + trusted_pending: Amount::ZERO, + untrusted_pending: Amount::ZERO, + confirmed: Amount::ZERO + } + ); + + // index tx after recovering + wallet + .insert_checkpoint(BlockId { + height: 1_000, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + wallet + .insert_checkpoint(BlockId { + height: 2_000, + hash: BlockHash::all_zeros(), + }) + .unwrap(); + wallet + .insert_tx( + txs[0].clone(), + ConfirmationTime::Confirmed { + height: 1_000, + time: 100, + }, + ) + .unwrap(); + wallet + .insert_tx( + txs[1].clone(), + ConfirmationTime::Confirmed { + height: 2_000, + time: 100, + }, + ) + .unwrap(); + + let balance = wallet.balance(ColorIdentifier::default()); + assert_eq!( + balance, + Balance { + immature: Amount::ZERO, + trusted_pending: Amount::ZERO, + untrusted_pending: Amount::ZERO, + confirmed: Amount::from_tap(30_000), + } + ); + } +} From 46cf177b541556787a2e5330089c480e7ff680e5 Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Sun, 18 Aug 2024 21:20:21 +0900 Subject: [PATCH 05/16] Implement Wallet#is_valid_payment_base --- crates/wallet/src/wallet/mod.rs | 35 ++++++++++++++------------------- crates/wallet/tests/wallet.rs | 6 ------ 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index c25437a9..1c529479 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -437,11 +437,6 @@ pub enum GenerateContractError { }, /// Invalid address InvalidAddress(tapyrus::address::Error), - /// Payment base is invalid. - InvalidPublicKey { - /// public key(payment base). - public_key: PublicKey, - }, } impl fmt::Display for GenerateContractError { @@ -453,9 +448,6 @@ impl fmt::Display for GenerateContractError { GenerateContractError::ContractTooLarge { length } => { write!(f, "contract is too large (size: {})", length) } - GenerateContractError::InvalidPublicKey { public_key } => { - write!(f, "invalid payment base ({})", public_key) - } GenerateContractError::InvalidAddress(e) => e.fmt(f), } } @@ -1048,18 +1040,6 @@ impl Wallet { length: contract.len(), }); } - let index = match self - .indexed_graph - .index - .index_of_spk(ScriptBuf::new_p2pkh(&payment_base.pubkey_hash()).as_script()) - { - Some(i) => i, - None => { - return Err(GenerateContractError::InvalidPublicKey { - public_key: payment_base.clone(), - }) - } - }; let commitment: Scalar = self.create_pay_to_contract_commitment(payment_base, contract); let pubkey = payment_base .inner @@ -2811,6 +2791,14 @@ impl Wallet { self.persist.stage(ChangeSet::from(indexed_graph_changeset)); } + /// Return if specified payment base is in wallet. + fn is_valid_payment_base(&self, payment_base: &PublicKey) -> bool { + self.indexed_graph + .index + .index_of_spk(ScriptBuf::new_p2pkh(&payment_base.pubkey_hash()).as_script()) + .is_some() + } + /// Store pay-to-contract information to the wallet. pub fn store_contract( &mut self, @@ -2823,6 +2811,13 @@ impl Wallet { if let Some(c) = self.contracts.get(&contract_id) { return Err(CreateContractError::ContractAlreadyExist { contract_id }); } + + if !self.is_valid_payment_base(&payment_base) { + return Err(CreateContractError::InvalidPublicKey { + public_key: payment_base.clone(), + }); + } + let new_contract = { let spk = self .create_pay_to_contract_script(&payment_base, contract.clone(), None) diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 472ea66d..bec307c7 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -3092,14 +3092,8 @@ fn test_pay_to_contract_key() { let change_desc = get_test_pkh(); let wallet = Wallet::new_no_persist(desc, change_desc, Network::Prod).unwrap(); - let payment_base = - PublicKey::from_str("02046e89be90d26872e1318feb7d5ca7a6f588118e76f4906cf5b8ef262b63ab49") - .unwrap(); let contract = "metadata".as_bytes().to_vec(); - let key = wallet.pay_to_contract_key(&payment_base, contract.clone()); - assert!(key.is_err()); - let payment_base = xprv_to_public_key("xprv9s21ZrQH143K4EXURwMHuLS469fFzZyXk7UUpdKfQwhoHcAiYTakpe8pMU2RiEdvrU9McyuE7YDoKcXkoAwEGoK53WBDnKKv2zZbb9BzttX"); let key = wallet.pay_to_contract_key(&payment_base, contract.clone()); assert!(key.is_ok()); From 1db948ff54c7f43e0d0da21311c6a5e60c4f3dff Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Wed, 7 Aug 2024 14:05:07 +0900 Subject: [PATCH 06/16] Implement TxBuilder#add_contract_utxo to add utxo to tx_builder related with contract --- crates/wallet/src/wallet/mod.rs | 26 ++++++++++++++++ crates/wallet/src/wallet/tx_builder.rs | 43 +++++++++++++++++++++++++- crates/wallet/tests/common.rs | 2 +- crates/wallet/tests/wallet.rs | 29 +++++++++++++++++ 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 1c529479..3bd381c0 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -1026,6 +1026,7 @@ impl Wallet { }; Ok(script) } + /// Generate pay-to-contract public key with the specified content hash. pub fn pay_to_contract_key( &self, @@ -2375,8 +2376,33 @@ impl Wallet { descriptor.at_derivation_index(child).ok() } + pub fn contract_for_utxo( + &self, + utxo: &LocalOutput, + ) -> Result, GenerateContractError> { + for (contract_id, contract) in self.contracts.iter() { + let color_id = utxo.txout.script_pubkey.color_id(); + let p2c_script = self.create_pay_to_contract_script( + &contract.payment_base, + contract.contract.clone(), + color_id, + )?; + if p2c_script == utxo.txout.script_pubkey { + return Ok(Some(contract.clone())); + } + } + Ok(None) + } + fn get_available_utxos(&self) -> Vec<(LocalOutput, usize)> { self.list_unspent() + .filter(|utxo| { + if let Some(contract) = self.contract_for_utxo(&utxo).unwrap_or(None) { + contract.spendable + } else { + true + } + }) .map(|utxo| { let keychain = utxo.keychain; (utxo, { diff --git a/crates/wallet/src/wallet/tx_builder.rs b/crates/wallet/src/wallet/tx_builder.rs index b8d7f933..03878538 100644 --- a/crates/wallet/src/wallet/tx_builder.rs +++ b/crates/wallet/src/wallet/tx_builder.rs @@ -44,13 +44,14 @@ use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec}; use core::cell::RefCell; use core::fmt; use tapyrus::script::color_identifier::ColorIdentifier; +use tdk_chain::Contract; use tapyrus::psbt::{self, Psbt}; use tapyrus::script::PushBytes; use tapyrus::{absolute, Amount, FeeRate, MalFixTxid, OutPoint, ScriptBuf, Sequence, Transaction}; use super::coin_selection::CoinSelectionAlgorithm; -use super::{CreateTxError, Wallet}; +use super::{CreateTxError, GenerateContractError, Wallet}; use crate::collections::{BTreeMap, HashSet}; use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo}; @@ -301,6 +302,15 @@ impl<'a, Cs> TxBuilder<'a, Cs> { for utxo in utxos { let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain); let satisfaction_weight = descriptor.max_weight_to_satisfy().unwrap(); + + let contract_opt = wallet + .contract_for_utxo(&utxo) + .map_err(|_| AddUtxoError::ContractError)?; + if let Some(contract) = contract_opt { + if !contract.spendable { + continue; + } + } self.params.utxos.push(WeightedUtxo { satisfaction_weight, utxo: Utxo::Local(utxo), @@ -319,6 +329,31 @@ impl<'a, Cs> TxBuilder<'a, Cs> { self.add_utxos(&[outpoint]) } + /// Add a contract utxo to the internal list of utxos that **must** be spent + /// + /// The utxo identified with the specified outpoint should be contained as a walllet contract + pub fn add_contract_utxo(&mut self, outpoint: OutPoint) -> Result<&mut Self, AddUtxoError> { + { + let wallet = self.wallet.borrow(); + let utxo = wallet + .get_utxo(outpoint) + .ok_or(AddUtxoError::UnknownUtxo(outpoint))?; + + let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain); + let satisfaction_weight = descriptor.max_weight_to_satisfy().unwrap(); + let contract = wallet + .contract_for_utxo(&utxo) + .map_err(|_| AddUtxoError::ContractError)?; + if contract.is_some() { + self.params.utxos.push(WeightedUtxo { + satisfaction_weight, + utxo: Utxo::Local(utxo), + }); + } + } + Ok(self) + } + /// Add a foreign UTXO i.e. a UTXO not owned by this wallet. /// /// At a minimum to add a foreign UTXO we need: @@ -706,6 +741,9 @@ impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs> { pub enum AddUtxoError { /// Happens when trying to spend an UTXO that is not in the internal database UnknownUtxo(OutPoint), + + /// Error abount contract + ContractError, } impl fmt::Display for AddUtxoError { @@ -716,6 +754,9 @@ impl fmt::Display for AddUtxoError { "UTXO not found in the internal database for txid: {} with vout: {}", outpoint.txid, outpoint.vout ), + Self::ContractError => { + write!(f, "Contract related with UTXO is invalid") + } } } } diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index d30c7cc7..a0e23ff3 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -398,7 +398,7 @@ fn get_p2c_address(wallet: &mut Wallet, color_id: Option) -> Ad "contract_id".to_string(), contract.clone(), payment_base, - true, + false, ); wallet .create_pay_to_contract_address(&payment_base, contract.clone(), color_id) diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index bec307c7..8dac45d5 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -1451,6 +1451,35 @@ fn test_create_tx_with_reissuable_no_tpc_change() { assert_eq!(sent_received, (Amount::from_tap(100), Amount::from_tap(2))); } +fn test_create_tx_with_contract() { + let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)"; + let (mut wallet, txid, address) = + get_funded_wallet_with_p2c_and_change(get_test_pkh(), change_desc); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_tap(25_000)) + .add_contract_utxo(OutPoint { txid, vout: 0 }); + let psbt = builder.finish().unwrap(); + check_fee!(wallet, psbt); + assert_eq!(psbt.unsigned_tx.output.len(), 2); + let sent_received = wallet.sent_and_received( + &psbt.clone().extract_tx().expect("failed to extract tx"), + &ColorIdentifier::default(), + ); + assert_eq!( + sent_received, + ( + Amount::from_tap(50_000), + Amount::from_tap(25_000) - psbt.fee_amount().unwrap() + ) + ); +} + +// TODO: Fix this test #[test] fn test_create_tx_multi_colored_coin_recipients() { let change_desc = "pkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"; From f7a292ba9327d41182ab5fcc22bb2cb4ea1ea97e Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Mon, 12 Aug 2024 17:37:53 +0900 Subject: [PATCH 07/16] Fix test failure --- crates/chain/src/keychain/txout_index.rs | 5 +++++ crates/chain/src/spk_txout_index.rs | 5 +++++ crates/wallet/src/wallet/mod.rs | 20 ++++++++++++++------ crates/wallet/tests/common.rs | 2 +- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index 31d0568c..c0a448d0 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -383,6 +383,11 @@ impl KeychainTxOutIndex { self.inner.insert_p2c_spk(spk, payment_base); } + /// Returns script pubkey of payment base for pay-to-contract script + pub fn p2c_spk(&self, spk: &ScriptBuf) -> Option<&ScriptBuf> { + self.inner.p2c_spk(spk) + } + /// Returns whether the spk under the `keychain`'s `index` has been used. /// /// Here, "unused" means that after the script pubkey was stored in the index, the index has diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index e5855088..7b9a76b8 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -206,6 +206,11 @@ impl SpkTxOutIndex { self.p2c_spks.insert(spk, p2c_spk); } + /// Returns script pubkey of payment base for pay-to-contract script + pub fn p2c_spk(&self, spk: &ScriptBuf) -> Option<&ScriptBuf> { + self.p2c_spks.get(spk) + } + /// Adds a script pubkey to scan for. Returns `false` and does nothing if spk already exists in the map /// /// the index will look for outputs spending to this spk whenever it scans new data. diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 3bd381c0..f96f900a 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -2602,15 +2602,23 @@ impl Wallet { // Try to find the prev_script in our db to figure out if this is internal or external, // and the derivation index let script = if utxo.txout.script_pubkey.is_colored() { - ScriptBuf::from_bytes(utxo.txout.script_pubkey.as_bytes()[35..].to_vec()) + utxo.txout.script_pubkey.remove_color() } else { utxo.txout.script_pubkey }; - let (keychain, child) = self - .indexed_graph - .index - .index_of_spk(&script) - .ok_or(CreateTxError::UnknownUtxo)?; + let payment_base = self.spk_index().p2c_spk(&script); + + let (keychain, child) = if let Some(p) = payment_base { + self.indexed_graph + .index + .index_of_spk(p.clone().as_script()) + .ok_or(CreateTxError::UnknownUtxo)? + } else { + self.indexed_graph + .index + .index_of_spk(&script) + .ok_or(CreateTxError::UnknownUtxo)? + }; let mut psbt_input = psbt::Input { sighash_type, diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index a0e23ff3..5b435c2c 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -455,7 +455,7 @@ pub fn get_funded_wallet_with_p2c_and_change( }], output: vec![TxOut { value: Amount::from_tap(76_000), - script_pubkey: sendto_address.script_pubkey(), + script_pubkey: fund_address.script_pubkey(), }], }; From 8d74a8332ac9ef85aabea1d397e6d12a9742292f Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Tue, 13 Aug 2024 22:33:09 +0900 Subject: [PATCH 08/16] Sign p2c output --- crates/chain/Cargo.toml | 1 + crates/chain/src/contract.rs | 46 ++++++++++++++++++++- crates/wallet/src/wallet/mod.rs | 44 ++++++-------------- crates/wallet/src/wallet/signer.rs | 66 ++++++++++++++++++++++++++++-- crates/wallet/tests/common.rs | 27 +++++++----- crates/wallet/tests/wallet.rs | 32 +++++++++++---- 6 files changed, 160 insertions(+), 56 deletions(-) diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index b02106b5..28ebe0b3 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -16,6 +16,7 @@ readme = "README.md" # For no-std, remember to enable the bitcoin/no-std feature tapyrus = { git = "https://github.com/chaintope/rust-tapyrus", branch = "update_on_bitcoin_0.31.x", default-features = false, subdirectory = "tapyrus" } serde_crate = { package = "serde", version = "1", optional = true, features = ["derive", "rc"] } +num-bigint = { version = "=0.4.4", default-features = false } # Use hashbrown as a feature flag to have HashSet and HashMap from it. hashbrown = { version = "0.9.1", optional = true, features = ["serde"] } diff --git a/crates/chain/src/contract.rs b/crates/chain/src/contract.rs index 9fd7b52b..8388fb30 100644 --- a/crates/chain/src/contract.rs +++ b/crates/chain/src/contract.rs @@ -4,7 +4,14 @@ use alloc::collections::BTreeMap; use alloc::{string::String, vec::Vec}; -use tapyrus::PublicKey; +use num_bigint::BigUint; +use tapyrus::key::Error; +use tapyrus::secp256k1::{Scalar, SecretKey}; +use tapyrus::{ + hashes::{Hash, HashEngine}, + PrivateKey, +}; +use tapyrus::{Network, PublicKey}; /// The [`ChangeSet`] represents changes to [`Contract`]. pub type ChangeSet = BTreeMap; @@ -26,3 +33,40 @@ pub struct Contract { /// Set to 1 if available for payment, 0 if not pub spendable: bool, } + +impl Contract { + /// Create private key for Pay-to-Contract + pub fn create_private_key( + &self, + payment_base: &PublicKey, + network: Network, + ) -> Result { + let commitment: Scalar = + Self::create_pay_to_contract_commitment(payment_base, self.contract.clone()); + let sk = SecretKey::from_slice(&commitment.to_be_bytes())?; + Ok(PrivateKey::new(sk, network)) + } + + /// Compute pay-to-contract commitment as Scalar. + pub fn create_pay_to_contract_commitment( + payment_base: &PublicKey, + contract: Vec, + ) -> Scalar { + let mut engine = tapyrus::hashes::sha256::HashEngine::default(); + engine.input(&payment_base.inner.serialize()); + engine.input(&contract); + let result = tapyrus::hashes::sha256::Hash::from_engine(engine); + Self::scalar_from(&result.to_byte_array()[..]) + } + + /// Generate Scalar from bytes + pub fn scalar_from(bytes: &[u8]) -> Scalar { + let order: BigUint = BigUint::from_bytes_be(&Scalar::MAX.to_be_bytes()) + 1u32; + let n: BigUint = BigUint::from_bytes_be(bytes); + let n = n % order; + let bytes = n.to_bytes_be(); + let mut value = [0u8; 32]; + value[32 - bytes.len()..].copy_from_slice(&bytes); + Scalar::from_be_bytes(value).unwrap() + } +} diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index f96f900a..bf638f52 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -28,7 +28,6 @@ use miniscript::{ psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}, Descriptor, DescriptorPublicKey, }; -use tapyrus::hashes::{Hash, HashEngine}; use tapyrus::sighash::{EcdsaSighashType, TapSighashType}; use tapyrus::{ absolute, psbt, script::color_identifier::ColorIdentifier, Address, Block, FeeRate, MalFixTxid, @@ -37,6 +36,7 @@ use tapyrus::{ use tapyrus::{address::NetworkChecked, secp256k1::Scalar}; use tapyrus::{consensus::encode::serialize, transaction, BlockHash, Psbt}; use tapyrus::{constants::mainnet_genesis_block, constants::testnet_genesis_block, Amount}; +use tapyrus::{secp256k1::SecretKey, PrivateKey}; use tapyrus::{ secp256k1::{All, Secp256k1}, TxIn, @@ -53,7 +53,7 @@ use tdk_chain::{ spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, tx_graph::{CanonicalTx, TxGraph}, Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, Contract, - DescriptorExt, DescriptorId, FullTxOut, IndexedTxGraph, + DescriptorId, FullTxOut, IndexedTxGraph, }; use tdk_persist::{Persist, PersistBackend}; @@ -88,17 +88,6 @@ use self::coin_selection::Error; const COINBASE_MATURITY: u32 = 100; -/// Generate Scalar from bytes -pub fn scalar_from(bytes: &[u8]) -> Scalar { - let order: BigUint = BigUint::from_bytes_be(&Scalar::MAX.to_be_bytes()) + 1u32; - let n: BigUint = BigUint::from_bytes_be(bytes); - let n = n % order; - let bytes = n.to_bytes_be(); - let mut value = [0u8; 32]; - value[32 - bytes.len()..].copy_from_slice(&bytes); - Scalar::from_be_bytes(value).unwrap() -} - /// A Bitcoin wallet /// /// The `Wallet` acts as a way of coherently interfacing with output descriptors and related transactions. @@ -603,7 +592,7 @@ impl Wallet { }); persist.commit().map_err(NewError::Persist)?; - let mut wallet = Wallet { + let wallet = Wallet { signers, change_signers, network, @@ -1053,19 +1042,6 @@ impl Wallet { Ok(key) } - /// Compute pay-to-contract commitment as Scalar. - pub fn create_pay_to_contract_commitment( - &self, - payment_base: &PublicKey, - contract: Vec, - ) -> Scalar { - let mut engine = tapyrus::hashes::sha256::HashEngine::default(); - engine.input(&payment_base.inner.serialize()); - engine.input(&contract); - let result = tapyrus::hashes::sha256::Hash::from_engine(engine); - scalar_from(&result.to_byte_array()[..]) - } - /// Marks an address used of the given `keychain` at `index`. /// /// Returns whether the given index was present and then removed from the unused set. @@ -2368,10 +2344,14 @@ impl Wallet { } fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option { - let (keychain, child) = self - .indexed_graph - .index - .index_of_spk(&txout.script_pubkey)?; + let payment_base = self.spk_index().p2c_spk(&txout.script_pubkey); + let script_pubkey_ref = if let Some(p) = payment_base { + p + } else { + &txout.script_pubkey + }; + + let (keychain, child) = self.indexed_graph.index.index_of_spk(script_pubkey_ref)?; let descriptor = self.get_descriptor_for_keychain(keychain); descriptor.at_derivation_index(child).ok() } @@ -2397,7 +2377,7 @@ impl Wallet { fn get_available_utxos(&self) -> Vec<(LocalOutput, usize)> { self.list_unspent() .filter(|utxo| { - if let Some(contract) = self.contract_for_utxo(&utxo).unwrap_or(None) { + if let Some(contract) = self.contract_for_utxo(utxo).unwrap_or(None) { contract.spendable } else { true diff --git a/crates/wallet/src/wallet/signer.rs b/crates/wallet/src/wallet/signer.rs index a7349463..c6945df2 100644 --- a/crates/wallet/src/wallet/signer.rs +++ b/crates/wallet/src/wallet/signer.rs @@ -87,12 +87,14 @@ use alloc::vec::Vec; use core::cmp::Ordering; use core::fmt; use core::ops::{Bound::Included, Deref}; +use tapyrus::key::Secp256k1; +use tdk_chain::Contract; use tapyrus::bip32::{ChildNumber, DerivationPath, Fingerprint, Xpriv}; use tapyrus::hashes::hash160; -use tapyrus::secp256k1::Message; +use tapyrus::secp256k1::{Message, SecretKey}; use tapyrus::sighash::{EcdsaSighashType, TapSighash, TapSighashType}; -use tapyrus::{ecdsa, psbt, sighash, taproot}; +use tapyrus::{ecdsa, psbt, sighash, taproot, Script, ScriptBuf, TxOut}; use tapyrus::{key::XOnlyPublicKey, secp256k1}; use tapyrus::{PrivateKey, Psbt, PublicKey}; @@ -103,6 +105,7 @@ use miniscript::descriptor::{ use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey}; use super::utils::SecpCtx; +use super::Utxo; use crate::descriptor::XKeyUtils; use crate::psbt::PsbtUtils; use crate::wallet::error::MiniscriptPsbtError; @@ -429,6 +432,53 @@ impl SignerCommon for SignerWrapper { } } +impl SignerWrapper { + // Return if a script is related + fn is_relevant_script(&self, script_pubkey: &ScriptBuf) -> bool { + script_pubkey.is_cp2pkh() || script_pubkey.is_p2pkh() + } + + fn same_pubkey_hash(&self, script_pubkey: &ScriptBuf, public_key: &PublicKey) -> bool { + *script_pubkey == ScriptBuf::new_p2pkh(&public_key.pubkey_hash()) + } + + fn find_contract_keys( + &self, + sign_options: &SignOptions, + script_pubkey: &ScriptBuf, + pubkey: &PublicKey, + secp: &SecpCtx, + ) -> Option<(SecretKey, PublicKey)> { + sign_options.contracts.iter().find_map(|(_, contract)| { + let p2c_private_key = contract.create_private_key(pubkey, self.network).ok()?; + let p2c_public_key = p2c_private_key.public_key(secp); + if self.same_pubkey_hash(script_pubkey, &p2c_public_key) { + Some((p2c_private_key.inner, p2c_public_key)) + } else { + None + } + }) + } + + fn get_secret_key( + &self, + utxo: &TxOut, + pubkey: PublicKey, + sign_options: &SignOptions, + secp: &SecpCtx, + ) -> (SecretKey, PublicKey) { + if !self.is_relevant_script(&utxo.script_pubkey) { + return (self.inner, pubkey); + } + let script_pubkey = utxo.script_pubkey.remove_color(); + if self.same_pubkey_hash(&script_pubkey, &pubkey) { + return (self.inner, pubkey); + } + + self.find_contract_keys(sign_options, &script_pubkey, &pubkey, secp) + .unwrap_or((self.inner, pubkey)) + } +} impl InputSigner for SignerWrapper { fn sign_input( &self, @@ -448,6 +498,7 @@ impl InputSigner for SignerWrapper { } let pubkey = PublicKey::from_private_key(secp, self); + let utxo = psbt.get_utxo_for(input_index).unwrap(); if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) { return Ok(()); @@ -456,9 +507,12 @@ impl InputSigner for SignerWrapper { let (hash, hash_ty) = match self.ctx { SignerContext::Legacy => Legacy::sighash(psbt, input_index, ())?, }; + + // search for contract + let (secret_key, public_key) = self.get_secret_key(&utxo, pubkey, sign_options, secp); sign_psbt_ecdsa( - &self.inner, - pubkey, + &secret_key, + public_key, &mut psbt.inputs[input_index], hash, hash_ty, @@ -699,6 +753,9 @@ pub struct SignOptions { /// or not. /// Defaults to `true`, i.e., we always grind ECDSA signature to sign with low r. pub allow_grinding: bool, + + /// Contracts for Pay-To-Contract transaction + pub contracts: BTreeMap, } /// Customize which taproot script-path leaves the signer should sign. @@ -728,6 +785,7 @@ impl Default for SignOptions { tap_leaves_options: TapLeavesOptions::default(), sign_with_tap_internal_key: true, allow_grinding: true, + contracts: BTreeMap::default(), } } } diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index 5b435c2c..d58856fd 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -391,18 +391,19 @@ pub fn get_funded_wallet_with_two_colored_coin_and_change( (wallet, tx1.malfix_txid(), color_id1, color_id2) } -fn get_p2c_address(wallet: &mut Wallet, color_id: Option) -> Address { +fn get_p2c_address(wallet: &mut Wallet, color_id: Option) -> (Address, Contract) { let payment_base = get_payment_base(wallet); let contract = "metadata".as_bytes().to_vec(); - wallet.store_contract( + let ret = wallet.store_contract( "contract_id".to_string(), contract.clone(), payment_base, false, ); - wallet + let address = wallet .create_pay_to_contract_address(&payment_base, contract.clone(), color_id) - .unwrap() + .unwrap(); + (address, ret.unwrap()) } pub fn get_payment_base(wallet: &Wallet) -> PublicKey { let descriptor = wallet.get_descriptor_for_keychain(KeychainKind::External); @@ -433,7 +434,7 @@ pub fn descriptor_to_public_key(descriptor: &Descriptor) -> pub fn get_funded_wallet_with_p2c_and_change( descriptor: &str, change: &str, -) -> (Wallet, MalFixTxid, Address) { +) -> (Wallet, MalFixTxid, Address, Contract) { let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Dev).unwrap(); let fund_address = wallet.peek_address(KeychainKind::External, 0).address; let sendto_address: Address = Address::from_str("msvWktzSViRZ5kiepVr6W8VrgE8a6mbiVu") @@ -464,7 +465,7 @@ pub fn get_funded_wallet_with_p2c_and_change( vout: 0, }; - let receive_address = get_p2c_address(&mut wallet, None); + let (receive_address, contract) = get_p2c_address(&mut wallet, None); let tx1 = Transaction { version: transaction::Version::ONE, @@ -518,13 +519,13 @@ pub fn get_funded_wallet_with_p2c_and_change( ) .unwrap(); - (wallet, tx1.malfix_txid(), receive_address) + (wallet, tx1.malfix_txid(), receive_address, contract) } pub fn get_funded_wallet_with_colored_p2c_and_change( descriptor: &str, change: &str, -) -> (Wallet, MalFixTxid, Address, ColorIdentifier) { +) -> (Wallet, MalFixTxid, Address, Contract, ColorIdentifier) { let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Dev).unwrap(); let fund_address = wallet.peek_address(KeychainKind::External, 0).address; let sendto_address: Address = Address::from_str("msvWktzSViRZ5kiepVr6W8VrgE8a6mbiVu") @@ -555,7 +556,7 @@ pub fn get_funded_wallet_with_colored_p2c_and_change( vout: 0, }; let color_id = ColorIdentifier::reissuable(fund_address.script_pubkey().as_script()); - let receive_address = get_p2c_address(&mut wallet, Some(color_id)); + let (receive_address, contract) = get_p2c_address(&mut wallet, Some(color_id)); let tx1 = Transaction { version: transaction::Version::ONE, lock_time: tapyrus::absolute::LockTime::ZERO, @@ -612,7 +613,13 @@ pub fn get_funded_wallet_with_colored_p2c_and_change( ) .unwrap(); - (wallet, tx1.malfix_txid(), receive_address, color_id) + ( + wallet, + tx1.malfix_txid(), + receive_address, + contract, + color_id, + ) } pub fn get_p2c_tx(wallet: &mut Wallet, contract: &Contract) -> Vec { diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 8dac45d5..03eb29bb 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -19,8 +19,8 @@ use tapyrus::{ }; use tapyrus::{psbt, AddressType, PublicKey}; use tdk_chain::collections::BTreeMap; -use tdk_chain::COINBASE_MATURITY; use tdk_chain::{BlockId, ConfirmationTime}; +use tdk_chain::{Contract, COINBASE_MATURITY}; use tdk_persist::PersistBackend; use tdk_sqlite::rusqlite::Connection; use tdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor}; @@ -30,8 +30,8 @@ use tdk_wallet::signer::{SignOptions, SignerError}; use tdk_wallet::wallet::coin_selection::{self, LargestFirstCoinSelection}; use tdk_wallet::wallet::error::CreateTxError; use tdk_wallet::wallet::tx_builder::AddForeignUtxoError; -use tdk_wallet::wallet::{scalar_from, CreateContractError, NewError}; use tdk_wallet::wallet::{AddressInfo, Balance, Wallet}; +use tdk_wallet::wallet::{CreateContractError, NewError}; use tdk_wallet::KeychainKind; mod common; @@ -350,7 +350,7 @@ fn test_get_funded_wallet_colored_balance() { #[test] fn test_get_funded_wallet_p2c_balance() { let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)"; - let (wallet, _, _) = get_funded_wallet_with_p2c_and_change(get_test_pkh(), change_desc); + let (wallet, _, _, _) = get_funded_wallet_with_p2c_and_change(get_test_pkh(), change_desc); assert_eq!( wallet.balance(ColorIdentifier::default()).confirmed, @@ -361,7 +361,7 @@ fn test_get_funded_wallet_p2c_balance() { #[test] fn test_get_funded_wallet_colored_p2c_balance() { let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)"; - let (wallet, _, _, color_id) = + let (wallet, _, _, _, color_id) = get_funded_wallet_with_colored_p2c_and_change(get_test_pkh(), change_desc); assert_eq!(wallet.balance(color_id).confirmed, Amount::from_tap(100)); @@ -423,7 +423,7 @@ fn test_get_funded_wallet_with_color_sent_and_received() { #[test] fn test_get_funded_wallet_with_p2c_sent_and_received() { let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)"; - let (wallet, txid, _, color_id) = + let (wallet, txid, _, _, color_id) = get_funded_wallet_with_colored_p2c_and_change(get_test_pkh(), change_desc); let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; @@ -1453,8 +1453,10 @@ fn test_create_tx_with_reissuable_no_tpc_change() { fn test_create_tx_with_contract() { let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)"; - let (mut wallet, txid, address) = + let (mut wallet, txid, address, contract) = get_funded_wallet_with_p2c_and_change(get_test_pkh(), change_desc); + let mut contracts = BTreeMap::new(); + contracts.insert(contract.clone().contract_id, contract.clone()); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") .unwrap() @@ -1463,7 +1465,7 @@ fn test_create_tx_with_contract() { builder .add_recipient(addr.script_pubkey(), Amount::from_tap(25_000)) .add_contract_utxo(OutPoint { txid, vout: 0 }); - let psbt = builder.finish().unwrap(); + let mut psbt = builder.finish().unwrap(); check_fee!(wallet, psbt); assert_eq!(psbt.unsigned_tx.output.len(), 2); let sent_received = wallet.sent_and_received( @@ -1477,6 +1479,18 @@ fn test_create_tx_with_contract() { Amount::from_tap(25_000) - psbt.fee_amount().unwrap() ) ); + + let finished = wallet.sign( + &mut psbt, + SignOptions { + contracts, + trust_witness_utxo: true, + ..Default::default() + }, + ); + + let ret = finished.unwrap(); + assert!(ret, "transaction should be signed"); } // TODO: Fix this test @@ -3152,7 +3166,7 @@ fn test_create_pay_to_contract_commitment() { PublicKey::from_str("02046e89be90d26872e1318feb7d5ca7a6f588118e76f4906cf5b8ef262b63ab49") .unwrap(); let contract = "metadata".as_bytes().to_vec(); - let commitment = wallet.create_pay_to_contract_commitment(&payment_base, contract); + let commitment = Contract::create_pay_to_contract_commitment(&payment_base, contract); let expected = [ 0xb3, 0x09, 0x1b, 0x18, 0x71, 0x39, 0xe7, 0xd1, 0xfc, 0x66, 0x1b, 0x25, 0xf3, 0xc0, 0x5c, 0x07, 0x78, 0x11, 0xbb, 0x8c, 0xb2, 0x8e, 0x49, 0xc1, 0xda, 0xba, 0x16, 0x6b, 0xf6, 0x5e, @@ -3168,7 +3182,7 @@ fn test_bytes_to_scalar() { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, ]; - let scalar = scalar_from(&bytes); + let scalar = Contract::scalar_from(&bytes); /// 0xFFFFF.... - Scalar::MAX let expected = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, From 7d779498d37a50dceada9f15e721a9450be99594 Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Fri, 16 Aug 2024 02:05:41 +0900 Subject: [PATCH 09/16] Fix signing for pay to contract transaction --- crates/chain/src/contract.rs | 69 ++++++++++++++++++++++-- crates/chain/src/keychain/txout_index.rs | 2 +- crates/chain/src/spk_txout_index.rs | 19 +++---- crates/wallet/src/wallet/mod.rs | 41 +++++++++----- crates/wallet/src/wallet/signer.rs | 7 ++- 5 files changed, 110 insertions(+), 28 deletions(-) diff --git a/crates/chain/src/contract.rs b/crates/chain/src/contract.rs index 8388fb30..4f94bf6f 100644 --- a/crates/chain/src/contract.rs +++ b/crates/chain/src/contract.rs @@ -5,8 +5,8 @@ use alloc::collections::BTreeMap; use alloc::{string::String, vec::Vec}; use num_bigint::BigUint; -use tapyrus::key::Error; -use tapyrus::secp256k1::{Scalar, SecretKey}; +use tapyrus::key::{Error, Secp256k1}; +use tapyrus::secp256k1::{All, Scalar}; use tapyrus::{ hashes::{Hash, HashEngine}, PrivateKey, @@ -38,13 +38,14 @@ impl Contract { /// Create private key for Pay-to-Contract pub fn create_private_key( &self, + payment_base_private_key: &PrivateKey, payment_base: &PublicKey, network: Network, ) -> Result { let commitment: Scalar = Self::create_pay_to_contract_commitment(payment_base, self.contract.clone()); - let sk = SecretKey::from_slice(&commitment.to_be_bytes())?; - Ok(PrivateKey::new(sk, network)) + let p2c_private_key = payment_base_private_key.inner.add_tweak(&commitment)?; + Ok(PrivateKey::new(p2c_private_key, network)) } /// Compute pay-to-contract commitment as Scalar. @@ -69,4 +70,64 @@ impl Contract { value[32 - bytes.len()..].copy_from_slice(&bytes); Scalar::from_be_bytes(value).unwrap() } + + /// Generate public key for Pay-to-Contract + pub fn create_pay_to_contract_public_key( + payment_base: &PublicKey, + contracts: Vec, + secp: &Secp256k1, + ) -> PublicKey { + let commitment: Scalar = + Self::create_pay_to_contract_commitment(payment_base, contracts.clone()); + let pubkey = payment_base.inner.add_exp_tweak(secp, &commitment).unwrap(); + PublicKey { + compressed: true, + inner: pubkey, + } + } +} + +#[cfg(test)] +mod signers_container_tests { + use core::str::FromStr; + use std::string::ToString; + + use tapyrus::key::Secp256k1; + + use super::*; + use crate::tapyrus::hashes::hex::FromHex; + + #[test] + fn test_create_private_key() { + let payment_base_private_key = PrivateKey::from_slice( + &Vec::::from_hex( + "c5580f6c26f83fb513dd5e0d1b03c36be26fcefa139b1720a7ca7c0dedd439c2", + ) + .unwrap(), + Network::Dev, + ) + .unwrap(); + let payment_base = + PublicKey::from_private_key(&Secp256k1::signing_only(), &payment_base_private_key); + let contract = Contract { + contract_id: "contract_id".to_string(), + contract: "metadata".as_bytes().to_vec(), + payment_base, + spendable: true, + }; + let key = + contract.create_private_key(&payment_base_private_key, &payment_base, Network::Dev); + assert!(key.is_ok()); + assert_eq!( + key.unwrap(), + PrivateKey::from_slice( + &Vec::::from_hex( + "78612a8498322787104379330ec41f749fd2ada016e0c0a6c2b233ed13fc8978" + ) + .unwrap(), + Network::Dev + ) + .unwrap() + ); + } } diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index c0a448d0..20109707 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -384,7 +384,7 @@ impl KeychainTxOutIndex { } /// Returns script pubkey of payment base for pay-to-contract script - pub fn p2c_spk(&self, spk: &ScriptBuf) -> Option<&ScriptBuf> { + pub fn p2c_spk(&self, spk: &ScriptBuf) -> Option<&PublicKey> { self.inner.p2c_spk(spk) } diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 7b9a76b8..25c205d3 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -42,7 +42,7 @@ pub struct SpkTxOutIndex { /// Lookup from spk index to outpoints that had that spk spk_txouts: BTreeSet<(I, OutPoint)>, /// Pay-to-contract payment_base lookup by p2c spk - p2c_spks: HashMap, + p2c_spks: HashMap, } impl Default for SpkTxOutIndex { @@ -114,7 +114,8 @@ impl SpkTxOutIndex { let payment_base = self.p2c_spks.get(&script_pubkey); let spk_i = if let Some(p) = payment_base { - self.spk_indices.get(p.as_script()) + self.spk_indices + .get(&ScriptBuf::new_p2pkh(&p.pubkey_hash())) } else { self.spk_indices.get(&script_pubkey) }; @@ -202,12 +203,11 @@ impl SpkTxOutIndex { /// Insert payment base key for pay-to-contract script pubkey pub fn insert_p2c_spk(&mut self, spk: ScriptBuf, payment_base: PublicKey) { - let p2c_spk = ScriptBuf::new_p2pkh(&payment_base.pubkey_hash()); - self.p2c_spks.insert(spk, p2c_spk); + self.p2c_spks.insert(spk, payment_base); } /// Returns script pubkey of payment base for pay-to-contract script - pub fn p2c_spk(&self, spk: &ScriptBuf) -> Option<&ScriptBuf> { + pub fn p2c_spk(&self, spk: &ScriptBuf) -> Option<&PublicKey> { self.p2c_spks.get(spk) } @@ -328,12 +328,13 @@ impl SpkTxOutIndex { txout.script_pubkey.clone() }; let payment_base = self.p2c_spks.get(&script_pubkey); - let script_pubkey_ref = if let Some(p) = payment_base { - p + let script_pubkey = if let Some(p) = payment_base { + let hash = p.pubkey_hash(); + ScriptBuf::new_p2pkh(&hash) } else { - &script_pubkey + script_pubkey }; - if let Some(index) = self.index_of_spk(script_pubkey_ref) { + if let Some(index) = self.index_of_spk(&script_pubkey) { if range.contains(index) && txout.script_pubkey.color_id().unwrap_or_default() == *color_id { diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index bf638f52..38a99bb4 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -21,14 +21,14 @@ use alloc::{ vec::Vec, }; -use core::fmt; use core::ops::Deref; +use core::{fmt, str::FromStr}; use descriptor::error::Error as DescriptorError; use miniscript::{ + descriptor::{SinglePub, SinglePubKey}, psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}, - Descriptor, DescriptorPublicKey, + DefiniteDescriptorKey, Descriptor, DescriptorPublicKey, ToPublicKey, }; -use tapyrus::sighash::{EcdsaSighashType, TapSighashType}; use tapyrus::{ absolute, psbt, script::color_identifier::ColorIdentifier, Address, Block, FeeRate, MalFixTxid, Network, OutPoint, PublicKey, Script, ScriptBuf, Sequence, Transaction, TxOut, Witness, @@ -36,6 +36,10 @@ use tapyrus::{ use tapyrus::{address::NetworkChecked, secp256k1::Scalar}; use tapyrus::{consensus::encode::serialize, transaction, BlockHash, Psbt}; use tapyrus::{constants::mainnet_genesis_block, constants::testnet_genesis_block, Amount}; +use tapyrus::{ + hex::DisplayHex, + sighash::{EcdsaSighashType, TapSighashType}, +}; use tapyrus::{secp256k1::SecretKey, PrivateKey}; use tapyrus::{ secp256k1::{All, Secp256k1}, @@ -2345,14 +2349,27 @@ impl Wallet { fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option { let payment_base = self.spk_index().p2c_spk(&txout.script_pubkey); - let script_pubkey_ref = if let Some(p) = payment_base { - p - } else { - &txout.script_pubkey - }; - - let (keychain, child) = self.indexed_graph.index.index_of_spk(script_pubkey_ref)?; - let descriptor = self.get_descriptor_for_keychain(keychain); + if let Some(p) = payment_base { + // find pay-to-contract + let contract = self.contracts.values().find(|c| *p == c.payment_base); + if let Some(c) = contract { + let public_key = Contract::create_pay_to_contract_public_key( + &c.payment_base, + c.contract.clone(), + &self.secp_ctx(), + ); + if let Ok(ddk) = DefiniteDescriptorKey::from_str(&public_key.to_string()) { + return Some(Descriptor::::new_pk(ddk)); + } + } + return None; + } + let (keychain, child) = self + .indexed_graph + .index + .index_of_spk(&txout.script_pubkey)?; + let descriptor: &Descriptor = + self.get_descriptor_for_keychain(keychain); descriptor.at_derivation_index(child).ok() } @@ -2591,7 +2608,7 @@ impl Wallet { let (keychain, child) = if let Some(p) = payment_base { self.indexed_graph .index - .index_of_spk(p.clone().as_script()) + .index_of_spk(&ScriptBuf::new_p2pkh(&p.pubkey_hash())) .ok_or(CreateTxError::UnknownUtxo)? } else { self.indexed_graph diff --git a/crates/wallet/src/wallet/signer.rs b/crates/wallet/src/wallet/signer.rs index c6945df2..c53dd937 100644 --- a/crates/wallet/src/wallet/signer.rs +++ b/crates/wallet/src/wallet/signer.rs @@ -433,11 +433,12 @@ impl SignerCommon for SignerWrapper { } impl SignerWrapper { - // Return if a script is related + /// Return if a script is related fn is_relevant_script(&self, script_pubkey: &ScriptBuf) -> bool { script_pubkey.is_cp2pkh() || script_pubkey.is_p2pkh() } + /// Return if script_pubkey equals to p2pkh generated with specified public key fn same_pubkey_hash(&self, script_pubkey: &ScriptBuf, public_key: &PublicKey) -> bool { *script_pubkey == ScriptBuf::new_p2pkh(&public_key.pubkey_hash()) } @@ -450,7 +451,9 @@ impl SignerWrapper { secp: &SecpCtx, ) -> Option<(SecretKey, PublicKey)> { sign_options.contracts.iter().find_map(|(_, contract)| { - let p2c_private_key = contract.create_private_key(pubkey, self.network).ok()?; + let p2c_private_key = contract + .create_private_key(&self, pubkey, self.network) + .ok()?; let p2c_public_key = p2c_private_key.public_key(secp); if self.same_pubkey_hash(script_pubkey, &p2c_public_key) { Some((p2c_private_key.inner, p2c_public_key)) From 615202d3935af3f68028ffd79091db80a7743742 Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Fri, 16 Aug 2024 02:36:09 +0900 Subject: [PATCH 10/16] Change method name Contract::create_private_key --- crates/chain/src/contract.rs | 6 +++--- crates/wallet/src/wallet/signer.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/chain/src/contract.rs b/crates/chain/src/contract.rs index 4f94bf6f..8f1a61b7 100644 --- a/crates/chain/src/contract.rs +++ b/crates/chain/src/contract.rs @@ -36,7 +36,7 @@ pub struct Contract { impl Contract { /// Create private key for Pay-to-Contract - pub fn create_private_key( + pub fn create_pay_to_contract_private_key( &self, payment_base_private_key: &PrivateKey, payment_base: &PublicKey, @@ -98,7 +98,7 @@ mod signers_container_tests { use crate::tapyrus::hashes::hex::FromHex; #[test] - fn test_create_private_key() { + fn test_create_pay_to_contract_private_key() { let payment_base_private_key = PrivateKey::from_slice( &Vec::::from_hex( "c5580f6c26f83fb513dd5e0d1b03c36be26fcefa139b1720a7ca7c0dedd439c2", @@ -116,7 +116,7 @@ mod signers_container_tests { spendable: true, }; let key = - contract.create_private_key(&payment_base_private_key, &payment_base, Network::Dev); + contract.create_pay_to_contract_private_key(&payment_base_private_key, &payment_base, Network::Dev); assert!(key.is_ok()); assert_eq!( key.unwrap(), diff --git a/crates/wallet/src/wallet/signer.rs b/crates/wallet/src/wallet/signer.rs index c53dd937..29cab537 100644 --- a/crates/wallet/src/wallet/signer.rs +++ b/crates/wallet/src/wallet/signer.rs @@ -452,7 +452,7 @@ impl SignerWrapper { ) -> Option<(SecretKey, PublicKey)> { sign_options.contracts.iter().find_map(|(_, contract)| { let p2c_private_key = contract - .create_private_key(&self, pubkey, self.network) + .create_pay_to_contract_private_key(&self, pubkey, self.network) .ok()?; let p2c_public_key = p2c_private_key.public_key(secp); if self.same_pubkey_hash(script_pubkey, &p2c_public_key) { From ca8acdbe4310fe4c4215fa4877c6201e42c087fb Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Mon, 19 Aug 2024 02:28:08 +0900 Subject: [PATCH 11/16] Cargo fmt --- crates/chain/src/contract.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/chain/src/contract.rs b/crates/chain/src/contract.rs index 8f1a61b7..7a2fbf74 100644 --- a/crates/chain/src/contract.rs +++ b/crates/chain/src/contract.rs @@ -115,8 +115,11 @@ mod signers_container_tests { payment_base, spendable: true, }; - let key = - contract.create_pay_to_contract_private_key(&payment_base_private_key, &payment_base, Network::Dev); + let key = contract.create_pay_to_contract_private_key( + &payment_base_private_key, + &payment_base, + Network::Dev, + ); assert!(key.is_ok()); assert_eq!( key.unwrap(), From 9db256e860af4865777a00442d03273524860063 Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Mon, 19 Aug 2024 04:39:27 +0900 Subject: [PATCH 12/16] Add test cases --- crates/wallet/src/wallet/mod.rs | 11 +--- crates/wallet/tests/wallet.rs | 93 ++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 38a99bb4..94e2701d 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -1034,15 +1034,8 @@ impl Wallet { length: contract.len(), }); } - let commitment: Scalar = self.create_pay_to_contract_commitment(payment_base, contract); - let pubkey = payment_base - .inner - .add_exp_tweak(&self.secp, &commitment) - .unwrap(); - let key = PublicKey { - compressed: true, - inner: pubkey, - }; + let key = + Contract::create_pay_to_contract_public_key(payment_base, contract, self.secp_ctx()); Ok(key) } diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 03eb29bb..6547645e 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -1463,7 +1463,7 @@ fn test_create_tx_with_contract() { .assume_checked(); let mut builder = wallet.build_tx(); builder - .add_recipient(addr.script_pubkey(), Amount::from_tap(25_000)) + .add_recipient(addr.script_pubkey(), Amount::from_tap(48_000)) .add_contract_utxo(OutPoint { txid, vout: 0 }); let mut psbt = builder.finish().unwrap(); check_fee!(wallet, psbt); @@ -1476,7 +1476,7 @@ fn test_create_tx_with_contract() { sent_received, ( Amount::from_tap(50_000), - Amount::from_tap(25_000) - psbt.fee_amount().unwrap() + Amount::from_tap(2_000) - psbt.fee_amount().unwrap() ) ); @@ -1493,6 +1493,95 @@ fn test_create_tx_with_contract() { assert!(ret, "transaction should be signed"); } +#[test] +fn test_create_tx_with_contract_unspendable() { + let change_desc = "pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/1)"; + let (mut wallet, txid, address, contract) = + get_funded_wallet_with_p2c_and_change(get_test_pkh(), change_desc); + let contract_id = contract.clone().contract_id; + let mut contracts = BTreeMap::new(); + contracts.insert(contract_id.clone(), contract.clone()); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") + .unwrap() + .assume_checked(); + + // make contract unspendable + wallet.update_contract(contract_id.clone(), false); + { + // Can use as contract utxo + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_tap(48_000)) + .add_contract_utxo(OutPoint { txid, vout: 0 }); + + let mut psbt = builder.finish().unwrap(); + let finished = wallet.sign( + &mut psbt, + SignOptions { + contracts: contracts.clone(), + trust_witness_utxo: true, + ..Default::default() + }, + ); + + let ret = finished.unwrap(); + assert!(ret, "can spend contract utxo"); + } + { + // Can not use as ordinal utxo + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_tap(48_000)); + + let ret = builder.finish(); + assert!(ret.is_err(), "can spend as ordinal utxo"); + } + + // make contract spendable + wallet.update_contract(contract_id.clone(), true); + + { + // Can use as contract utxo + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), Amount::from_tap(48_000)) + .add_contract_utxo(OutPoint { txid, vout: 0 }); + + let mut psbt = builder.finish().unwrap(); + let finished = wallet.sign( + &mut psbt, + SignOptions { + contracts: contracts.clone(), + trust_witness_utxo: true, + ..Default::default() + }, + ); + + let ret = finished.unwrap(); + println!("test_create_tx_with_contract_unspendable: {:?}", ret); + assert!(ret, "can spend as contract"); + } + + { + // Can use as ordinal utxo + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_tap(48_000)); + + let mut psbt = builder.finish().unwrap(); + let finished = wallet.sign( + &mut psbt, + SignOptions { + contracts: contracts.clone(), + trust_witness_utxo: true, + ..Default::default() + }, + ); + + let ret = finished.unwrap(); + assert!(ret, "can spend as ordinal utxo"); + } +} + // TODO: Fix this test #[test] fn test_create_tx_multi_colored_coin_recipients() { From cc700c48dd1502d2841322f8443cf15c46360855 Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Mon, 19 Aug 2024 12:56:42 +0900 Subject: [PATCH 13/16] Fix CI Failure --- crates/chain/src/contract.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/chain/src/contract.rs b/crates/chain/src/contract.rs index 7a2fbf74..d49a227c 100644 --- a/crates/chain/src/contract.rs +++ b/crates/chain/src/contract.rs @@ -89,7 +89,6 @@ impl Contract { #[cfg(test)] mod signers_container_tests { - use core::str::FromStr; use std::string::ToString; use tapyrus::key::Secp256k1; From bd176b704f682186f9739759395aa7f0c4ee709b Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Thu, 22 Aug 2024 03:13:02 +0900 Subject: [PATCH 14/16] Fix signing probrem for P2C script --- crates/wallet/src/wallet/mod.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 94e2701d..ce7d06d9 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -12,7 +12,10 @@ //! Wallet //! //! This module defines the [`Wallet`]. -use crate::collections::{BTreeMap, HashMap}; +use crate::{ + collections::{BTreeMap, HashMap}, + keys::DescriptorKey, +}; use alloc::{ borrow::ToOwned, boxed::Box, @@ -2351,8 +2354,14 @@ impl Wallet { c.contract.clone(), &self.secp_ctx(), ); - if let Ok(ddk) = DefiniteDescriptorKey::from_str(&public_key.to_string()) { - return Some(Descriptor::::new_pk(ddk)); + + if let Ok(ddk) = DefiniteDescriptorKey::from_str( + &public_key.inner.serialize().to_lower_hex_string(), + ) { + return Some( + Descriptor::::new_pkh(ddk) + .expect("can not create new descriptor"), + ); } } return None; From e48c9700f97dc714bc06a09e91c6a9b6865d3927 Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Mon, 26 Aug 2024 02:01:56 +0900 Subject: [PATCH 15/16] Add p2c contracts to sign_options in wallet.sign() method --- crates/wallet/src/wallet/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index ce7d06d9..f487b09e 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -2162,6 +2162,9 @@ impl Wallet { return Err(SignerError::NonStandardSighash); } + /// Add all contracts to sign_options + let mut sign_options = sign_options.clone(); + sign_options.contracts.append(&mut self.contracts.clone()); for signer in self .signers .signers() From 750052632c40915583039d586dac4b6a76ebaefc Mon Sep 17 00:00:00 2001 From: Yamaguchi Date: Tue, 27 Aug 2024 16:37:55 +0900 Subject: [PATCH 16/16] Fix signing problem for colored p2c input --- crates/wallet/src/wallet/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index f487b09e..f823459b 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -2347,7 +2347,9 @@ impl Wallet { } fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option { - let payment_base = self.spk_index().p2c_spk(&txout.script_pubkey); + let payment_base = self + .spk_index() + .p2c_spk(&txout.script_pubkey.remove_color()); if let Some(p) = payment_base { // find pay-to-contract let contract = self.contracts.values().find(|c| *p == c.payment_base);