-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add alternative function for creating funding txes
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
1 parent
ced801c
commit 761e3a9
Showing
3 changed files
with
464 additions
and
219 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.