[cli] Add a few commands to handle psbts

This commit is contained in:
Alekos Filini 2020-05-17 18:01:52 +02:00
parent fb4abfb99c
commit 4f865ab2c0
No known key found for this signature in database
GPG Key ID: 5E8AFC3034FDFA4F
3 changed files with 190 additions and 73 deletions

View File

@ -7,6 +7,7 @@ use clap::{App, Arg, ArgMatches, SubCommand};
use log::{debug, error, info, trace, LevelFilter}; use log::{debug, error, info, trace, LevelFilter};
use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex}; use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::util::psbt::PartiallySignedTransaction; use bitcoin::util::psbt::PartiallySignedTransaction;
use bitcoin::{Address, OutPoint}; use bitcoin::{Address, OutPoint};
@ -147,16 +148,69 @@ pub fn make_cli_subcommands<'a, 'b>() -> App<'a, 'b> {
)) ))
.subcommand( .subcommand(
SubCommand::with_name("broadcast") 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(
Arg::with_name("psbt") Arg::with_name("psbt")
.long("psbt") .long("psbt")
.value_name("BASE64_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) .takes_value(true)
.number_of_values(1) .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> { pub fn add_global_flags<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
@ -240,8 +294,9 @@ where
let addressees = sub_matches let addressees = sub_matches
.values_of("to") .values_of("to")
.unwrap() .unwrap()
.map(|s| parse_addressee(s).unwrap()) .map(|s| parse_addressee(s))
.collect(); .collect::<Result<Vec<_>, _>>()
.map_err(|s| Error::Generic(s))?;
let send_all = sub_matches.is_present("send_all"); let send_all = sub_matches.is_present("send_all");
let fee_rate = sub_matches let fee_rate = sub_matches
.value_of("fee_rate") .value_of("fee_rate")
@ -308,11 +363,73 @@ where
Ok(Some(res)) Ok(Some(res))
} else if let Some(sub_matches) = matches.subcommand_matches("broadcast") { } else if let Some(sub_matches) = matches.subcommand_matches("broadcast") {
let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap(); 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(); let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
let (txid, _) = wallet.broadcast(psbt).await?; psbt.extract_tx()
} else if sub_matches.value_of("tx").is_some() {
deserialize(&Vec::<u8>::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))) 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::<Vec<_>>();
let init_psbt = psbts.pop().unwrap();
let final_psbt = psbts
.into_iter()
.try_fold::<_, _, Result<PartiallySignedTransaction, Error>>(
init_psbt,
|mut acc, x| {
acc.merge(x)?;
Ok(acc)
},
)?;
Ok(Some(format!(
"PSBT: {}",
base64::encode(&serialize(&final_psbt))
)))
} else { } else {
Ok(None) Ok(None)
} }

View File

@ -194,11 +194,11 @@ impl ExtendedDescriptor {
_ => return Err(Error::CantDeriveWithMiniscript), _ => return Err(Error::CantDeriveWithMiniscript),
}; };
if !self.same_structure(&derived_desc) { // if !self.same_structure(&derived_desc) {
Err(Error::CantDeriveWithMiniscript) // Err(Error::CantDeriveWithMiniscript)
} else { // } else {
Ok(derived_desc) Ok(derived_desc)
} // }
} }
pub fn derive_from_psbt_input( pub fn derive_from_psbt_input(

View File

@ -483,7 +483,7 @@ where
} }
// attempt to finalize // 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)) Ok((psbt, finalized))
} }
@ -507,6 +507,61 @@ where
} }
} }
pub fn finalize_psbt(
&self,
psbt: &mut PSBT,
assume_height: Option<u32>,
) -> Result<bool, Error> {
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 // Internals
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -652,60 +707,6 @@ where
Ok((answer, paths, selected_amount, fee_val)) Ok((answer, paths, selected_amount, fee_val))
} }
fn finalize_psbt(
&self,
mut tx: Transaction,
psbt: &mut PSBT,
assume_height: Option<u32>,
) -> Result<bool, Error> {
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<B, D> Wallet<B, D> impl<B, D> Wallet<B, D>
@ -828,10 +829,9 @@ where
.await .await
} }
pub async fn broadcast(&self, psbt: PSBT) -> Result<(Txid, Transaction), Error> { pub async fn broadcast(&self, tx: Transaction) -> Result<Txid, Error> {
let extracted = psbt.extract_tx(); self.client.borrow_mut().broadcast(&tx).await?;
self.client.borrow_mut().broadcast(&extracted).await?;
Ok((extracted.txid(), extracted)) Ok(tx.txid())
} }
} }