diff --git a/crates/bdk/src/wallet/coin_selection.rs b/crates/bdk/src/wallet/coin_selection.rs index 6f30fd14ff..a4f30c49f0 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 e688eff3e1..3d3a6dece7 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 0f35c1d0be..087b7e3404 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 8a8a593784..dc30a01b40 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 30b2db9179..490749301a 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 38366b6402..d63a23eb4c 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 4d9124c716..0bfe79d45d 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 a007c3dbc8..dc0fad499b 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 0000000000..3d2e269464 --- /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); +}