From 4f865ab2c06710a682990c67683efe20b7189993 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Sun, 17 May 2020 18:01:52 +0200 Subject: [PATCH] [cli] Add a few commands to handle psbts --- src/cli.rs | 135 +++++++++++++++++++++++++++++++++++++++--- src/descriptor/mod.rs | 10 ++-- src/wallet/mod.rs | 118 ++++++++++++++++++------------------ 3 files changed, 190 insertions(+), 73 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 3dd23c04..a6f34625 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,6 +7,7 @@ use clap::{App, Arg, ArgMatches, SubCommand}; use log::{debug, error, info, trace, LevelFilter}; use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex}; +use bitcoin::hashes::hex::{FromHex, ToHex}; use bitcoin::util::psbt::PartiallySignedTransaction; use bitcoin::{Address, OutPoint}; @@ -147,16 +148,69 @@ pub fn make_cli_subcommands<'a, 'b>() -> App<'a, 'b> { )) .subcommand( SubCommand::with_name("broadcast") - .about("Extracts the finalized transaction from a PSBT and broadcasts it to the network") + .about("Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract") .arg( Arg::with_name("psbt") .long("psbt") .value_name("BASE64_PSBT") - .help("Sets the PSBT to broadcast") + .help("Sets the PSBT to extract and broadcast") + .takes_value(true) + .required_unless("tx") + .number_of_values(1)) + .arg( + Arg::with_name("tx") + .long("tx") + .value_name("RAWTX") + .help("Sets the raw transaction to broadcast") + .takes_value(true) + .required_unless("psbt") + .number_of_values(1)) + ) + .subcommand( + SubCommand::with_name("extract_psbt") + .about("Extracts a raw transaction from a PSBT") + .arg( + Arg::with_name("psbt") + .long("psbt") + .value_name("BASE64_PSBT") + .help("Sets the PSBT to extract") + .takes_value(true) + .required(true) + .number_of_values(1)) + ) + .subcommand( + SubCommand::with_name("finalize_psbt") + .about("Finalizes a psbt") + .arg( + Arg::with_name("psbt") + .long("psbt") + .value_name("BASE64_PSBT") + .help("Sets the PSBT to finalize") + .takes_value(true) + .required(true) + .number_of_values(1)) + .arg( + Arg::with_name("assume_height") + .long("assume_height") + .value_name("HEIGHT") + .help("Assume the blockchain has reached a specific height") .takes_value(true) .number_of_values(1) - .required(true), - )) + .required(false)) + ) + .subcommand( + SubCommand::with_name("combine_psbt") + .about("Combines multiple PSBTs into one") + .arg( + Arg::with_name("psbt") + .long("psbt") + .value_name("BASE64_PSBT") + .help("Add one PSBT to comine. This option can be repeated multiple times, one for each PSBT") + .takes_value(true) + .number_of_values(1) + .required(true) + .multiple(true)) + ) } pub fn add_global_flags<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { @@ -240,8 +294,9 @@ where let addressees = sub_matches .values_of("to") .unwrap() - .map(|s| parse_addressee(s).unwrap()) - .collect(); + .map(|s| parse_addressee(s)) + .collect::, _>>() + .map_err(|s| Error::Generic(s))?; let send_all = sub_matches.is_present("send_all"); let fee_rate = sub_matches .value_of("fee_rate") @@ -308,11 +363,73 @@ where Ok(Some(res)) } else if let Some(sub_matches) = matches.subcommand_matches("broadcast") { - let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap(); - let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); - let (txid, _) = wallet.broadcast(psbt).await?; + let tx = if sub_matches.value_of("psbt").is_some() { + let psbt = base64::decode(&sub_matches.value_of("psbt").unwrap()).unwrap(); + let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); + psbt.extract_tx() + } else if sub_matches.value_of("tx").is_some() { + deserialize(&Vec::::from_hex(&sub_matches.value_of("tx").unwrap()).unwrap()) + .unwrap() + } else { + panic!("Missing `psbt` and `tx` option"); + }; + + let txid = wallet.broadcast(tx).await?; Ok(Some(format!("TXID: {}", txid))) + } else if let Some(sub_matches) = matches.subcommand_matches("extract_psbt") { + let psbt = base64::decode(&sub_matches.value_of("psbt").unwrap()).unwrap(); + let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); + + Ok(Some(format!( + "TX: {}", + serialize(&psbt.extract_tx()).to_hex() + ))) + } else if let Some(sub_matches) = matches.subcommand_matches("finalize_psbt") { + let psbt = base64::decode(&sub_matches.value_of("psbt").unwrap()).unwrap(); + let mut psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); + + let assume_height = sub_matches + .value_of("assume_height") + .and_then(|s| Some(s.parse().unwrap())); + + let finalized = wallet.finalize_psbt(&mut psbt, assume_height)?; + + let mut res = String::new(); + res += &format!("PSBT: {}\n", base64::encode(&serialize(&psbt))); + res += &format!("Finalized: {}", finalized); + if finalized { + res += &format!("\nExtracted: {}", serialize_hex(&psbt.extract_tx())); + } + + Ok(Some(res)) + } else if let Some(sub_matches) = matches.subcommand_matches("combine_psbt") { + let mut psbts = sub_matches + .values_of("psbt") + .unwrap() + .map(|s| { + let psbt = base64::decode(&s).unwrap(); + let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); + + psbt + }) + .collect::>(); + + let init_psbt = psbts.pop().unwrap(); + let final_psbt = psbts + .into_iter() + .try_fold::<_, _, Result>( + init_psbt, + |mut acc, x| { + acc.merge(x)?; + Ok(acc) + }, + )?; + + Ok(Some(format!( + "PSBT: {}", + base64::encode(&serialize(&final_psbt)) + ))) } else { Ok(None) } diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index a2db112d..6f142a83 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -194,11 +194,11 @@ impl ExtendedDescriptor { _ => return Err(Error::CantDeriveWithMiniscript), }; - if !self.same_structure(&derived_desc) { - Err(Error::CantDeriveWithMiniscript) - } else { - Ok(derived_desc) - } + // if !self.same_structure(&derived_desc) { + // Err(Error::CantDeriveWithMiniscript) + // } else { + Ok(derived_desc) + // } } pub fn derive_from_psbt_input( diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 7ebbdd99..7b2c7d4a 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -483,7 +483,7 @@ where } // attempt to finalize - let finalized = self.finalize_psbt(tx.clone(), &mut psbt, assume_height)?; + let finalized = self.finalize_psbt(&mut psbt, assume_height)?; Ok((psbt, finalized)) } @@ -507,6 +507,61 @@ where } } + pub fn finalize_psbt( + &self, + psbt: &mut PSBT, + assume_height: Option, + ) -> Result { + let mut tx = psbt.global.unsigned_tx.clone(); + + for (n, input) in tx.input.iter_mut().enumerate() { + // safe to run only on the descriptor because we assume the change descriptor also has + // the same structure + let desc = self.descriptor.derive_from_psbt_input(psbt, n); + debug!("{:?}", psbt.inputs[n].hd_keypaths); + debug!("reconstructed descriptor is {:?}", desc); + + let desc = match desc { + Err(_) => return Ok(false), + Ok(desc) => desc, + }; + + // if the height is None in the database it means it's still unconfirmed, so consider + // that as a very high value + let create_height = self + .database + .borrow() + .get_tx(&input.previous_output.txid, false)? + .and_then(|tx| Some(tx.height.unwrap_or(std::u32::MAX))); + let current_height = assume_height.or(self.current_height); + + debug!( + "Input #{} - {}, using `create_height` = {:?}, `current_height` = {:?}", + n, input.previous_output, create_height, current_height + ); + + // TODO: use height once we sync headers + let satisfier = + PSBTSatisfier::new(&psbt.inputs[n], false, create_height, current_height); + + match desc.satisfy(input, satisfier) { + Ok(_) => continue, + Err(e) => { + debug!("satisfy error {:?} for input {}", e, n); + return Ok(false); + } + } + } + + // consume tx to extract its input's script_sig and witnesses and move them into the psbt + for (input, psbt_input) in tx.input.into_iter().zip(psbt.inputs.iter_mut()) { + psbt_input.final_script_sig = Some(input.script_sig); + psbt_input.final_script_witness = Some(input.witness); + } + + Ok(true) + } + // Internals #[cfg(not(target_arch = "wasm32"))] @@ -652,60 +707,6 @@ where Ok((answer, paths, selected_amount, fee_val)) } - - fn finalize_psbt( - &self, - mut tx: Transaction, - psbt: &mut PSBT, - assume_height: Option, - ) -> Result { - for (n, input) in tx.input.iter_mut().enumerate() { - // safe to run only on the descriptor because we assume the change descriptor also has - // the same structure - let desc = self.descriptor.derive_from_psbt_input(psbt, n); - debug!("{:?}", psbt.inputs[n].hd_keypaths); - debug!("reconstructed descriptor is {:?}", desc); - - let desc = match desc { - Err(_) => return Ok(false), - Ok(desc) => desc, - }; - - // if the height is None in the database it means it's still unconfirmed, so consider - // that as a very high value - let create_height = self - .database - .borrow() - .get_tx(&input.previous_output.txid, false)? - .and_then(|tx| Some(tx.height.unwrap_or(std::u32::MAX))); - let current_height = assume_height.or(self.current_height); - - debug!( - "Input #{} - {}, using `create_height` = {:?}, `current_height` = {:?}", - n, input.previous_output, create_height, current_height - ); - - // TODO: use height once we sync headers - let satisfier = - PSBTSatisfier::new(&psbt.inputs[n], false, create_height, current_height); - - match desc.satisfy(input, satisfier) { - Ok(_) => continue, - Err(e) => { - debug!("satisfy error {:?} for input {}", e, n); - return Ok(false); - } - } - } - - // consume tx to extract its input's script_sig and witnesses and move them into the psbt - for (input, psbt_input) in tx.input.into_iter().zip(psbt.inputs.iter_mut()) { - psbt_input.final_script_sig = Some(input.script_sig); - psbt_input.final_script_witness = Some(input.witness); - } - - Ok(true) - } } impl Wallet @@ -828,10 +829,9 @@ where .await } - pub async fn broadcast(&self, psbt: PSBT) -> Result<(Txid, Transaction), Error> { - let extracted = psbt.extract_tx(); - self.client.borrow_mut().broadcast(&extracted).await?; + pub async fn broadcast(&self, tx: Transaction) -> Result { + self.client.borrow_mut().broadcast(&tx).await?; - Ok((extracted.txid(), extracted)) + Ok(tx.txid()) } }