Skip to content

Commit 8e6e69d

Browse files
committed
WIP
1 parent 6e2414e commit 8e6e69d

File tree

10 files changed

+742
-611
lines changed

10 files changed

+742
-611
lines changed

src/coin_control.rs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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

Comments
 (0)