|
| 1 | +use core::{convert::Infallible, fmt::Display}; |
| 2 | + |
| 3 | +use crate::{ |
| 4 | + collections::{BTreeMap, HashMap, HashSet}, |
| 5 | + Input, InputCandidates, InputGroup, InputStatus, |
| 6 | +}; |
| 7 | +use alloc::vec::Vec; |
| 8 | +use bdk_chain::{BlockId, ChainOracle, ConfirmationBlockTime, TxGraph}; |
| 9 | +use bitcoin::{absolute, OutPoint, Txid}; |
| 10 | +use miniscript::{bitcoin, plan::Plan}; |
| 11 | + |
| 12 | +/// Coin control. |
| 13 | +/// |
| 14 | +/// Builds the set of input candidates. |
| 15 | +/// Tries to ensure that all candidates are part of a consistent view of history. |
| 16 | +/// |
| 17 | +/// Does not check ownership of coins before placing them in candidate set. |
| 18 | +#[must_use] |
| 19 | +pub struct CoinControl<'g, C> { |
| 20 | + /// Chain Oracle. |
| 21 | + chain: &'g C, |
| 22 | + /// Consistent chain tip. |
| 23 | + chain_tip: BlockId, |
| 24 | + |
| 25 | + /// Tx graph. |
| 26 | + tx_graph: &'g TxGraph<ConfirmationBlockTime>, |
| 27 | + /// Stops the caller from adding inputs (local or foreign) that are definitely not canonical. |
| 28 | + /// |
| 29 | + /// This is not a perfect check for callers that add foreign inputs, or if the caller's |
| 30 | + /// `TxGraph` has incomplete information. However, this will stop most unintended double-spends |
| 31 | + /// and/or money-printing-txs. |
| 32 | + canonical: HashSet<Txid>, |
| 33 | + |
| 34 | + /// All candidates. |
| 35 | + candidate_inputs: HashMap<OutPoint, Input>, |
| 36 | + ///// Maintains candidate order. |
| 37 | + //pub order: VecDeque<OutPoint>, |
| 38 | + /// Excluded stuff goes here. |
| 39 | + excluded_inputs: HashMap<OutPoint, ExcludeInputReason>, |
| 40 | +} |
| 41 | + |
| 42 | +/// ExcludedReason. |
| 43 | +#[derive(Debug, Clone)] |
| 44 | +pub enum ExcludeInputReason { |
| 45 | + /// Cannot find outpoint in the graph. |
| 46 | + DoesNotExist, |
| 47 | + /// Input already spent. |
| 48 | + AlreadySpent, |
| 49 | + /// Input spends from an output that is not canonical. |
| 50 | + NotCanonical, |
| 51 | +} |
| 52 | + |
| 53 | +impl Display for ExcludeInputReason { |
| 54 | + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
| 55 | + match self { |
| 56 | + ExcludeInputReason::DoesNotExist => { |
| 57 | + write!(f, "outpoint does not exist") |
| 58 | + } |
| 59 | + ExcludeInputReason::AlreadySpent => { |
| 60 | + write!(f, "including this input is a double spend") |
| 61 | + } |
| 62 | + ExcludeInputReason::NotCanonical => { |
| 63 | + write!(f, "outpoint is in tx that is not canonical") |
| 64 | + } |
| 65 | + } |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +#[cfg(feature = "std")] |
| 70 | +impl std::error::Error for ExcludeInputReason {} |
| 71 | + |
| 72 | +impl<'g, C: ChainOracle<Error = Infallible>> CoinControl<'g, C> { |
| 73 | + /// New |
| 74 | + /// |
| 75 | + /// TODO: Replace `to_exclude` with `CanonicalizationParams` when that is available. |
| 76 | + pub fn new( |
| 77 | + tx_graph: &'g TxGraph<ConfirmationBlockTime>, |
| 78 | + chain: &'g C, |
| 79 | + chain_tip: BlockId, |
| 80 | + replace: impl IntoIterator<Item = Txid>, |
| 81 | + ) -> Self { |
| 82 | + let mut canonical = tx_graph |
| 83 | + .canonical_iter(chain, chain_tip) |
| 84 | + .map(|r| r.expect("infallible")) |
| 85 | + .map(|(txid, _, _)| txid) |
| 86 | + .collect::<HashSet<Txid>>(); |
| 87 | + let exclude = replace |
| 88 | + .into_iter() |
| 89 | + .filter_map(|txid| tx_graph.get_tx(txid)) |
| 90 | + .flat_map(|tx| { |
| 91 | + tx_graph |
| 92 | + .walk_conflicts(&tx, move |_, txid| Some(txid)) |
| 93 | + .collect::<Vec<_>>() |
| 94 | + }); |
| 95 | + for txid in exclude { |
| 96 | + canonical.remove(&txid); |
| 97 | + } |
| 98 | + Self { |
| 99 | + tx_graph, |
| 100 | + chain, |
| 101 | + canonical, |
| 102 | + chain_tip, |
| 103 | + candidate_inputs: HashMap::new(), |
| 104 | + excluded_inputs: HashMap::new(), |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + /// Try include the given input. |
| 109 | + pub fn try_include_input(&mut self, outpoint: OutPoint, plan: Plan) -> &mut Self { |
| 110 | + match self._try_include_input(outpoint, plan) { |
| 111 | + Ok(_) => self.excluded_inputs.remove(&outpoint), |
| 112 | + Err(err) => self.excluded_inputs.insert(outpoint, err), |
| 113 | + }; |
| 114 | + self |
| 115 | + } |
| 116 | + |
| 117 | + /// Try include the given inputs. |
| 118 | + pub fn try_include_inputs<I>(&mut self, inputs: I) -> &mut Self |
| 119 | + where |
| 120 | + I: IntoIterator<Item = (OutPoint, Plan)>, |
| 121 | + { |
| 122 | + for (outpoint, plan) in inputs { |
| 123 | + self.try_include_input(outpoint, plan); |
| 124 | + } |
| 125 | + self |
| 126 | + } |
| 127 | + |
| 128 | + fn _try_include_input( |
| 129 | + &mut self, |
| 130 | + outpoint: OutPoint, |
| 131 | + plan: Plan, |
| 132 | + ) -> Result<(), ExcludeInputReason> { |
| 133 | + let tx_node = self |
| 134 | + .tx_graph |
| 135 | + .get_tx_node(outpoint.txid) |
| 136 | + .ok_or(ExcludeInputReason::DoesNotExist)?; |
| 137 | + if self.canonical.contains(&tx_node.txid) { |
| 138 | + return Err(ExcludeInputReason::NotCanonical); |
| 139 | + } |
| 140 | + if self.is_spent(outpoint) { |
| 141 | + return Err(ExcludeInputReason::AlreadySpent); |
| 142 | + } |
| 143 | + |
| 144 | + let status = tx_node |
| 145 | + .anchors |
| 146 | + .iter() |
| 147 | + .find(|anchor| { |
| 148 | + self.chain |
| 149 | + .is_block_in_chain(anchor.block_id, self.chain_tip) |
| 150 | + .expect("infallible") |
| 151 | + .unwrap_or(false) |
| 152 | + }) |
| 153 | + .map(|anchor| InputStatus { |
| 154 | + height: absolute::Height::from_consensus(anchor.block_id.height) |
| 155 | + .expect("height must not overflow"), |
| 156 | + time: absolute::Time::from_consensus(anchor.confirmation_time as u32) |
| 157 | + .expect("time must not overflow"), |
| 158 | + }); |
| 159 | + |
| 160 | + let input = Input::from_prev_tx( |
| 161 | + plan, |
| 162 | + tx_node.tx, |
| 163 | + outpoint.vout.try_into().expect("u32 must fit into usize"), |
| 164 | + status, |
| 165 | + ) |
| 166 | + .map_err(|_| ExcludeInputReason::DoesNotExist)?; |
| 167 | + |
| 168 | + self.candidate_inputs.insert(outpoint, input); |
| 169 | + Ok(()) |
| 170 | + } |
| 171 | + |
| 172 | + /// Whether the outpoint is spent already. |
| 173 | + /// |
| 174 | + /// Spent outputs cannot be candidates for coin selection. |
| 175 | + fn is_spent(&self, outpoint: OutPoint) -> bool { |
| 176 | + self.tx_graph |
| 177 | + .outspends(outpoint) |
| 178 | + .iter() |
| 179 | + .any(|txid| self.canonical.contains(txid)) |
| 180 | + } |
| 181 | + |
| 182 | + /// Map of excluded inputs and their exclusion reasons. |
| 183 | + pub fn excluded(&self) -> &HashMap<OutPoint, ExcludeInputReason> { |
| 184 | + &self.excluded_inputs |
| 185 | + } |
| 186 | + |
| 187 | + /// Into candidates. |
| 188 | + pub fn into_candidates<G: Ord>( |
| 189 | + self, |
| 190 | + group_policy: impl Fn(&Input) -> G, |
| 191 | + filter_policy: impl Fn(&InputGroup) -> bool, |
| 192 | + ) -> InputCandidates { |
| 193 | + let mut group_map = BTreeMap::<G, InputGroup>::new(); |
| 194 | + for input in self.candidate_inputs.into_values() { |
| 195 | + let group_key = group_policy(&input); |
| 196 | + use std::collections::btree_map::Entry; |
| 197 | + match group_map.entry(group_key) { |
| 198 | + Entry::Vacant(entry) => { |
| 199 | + entry.insert(InputGroup::from_input(input)); |
| 200 | + } |
| 201 | + Entry::Occupied(mut entry) => entry.get_mut().push(input), |
| 202 | + }; |
| 203 | + } |
| 204 | + InputCandidates::new(None, group_map.into_values().filter(filter_policy)) |
| 205 | + } |
| 206 | +} |
| 207 | + |
| 208 | +/// Default group policy. |
| 209 | +pub fn group_by_spk() -> impl Fn(&Input) -> bitcoin::ScriptBuf { |
| 210 | + |input| input.prev_txout().script_pubkey.clone() |
| 211 | +} |
| 212 | + |
| 213 | +/// No grouping. |
| 214 | +pub fn no_grouping() -> impl Fn(&Input) -> OutPoint { |
| 215 | + |input| input.prev_outpoint() |
| 216 | +} |
| 217 | + |
| 218 | +/// Filter out inputs that cannot be spent now. |
| 219 | +pub fn filter_unspendable_now( |
| 220 | + tip_height: absolute::Height, |
| 221 | + tip_time: absolute::Time, |
| 222 | +) -> impl Fn(&InputGroup) -> bool { |
| 223 | + move |group| group.is_spendable_now(tip_height, tip_time) |
| 224 | +} |
| 225 | + |
| 226 | +/// No filtering. |
| 227 | +pub fn no_filtering() -> impl Fn(&InputGroup) -> bool { |
| 228 | + |_| true |
| 229 | +} |
0 commit comments