From 113a095d30974034d9c60cde4ad61674a1c79193 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Mon, 10 Aug 2026 17:27:20 +1000 Subject: [PATCH] fix: pay the empty-witness byte per legacy input, not per candidate `CoinSelector::input_weight` undercounted the weight of any `Candidate` representing more than one input. In a segwit transaction every input serializes a witness, and a legacy input's empty one costs 1 weight unit, but the code paid that byte once per candidate rather than once per legacy input, so a group of N legacy inputs came out N-1 short. Only single-input candidates were correct, and those were the only ones tested. A candidate already tells us `input_count` and `is_segwit`, which is enough to price this exactly, provided a group does not mix script types. That restriction is now documented: `is_segwit` describes all of a candidate's inputs rather than merely asserting one of them is segwit. It is a precondition of the same kind the type already relies on -- nothing enforces that `weight` or `is_segwit` are truthful either -- and grouping exists to spend UTXOs together, which gives no reason to straddle script types. `input_weight` then becomes pure addition: the witness marker, flag and one empty witness per legacy input are added when the selection contains a segwit spend, and nothing is added when it does not. `Candidate::weight` keeps the meaning it already had, so no existing fee calculation changes, and it stays at or below true marginal input weight in every context -- which is what `min_input_weight` promises and what both `Target::max_weight` prunes in `metrics/lowest_fee.rs` rely on to avoid discarding valid solutions. --- CHANGELOG.md | 1 + README.md | 16 +++-- src/coin_selector.rs | 66 +++++++++++++-------- tests/weight.rs | 137 +++++++++++++++++++++++++++++++++---------- 4 files changed, 161 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e5daf0..236c9ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - **Breaking:** Removed the `BnbMetric` tuple implementations (`impl BnbMetric for ((A, f32), ...)`). Weighted composition of independent metrics is no longer supported; the only composition still provided is the changeless constraint, now expressed as `Changeless`. If you relied on tuples to blend multiple objectives, there is no drop-in replacement. - **Breaking:** `CoinSelector::selected_indices` and `CoinSelector::banned` now return `&Bitset` instead of `&BTreeSet`. `Bitset` exposes `contains`/`len`/`is_empty`/`iter` (#46) - Replace the internal `Cow`/`Cow<[usize]>` selection state with a `Bitset` and an `Arc`-shared candidate order, making the per-branch clones in branch-and-bound substantially cheaper (#46) +- Fix `CoinSelector::input_weight` for candidates representing more than one input. In a segwit transaction every input serializes a witness, and a legacy input's empty one costs 1 weight unit, but that byte was added once per *candidate* rather than once per legacy *input*, undercounting a group of N legacy inputs by N-1. `Candidate::is_segwit` now applies to all of a candidate's inputs: a group must not mix script types. `Candidate::weight` is unchanged. - Fix compilation error when building with `--no-default-features` (#36) # 0.4.0 diff --git a/README.md b/README.md index 4335d4b..e6d050c 100644 --- a/README.md +++ b/README.md @@ -36,16 +36,20 @@ let candidates = vec![ input_count: 1, // the value of the input value: 1_000_000, - // the total weight of the input(s) including their witness/scriptSig - // you may need to use miniscript to figure out the correct value here. + // the total weight of the input(s): prevout, nSequence, scriptSig and, + // for segwit inputs, the witness. You may need to use miniscript to + // figure out the correct value here. Don't count the empty witness a + // legacy input serializes in a segwit transaction -- whether that + // applies depends on the rest of the selection, so it's added for you. weight: TR_KEYSPEND_TXIN_WEIGHT, - // wether it's a segwit input. Needed so we know whether to include the - // segwit header in total weight calculations. + // whether these are segwit inputs. Decides the segwit header, and how + // many empty witnesses legacy inputs owe. is_segwit: true }, Candidate { - // A candidate can represent multiple inputs in the case where you - // always want some inputs to be spent together. + // A candidate can represent multiple inputs in the case where you + // always want some inputs to be spent together. They must all be the + // same script type -- either all segwit or all legacy. input_count: 2, weight: 2*TR_KEYSPEND_TXIN_WEIGHT, value: 3_000_000, diff --git a/src/coin_selector.rs b/src/coin_selector.rs index 604abd8..5c467dd 100644 --- a/src/coin_selector.rs +++ b/src/coin_selector.rs @@ -139,29 +139,31 @@ impl<'a> CoinSelector<'a> { self.selected.is_empty() } - /// The weight of the inputs including the witness header and the varint for the number of - /// inputs. + /// The weight of the inputs, including the varint for the number of inputs and — when the + /// selection contains a segwit spend — the witness marker and flag plus the empty witness each + /// legacy input serializes. pub fn input_weight(&self) -> u64 { - let is_segwit_tx = self.selected().any(|(_, wv)| wv.is_segwit); - let witness_header_extra_weight = is_segwit_tx as u64 * 2; - let input_count = self.selected().map(|(_, wv)| wv.input_count).sum::(); let input_varint_weight = varint_size(input_count) * 4; - let selected_weight: u64 = 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(); + let selected_weight: u64 = self.selected().map(|(_, wv)| wv.weight).sum(); + + // One segwit spend anywhere makes the whole tx serialize a witness section: the marker and + // flag, plus an empty witness for every legacy input. With no segwit spend the tx has no + // witness section at all, so none of that is paid for. + let witness_weight = match self.selected().any(|(_, wv)| wv.is_segwit) { + true => { + let legacy_inputs: u64 = self + .selected() + .filter(|(_, wv)| !wv.is_segwit) + .map(|(_, wv)| wv.input_count as u64) + .sum(); + 2 + legacy_inputs + } + false => 0, + }; - input_varint_weight + selected_weight + witness_header_extra_weight + input_varint_weight + selected_weight + witness_weight } /// Absolute value sum of all selected inputs. @@ -890,17 +892,33 @@ impl std::error::Error for NoBnbSolution {} /// A `Candidate` represents an input candidate for [`CoinSelector`]. /// /// This can either be a single UTXO, or a group of UTXOs that should be spent together. +/// +/// A group must not mix script types: either every input it represents is a segwit spend or every +/// input is a legacy spend. [`input_weight`] relies on this to know how many empty witnesses a +/// group owes. +/// +/// [`input_weight`]: CoinSelector::input_weight #[derive(Debug, Clone, Copy)] pub struct Candidate { /// Total value of the UTXO(s) that this [`Candidate`] represents. pub value: u64, /// Total weight of including this/these UTXO(s). - /// `txin` fields: `prevout`, `nSequence`, `scriptSigLen`, `scriptSig`, `scriptWitnessLen`, - /// `scriptWitness` should all be included. + /// + /// Include these `txin` fields for every input: `prevout`, `nSequence`, `scriptSigLen`, + /// `scriptSig`, and — for a segwit input — `scriptWitnessLen` and `scriptWitness`. + /// + /// Do *not* include the empty witness a legacy input serializes when it lands in a segwit + /// transaction. Whether that applies depends on the rest of the selection, so + /// [`CoinSelector::input_weight`] adds it. pub weight: u64, /// Total number of inputs; so we can calculate extra `varint` weight due to `vin` len changes. pub input_count: usize, - /// Whether this [`Candidate`] contains at least one segwit spend. + /// Whether the inputs this [`Candidate`] represents are segwit spends. + /// + /// All of them, or none of them — a candidate must not mix script types. When `false`, every + /// one of its [`input_count`] inputs owes an empty witness in a segwit transaction. + /// + /// [`input_count`]: Self::input_count pub is_segwit: bool, } @@ -913,8 +931,10 @@ impl Candidate { /// Create a new [`Candidate`] that represents a single input. /// - /// `satisfaction_weight` is the weight of `scriptSigLen + scriptSig + scriptWitnessLen + - /// scriptWitness`. + /// `satisfaction_weight` is the weight of `scriptSigLen + scriptSig`, plus + /// `scriptWitnessLen + scriptWitness` for a segwit input. For a legacy input it is just the + /// `scriptSig` part — the empty witness a segwit transaction would give it is added by + /// [`CoinSelector::input_weight`], not here. pub fn new(value: u64, satisfaction_weight: u64, is_segwit: bool) -> Candidate { let weight = TXIN_BASE_WEIGHT + satisfaction_weight; Candidate { diff --git a/tests/weight.rs b/tests/weight.rs index 6a8dbb5..e6a0c22 100644 --- a/tests/weight.rs +++ b/tests/weight.rs @@ -40,11 +40,7 @@ fn segwit_one_input_one_output() { }) .collect::>(); - let target_ouputs = TargetOutputs { - value_sum: tx.output.iter().map(|output| output.value.to_sat()).sum(), - weight_sum: tx.output.iter().map(|output| output.weight().to_wu()).sum(), - n_outputs: tx.output.len(), - }; + let target_ouputs = target_outputs_of(&tx); let mut coin_selector = CoinSelector::new(&candidates); coin_selector.select_all(); @@ -108,13 +104,19 @@ fn segwit_two_inputs_one_output() { ); } +/// FROM https://mempool.space/tx/5f231df4f73694b3cca9211e336451c20dab136e0a843c2e3166cdcb093e91f4 +fn legacy_three_input_tx() -> Transaction { + let tx_bytes = hex_decode("0100000003fe785783e14669f638ba902c26e8e3d7036fb183237bc00f8a10542191c7171300000000fdfd00004730440220418996f20477d143d02ad47e74e5949641b6c2904159ab7c592d2cfc659f9bd802205b18f18ac86b714971f84a8b74a4cb14ad5c1a5b9d0d939bb32c6ae4032f4ea10148304502210091296ff8dd87b5ebfc3d47cb82cfe4750d52c544a2b88a85970354a4d0d4b1db022069632067ee6f30f06145f649bc76d5e5d5e6404dbe985e006fcde938f778c297014c695221030502b8ade694d57a6e86998180a64f4ce993372830dc796c3d561ad8b2a504de210272b68e1c037c4630eff7ea5858640cc0748e36f5de82fb38529ef1fd0a89670d2103ba0544a3a2aa9f2314022760b78b5c833aebf6f88468a089550f93834a2886ed53aeffffffff7e048a7c53a8af656e24442c65fe4c4299b1494f6c7579fe0fd9fa741ce83e3279000000fc004730440220018fa343acccd048ed8f8f179e1b6ae27435a41b5fb2c1d96a5a772777acc6dc022074783814f2100c6fc4d4c976f941212be50825814502ca0cbe3f929db789979e0147304402206373f01b73fb09876d0f5ee3087e0614cab3be249934bc2b7eb64ee67f53dc8302200b50f8a327020172b82aaba7480c77ecf07bb32322a05f4afbc543aa97d2fde8014c69522103039d906b2494e310f6c7774c98618be552720d04781e073dd3ff25d5906f22662103d82026baa529619b103ec6341d548a7eb6d924061a8469a7416155513a3071c12102e452bc4aa726d44646ba80db70465683b30efde282a19aa35c6029ae8925df5e53aeffffffffef80f0b1cc543de4f73d59c02a3c575ae5d0af17c1e11e6be7abe3325c777507ad000000fdfd00004730440220220fee11bf836621a11a8ea9100a4600c109c13895f11468d3e2062210c5481902201c5c8a462175538e87b8248e1ed3927c3a461c66d1b46215641c875e86eb22c4014830450221008d2de8c2f20a720129c372791e595b9602b1a9bce99618497aec5266148ffc1302203a493359d700ed96323f8805ed03e909959ff0f22eff359028db6861486b1555014c6952210374a4add33567f09967592c5bcdc3db421fdbba67bac4636328f96d941da31bd221039636c2ffac90afb7499b16e265078113dfb2d77b54270e37353217c9eaeaf3052103d0bcea6d10cdd2f16018ea71572631708e26f457f67cda36a7f816a87f7791d253aeffffffff04977261000000000016001470385d054721987f41521648d7b2f5c77f735d6bee92030000000000225120d0cda1b675a0b369964cbfa381721aae3549dd2c9c6f2cf71ff67d5bc277afd3f2aaf30000000000160014ed2d41ba08313dbb2630a7106b2fedafc14aa121d4f0c70000000000220020e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec00000000"); + Transaction::consensus_decode(&mut tx_bytes.as_slice()).unwrap() +} + +/// Input values of [`legacy_three_input_tx`], in input order. +const LEGACY_TX_INPUT_VALUES: [u64; 3] = [022_680_000, 006_558_175, 006_558_200]; + #[test] fn legacy_three_inputs() { - // FROM https://mempool.space/tx/5f231df4f73694b3cca9211e336451c20dab136e0a843c2e3166cdcb093e91f4 - let tx_bytes = hex_decode("0100000003fe785783e14669f638ba902c26e8e3d7036fb183237bc00f8a10542191c7171300000000fdfd00004730440220418996f20477d143d02ad47e74e5949641b6c2904159ab7c592d2cfc659f9bd802205b18f18ac86b714971f84a8b74a4cb14ad5c1a5b9d0d939bb32c6ae4032f4ea10148304502210091296ff8dd87b5ebfc3d47cb82cfe4750d52c544a2b88a85970354a4d0d4b1db022069632067ee6f30f06145f649bc76d5e5d5e6404dbe985e006fcde938f778c297014c695221030502b8ade694d57a6e86998180a64f4ce993372830dc796c3d561ad8b2a504de210272b68e1c037c4630eff7ea5858640cc0748e36f5de82fb38529ef1fd0a89670d2103ba0544a3a2aa9f2314022760b78b5c833aebf6f88468a089550f93834a2886ed53aeffffffff7e048a7c53a8af656e24442c65fe4c4299b1494f6c7579fe0fd9fa741ce83e3279000000fc004730440220018fa343acccd048ed8f8f179e1b6ae27435a41b5fb2c1d96a5a772777acc6dc022074783814f2100c6fc4d4c976f941212be50825814502ca0cbe3f929db789979e0147304402206373f01b73fb09876d0f5ee3087e0614cab3be249934bc2b7eb64ee67f53dc8302200b50f8a327020172b82aaba7480c77ecf07bb32322a05f4afbc543aa97d2fde8014c69522103039d906b2494e310f6c7774c98618be552720d04781e073dd3ff25d5906f22662103d82026baa529619b103ec6341d548a7eb6d924061a8469a7416155513a3071c12102e452bc4aa726d44646ba80db70465683b30efde282a19aa35c6029ae8925df5e53aeffffffffef80f0b1cc543de4f73d59c02a3c575ae5d0af17c1e11e6be7abe3325c777507ad000000fdfd00004730440220220fee11bf836621a11a8ea9100a4600c109c13895f11468d3e2062210c5481902201c5c8a462175538e87b8248e1ed3927c3a461c66d1b46215641c875e86eb22c4014830450221008d2de8c2f20a720129c372791e595b9602b1a9bce99618497aec5266148ffc1302203a493359d700ed96323f8805ed03e909959ff0f22eff359028db6861486b1555014c6952210374a4add33567f09967592c5bcdc3db421fdbba67bac4636328f96d941da31bd221039636c2ffac90afb7499b16e265078113dfb2d77b54270e37353217c9eaeaf3052103d0bcea6d10cdd2f16018ea71572631708e26f457f67cda36a7f816a87f7791d253aeffffffff04977261000000000016001470385d054721987f41521648d7b2f5c77f735d6bee92030000000000225120d0cda1b675a0b369964cbfa381721aae3549dd2c9c6f2cf71ff67d5bc277afd3f2aaf30000000000160014ed2d41ba08313dbb2630a7106b2fedafc14aa121d4f0c70000000000220020e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec00000000"); - let tx = Transaction::consensus_decode(&mut tx_bytes.as_slice()).unwrap(); - let orig_weight = tx.weight(); - let input_values = vec![022_680_000, 006_558_175, 006_558_200]; + let tx = legacy_three_input_tx(); + let input_values = LEGACY_TX_INPUT_VALUES; let candidates = tx .input .iter() @@ -127,18 +129,14 @@ fn legacy_three_inputs() { }) .collect::>(); - let target_ouputs = TargetOutputs { - value_sum: tx.output.iter().map(|output| output.value.to_sat()).sum(), - weight_sum: tx.output.iter().map(|output| output.weight().to_wu()).sum(), - n_outputs: tx.output.len(), - }; + let target_ouputs = target_outputs_of(&tx); let mut coin_selector = CoinSelector::new(&candidates); coin_selector.select_all(); assert_eq!( coin_selector.weight(target_ouputs, DrainWeights::NONE), - orig_weight.to_wu() + tx.weight().to_wu() ); assert_eq!( (coin_selector @@ -151,10 +149,9 @@ fn legacy_three_inputs() { ); } -#[test] -fn legacy_three_inputs_one_segwit() { - // FROM https://mempool.space/tx/5f231df4f73694b3cca9211e336451c20dab136e0a843c2e3166cdcb093e91f4 - // Except we change the middle input to segwit +/// FROM https://mempool.space/tx/5f231df4f73694b3cca9211e336451c20dab136e0a843c2e3166cdcb093e91f4 +/// Except we change the middle input to segwit +fn legacy_tx_with_middle_input_segwit() -> Transaction { let tx_bytes = hex_decode("0100000003fe785783e14669f638ba902c26e8e3d7036fb183237bc00f8a10542191c7171300000000fdfd00004730440220418996f20477d143d02ad47e74e5949641b6c2904159ab7c592d2cfc659f9bd802205b18f18ac86b714971f84a8b74a4cb14ad5c1a5b9d0d939bb32c6ae4032f4ea10148304502210091296ff8dd87b5ebfc3d47cb82cfe4750d52c544a2b88a85970354a4d0d4b1db022069632067ee6f30f06145f649bc76d5e5d5e6404dbe985e006fcde938f778c297014c695221030502b8ade694d57a6e86998180a64f4ce993372830dc796c3d561ad8b2a504de210272b68e1c037c4630eff7ea5858640cc0748e36f5de82fb38529ef1fd0a89670d2103ba0544a3a2aa9f2314022760b78b5c833aebf6f88468a089550f93834a2886ed53aeffffffff7e048a7c53a8af656e24442c65fe4c4299b1494f6c7579fe0fd9fa741ce83e3279000000fc004730440220018fa343acccd048ed8f8f179e1b6ae27435a41b5fb2c1d96a5a772777acc6dc022074783814f2100c6fc4d4c976f941212be50825814502ca0cbe3f929db789979e0147304402206373f01b73fb09876d0f5ee3087e0614cab3be249934bc2b7eb64ee67f53dc8302200b50f8a327020172b82aaba7480c77ecf07bb32322a05f4afbc543aa97d2fde8014c69522103039d906b2494e310f6c7774c98618be552720d04781e073dd3ff25d5906f22662103d82026baa529619b103ec6341d548a7eb6d924061a8469a7416155513a3071c12102e452bc4aa726d44646ba80db70465683b30efde282a19aa35c6029ae8925df5e53aeffffffffef80f0b1cc543de4f73d59c02a3c575ae5d0af17c1e11e6be7abe3325c777507ad000000fdfd00004730440220220fee11bf836621a11a8ea9100a4600c109c13895f11468d3e2062210c5481902201c5c8a462175538e87b8248e1ed3927c3a461c66d1b46215641c875e86eb22c4014830450221008d2de8c2f20a720129c372791e595b9602b1a9bce99618497aec5266148ffc1302203a493359d700ed96323f8805ed03e909959ff0f22eff359028db6861486b1555014c6952210374a4add33567f09967592c5bcdc3db421fdbba67bac4636328f96d941da31bd221039636c2ffac90afb7499b16e265078113dfb2d77b54270e37353217c9eaeaf3052103d0bcea6d10cdd2f16018ea71572631708e26f457f67cda36a7f816a87f7791d253aeffffffff04977261000000000016001470385d054721987f41521648d7b2f5c77f735d6bee92030000000000225120d0cda1b675a0b369964cbfa381721aae3549dd2c9c6f2cf71ff67d5bc277afd3f2aaf30000000000160014ed2d41ba08313dbb2630a7106b2fedafc14aa121d4f0c70000000000220020e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec00000000"); let mut tx = Transaction::consensus_decode(&mut tx_bytes.as_slice()).unwrap(); tx.input[1].script_sig = ScriptBuf::default(); @@ -163,7 +160,24 @@ fn legacy_three_inputs_one_segwit() { hex_decode("3045022100bdc115b86e9c863279132b4808459cf9b266c8f6a9c14a3dfd956986b807e3320220265833b85197679687c5d5eed1b2637489b34249d44cf5d2d40bc7b514181a5101"), hex_decode("02077741a668889ce15d59365886375aea47a7691941d7a0d301697edbc773b45b"), ].into(); - let input_values = vec![022_680_000, 006_558_175, 006_558_200]; + tx +} + +/// Input values of [`legacy_tx_with_middle_input_segwit`], in input order. +const MIXED_TX_INPUT_VALUES: [u64; 3] = [022_680_000, 006_558_175, 006_558_200]; + +fn target_outputs_of(tx: &Transaction) -> TargetOutputs { + TargetOutputs { + value_sum: tx.output.iter().map(|output| output.value.to_sat()).sum(), + weight_sum: tx.output.iter().map(|output| output.weight().to_wu()).sum(), + n_outputs: tx.output.len(), + } +} + +#[test] +fn legacy_three_inputs_one_segwit() { + let tx = legacy_tx_with_middle_input_segwit(); + let input_values = MIXED_TX_INPUT_VALUES; let candidates = tx .input .iter() @@ -174,22 +188,17 @@ fn legacy_three_inputs_one_segwit() { Candidate { value, weight: if is_segwit { - txin.segwit_weight() + txin.segwit_weight().to_wu() } else { - txin.legacy_weight() - } - .to_wu(), + txin.legacy_weight().to_wu() + }, input_count: 1, is_segwit, } }) .collect::>(); - let target_ouputs = TargetOutputs { - value_sum: tx.output.iter().map(|output| output.value.to_sat()).sum(), - weight_sum: tx.output.iter().map(|output| output.weight().to_wu()).sum(), - n_outputs: tx.output.len(), - }; + let target_ouputs = target_outputs_of(&tx); let mut coin_selector = CoinSelector::new(&candidates); coin_selector.select_all(); @@ -200,6 +209,74 @@ fn legacy_three_inputs_one_segwit() { ); } +/// How inputs are grouped into candidates must not change the weight of the transaction the +/// selection implies. Each grouping below covers the same three inputs as +/// `legacy_three_inputs_one_segwit` does one-per-candidate. +#[test] +fn grouping_inputs_does_not_change_weight() { + let tx = legacy_tx_with_middle_input_segwit(); + let [v0, v1, v2] = MIXED_TX_INPUT_VALUES; + // Inputs 0 and 2 are legacy, input 1 is segwit. + let (l0, sw1, l2) = ( + tx.input[0].legacy_weight().to_wu(), + tx.input[1].segwit_weight().to_wu(), + tx.input[2].legacy_weight().to_wu(), + ); + + // The two legacy inputs share a candidate. + let legacy_grouped = [ + Candidate { + value: v0 + v2, + weight: l0 + l2, + input_count: 2, + is_segwit: false, + }, + Candidate { + value: v1, + weight: sw1, + input_count: 1, + is_segwit: true, + }, + ]; + + let target_ouputs = target_outputs_of(&tx); + for candidates in [&legacy_grouped[..]] { + let mut coin_selector = CoinSelector::new(candidates); + coin_selector.select_all(); + + assert_eq!( + coin_selector.weight(target_ouputs, DrainWeights::NONE), + tx.weight().to_wu() + ); + } +} + +/// A legacy transaction whose inputs are all carried by one grouped candidate. Guards the +/// no-witness-section path: with no segwit spend anywhere there is no marker, no flag and no +/// per-input empty witness, however the inputs are grouped. +#[test] +fn grouped_legacy_inputs_weigh_as_a_legacy_tx() { + let tx = legacy_three_input_tx(); + let candidates = [Candidate { + value: LEGACY_TX_INPUT_VALUES.iter().sum(), + weight: tx + .input + .iter() + .map(|txin| txin.legacy_weight().to_wu()) + .sum(), + input_count: 3, + is_segwit: false, + }]; + + let mut coin_selector = CoinSelector::new(&candidates); + coin_selector.select_all(); + + assert_eq!( + coin_selector.weight(target_outputs_of(&tx), DrainWeights::NONE), + tx.weight().to_wu() + ); +} + #[test] fn new_tr_keyspend_correct_weight() { // FROM https://mempool.space/tx/4936a1a4ea1a0085b9dc2a1d5b59d361f5b1b41241772f3e465153712b6d8dc0