-
Notifications
You must be signed in to change notification settings - Fork 83
Add experimental silent payment transaction creation support #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,9 @@ | |
|
|
||
| #![allow(clippy::large_enum_variant)] | ||
|
|
||
| #[cfg(feature = "sp")] | ||
| use {crate::utils::parse_sp_code_value_pairs, bdk_sp::encoding::SilentPaymentCode}; | ||
|
|
||
| use bdk_wallet::bitcoin::{ | ||
| Address, Network, OutPoint, ScriptBuf, | ||
| bip32::{DerivationPath, Xpriv}, | ||
|
|
@@ -315,6 +318,62 @@ pub enum OfflineWalletSubCommand { | |
| )] | ||
| add_data: Option<String>, //base 64 econding | ||
| }, | ||
| /// Creates a silent payment transaction | ||
| /// | ||
| /// This sub-command is **EXPERIMENTAL** and should only be used for testing. Do not use this | ||
| /// feature to create transactions that spend actual funds on the Bitcoin mainnet. | ||
|
|
||
| // This command DOES NOT return a PSBT. Instead, it directly returns a signed transaction | ||
| // ready for broadcast, as it is not yet possible to perform a shared derivation of a silent | ||
| // payment script pubkey in a secure and trustless manner. | ||
| #[cfg(feature = "sp")] | ||
| CreateSpTx { | ||
| /// Adds a recipient to the transaction. | ||
| // Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704. | ||
| // Address and amount parsing is done at run time in handler function. | ||
| #[arg(env = "ADDRESS:SAT", long = "to", required = false, value_parser = parse_recipient)] | ||
| recipients: Option<Vec<(ScriptBuf, u64)>>, | ||
| /// Parse silent payment recipients | ||
| #[arg(long = "to-sp", required = true, value_parser = parse_sp_code_value_pairs)] | ||
| silent_payment_recipients: Vec<(SilentPaymentCode, u64)>, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how can one construct sp_recipients? can you add that to the readme? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Each silent payment recipient is a string of the form There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I think is that, if it is possible, you should add deriving SP addresses to this PR. I wanted to test how you implemented it in the CLI v2 but it broke. Else you can add a link to the test addresses so it is easy for users to find them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But if we add the chances to derive addresses, then we are allowing the creation of transactions locking funds into those addresses. I could restrict this functionality to only derive testnet addresses, so no one lose any real funds. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, only testnet addresses are fine. We generally warn users against using this tool for mainnet. |
||
| /// Sends all the funds (or all the selected utxos). Requires only one recipient with value 0. | ||
| #[arg(long = "send_all", short = 'a')] | ||
| send_all: bool, | ||
| /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output. | ||
| #[arg(long = "offline_signer")] | ||
| offline_signer: bool, | ||
| /// Selects which utxos *must* be spent. | ||
| #[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)] | ||
| utxos: Option<Vec<OutPoint>>, | ||
| /// Marks a utxo as unspendable. | ||
| #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)] | ||
| unspendable: Option<Vec<OutPoint>>, | ||
| /// Fee rate to use in sat/vbyte. | ||
| #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate")] | ||
| fee_rate: Option<f32>, | ||
| /// Selects which policy should be used to satisfy the external descriptor. | ||
| #[arg(env = "EXT_POLICY", long = "external_policy")] | ||
| external_policy: Option<String>, | ||
| /// Selects which policy should be used to satisfy the internal descriptor. | ||
| #[arg(env = "INT_POLICY", long = "internal_policy")] | ||
| internal_policy: Option<String>, | ||
| /// Optionally create an OP_RETURN output containing given String in utf8 encoding (max 80 bytes) | ||
| #[arg( | ||
| env = "ADD_STRING", | ||
| long = "add_string", | ||
| short = 's', | ||
| conflicts_with = "add_data" | ||
| )] | ||
| add_string: Option<String>, | ||
| /// Optionally create an OP_RETURN output containing given base64 encoded String. (max 80 bytes) | ||
| #[arg( | ||
| env = "ADD_DATA", | ||
| long = "add_data", | ||
| short = 'o', | ||
| conflicts_with = "add_string" | ||
| )] | ||
| add_data: Option<String>, //base 64 econding | ||
| }, | ||
| /// Bumps the fees of an RBF transaction. | ||
| BumpFee { | ||
| /// TXID of the transaction to update. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,11 @@ use bdk_wallet::bitcoin::{ | |
| }; | ||
| use bdk_wallet::chain::ChainPosition; | ||
| use bdk_wallet::descriptor::Segwitv0; | ||
| use bdk_wallet::keys::{ | ||
| DerivableKey, DescriptorKey, DescriptorKey::Secret, ExtendedKey, GeneratableKey, GeneratedKey, | ||
| bip39::WordCount, | ||
| }; | ||
| use bdk_wallet::miniscript::miniscript; | ||
| #[cfg(feature = "sqlite")] | ||
| use bdk_wallet::rusqlite::Connection; | ||
| use bdk_wallet::{KeychainKind, SignOptions, Wallet}; | ||
|
|
@@ -39,12 +44,6 @@ use bdk_wallet::{ | |
| miniscript::policy::Concrete, | ||
| }; | ||
| use cli_table::{Cell, CellStruct, Style, Table, format::Justify}; | ||
|
|
||
| use bdk_wallet::keys::{ | ||
| DerivableKey, DescriptorKey, DescriptorKey::Secret, ExtendedKey, GeneratableKey, GeneratedKey, | ||
| bip39::WordCount, | ||
| }; | ||
| use bdk_wallet::miniscript::miniscript; | ||
| use serde_json::json; | ||
| use std::collections::BTreeMap; | ||
| #[cfg(any(feature = "electrum", feature = "esplora"))] | ||
|
|
@@ -53,6 +52,16 @@ use std::convert::TryFrom; | |
| #[cfg(any(feature = "repl", feature = "electrum", feature = "esplora"))] | ||
| use std::io::Write; | ||
| use std::str::FromStr; | ||
| #[cfg(feature = "sp")] | ||
| use { | ||
| bdk_sp::{ | ||
| bitcoin::{PrivateKey, PublicKey, ScriptBuf, XOnlyPublicKey}, | ||
| encoding::SilentPaymentCode, | ||
| send::psbt::derive_sp, | ||
| }, | ||
| bdk_wallet::keys::{DescriptorPublicKey, DescriptorSecretKey, SinglePubKey}, | ||
| std::collections::HashMap, | ||
| }; | ||
|
|
||
| #[cfg(feature = "electrum")] | ||
| use crate::utils::BlockchainClient::Electrum; | ||
|
|
@@ -318,7 +327,152 @@ pub fn handle_offline_wallet_subcommand( | |
| )?) | ||
| } | ||
| } | ||
| #[cfg(feature = "sp")] | ||
| CreateSpTx { | ||
| recipients: maybe_recipients, | ||
| silent_payment_recipients, | ||
| send_all, | ||
| offline_signer, | ||
| utxos, | ||
| unspendable, | ||
| fee_rate, | ||
| external_policy, | ||
| internal_policy, | ||
| add_data, | ||
| add_string, | ||
| } => { | ||
| let mut tx_builder = wallet.build_tx(); | ||
|
|
||
| let sp_recipients: Vec<SilentPaymentCode> = silent_payment_recipients | ||
| .iter() | ||
| .map(|(sp_code, _)| sp_code.clone()) | ||
| .collect(); | ||
|
|
||
| let mut outputs: Vec<(ScriptBuf, Amount)> = silent_payment_recipients | ||
| .iter() | ||
| .map(|(sp_code, amount)| { | ||
| let script = sp_code.get_placeholder_p2tr_spk(); | ||
| (script, Amount::from_sat(*amount)) | ||
| }) | ||
| .collect(); | ||
|
|
||
| if let Some(recipients) = maybe_recipients { | ||
| if send_all { | ||
| tx_builder.drain_wallet().drain_to(recipients[0].0.clone()); | ||
| } else { | ||
| let recipients = recipients | ||
| .into_iter() | ||
| .map(|(script, amount)| (script, Amount::from_sat(amount))); | ||
|
|
||
| outputs.extend(recipients); | ||
| } | ||
| } | ||
|
|
||
| tx_builder.set_recipients(outputs); | ||
|
|
||
| // Do not enable RBF for this transaction | ||
| tx_builder.set_exact_sequence(Sequence::MAX); | ||
|
|
||
| if offline_signer { | ||
| tx_builder.include_output_redeem_witness_script(); | ||
| } | ||
|
|
||
| if let Some(fee_rate) = fee_rate { | ||
| if let Some(fee_rate) = FeeRate::from_sat_per_vb(fee_rate as u64) { | ||
| tx_builder.fee_rate(fee_rate); | ||
| } | ||
| } | ||
|
|
||
| if let Some(utxos) = utxos { | ||
| tx_builder.add_utxos(&utxos[..]).unwrap(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you use the Error enum from the error module to gracefully handle errors rather than unwrapping? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm reviewing this unwraps again. I was basing these changes in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I have noticed that the |
||
| } | ||
|
|
||
| if let Some(unspendable) = unspendable { | ||
| tx_builder.unspendable(unspendable); | ||
| } | ||
|
|
||
| if let Some(base64_data) = add_data { | ||
| let op_return_data = BASE64_STANDARD.decode(base64_data).unwrap(); | ||
| tx_builder.add_data(&PushBytesBuf::try_from(op_return_data).unwrap()); | ||
| } else if let Some(string_data) = add_string { | ||
| let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec()).unwrap(); | ||
| tx_builder.add_data(&data); | ||
| } | ||
|
|
||
| let policies = vec![ | ||
| external_policy.map(|p| (p, KeychainKind::External)), | ||
| internal_policy.map(|p| (p, KeychainKind::Internal)), | ||
| ]; | ||
|
|
||
| for (policy, keychain) in policies.into_iter().flatten() { | ||
| let policy = serde_json::from_str::<BTreeMap<String, Vec<usize>>>(&policy)?; | ||
| tx_builder.policy_path(policy, keychain); | ||
| } | ||
|
|
||
| let mut psbt = tx_builder.finish()?; | ||
|
|
||
| let unsigned_psbt = psbt.clone(); | ||
|
|
||
| let _signed = wallet.sign(&mut psbt, SignOptions::default())?; | ||
|
|
||
| for (full_input, psbt_input) in unsigned_psbt.inputs.iter().zip(psbt.inputs.iter_mut()) | ||
| { | ||
| // repopulate key derivation data | ||
| psbt_input.bip32_derivation = full_input.bip32_derivation.clone(); | ||
| psbt_input.tap_key_origins = full_input.tap_key_origins.clone(); | ||
| } | ||
|
|
||
| let secp = Secp256k1::new(); | ||
| let mut external_signers = wallet.get_signers(KeychainKind::External).as_key_map(&secp); | ||
| let internal_signers = wallet.get_signers(KeychainKind::Internal).as_key_map(&secp); | ||
| external_signers.extend(internal_signers); | ||
|
|
||
| match external_signers.iter().next().expect("not empty") { | ||
| (DescriptorPublicKey::Single(single_pub), DescriptorSecretKey::Single(prv)) => { | ||
| match single_pub.key { | ||
| SinglePubKey::FullKey(pk) => { | ||
| let keys: HashMap<PublicKey, PrivateKey> = [(pk, prv.key)].into(); | ||
| derive_sp(&mut psbt, &keys, &sp_recipients, &secp) | ||
| .expect("will fix later"); | ||
| } | ||
| SinglePubKey::XOnly(xonly) => { | ||
| let keys: HashMap<XOnlyPublicKey, PrivateKey> = | ||
| [(xonly, prv.key)].into(); | ||
| derive_sp(&mut psbt, &keys, &sp_recipients, &secp) | ||
| .expect("will fix later"); | ||
| } | ||
| }; | ||
| } | ||
| (_, DescriptorSecretKey::XPrv(k)) => { | ||
| derive_sp(&mut psbt, &k.xkey, &sp_recipients, &secp).expect("will fix later"); | ||
| } | ||
| _ => unimplemented!("multi xkey signer"), | ||
| }; | ||
|
|
||
| // Unfinalize PSBT to resign | ||
| for psbt_input in psbt.inputs.iter_mut() { | ||
| psbt_input.final_script_sig = None; | ||
| psbt_input.final_script_witness = None; | ||
| } | ||
|
|
||
| let _resigned = wallet.sign(&mut psbt, SignOptions::default())?; | ||
|
|
||
| let raw_tx = psbt.extract_tx()?; | ||
| if cli_opts.pretty { | ||
| let table = vec![vec![ | ||
| "Raw Transaction".cell().bold(true), | ||
| serialize_hex(&raw_tx).cell(), | ||
| ]] | ||
| .table() | ||
| .display() | ||
| .map_err(|e| Error::Generic(e.to_string()))?; | ||
| Ok(format!("{table}")) | ||
| } else { | ||
| Ok(serde_json::to_string_pretty( | ||
| &json!({"raw_tx": serialize_hex(&raw_tx)}), | ||
| )?) | ||
| } | ||
| } | ||
| CreateTx { | ||
| recipients, | ||
| send_all, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,8 @@ use bdk_kyoto::{ | |
| UnboundedReceiver, Warning, | ||
| builder::NodeBuilder, | ||
| }; | ||
| #[cfg(feature = "sp")] | ||
| use bdk_sp::encoding::SilentPaymentCode; | ||
| use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf}; | ||
|
|
||
| #[cfg(any( | ||
|
|
@@ -51,6 +53,25 @@ pub(crate) fn parse_recipient(s: &str) -> Result<(ScriptBuf, u64), String> { | |
| Ok((addr.script_pubkey(), val)) | ||
| } | ||
|
|
||
| #[cfg(feature = "sp")] | ||
| pub(crate) fn parse_sp_code_value_pairs(s: &str) -> Result<(SilentPaymentCode, u64), String> { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can return the Error and use the Generic variant instead of String here too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment than above, this function is following the style of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if your library is returning a specific error for this, you can add it to the enum or you can just use the |
||
| let parts: Vec<&str> = s.split(':').collect(); | ||
| if parts.len() != 2 { | ||
| return Err(format!("Invalid format '{}'. Expected 'key:value'", s)); | ||
| } | ||
|
|
||
| let value_0 = parts[0].trim(); | ||
| let key = SilentPaymentCode::try_from(value_0) | ||
| .map_err(|_| format!("Invalid silent payment address: {}", value_0))?; | ||
|
|
||
| let value = parts[1] | ||
| .trim() | ||
| .parse::<u64>() | ||
| .map_err(|_| format!("Invalid number '{}' for key '{}'", parts[1], key))?; | ||
|
|
||
| Ok((key, value)) | ||
| } | ||
|
|
||
| #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] | ||
| /// Parse the proxy (Socket:Port) argument from the cli input. | ||
| pub(crate) fn parse_proxy_auth(s: &str) -> Result<(String, String), Error> { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a need to add these recipients again since the focus is on sp_recipients?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to show that's still possible to send a non silent payment output together with a silent payment output in the same transaction, that's why I left this here.