Skip to content

Commit 75b5141

Browse files
committed
refactor(sp-payment): Rebase silent payment feat
- rebase silent payment feature
1 parent aa8876d commit 75b5141

6 files changed

Lines changed: 292 additions & 4 deletions

File tree

src/commands.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ use crate::handlers::{
2626
},
2727
};
2828

29+
#[cfg(feature = "silent-payments")]
30+
use crate::handlers::{descriptor::SilentPaymentCodeCommand, offline::CreateSpTxCommand};
31+
2932
#[cfg(any(
3033
feature = "electrum",
3134
feature = "esplora",
@@ -189,6 +192,12 @@ pub enum CliSubCommand {
189192
#[arg(value_enum)]
190193
shell: Shell,
191194
},
195+
/// Silent payment code generation tool.
196+
///
197+
/// Allows the encoding of two public keys into a silent payment code.
198+
/// Useful to create silent payment transactions using fake silent payment codes.
199+
#[cfg(feature = "silent-payments")]
200+
SilentPaymentCode(SilentPaymentCodeCommand),
192201
}
193202

194203
/// Wallet operation subcommands.
@@ -321,6 +330,15 @@ pub enum OfflineWalletSubCommand {
321330
Balance(BalanceCommand),
322331
/// Creates a new unsigned transaction.
323332
CreateTx(CreateTxCommand),
333+
/// Creates a silent payment transaction
334+
///
335+
/// This sub-command is **EXPERIMENTAL** and should only be used for testing. Do not use this
336+
/// feature to create transactions that spend actual funds on the Bitcoin mainnet.
337+
// This command DOES NOT return a PSBT. Instead, it directly returns a signed transaction
338+
// ready for broadcast, as it is not yet possible to perform a shared derivation of a silent
339+
// payment script pubkey in a secure and trustless manner.
340+
#[cfg(feature = "silent-payments")]
341+
CreateSpTx(CreateSpTxCommand),
324342
/// Bumps the fees of an RBF transaction.
325343
BumpFee(BumpFeeCommand),
326344
/// Returns the available spending policies for the descriptor.
@@ -329,7 +347,6 @@ pub enum OfflineWalletSubCommand {
329347
PublicDescriptor(PublicDescriptorCommand),
330348
/// Signs and tries to finalize a PSBT.
331349
Sign(SignCommand),
332-
333350
/// Extracts a raw transaction from a PSBT.
334351
ExtractPsbt(ExtractPsbtCommand),
335352
/// Finalizes a PSBT.

src/handlers/descriptor.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ use {
2121
std::{str::FromStr, sync::Arc},
2222
};
2323

24+
#[cfg(feature = "silent-payments")]
25+
use crate::utils::types::StatusResult;
26+
2427
#[cfg(feature = "compiler")]
2528
const NUMS_UNSPENDABLE_KEY_HEX: &str =
2629
"50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
@@ -113,3 +116,28 @@ impl AppCommand<AppContext<Init>> for CompileCommand {
113116
})
114117
}
115118
}
119+
120+
#[cfg(feature = "silent-payments")]
121+
#[derive(Parser, Debug, Clone, PartialEq)]
122+
pub struct SilentPaymentCodeCommand {
123+
/// The scan public key to use on the silent payment code.
124+
#[arg(long = "scan_key")]
125+
scan: bdk_sp::bitcoin::secp256k1::PublicKey,
126+
/// The spend public key to use on the silent payment code.
127+
#[arg(long = "spend_key")]
128+
spend: bdk_sp::bitcoin::secp256k1::PublicKey,
129+
}
130+
131+
#[cfg(feature = "silent-payments")]
132+
impl AppCommand<AppContext<Init>> for SilentPaymentCodeCommand {
133+
type Output = StatusResult;
134+
135+
fn execute(&self, ctx: &mut AppContext<Init>) -> Result<Self::Output, Error> {
136+
let sp_code =
137+
bdk_sp::encoding::SilentPaymentCode::new_v0(self.scan, self.spend, ctx.network);
138+
139+
Ok(StatusResult {
140+
message: sp_code.to_string(),
141+
})
142+
}
143+
}

src/handlers/offline.rs

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ use clap::Parser;
1717
use serde_json::json;
1818
use std::collections::BTreeMap;
1919
use std::str::FromStr;
20+
#[cfg(feature = "silent-payments")]
21+
use {
22+
crate::utils::common::parse_sp_code_value_pairs,
23+
bdk_sp::{
24+
bitcoin::{PrivateKey, PublicKey},
25+
encoding::SilentPaymentCode,
26+
send::psbt::derive_sp,
27+
},
28+
bdk_wallet::bitcoin::key::Secp256k1,
29+
bdk_wallet::keys::{DescriptorPublicKey, DescriptorSecretKey, SinglePubKey},
30+
std::collections::HashMap,
31+
};
2032
#[cfg(feature = "bip322")]
2133
use {
2234
crate::utils::parse_signature_format,
@@ -41,6 +53,8 @@ impl OfflineWalletSubCommand {
4153
Self::CreateTx(createtx_command) => {
4254
createtx_command.execute(ctx)?.write_out(std::io::stdout())
4355
}
56+
#[cfg(feature = "silent-payments")]
57+
Self::CreateSpTx(cmd) => cmd.execute(ctx)?.write_out(std::io::stdout()),
4458
Self::BumpFee(bumpfee_command) => {
4559
bumpfee_command.execute(ctx)?.write_out(std::io::stdout())
4660
}
@@ -293,6 +307,227 @@ impl AppCommand<AppContext<OfflineOperations<'_>>> for CreateTxCommand {
293307
}
294308
}
295309

310+
#[cfg(feature = "silent-payments")]
311+
#[derive(Debug, Parser, Clone, PartialEq)]
312+
pub struct CreateSpTxCommand {
313+
/// Adds a recipient to the transaction.
314+
// Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
315+
// Address and amount parsing is done at run time in handler function.
316+
#[arg(env = "ADDRESS:SAT", long = "to", required = false, value_parser = parse_recipient)]
317+
pub recipients: Option<Vec<(ScriptBuf, u64)>>,
318+
/// Parse silent payment recipients
319+
#[arg(long = "to-sp", required = true, value_parser = parse_sp_code_value_pairs)]
320+
pub silent_payment_recipients: Vec<(SilentPaymentCode, u64)>,
321+
/// Sends all the funds (or all the selected utxos). Requires only one recipient with value 0.
322+
#[arg(long = "send_all", short = 'a')]
323+
pub send_all: bool,
324+
/// 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.
325+
#[arg(long = "offline_signer")]
326+
pub offline_signer: bool,
327+
/// Selects which utxos *must* be spent.
328+
#[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)]
329+
pub utxos: Option<Vec<OutPoint>>,
330+
/// Marks a utxo as unspendable.
331+
#[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)]
332+
pub unspendable: Option<Vec<OutPoint>>,
333+
/// Fee rate to use in sat/vbyte.
334+
#[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate")]
335+
pub fee_rate: Option<f32>,
336+
/// Selects which policy should be used to satisfy the external descriptor.
337+
#[arg(env = "EXT_POLICY", long = "external_policy")]
338+
pub external_policy: Option<String>,
339+
/// Selects which policy should be used to satisfy the internal descriptor.
340+
#[arg(env = "INT_POLICY", long = "internal_policy")]
341+
pub internal_policy: Option<String>,
342+
/// Optionally create an OP_RETURN output containing given String in utf8 encoding (max 80 bytes)
343+
#[arg(
344+
env = "ADD_STRING",
345+
long = "add_string",
346+
short = 's',
347+
conflicts_with = "add_data"
348+
)]
349+
pub add_string: Option<String>,
350+
/// Optionally create an OP_RETURN output containing given base64 encoded String. (max 80 bytes)
351+
#[arg(
352+
env = "ADD_DATA",
353+
long = "add_data",
354+
short = 'o',
355+
conflicts_with = "add_string"
356+
)]
357+
pub add_data: Option<String>, //base 64 econding
358+
}
359+
360+
#[cfg(feature = "silent-payments")]
361+
impl AppCommand<AppContext<OfflineOperations<'_>>> for CreateSpTxCommand {
362+
type Output = RawPsbt;
363+
364+
fn execute(&self, ctx: &mut AppContext<OfflineOperations<'_>>) -> Result<Self::Output, Error> {
365+
let mut tx_builder = ctx.state.wallet.build_tx();
366+
367+
let sp_recipients: Vec<SilentPaymentCode> = self
368+
.silent_payment_recipients
369+
.iter()
370+
.map(|(sp_code, _)| sp_code.clone())
371+
.collect();
372+
373+
if self.send_all {
374+
if sp_recipients.len() == 1 && self.recipients.is_none() {
375+
tx_builder
376+
.drain_wallet()
377+
.drain_to(sp_recipients[0].get_placeholder_p2tr_spk());
378+
} else if let Some(ref recipients) = self.recipients
379+
&& sp_recipients.is_empty()
380+
{
381+
if recipients.len() == 1 {
382+
tx_builder.drain_wallet().drain_to(recipients[0].0.clone());
383+
} else {
384+
return Err(Error::Generic(
385+
"Wallet can only be drain to a single output".to_string(),
386+
));
387+
}
388+
} else {
389+
return Err(Error::Generic(
390+
"Wallet can only be drain to a single output".to_string(),
391+
));
392+
}
393+
} else {
394+
let mut outputs: Vec<(ScriptBuf, Amount)> = self
395+
.silent_payment_recipients
396+
.iter()
397+
.map(|(sp_code, amount)| {
398+
let script = sp_code.get_placeholder_p2tr_spk();
399+
(script, Amount::from_sat(*amount))
400+
})
401+
.collect();
402+
403+
if let Some(recipients) = &self.recipients {
404+
let recipients = recipients
405+
.iter()
406+
.map(|(script, amount)| (script.clone(), Amount::from_sat(*amount)));
407+
408+
outputs.extend(recipients);
409+
}
410+
411+
tx_builder.set_recipients(outputs);
412+
}
413+
414+
// Do not enable RBF for this transaction
415+
tx_builder.set_exact_sequence(Sequence::MAX);
416+
417+
if self.offline_signer {
418+
tx_builder.include_output_redeem_witness_script();
419+
}
420+
421+
if let Some(fee_rate) = self.fee_rate
422+
&& let Some(fee_rate) = FeeRate::from_sat_per_vb(fee_rate as u64)
423+
{
424+
tx_builder.fee_rate(fee_rate);
425+
}
426+
427+
if let Some(utxos) = &self.utxos {
428+
tx_builder
429+
.add_utxos(&utxos[..])
430+
.map_err(|_| bdk_wallet::error::CreateTxError::UnknownUtxo)?;
431+
}
432+
433+
if let Some(unspendable) = &self.unspendable {
434+
tx_builder.unspendable(unspendable.to_vec());
435+
}
436+
437+
if let Some(base64_data) = &self.add_data {
438+
let op_return_data = BASE64_STANDARD
439+
.decode(base64_data)
440+
.map_err(|e| Error::Generic(e.to_string()))?;
441+
tx_builder.add_data(
442+
&PushBytesBuf::try_from(op_return_data)
443+
.map_err(|e| Error::Generic(e.to_string()))?,
444+
);
445+
} else if let Some(string_data) = &self.add_string {
446+
let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec())
447+
.map_err(|e| Error::Generic(e.to_string()))?;
448+
tx_builder.add_data(&data);
449+
}
450+
451+
let policies = vec![
452+
self.external_policy
453+
.as_ref()
454+
.map(|p| (p, KeychainKind::External)),
455+
self.internal_policy
456+
.as_ref()
457+
.map(|p| (p, KeychainKind::Internal)),
458+
];
459+
460+
for (policy, keychain) in policies.into_iter().flatten() {
461+
let policy = serde_json::from_str::<BTreeMap<String, Vec<usize>>>(policy)?;
462+
tx_builder.policy_path(policy, keychain);
463+
}
464+
465+
let mut psbt = tx_builder.finish()?;
466+
467+
let unsigned_psbt = psbt.clone();
468+
469+
let finalized = ctx.state.wallet.sign(&mut psbt, SignOptions::default())?;
470+
471+
if !finalized {
472+
return Err(Error::Generic(
473+
"Cannot produce silent payment outputs without intermediate signing phase."
474+
.to_string(),
475+
));
476+
}
477+
478+
for (full_input, psbt_input) in unsigned_psbt.inputs.iter().zip(psbt.inputs.iter_mut()) {
479+
// repopulate key derivation data
480+
psbt_input.bip32_derivation = full_input.bip32_derivation.clone();
481+
psbt_input.tap_key_origins = full_input.tap_key_origins.clone();
482+
}
483+
484+
let secp = Secp256k1::new();
485+
let mut external_signers = ctx
486+
.state
487+
.wallet
488+
.get_signers(KeychainKind::External)
489+
.as_key_map(&secp);
490+
let internal_signers = ctx
491+
.state
492+
.wallet
493+
.get_signers(KeychainKind::Internal)
494+
.as_key_map(&secp);
495+
external_signers.extend(internal_signers);
496+
497+
match external_signers.iter().next().expect("not empty") {
498+
(DescriptorPublicKey::Single(single_pub), DescriptorSecretKey::Single(prv)) => {
499+
match single_pub.key {
500+
SinglePubKey::FullKey(pk) => {
501+
let keys: HashMap<PublicKey, PrivateKey> = [(pk, prv.key)].into();
502+
derive_sp(&mut psbt, &keys, &sp_recipients, &secp).expect("will fix later");
503+
}
504+
SinglePubKey::XOnly(xonly) => {
505+
let keys: HashMap<bdk_sp::bitcoin::XOnlyPublicKey, PrivateKey> =
506+
[(xonly, prv.key)].into();
507+
derive_sp(&mut psbt, &keys, &sp_recipients, &secp).expect("will fix later");
508+
}
509+
};
510+
}
511+
(_, DescriptorSecretKey::XPrv(k)) => {
512+
derive_sp(&mut psbt, &k.xkey, &sp_recipients, &secp).expect("will fix later");
513+
}
514+
_ => unimplemented!("multi xkey signer"),
515+
};
516+
517+
// Unfinalize PSBT to resign
518+
for psbt_input in psbt.inputs.iter_mut() {
519+
psbt_input.final_script_sig = None;
520+
psbt_input.final_script_witness = None;
521+
}
522+
523+
let _resigned = ctx.state.wallet.sign(&mut psbt, SignOptions::default())?;
524+
525+
let raw_tx = psbt.extract_tx()?;
526+
527+
Ok(RawPsbt::new(&raw_tx))
528+
}
529+
}
530+
296531
#[derive(Debug, Parser, Clone, PartialEq)]
297532
pub struct BumpFeeCommand {
298533
/// TXID of the transaction to update.

src/main.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,13 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> {
185185
cmd.execute(&mut ctx)?.write_out(std::io::stdout())?;
186186
}
187187
CliSubCommand::Completions { shell: _ } => unimplemented!(),
188+
189+
#[cfg(feature = "silent-payments")]
190+
CliSubCommand::SilentPaymentCode(cmd) => {
191+
let mut ctx = AppContext::new(cli_opts.network, home_dir);
192+
193+
cmd.execute(&mut ctx)?.write_out(std::io::stdout())?;
194+
}
188195
}
189196

190197
Ok(())

src/utils/common.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use crate::{commands::WalletOpts, config::WalletConfig, error::BDKCliError as Er
33
use bdk_bip322::SignatureFormat;
44
#[cfg(feature = "cbf")]
55
use bdk_kyoto::{Info, Receiver, UnboundedReceiver, Warning};
6+
#[cfg(feature = "silent-payments")]
7+
use bdk_sp::encoding::SilentPaymentCode;
68
#[cfg(any(
79
feature = "electrum",
810
feature = "esplora",
@@ -11,8 +13,6 @@ use bdk_kyoto::{Info, Receiver, UnboundedReceiver, Warning};
1113
))]
1214
use bdk_wallet::bitcoin::Psbt;
1315
use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf};
14-
#[cfg(feature = "silent-payments")]
15-
use bdk_sp::encoding::SilentPaymentCode;
1616

1717
use crate::commands::OfflineWalletSubCommand;
1818
use std::{
@@ -241,5 +241,7 @@ pub fn command_requires_db(command: &OfflineWalletSubCommand) -> bool {
241241

242242
#[cfg(feature = "bip322")]
243243
OfflineWalletSubCommand::VerifyMessage(_) => false,
244+
#[cfg(feature = "silent-payments")]
245+
OfflineWalletSubCommand::CreateSpTx(_) => false,
244246
}
245247
}

tests/integration.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,5 +244,4 @@ mod test {
244244
.unwrap();
245245
assert_eq!(confirmed_balance, 1000000000u64);
246246
}
247-
248247
}

0 commit comments

Comments
 (0)