From 0e504ef2e85a02cbae06c3dda7e20010156f1ab7 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Tue, 4 Jul 2023 13:50:43 +0800 Subject: [PATCH] Fix weight calculations for mixed legacy and segwit see: https://github.com/bitcoindevkit/bdk/pull/924#discussion_r1243605989 Was a PITA since branch and bound is hard to do with this interference between segiwt and legacy weights. It would find solutions that looked good until you add the final input which was segwit and then the solution would be suboptimal and fail the test. --- crates/bdk/src/wallet/coin_selection.rs | 20 +-- nursery/coin_select/README.md | 15 +-- nursery/coin_select/src/bnb.rs | 10 +- nursery/coin_select/src/coin_selector.rs | 43 +++--- nursery/coin_select/src/lib.rs | 3 +- nursery/coin_select/src/metrics/waste.rs | 16 ++- nursery/coin_select/tests/bnb.rs | 47 +++++-- nursery/coin_select/tests/waste.rs | 7 +- nursery/coin_select/tests/weight.rs | 165 +++++++++++++++++++++++ 9 files changed, 259 insertions(+), 67 deletions(-) create mode 100644 nursery/coin_select/tests/weight.rs diff --git a/crates/bdk/src/wallet/coin_selection.rs b/crates/bdk/src/wallet/coin_selection.rs index c3e84af2b..a0179d31b 100644 --- a/crates/bdk/src/wallet/coin_selection.rs +++ b/crates/bdk/src/wallet/coin_selection.rs @@ -836,7 +836,7 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 250_000 + FEE_AMOUNT; - let result = LargestFirstCoinSelection::default() + let result = LargestFirstCoinSelection .coin_select( utxos, vec![], @@ -857,7 +857,7 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 20_000 + FEE_AMOUNT; - let result = LargestFirstCoinSelection::default() + let result = LargestFirstCoinSelection .coin_select( utxos, vec![], @@ -878,7 +878,7 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 20_000 + FEE_AMOUNT; - let result = LargestFirstCoinSelection::default() + let result = LargestFirstCoinSelection .coin_select( vec![], utxos, @@ -900,7 +900,7 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 500_000 + FEE_AMOUNT; - LargestFirstCoinSelection::default() + LargestFirstCoinSelection .coin_select( vec![], utxos, @@ -918,7 +918,7 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 250_000 + FEE_AMOUNT; - LargestFirstCoinSelection::default() + LargestFirstCoinSelection .coin_select( vec![], utxos, @@ -935,7 +935,7 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 180_000 + FEE_AMOUNT; - let result = OldestFirstCoinSelection::default() + let result = OldestFirstCoinSelection .coin_select( vec![], utxos, @@ -956,7 +956,7 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 20_000 + FEE_AMOUNT; - let result = OldestFirstCoinSelection::default() + let result = OldestFirstCoinSelection .coin_select( utxos, vec![], @@ -977,7 +977,7 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 20_000 + FEE_AMOUNT; - let result = OldestFirstCoinSelection::default() + let result = OldestFirstCoinSelection .coin_select( vec![], utxos, @@ -999,7 +999,7 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 600_000 + FEE_AMOUNT; - OldestFirstCoinSelection::default() + OldestFirstCoinSelection .coin_select( vec![], utxos, @@ -1018,7 +1018,7 @@ mod test { let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::() - 50; let drain_script = ScriptBuf::default(); - OldestFirstCoinSelection::default() + OldestFirstCoinSelection .coin_select( vec![], utxos, diff --git a/nursery/coin_select/README.md b/nursery/coin_select/README.md index e688eff3e..3d3a6dece 100644 --- a/nursery/coin_select/README.md +++ b/nursery/coin_select/README.md @@ -10,8 +10,8 @@ use bdk_coin_select::{CoinSelector, Candidate, TXIN_BASE_WEIGHT}; use bitcoin::{ Transaction, TxIn }; // You should use miniscript to figure out the satisfaction weight for your coins! -const tr_satisfaction_weight: u32 = 66; -const tr_input_weight: u32 = txin_base_weight + tr_satisfaction_weight; +const TR_SATISFACTION_WEIGHT: u32 = 66; +const TR_INPUT_WEIGHT: u32 = TXIN_BASE_WEIGHT + TR_SATISFACTION_WEIGHT; let candidates = vec![ @@ -21,17 +21,17 @@ let candidates = vec![ input_count: 1, // the value of the input value: 1_000_000, - // the total weight of the input(s). This doesn't include + // the total weight of the input(s). This doesn't include weight: TR_INPUT_WEIGHT, // wether it's a segwit input. Needed so we know whether to include the segwit header // in total weight calculations. is_segwit: true }, Candidate { - // A candidate can represent multiple inputs in the case where you always want some inputs + // A candidate can represent multiple inputs in the case where you always want some inputs // to be spent together. input_count: 2, - weight: 2*tr_input_weight, + weight: 2*TR_INPUT_WEIGHT, value: 3_000_000, is_segwit: true }, @@ -50,10 +50,7 @@ let base_weight = Transaction { version: 1, }.weight().to_wu() as u32; -panic!("{}", base_weight); +println!("base weight: {}", base_weight); let mut coin_selector = CoinSelector::new(&candidates,base_weight); - - ``` - diff --git a/nursery/coin_select/src/bnb.rs b/nursery/coin_select/src/bnb.rs index 0f35c1d0b..087b7e340 100644 --- a/nursery/coin_select/src/bnb.rs +++ b/nursery/coin_select/src/bnb.rs @@ -42,13 +42,13 @@ impl<'a, M: BnBMetric> Iterator for BnbIter<'a, M> { None => return Some(None), }; - match &self.best { - Some(best_score) if score >= *best_score => Some(None), - _ => { - self.best = Some(score.clone()); - Some(Some((selector, score))) + if let Some(best_score) = &self.best { + if score >= *best_score { + return Some(None); } } + self.best = Some(score.clone()); + Some(Some((selector, score))) } } diff --git a/nursery/coin_select/src/coin_selector.rs b/nursery/coin_select/src/coin_selector.rs index 8a8a59378..dc30a01b4 100644 --- a/nursery/coin_select/src/coin_selector.rs +++ b/nursery/coin_select/src/coin_selector.rs @@ -167,28 +167,31 @@ impl<'a> CoinSelector<'a> { pub fn is_empty(&self) -> bool { self.selected.is_empty() } - /// Weight sum of all selected inputs. - pub fn selected_weight(&self) -> u32 { - self.selected - .iter() - .map(|&index| self.candidates[index].weight) - .sum() - } /// The weight of the inputs including the witness header and the varint for the number of /// inputs. - fn input_weight(&self) -> u32 { - let witness_header_extra_weight = self - .selected() - .find(|(_, wv)| wv.is_segwit) - .map(|_| 2) - .unwrap_or(0); + pub fn input_weight(&self) -> u32 { + let is_segwit_tx = self.selected().any(|(_, wv)| wv.is_segwit); + let witness_header_extra_weight = is_segwit_tx as u32 * 2; let vin_count_varint_extra_weight = { let input_count = self.selected().map(|(_, wv)| wv.input_count).sum::(); (varint_size(input_count) - 1) * 4 }; - self.selected_weight() + witness_header_extra_weight + vin_count_varint_extra_weight + let selected_weight: u32 = self + .selected() + .map(|(_, candidate)| { + let mut weight = candidate.weight; + if is_segwit_tx && !candidate.is_segwit { + // non-segwit candidates do not have the witness length field included in their + // weight field so we need to add 1 here if it's in a segwit tx. + weight += 1; + } + weight + }) + .sum(); + + selected_weight + witness_header_extra_weight + vin_count_varint_extra_weight } /// Absolute value sum of all selected inputs. @@ -202,8 +205,6 @@ impl<'a> CoinSelector<'a> { /// Current weight of template tx + selected inputs. pub fn weight(&self, drain_weight: u32) -> u32 { // TODO take into account whether drain tips over varint for number of outputs - // - // TODO: take into account the witness stack length for each input self.base_weight + self.input_weight() + drain_weight } @@ -235,8 +236,8 @@ impl<'a> CoinSelector<'a> { - target.min_fee as i64 } - /// The feerate the transaction would have if we were to use this selection of inputs to achieve - /// the ??? + /// The feerate the transaction would have if we were to use this selection of inputs to acheive + /// the `target_value` pub fn implied_feerate(&self, target_value: u64, drain: Drain) -> FeeRate { let numerator = self.selected_value() as i64 - target_value as i64 - drain.value as i64; let denom = self.weight(drain.weight); @@ -258,8 +259,8 @@ impl<'a> CoinSelector<'a> { } // /// Waste sum of all selected inputs. - fn selected_waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 { - self.selected_weight() as f32 * (feerate.spwu() - long_term_feerate.spwu()) + fn input_waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 { + self.input_weight() as f32 * (feerate.spwu() - long_term_feerate.spwu()) } /// Sorts the candidates by the comparision function. @@ -315,7 +316,7 @@ impl<'a> CoinSelector<'a> { excess_discount: f32, ) -> f32 { debug_assert!((0.0..=1.0).contains(&excess_discount)); - let mut waste = self.selected_waste(target.feerate, long_term_feerate); + let mut waste = self.input_waste(target.feerate, long_term_feerate); if drain.is_none() { // We don't allow negative excess waste since negative excess just means you haven't diff --git a/nursery/coin_select/src/lib.rs b/nursery/coin_select/src/lib.rs index 30b2db917..490749301 100644 --- a/nursery/coin_select/src/lib.rs +++ b/nursery/coin_select/src/lib.rs @@ -28,7 +28,8 @@ pub mod change_policy; /// length. pub const TXIN_BASE_WEIGHT: u32 = (32 + 4 + 4 + 1) * 4; -/// The weight of a TXOUT with a zero length `scriptPubkey` +/// The weight of a TXOUT with a zero length `scriptPubKey` +#[allow(clippy::identity_op)] pub const TXOUT_BASE_WEIGHT: u32 = // The value 4 * core::mem::size_of::() as u32 diff --git a/nursery/coin_select/src/metrics/waste.rs b/nursery/coin_select/src/metrics/waste.rs index 38366b640..d63a23eb4 100644 --- a/nursery/coin_select/src/metrics/waste.rs +++ b/nursery/coin_select/src/metrics/waste.rs @@ -3,19 +3,21 @@ use crate::{bnb::BnBMetric, float::Ordf32, Candidate, CoinSelector, Drain, FeeRa /// The "waste" metric used by bitcoin core. /// -/// See this [great -/// explanation](https://bitcoin.stackexchange.com/questions/113622/what-does-waste-metric-mean-in-the-context-of-coin-selection) for an understanding of the waste metric. +/// See this [great explanation](https://bitcoin.stackexchange.com/questions/113622/what-does-waste-metric-mean-in-the-context-of-coin-selection) +/// for an understanding of the waste metric. /// /// ## WARNING: Waste metric considered wasteful /// /// Note that bitcoin core at the time of writing use the waste metric to /// /// 1. minimise the waste while searching for changeless solutions. -/// 2. It tiebreaks multiple valid selections from different algorithms (which do not try and minimise waste) with waste. +/// 2. It tiebreaks multiple valid selections from different algorithms (which do not try and +/// minimise waste) with waste. /// -/// This is **very** different from minimising waste in general which is what this metric will do when used in [`CoinSelector::branch_and_bound`]. -/// The waste metric tends to over consolidate funds. If the `long_term_feerate` is even slightly -/// higher than the current feerate (specified in `target`) it will select all your coins! +/// This is **very** different from minimising waste in general which is what this metric will do +/// when used in [`CoinSelector::branch_and_bound`]. The waste metric tends to over consolidate +/// funds. If the `long_term_feerate` is even slightly higher than the current feerate (specified +/// in `target`) it will select all your coins! pub struct Waste<'c, C> { pub target: Target, pub long_term_feerate: FeeRate, @@ -154,7 +156,7 @@ where debug_assert!(weight_to_satisfy <= to_slurp.weight as f32); weight_to_satisfy }; - let weight_lower_bound = cs.selected_weight() as f32 + ideal_next_weight; + let weight_lower_bound = cs.input_weight() as f32 + ideal_next_weight; let mut waste = weight_lower_bound * rate_diff; waste += change_lower_bound.waste(self.target.feerate, self.long_term_feerate); diff --git a/nursery/coin_select/tests/bnb.rs b/nursery/coin_select/tests/bnb.rs index 4d9124c71..0bfe79d45 100644 --- a/nursery/coin_select/tests/bnb.rs +++ b/nursery/coin_select/tests/bnb.rs @@ -12,12 +12,17 @@ use rand::{Rng, RngCore}; fn test_wv(mut rng: impl RngCore) -> impl Iterator { core::iter::repeat_with(move || { let value = rng.gen_range(0..1_000); - Candidate { + let mut candidate = Candidate { value, weight: 100, input_count: rng.gen_range(1..2), is_segwit: rng.gen_bool(0.5), - } + }; + // HACK: set is_segwit = true for all these tests because you can't actually lower bound + // things easily with how segwit inputs interfere with their weights. We can't modify the + // above since that would change what we pull from rng. + candidate.is_segwit = true; + candidate }) } @@ -28,21 +33,21 @@ struct MinExcessThenWeight { impl BnBMetric for MinExcessThenWeight { type Score = (i64, u32); - fn score<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { + fn score(&mut self, cs: &CoinSelector<'_>) -> Option { if cs.excess(self.target, Drain::none()) < 0 { None } else { - Some((cs.excess(self.target, Drain::none()), cs.selected_weight())) + Some((cs.excess(self.target, Drain::none()), cs.input_weight())) } } - fn bound<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { + fn bound(&mut self, cs: &CoinSelector<'_>) -> Option { let lower_bound_excess = cs.excess(self.target, Drain::none()).max(0); let lower_bound_weight = { let mut cs = cs.clone(); cs.select_until_target_met(self.target, Drain::none()) .ok()?; - cs.selected_weight() + cs.input_weight() }; Some((lower_bound_excess, lower_bound_weight)) } @@ -56,13 +61,21 @@ fn bnb_finds_an_exact_solution_in_n_iter() { let num_additional_canidates = 50; let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); - let mut wv = test_wv(&mut rng); + let mut wv = test_wv(&mut rng).map(|mut candidate| { + candidate.is_segwit = true; + candidate + }); let solution: Vec = (0..solution_len).map(|_| wv.next().unwrap()).collect(); - let solution_weight = solution.iter().map(|sol| sol.weight).sum(); + let solution_weight = { + let mut cs = CoinSelector::new(&solution, 0); + cs.select_all(); + cs.input_weight() + }; + let target = solution.iter().map(|c| c.value).sum(); - let mut candidates = solution.clone(); + let mut candidates = solution; candidates.extend(wv.take(num_additional_canidates)); candidates.sort_unstable_by_key(|wv| core::cmp::Reverse(wv.value)); @@ -86,7 +99,7 @@ fn bnb_finds_an_exact_solution_in_n_iter() { assert_eq!(i, 806); - assert!(best.selected_weight() <= solution_weight); + assert!(best.input_weight() <= solution_weight); assert_eq!(best.selected_value(), target.value); } @@ -97,6 +110,7 @@ fn bnb_finds_solution_if_possible_in_n_iter() { let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); let wv = test_wv(&mut rng); let candidates = wv.take(num_inputs).collect::>(); + let cs = CoinSelector::new(&candidates, 0); let target = Target { @@ -151,13 +165,20 @@ proptest! { let mut wv = test_wv(&mut rng); let solution: Vec = (0..solution_len).map(|_| wv.next().unwrap()).collect(); + let solution_weight = { + let mut cs = CoinSelector::new(&solution, 0); + cs.select_all(); + cs.input_weight() + }; + let target = solution.iter().map(|c| c.value).sum(); - let solution_weight = solution.iter().map(|sol| sol.weight).sum(); - let mut candidates = solution.clone(); + let mut candidates = solution; candidates.extend(wv.take(num_additional_canidates)); let mut cs = CoinSelector::new(&candidates, 0); + + for i in 0..num_preselected.min(solution_len) { cs.select(i); } @@ -182,7 +203,7 @@ proptest! { - prop_assert!(best.selected_weight() <= solution_weight); + prop_assert!(best.input_weight() <= solution_weight); prop_assert_eq!(best.selected_value(), target.value); } } diff --git a/nursery/coin_select/tests/waste.rs b/nursery/coin_select/tests/waste.rs index a007c3dbc..dc0fad499 100644 --- a/nursery/coin_select/tests/waste.rs +++ b/nursery/coin_select/tests/waste.rs @@ -257,7 +257,12 @@ fn waste_low_but_non_negative_rate_diff_means_adding_more_inputs_might_reduce_ex let change_policy = change_policy::min_waste(drain, long_term_feerate); let wv = test_wv(&mut rng); - let candidates = wv.take(num_inputs).collect::>(); + let mut candidates = wv.take(num_inputs).collect::>(); + // HACK: for this test had to set segwit true to keep it working once we + // started properly accounting for legacy weight variations + candidates + .iter_mut() + .for_each(|candidate| candidate.is_segwit = true); let cs = CoinSelector::new(&candidates, base_weight); diff --git a/nursery/coin_select/tests/weight.rs b/nursery/coin_select/tests/weight.rs new file mode 100644 index 000000000..3d2e26946 --- /dev/null +++ b/nursery/coin_select/tests/weight.rs @@ -0,0 +1,165 @@ +#![allow(clippy::zero_prefixed_literal)] + +use bdk_coin_select::{Candidate, CoinSelector, Drain}; +use bitcoin::{consensus::Decodable, ScriptBuf, Transaction}; + +fn hex_val(c: u8) -> u8 { + match c { + b'A'..=b'F' => c - b'A' + 10, + b'a'..=b'f' => c - b'a' + 10, + b'0'..=b'9' => c - b'0', + _ => panic!("invalid"), + } +} + +// Appears that Transaction has no from_str so I had to roll my own hex decoder +pub fn hex_decode(hex: &str) -> Vec { + let mut bytes = Vec::with_capacity(hex.len() * 2); + for hex_byte in hex.as_bytes().chunks(2) { + bytes.push(hex_val(hex_byte[0]) << 4 | hex_val(hex_byte[1])) + } + bytes +} + +#[test] +fn segwit_one_input_one_output() { + // FROM https://mempool.space/tx/e627fbb7f775a57fd398bf9b150655d4ac3e1f8afed4255e74ee10d7a345a9cc + let mut tx_bytes = hex_decode("01000000000101b2ec00fd7d3f2c89eb27e3e280960356f69fc88a324a4bca187dd4b020aa36690000000000ffffffff01d0bb9321000000001976a9141dc94fe723f43299c6187094b1dc5a032d47b06888ac024730440220669b764de7e9dcedcba6d6d57c8c761be2acc4e1a66938ceecacaa6d494f582d02202641df89d1758eeeed84290079dd9ad36611c73cd9e381dd090b83f5e5b1422e012103f6544e4ffaff4f8649222003ada5d74bd6d960162bcd85af2b619646c8c45a5298290c00"); + let mut cursor = std::io::Cursor::new(&mut tx_bytes); + let mut tx = Transaction::consensus_decode(&mut cursor).unwrap(); + let input_values = vec![563_336_755]; + let inputs = core::mem::take(&mut tx.input); + + let candidates = inputs + .iter() + .zip(input_values) + .map(|(txin, value)| Candidate { + value, + weight: txin.segwit_weight() as u32, + input_count: 1, + is_segwit: true, + }) + .collect::>(); + + let mut coin_selector = CoinSelector::new(&candidates, tx.weight().to_wu() as u32); + coin_selector.select_all(); + + assert_eq!(coin_selector.weight(0), 449); + assert_eq!( + (coin_selector + .implied_feerate(tx.output[0].value, Drain::none()) + .as_sat_vb() + * 10.0) + .round(), + 60.2 * 10.0 + ); +} + +#[test] +fn segwit_two_inputs_one_output() { + // FROM https://mempool.space/tx/37d2883bdf1b4c110b54cb624d36ab6a30140f8710ed38a52678260a7685e708 + let mut tx_bytes = hex_decode("020000000001021edcae5160b1ba2370a45ea9342b4c883a8941274539612bddf1c379ba7ecf180700000000ffffffff5c85e19bf4f0e293c0d5f9665cb05d2a55d8bba959edc5ef02075f6a1eb9fc120100000000ffffffff0168ce3000000000001976a9145ff742d992276a1f46e5113dde7382896ff86e2a88ac0247304402202e588db55227e0c24db7f07b65f221ebcae323fb595d13d2e1c360b773d809b0022008d2f57a618bd346cfd031549a3971f22464e3e3308cee340a976f1b47a96f0b012102effbcc87e6c59b810c2fa20b0bc3eb909a20b40b25b091cf005d416b85db8c8402483045022100bdc115b86e9c863279132b4808459cf9b266c8f6a9c14a3dfd956986b807e3320220265833b85197679687c5d5eed1b2637489b34249d44cf5d2d40bc7b514181a51012102077741a668889ce15d59365886375aea47a7691941d7a0d301697edbc773b45b00000000"); + let mut cursor = std::io::Cursor::new(&mut tx_bytes); + let mut tx = Transaction::consensus_decode(&mut cursor).unwrap(); + let input_values = vec![003_194_967, 000_014_068]; + let inputs = core::mem::take(&mut tx.input); + + let candidates = inputs + .iter() + .zip(input_values) + .map(|(txin, value)| Candidate { + value, + weight: txin.segwit_weight() as u32, + input_count: 1, + is_segwit: true, + }) + .collect::>(); + + let mut coin_selector = CoinSelector::new(&candidates, tx.weight().to_wu() as u32); + coin_selector.select_all(); + + assert_eq!(coin_selector.weight(0), 721); + assert_eq!( + (coin_selector + .implied_feerate(tx.output[0].value, Drain::none()) + .as_sat_vb() + * 10.0) + .round(), + 58.1 * 10.0 + ); +} + +#[test] +fn legacy_three_inputs() { + // FROM https://mempool.space/tx/5f231df4f73694b3cca9211e336451c20dab136e0a843c2e3166cdcb093e91f4 + let mut tx_bytes = hex_decode("0100000003fe785783e14669f638ba902c26e8e3d7036fb183237bc00f8a10542191c7171300000000fdfd00004730440220418996f20477d143d02ad47e74e5949641b6c2904159ab7c592d2cfc659f9bd802205b18f18ac86b714971f84a8b74a4cb14ad5c1a5b9d0d939bb32c6ae4032f4ea10148304502210091296ff8dd87b5ebfc3d47cb82cfe4750d52c544a2b88a85970354a4d0d4b1db022069632067ee6f30f06145f649bc76d5e5d5e6404dbe985e006fcde938f778c297014c695221030502b8ade694d57a6e86998180a64f4ce993372830dc796c3d561ad8b2a504de210272b68e1c037c4630eff7ea5858640cc0748e36f5de82fb38529ef1fd0a89670d2103ba0544a3a2aa9f2314022760b78b5c833aebf6f88468a089550f93834a2886ed53aeffffffff7e048a7c53a8af656e24442c65fe4c4299b1494f6c7579fe0fd9fa741ce83e3279000000fc004730440220018fa343acccd048ed8f8f179e1b6ae27435a41b5fb2c1d96a5a772777acc6dc022074783814f2100c6fc4d4c976f941212be50825814502ca0cbe3f929db789979e0147304402206373f01b73fb09876d0f5ee3087e0614cab3be249934bc2b7eb64ee67f53dc8302200b50f8a327020172b82aaba7480c77ecf07bb32322a05f4afbc543aa97d2fde8014c69522103039d906b2494e310f6c7774c98618be552720d04781e073dd3ff25d5906f22662103d82026baa529619b103ec6341d548a7eb6d924061a8469a7416155513a3071c12102e452bc4aa726d44646ba80db70465683b30efde282a19aa35c6029ae8925df5e53aeffffffffef80f0b1cc543de4f73d59c02a3c575ae5d0af17c1e11e6be7abe3325c777507ad000000fdfd00004730440220220fee11bf836621a11a8ea9100a4600c109c13895f11468d3e2062210c5481902201c5c8a462175538e87b8248e1ed3927c3a461c66d1b46215641c875e86eb22c4014830450221008d2de8c2f20a720129c372791e595b9602b1a9bce99618497aec5266148ffc1302203a493359d700ed96323f8805ed03e909959ff0f22eff359028db6861486b1555014c6952210374a4add33567f09967592c5bcdc3db421fdbba67bac4636328f96d941da31bd221039636c2ffac90afb7499b16e265078113dfb2d77b54270e37353217c9eaeaf3052103d0bcea6d10cdd2f16018ea71572631708e26f457f67cda36a7f816a87f7791d253aeffffffff04977261000000000016001470385d054721987f41521648d7b2f5c77f735d6bee92030000000000225120d0cda1b675a0b369964cbfa381721aae3549dd2c9c6f2cf71ff67d5bc277afd3f2aaf30000000000160014ed2d41ba08313dbb2630a7106b2fedafc14aa121d4f0c70000000000220020e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec00000000"); + let mut cursor = std::io::Cursor::new(&mut tx_bytes); + let mut tx = Transaction::consensus_decode(&mut cursor).unwrap(); + let orig_weight = tx.weight(); + let input_values = vec![022_680_000, 006_558_175, 006_558_200]; + let inputs = core::mem::take(&mut tx.input); + let candidates = inputs + .iter() + .zip(input_values) + .map(|(txin, value)| Candidate { + value, + weight: txin.legacy_weight() as u32, + input_count: 1, + is_segwit: false, + }) + .collect::>(); + + let mut coin_selector = CoinSelector::new(&candidates, tx.weight().to_wu() as u32); + coin_selector.select_all(); + + assert_eq!(coin_selector.weight(0), orig_weight.to_wu() as u32); + assert_eq!( + (coin_selector + .implied_feerate(tx.output.iter().map(|o| o.value).sum(), Drain::none()) + .as_sat_vb() + * 10.0) + .round(), + 99.2 * 10.0 + ); +} + +#[test] +fn legacy_three_inputs_one_segwit() { + // FROM https://mempool.space/tx/5f231df4f73694b3cca9211e336451c20dab136e0a843c2e3166cdcb093e91f4 + // Except we change the middle input to segwit + let mut tx_bytes = hex_decode("0100000003fe785783e14669f638ba902c26e8e3d7036fb183237bc00f8a10542191c7171300000000fdfd00004730440220418996f20477d143d02ad47e74e5949641b6c2904159ab7c592d2cfc659f9bd802205b18f18ac86b714971f84a8b74a4cb14ad5c1a5b9d0d939bb32c6ae4032f4ea10148304502210091296ff8dd87b5ebfc3d47cb82cfe4750d52c544a2b88a85970354a4d0d4b1db022069632067ee6f30f06145f649bc76d5e5d5e6404dbe985e006fcde938f778c297014c695221030502b8ade694d57a6e86998180a64f4ce993372830dc796c3d561ad8b2a504de210272b68e1c037c4630eff7ea5858640cc0748e36f5de82fb38529ef1fd0a89670d2103ba0544a3a2aa9f2314022760b78b5c833aebf6f88468a089550f93834a2886ed53aeffffffff7e048a7c53a8af656e24442c65fe4c4299b1494f6c7579fe0fd9fa741ce83e3279000000fc004730440220018fa343acccd048ed8f8f179e1b6ae27435a41b5fb2c1d96a5a772777acc6dc022074783814f2100c6fc4d4c976f941212be50825814502ca0cbe3f929db789979e0147304402206373f01b73fb09876d0f5ee3087e0614cab3be249934bc2b7eb64ee67f53dc8302200b50f8a327020172b82aaba7480c77ecf07bb32322a05f4afbc543aa97d2fde8014c69522103039d906b2494e310f6c7774c98618be552720d04781e073dd3ff25d5906f22662103d82026baa529619b103ec6341d548a7eb6d924061a8469a7416155513a3071c12102e452bc4aa726d44646ba80db70465683b30efde282a19aa35c6029ae8925df5e53aeffffffffef80f0b1cc543de4f73d59c02a3c575ae5d0af17c1e11e6be7abe3325c777507ad000000fdfd00004730440220220fee11bf836621a11a8ea9100a4600c109c13895f11468d3e2062210c5481902201c5c8a462175538e87b8248e1ed3927c3a461c66d1b46215641c875e86eb22c4014830450221008d2de8c2f20a720129c372791e595b9602b1a9bce99618497aec5266148ffc1302203a493359d700ed96323f8805ed03e909959ff0f22eff359028db6861486b1555014c6952210374a4add33567f09967592c5bcdc3db421fdbba67bac4636328f96d941da31bd221039636c2ffac90afb7499b16e265078113dfb2d77b54270e37353217c9eaeaf3052103d0bcea6d10cdd2f16018ea71572631708e26f457f67cda36a7f816a87f7791d253aeffffffff04977261000000000016001470385d054721987f41521648d7b2f5c77f735d6bee92030000000000225120d0cda1b675a0b369964cbfa381721aae3549dd2c9c6f2cf71ff67d5bc277afd3f2aaf30000000000160014ed2d41ba08313dbb2630a7106b2fedafc14aa121d4f0c70000000000220020e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec00000000"); + let mut cursor = std::io::Cursor::new(&mut tx_bytes); + let mut tx = Transaction::consensus_decode(&mut cursor).unwrap(); + tx.input[1].script_sig = ScriptBuf::default(); + tx.input[1].witness = vec![ + // semi-realistic p2wpkh spend + hex_decode("3045022100bdc115b86e9c863279132b4808459cf9b266c8f6a9c14a3dfd956986b807e3320220265833b85197679687c5d5eed1b2637489b34249d44cf5d2d40bc7b514181a5101"), + hex_decode("02077741a668889ce15d59365886375aea47a7691941d7a0d301697edbc773b45b"), + ].into(); + let orig_weight = tx.weight(); + let input_values = vec![022_680_000, 006_558_175, 006_558_200]; + let inputs = core::mem::take(&mut tx.input); + let candidates = inputs + .iter() + .zip(input_values) + .enumerate() + .map(|(i, (txin, value))| { + let is_segwit = i == 1; + Candidate { + value, + weight: if is_segwit { + txin.segwit_weight() + } else { + txin.legacy_weight() + } as u32, + input_count: 1, + is_segwit, + } + }) + .collect::>(); + + let mut coin_selector = CoinSelector::new(&candidates, tx.weight().to_wu() as u32); + coin_selector.select_all(); + + assert_eq!(coin_selector.weight(0), orig_weight.to_wu() as u32); +}