Skip to content

Commit

Permalink
Add alternative function for creating funding txes
Browse files Browse the repository at this point in the history
Based on my dogfooding which showed that sometimes the code is unable
to properly choose UTXOs to create funding transactions. So a fallback
is needed and maybe more than one.

This commit renames the functions create_spending_txes -> create_funding_txes

This commit also moves many of those routines to a new file funding_tx.rs
as wallet_sync.rs was getting pretty big, and going forward the creation
of funding transactions will have to be very careful in order to get
the best privacy.
  • Loading branch information
chris-belcher committed Jun 25, 2022
1 parent ced801c commit 761e3a9
Show file tree
Hide file tree
Showing 3 changed files with 464 additions and 219 deletions.
375 changes: 375 additions & 0 deletions src/funding_tx.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,375 @@
//this file contains routines for creating funding transactions

use std::collections::HashMap;

use itertools::izip;

use bitcoin::{hashes::hex::FromHex, Address, Amount, OutPoint, Transaction, Txid};

use bitcoincore_rpc::json::{CreateRawTransactionInput, WalletCreateFundedPsbtOptions};
use bitcoincore_rpc::{Client, RpcApi};

use serde_json::Value;

use rand::rngs::OsRng;
use rand::RngCore;

use crate::error::Error;
use crate::wallet_sync::{convert_json_rpc_bitcoin_to_satoshis, Wallet};

pub struct CreateFundingTxesResult {
pub funding_txes: Vec<Transaction>,
pub payment_output_positions: Vec<u32>,
pub total_miner_fee: u64,
}

impl Wallet {
pub fn create_funding_txes(
&self,
rpc: &Client,
coinswap_amount: u64,
destinations: &[Address],
fee_rate: u64,
) -> Result<CreateFundingTxesResult, Error> {
let ret =
self.create_funding_txes_random_amounts(rpc, coinswap_amount, destinations, fee_rate);
if ret.is_ok() {
log::debug!(target: "wallet", "created funding txes with random amounts");
return ret;
}

let ret =
self.create_funding_txes_utxo_max_sends(rpc, coinswap_amount, destinations, fee_rate);
if ret.is_ok() {
log::debug!(target: "wallet", "created funding txes with fully-spending utxos");
return ret;
}

ret
}

fn generate_amount_fractions(
count: usize,
total_amount: u64,
lower_limit: u64,
) -> Result<Vec<f32>, Error> {
for _ in 0..100000 {
let mut knives = (1..count)
.map(|_| OsRng.next_u32() as f32 / u32::MAX as f32)
.collect::<Vec<f32>>();
knives.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));

let mut fractions = Vec::<f32>::new();
let mut last: f32 = 1.0;
for k in knives {
fractions.push(last - k);
last = k;
}
fractions.push(last);

if fractions
.iter()
.all(|f| *f * (total_amount as f32) > lower_limit as f32)
{
return Ok(fractions);
}
}
Err(Error::Protocol(
"unable to generate amount fractions, probably amount too small",
))
}

fn create_funding_txes_random_amounts(
&self,
rpc: &Client,
coinswap_amount: u64,
destinations: &[Address],
fee_rate: u64,
) -> Result<CreateFundingTxesResult, Error> {
log::debug!(target: "wallet", "coinswap_amount = {} destinations = {:?}",
coinswap_amount, destinations);

//TODO needs perhaps better way to create multiple txes for
//multi-tx-coinswap could try multiple ways, and in combination
//* come up with your own algorithm that sums up UTXOs
// would lose bitcoin core's cool utxo choosing algorithm though
// until their total value is >desired_amount
//* use listunspent with minimumSumAmount
//* pick individual utxos for no-change txes, and for the last one
// use walletcreatefundedpsbt which will create change

//* randomly generate some satoshi amounts and send them into
// walletcreatefundedpsbt to create funding txes that create change
//this is the solution used right now

let change_addresses = self.get_next_internal_addresses(rpc, destinations.len() as u32)?;
log::debug!(target: "wallet", "change addrs = {:?}", change_addresses);

let mut output_values = Wallet::generate_amount_fractions(
destinations.len(),
coinswap_amount,
5000, //use 5000 satoshi as the lower limit for now
//there should always be enough to pay miner fees
)?
.iter()
.map(|f| (*f * coinswap_amount as f32) as u64)
.collect::<Vec<u64>>();

//rounding errors mean usually 1 or 2 satoshis are lost, add them back

//this calculation works like this:
//o = [a, b, c, ...] | list of output values
//t = coinswap amount | total desired value
//a' <-- a + (t - (a+b+c+...)) | assign new first output value
//a' <-- a + (t -a-b-c-...) | rearrange
//a' <-- t - b - c -... |
*output_values.first_mut().unwrap() =
coinswap_amount - output_values.iter().skip(1).sum::<u64>();
assert_eq!(output_values.iter().sum::<u64>(), coinswap_amount);
log::debug!(target: "wallet", "output values = {:?}", output_values);

self.lock_all_nonwallet_unspents(rpc)?;

let mut funding_txes = Vec::<Transaction>::new();
let mut payment_output_positions = Vec::<u32>::new();
let mut total_miner_fee = 0;
for (address, &output_value, change_address) in izip!(
destinations.iter(),
output_values.iter(),
change_addresses.iter()
) {
log::debug!(target: "wallet", "output_value = {} to addr={}", output_value, address);

let mut outputs = HashMap::<String, Amount>::new();
outputs.insert(address.to_string(), Amount::from_sat(output_value));

let wcfp_result = rpc.wallet_create_funded_psbt(
&[],
&outputs,
None,
Some(WalletCreateFundedPsbtOptions {
include_watching: Some(true),
change_address: Some(change_address.clone()),
fee_rate: Some(Amount::from_sat(fee_rate)),
..Default::default()
}),
None,
)?;
total_miner_fee += wcfp_result.fee.as_sat();
log::debug!(target: "wallet", "created funding tx, miner fee={}", wcfp_result.fee);

let funding_tx = self.from_walletcreatefundedpsbt_to_tx(rpc, &wcfp_result.psbt)?;

rpc.lock_unspent(
&funding_tx
.input
.iter()
.map(|vin| vin.previous_output)
.collect::<Vec<OutPoint>>(),
)?;

let payment_pos = if wcfp_result.change_position == 0 {
1
} else {
0
};
log::debug!(target: "wallet", "payment_pos = {}", payment_pos);

funding_txes.push(funding_tx);
payment_output_positions.push(payment_pos);
}

Ok(CreateFundingTxesResult {
funding_txes,
payment_output_positions,
total_miner_fee,
})
}

fn create_funding_txes_utxo_max_sends(
&self,
rpc: &Client,
coinswap_amount: u64,
destinations: &[Address],
fee_rate: u64,
) -> Result<CreateFundingTxesResult, Error> {
//this function creates txes by
//using walletcreatefundedpsbt for the total amount, and if
//the number if inputs UTXOs is >number_of_txes then split those inputs into groups
//across multiple transactions

let mut outputs = HashMap::<String, Amount>::new();
outputs.insert(
destinations[0].to_string(),
Amount::from_sat(coinswap_amount),
);
let change_address = self.get_next_internal_addresses(rpc, 1)?[0].clone();

self.lock_all_nonwallet_unspents(rpc)?;
let wcfp_result = rpc.wallet_create_funded_psbt(
&[],
&outputs,
None,
Some(WalletCreateFundedPsbtOptions {
include_watching: Some(true),
change_address: Some(change_address.clone()),
fee_rate: Some(Amount::from_sat(fee_rate)),
..Default::default()
}),
None,
)?;
//TODO rust-bitcoin handles psbt, use those functions instead
let decoded_psbt = rpc.call::<Value>("decodepsbt", &[Value::String(wcfp_result.psbt)])?;
log::debug!(target: "wallet", "total tx decoded_psbt = {:?}", decoded_psbt);

let total_tx_inputs_len = decoded_psbt["inputs"].as_array().unwrap().len();
log::debug!(target: "wallet", "total tx inputs.len = {}", total_tx_inputs_len);
if total_tx_inputs_len < destinations.len() {
//not enough UTXOs found, cant use this method
return Err(Error::Protocol(
"not enough UTXOs found, cant use this method",
));
}

let mut total_tx_inputs = decoded_psbt["tx"]["vin"]
.as_array()
.unwrap()
.iter()
.zip(decoded_psbt["inputs"].as_array().unwrap().iter())
.collect::<Vec<(&Value, &Value)>>();

total_tx_inputs.sort_by(|(_, a), (_, b)| {
b["witness_utxo"]["amount"]
.as_f64()
.unwrap()
.partial_cmp(&a["witness_utxo"]["amount"].as_f64().unwrap())
.unwrap_or(std::cmp::Ordering::Equal)
});

let mut total_tx_inputs_iter = total_tx_inputs.iter();

let first_tx_input = total_tx_inputs_iter.next().unwrap();

let mut destinations_iter = destinations.iter();

let mut funding_txes = Vec::<Transaction>::new();
let mut payment_output_positions = Vec::<u32>::new();
let mut total_miner_fee = 0;

let mut leftover_coinswap_amount = coinswap_amount;

for _ in 0..(destinations.len() - 2) {
let (vin, input_info) = total_tx_inputs_iter.next().unwrap();

let mut outputs = HashMap::<String, Amount>::new();
outputs.insert(
destinations_iter.next().unwrap().to_string(),
Amount::from_sat(convert_json_rpc_bitcoin_to_satoshis(
&input_info["witness_utxo"]["amount"],
)),
);
let wcfp_result = rpc.wallet_create_funded_psbt(
&[CreateRawTransactionInput {
txid: Txid::from_hex(vin["txid"].as_str().unwrap()).unwrap(),
vout: vin["vout"].as_u64().unwrap() as u32,
sequence: None,
}],
&outputs,
None,
Some(WalletCreateFundedPsbtOptions {
add_inputs: Some(false),
subtract_fee_from_outputs: vec![0],
fee_rate: Some(Amount::from_sat(fee_rate)),
..Default::default()
}),
None,
)?;
let funding_tx = self.from_walletcreatefundedpsbt_to_tx(rpc, &wcfp_result.psbt)?;
leftover_coinswap_amount -= funding_tx.output[0].value;

total_miner_fee += wcfp_result.fee.as_sat();
log::debug!(target: "wallet", "created funding tx, miner fee={}", wcfp_result.fee);

funding_txes.push(funding_tx);
payment_output_positions.push(0);
}

let (leftover_inputs, leftover_inputs_values): (Vec<_>, Vec<_>) = total_tx_inputs_iter
.map(|(vin, input_info)| {
(
CreateRawTransactionInput {
txid: Txid::from_hex(vin["txid"].as_str().unwrap()).unwrap(),
vout: vin["vout"].as_u64().unwrap() as u32,
sequence: None,
},
convert_json_rpc_bitcoin_to_satoshis(&input_info["witness_utxo"]["amount"]),
)
})
.unzip();
let mut outputs = HashMap::<String, Amount>::new();
outputs.insert(
destinations_iter.next().unwrap().to_string(),
Amount::from_sat(leftover_inputs_values.iter().sum::<u64>()),
);
let wcfp_result = rpc.wallet_create_funded_psbt(
&leftover_inputs,
&outputs,
None,
Some(WalletCreateFundedPsbtOptions {
add_inputs: Some(false),
subtract_fee_from_outputs: vec![0],
fee_rate: Some(Amount::from_sat(fee_rate)),
..Default::default()
}),
None,
)?;
let funding_tx = self.from_walletcreatefundedpsbt_to_tx(rpc, &wcfp_result.psbt)?;
leftover_coinswap_amount -= funding_tx.output[0].value;

total_miner_fee += wcfp_result.fee.as_sat();
log::debug!(target: "wallet", "created funding tx, miner fee={}", wcfp_result.fee);

funding_txes.push(funding_tx);
payment_output_positions.push(0);

let (first_vin, _first_input_info) = first_tx_input;
let mut outputs = HashMap::<String, Amount>::new();
outputs.insert(
destinations_iter.next().unwrap().to_string(),
Amount::from_sat(leftover_coinswap_amount),
);
let wcfp_result = rpc.wallet_create_funded_psbt(
&[CreateRawTransactionInput {
txid: Txid::from_hex(first_vin["txid"].as_str().unwrap()).unwrap(),
vout: first_vin["vout"].as_u64().unwrap() as u32,
sequence: None,
}],
&outputs,
None,
Some(WalletCreateFundedPsbtOptions {
add_inputs: Some(false),
change_address: Some(change_address.clone()),
fee_rate: Some(Amount::from_sat(fee_rate)),
..Default::default()
}),
None,
)?;
let funding_tx = self.from_walletcreatefundedpsbt_to_tx(rpc, &wcfp_result.psbt)?;

total_miner_fee += wcfp_result.fee.as_sat();
log::debug!(target: "wallet", "created funding tx, miner fee={}", wcfp_result.fee);

funding_txes.push(funding_tx);
payment_output_positions.push(if wcfp_result.change_position == 0 {
1
} else {
0
});

Ok(CreateFundingTxesResult {
funding_txes,
payment_output_positions,
total_miner_fee,
})
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ use fidelity_bonds::{get_locktime_from_index, YearAndMonth};

pub mod directory_servers;
pub mod error;
pub mod funding_tx;
pub mod messages;
pub mod watchtower_client;
pub mod watchtower_protocol;
Expand Down
Loading

0 comments on commit 761e3a9

Please sign in to comment.