Skip to content

Commit

Permalink
Change Signer::sign_tx_input to Signer::sign_psbt_input
Browse files Browse the repository at this point in the history
With the previous API, signing taproot inputs comes with a lot of
problems. Taproot signatures commit to the previous txo of all the
inputs so to be able to construct a signature, you need to have all of
them available. The previous API would require the user to look up the
outputs themself which does not really work for mobile clients and it is
a bad practice. This changes it to a PSBT so we can give all the
necessary info to the user directly.
  • Loading branch information
benthecarman committed Dec 13, 2023
1 parent 102b372 commit d841f2a
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 75 deletions.
40 changes: 32 additions & 8 deletions bitcoin-rpc-provider/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::sync::{Arc, Mutex};
use std::time::Duration;

use bitcoin::consensus::encode::Error as EncodeError;
use bitcoin::psbt::PartiallySignedTransaction;
use bitcoin::secp256k1::rand::thread_rng;
use bitcoin::secp256k1::{PublicKey, SecretKey};
use bitcoin::{
Expand Down Expand Up @@ -185,14 +186,35 @@ impl Signer for BitcoinCoreProvider {
Ok(pk.inner)
}

fn sign_tx_input(
fn sign_psbt_input(
&self,
tx: &mut Transaction,
psbt: &mut PartiallySignedTransaction,
input_index: usize,
tx_out: &TxOut,
redeem_script: Option<Script>,
) -> Result<(), ManagerError> {
let outpoint = &tx.input[input_index].previous_output;
let outpoint = &psbt.unsigned_tx.input[input_index].previous_output;
let tx_out = if let Some(input) = psbt.inputs.get(input_index) {
if let Some(wit_utxo) = &input.witness_utxo {
Ok(wit_utxo.clone())
} else if let Some(in_tx) = &input.non_witness_utxo {
Ok(
in_tx.output[psbt.unsigned_tx.input[input_index].previous_output.vout as usize]
.clone(),
)
} else {
Err(ManagerError::InvalidParameters(
"No TxOut for PSBT inout".to_string(),
))
}
} else {
Err(ManagerError::InvalidParameters(
"No TxOut for PSBT inout".to_string(),
))
}?;

let redeem_script = psbt
.inputs
.get(input_index)
.and_then(|i| i.redeem_script.clone());

let input = json::SignRawTransactionInput {
txid: outpoint.txid,
Expand All @@ -206,13 +228,15 @@ impl Signer for BitcoinCoreProvider {
.client
.lock()
.unwrap()
.sign_raw_transaction_with_wallet(&*tx, Some(&[input]), None)
.sign_raw_transaction_with_wallet(&psbt.unsigned_tx, Some(&[input]), None)
.map_err(rpc_err_to_manager_err)?;
let signed_tx = Transaction::consensus_decode(&mut sign_result.hex.as_slice())
.map_err(enc_err_to_manager_err)?;

tx.input[input_index].script_sig = signed_tx.input[input_index].script_sig.clone();
tx.input[input_index].witness = signed_tx.input[input_index].witness.clone();
psbt.inputs[input_index].final_script_sig =
Some(signed_tx.input[input_index].script_sig.clone());
psbt.inputs[input_index].final_script_witness =
Some(signed_tx.input[input_index].witness.clone());

Ok(())
}
Expand Down
116 changes: 66 additions & 50 deletions dlc-manager/src/contract_updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use std::ops::Deref;

use bitcoin::psbt::PartiallySignedTransaction;
use bitcoin::{consensus::Decodable, Script, Transaction, Witness};
use dlc::{DlcTransactions, PartyParams};
use dlc_messages::{
Expand Down Expand Up @@ -284,6 +285,31 @@ where
Ok((signed_contract, signed_msg))
}

fn populate_psbt(
psbt: &mut PartiallySignedTransaction,
all_funding_inputs: &[&FundingInputInfo],
) -> Result<(), Error> {
// add witness utxo to fund_psbt for all inputs
for (input_index, x) in all_funding_inputs.iter().enumerate() {
let tx = Transaction::consensus_decode(&mut x.funding_input.prev_tx.as_slice()).map_err(
|_| {
Error::InvalidParameters(
"Could not decode funding input previous tx parameter".to_string(),
)
},
)?;
let vout = x.funding_input.prev_tx_vout;
let tx_out = tx.output.get(vout as usize).ok_or_else(|| {
Error::InvalidParameters(format!("Previous tx output not found at index {}", vout))
})?;

psbt.inputs[input_index].witness_utxo = Some(tx_out.clone());
psbt.inputs[input_index].redeem_script = Some(x.funding_input.redeem_script.clone());
}

Ok(())
}

pub(crate) fn verify_accepted_and_sign_contract_internal<S: Deref>(
secp: &Secp256k1<All>,
offered_contract: &OfferedContract,
Expand All @@ -309,7 +335,7 @@ where
funding_script_pubkey,
} = dlc_transactions;

let mut fund = fund.clone();
let mut fund_psbt = PartiallySignedTransaction::from_unsigned_tx(fund.clone()).unwrap();
let mut cets = cets.clone();

let input_script_pubkey = input_script_pubkey.unwrap_or_else(|| funding_script_pubkey.clone());
Expand Down Expand Up @@ -392,43 +418,42 @@ where
own_signatures.extend(sigs);
}

let mut input_serial_ids: Vec<_> = offered_contract
// get all funding inputs
let mut all_funding_inputs = offered_contract
.funding_inputs_info
.iter()
.map(|x| x.funding_input.input_serial_id)
.chain(accept_params.inputs.iter().map(|x| x.serial_id))
.collect();
input_serial_ids.sort_unstable();
.chain(funding_inputs_info.iter())
.collect::<Vec<_>>();
// sort by serial id
all_funding_inputs.sort_by_key(|x| x.funding_input.input_serial_id);

populate_psbt(&mut fund_psbt, &all_funding_inputs)?;

// Vec<Witness>
let witnesses: Vec<Witness> = offered_contract
.funding_inputs_info
.iter()
.map(|x| {
let input_index = input_serial_ids
let input_index = all_funding_inputs
.iter()
.position(|y| y == &x.funding_input.input_serial_id)
.position(|y| y.funding_input == x.funding_input)
.ok_or_else(|| {
Error::InvalidState(format!(
"Could not find input for serial id {}",
x.funding_input.input_serial_id
))
})?;
let tx = Transaction::consensus_decode(&mut x.funding_input.prev_tx.as_slice())
.map_err(|_| {
Error::InvalidParameters(
"Could not decode funding input previous tx parameter".to_string(),
)
})?;
let vout = x.funding_input.prev_tx_vout;
let tx_out = tx.output.get(vout as usize).ok_or_else(|| {
Error::InvalidParameters(format!("Previous tx output not found at index {}", vout))
})?;

// pass wallet instead of privkeys
signer.sign_tx_input(&mut fund, input_index, tx_out, None)?;
signer.sign_psbt_input(&mut fund_psbt, input_index)?;

Ok(fund.input[input_index].witness.clone())
let witness = fund_psbt.inputs[input_index]
.final_script_witness
.clone()
.ok_or(Error::InvalidParameters(
"No witness from signing psbt input".to_string(),
))?;

Ok(witness)
})
.collect::<Result<Vec<_>, Error>>()?;

Expand All @@ -445,8 +470,6 @@ where
})
.collect::<Result<Vec<_>, Error>>()?;

input_serial_ids.sort_unstable();

let offer_refund_signature = dlc::util::get_raw_sig_for_tx_input(
secp,
refund,
Expand All @@ -457,7 +480,7 @@ where
)?;

let dlc_transactions = DlcTransactions {
fund,
fund: fund.clone(),
cets,
refund: refund.clone(),
funding_script_pubkey: funding_script_pubkey.clone(),
Expand Down Expand Up @@ -565,63 +588,56 @@ where
)?;
}

let mut input_serials: Vec<_> = offered_contract
let fund_tx = accepted_contract.dlc_transactions.fund.clone();
let mut fund_psbt = PartiallySignedTransaction::from_unsigned_tx(fund_tx).unwrap();

// get all funding inputs
let mut all_funding_inputs = offered_contract
.funding_inputs_info
.iter()
.chain(accepted_contract.funding_inputs.iter())
.map(|x| x.funding_input.input_serial_id)
.collect();
input_serials.sort_unstable();
.collect::<Vec<_>>();
// sort by serial id
all_funding_inputs.sort_by_key(|x| x.funding_input.input_serial_id);

let mut fund_tx = accepted_contract.dlc_transactions.fund.clone();
populate_psbt(&mut fund_psbt, &all_funding_inputs)?;

for (funding_input, funding_signatures) in offered_contract
.funding_inputs_info
.iter()
.zip(funding_signatures.funding_signatures.iter())
{
let input_index = input_serials
let input_index = all_funding_inputs
.iter()
.position(|x| x == &funding_input.funding_input.input_serial_id)
.position(|x| x.funding_input == funding_input.funding_input)
.ok_or_else(|| {
Error::InvalidState(format!(
"Could not find input for serial id {}",
funding_input.funding_input.input_serial_id
))
})?;

fund_tx.input[input_index].witness = Witness::from_vec(
fund_psbt.inputs[input_index].final_script_witness = Some(Witness::from_vec(
funding_signatures
.witness_elements
.iter()
.map(|x| x.witness.clone())
.collect(),
);
));
}

for funding_input_info in &accepted_contract.funding_inputs {
let input_index = input_serials
for funding_input in &accepted_contract.funding_inputs {
let input_index = all_funding_inputs
.iter()
.position(|x| x == &funding_input_info.funding_input.input_serial_id)
.position(|x| x.funding_input == funding_input.funding_input)
.ok_or_else(|| {
Error::InvalidState(format!(
"Could not find input for serial id {}",
funding_input_info.funding_input.input_serial_id,
funding_input.funding_input.input_serial_id
))
})?;
let tx =
Transaction::consensus_decode(&mut funding_input_info.funding_input.prev_tx.as_slice())
.map_err(|_| {
Error::InvalidParameters(
"Could not decode funding input previous tx parameter".to_string(),
)
})?;
let vout = funding_input_info.funding_input.prev_tx_vout;
let tx_out = tx.output.get(vout as usize).ok_or_else(|| {
Error::InvalidParameters(format!("Previous tx output not found at index {}", vout))
})?;

signer.sign_tx_input(&mut fund_tx, input_index, tx_out, None)?;
signer.sign_psbt_input(&mut fund_psbt, input_index)?;
}

let signed_contract = SignedContract {
Expand All @@ -632,7 +648,7 @@ where
channel_id,
};

Ok((signed_contract, fund_tx))
Ok((signed_contract, fund_psbt.extract_tx()))
}

/// Signs and return the CET that can be used to close the given contract.
Expand Down
7 changes: 3 additions & 4 deletions dlc-manager/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub mod manager;
pub mod payout_curve;
mod utils;

use bitcoin::psbt::PartiallySignedTransaction;
use bitcoin::{Address, Block, OutPoint, Script, Transaction, TxOut, Txid};
use chain_monitor::ChainMonitor;
use channel::offered_channel::OfferedChannel;
Expand Down Expand Up @@ -78,12 +79,10 @@ impl Time for SystemTimeProvider {
/// Provides signing related functionalities.
pub trait Signer {
/// Signs a transaction input
fn sign_tx_input(
fn sign_psbt_input(
&self,
tx: &mut Transaction,
psbt: &mut PartiallySignedTransaction,
input_index: usize,
tx_out: &TxOut,
redeem_script: Option<Script>,
) -> Result<(), Error>;
/// Get the secret key associated with the provided public key.
fn get_secret_key_for_pubkey(&self, pubkey: &PublicKey) -> Result<SecretKey, Error>;
Expand Down
11 changes: 5 additions & 6 deletions mocks/src/mock_wallet.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::rc::Rc;

use bitcoin::psbt::PartiallySignedTransaction;
use bitcoin::{Address, PackedLockTime, Script, Transaction, TxOut};
use dlc_manager::{error::Error, Blockchain, Signer, Utxo, Wallet};
use secp256k1_zkp::{rand::seq::SliceRandom, SecretKey};
Expand Down Expand Up @@ -45,13 +46,11 @@ impl MockWallet {
}

impl Signer for MockWallet {
fn sign_tx_input(
fn sign_psbt_input(
&self,
_tx: &mut bitcoin::Transaction,
_input_index: usize,
_tx_out: &bitcoin::TxOut,
_redeem_script: Option<bitcoin::Script>,
) -> Result<(), dlc_manager::error::Error> {
_psbt: &mut PartiallySignedTransaction,
_idx: usize,
) -> Result<(), Error> {
Ok(())
}

Expand Down
Loading

0 comments on commit d841f2a

Please sign in to comment.