Skip to content

Commit e24482e

Browse files
committed
WIP: Removed bdk_chain dependency
This now makes it more involved to interface this with `bdk_chain` and `bdk_wallet`. However, I think it is worth it? `CanonicalUnspents` is our "single source of truth" and all UTXOs added to the selector should really pass through it to ensure we don't create transactions that double-spend themselves.
1 parent 030018a commit e24482e

File tree

9 files changed

+619
-564
lines changed

9 files changed

+619
-564
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ readme = "README.md"
1111
[dependencies]
1212
miniscript = { version = "12", default-features = false }
1313
bdk_coin_select = "0.4.0"
14-
bdk_chain = { version = "0.21" }
1514

1615
[dev-dependencies]
1716
anyhow = "1"
1817
bdk_tx = { path = "." }
1918
bitcoin = { version = "0.32", features = ["rand-std"] }
2019
bdk_testenv = "0.11.1"
2120
bdk_bitcoind_rpc = "0.18.0"
21+
bdk_chain = { version = "0.21" }
2222

2323
[features]
2424
default = ["std"]

src/canonical_unspents.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
use alloc::vec::Vec;
2+
3+
use alloc::sync::Arc;
4+
5+
use bitcoin::{psbt, OutPoint, Sequence, Transaction, TxOut, Txid};
6+
use miniscript::{bitcoin, plan::Plan};
7+
8+
use crate::{collections::HashMap, Input, InputStatus, RbfSet};
9+
10+
/// Tx with confirmation status.
11+
pub type TxWithStatus<T> = (T, Option<InputStatus>);
12+
13+
/// Our canonical view of unspent outputs.
14+
#[derive(Debug, Clone)]
15+
pub struct CanonicalUnspents {
16+
txs: HashMap<Txid, Arc<Transaction>>,
17+
statuses: HashMap<Txid, InputStatus>,
18+
spends: HashMap<OutPoint, Txid>,
19+
}
20+
21+
impl CanonicalUnspents {
22+
/// Construct.
23+
pub fn new<T>(canonical_txs: impl IntoIterator<Item = TxWithStatus<T>>) -> Self
24+
where
25+
T: Into<Arc<Transaction>>,
26+
{
27+
let mut txs = HashMap::new();
28+
let mut statuses = HashMap::new();
29+
let mut spends = HashMap::new();
30+
for (tx, status) in canonical_txs {
31+
let tx: Arc<Transaction> = tx.into();
32+
let txid = tx.compute_txid();
33+
spends.extend(tx.input.iter().map(|txin| (txin.previous_output, txid)));
34+
txs.insert(txid, tx);
35+
if let Some(status) = status {
36+
statuses.insert(txid, status);
37+
}
38+
}
39+
Self {
40+
txs,
41+
statuses,
42+
spends,
43+
}
44+
}
45+
46+
/// TODO: This should return a descriptive error on why it failed.
47+
/// TODO: Error if trying to replace coinbase.
48+
pub fn extract_replacements(
49+
&mut self,
50+
replace: impl IntoIterator<Item = Txid>,
51+
) -> Option<RbfSet> {
52+
let mut rbf_txs = replace
53+
.into_iter()
54+
.map(|txid| self.txs.get(&txid).cloned().map(|tx| (txid, tx)))
55+
.collect::<Option<HashMap<Txid, _>>>()?;
56+
57+
// Remove txs in this set which have ancestors of other members of this set.
58+
let mut to_remove_from_rbf_txs = Vec::<Txid>::new();
59+
let mut to_remove_stack = rbf_txs
60+
.iter()
61+
.map(|(txid, tx)| (*txid, tx.clone()))
62+
.collect::<Vec<_>>();
63+
while let Some((txid, tx)) = to_remove_stack.pop() {
64+
if to_remove_from_rbf_txs.contains(&txid) {
65+
continue;
66+
}
67+
for vout in 0..tx.output.len() as u32 {
68+
let op = OutPoint::new(txid, vout);
69+
if let Some(next_txid) = self.spends.get(&op) {
70+
if let Some(next_tx) = self.txs.get(next_txid) {
71+
to_remove_from_rbf_txs.push(*next_txid);
72+
to_remove_stack.push((*next_txid, next_tx.clone()));
73+
}
74+
}
75+
}
76+
}
77+
for txid in &to_remove_from_rbf_txs {
78+
rbf_txs.remove(txid);
79+
}
80+
81+
// Find prev outputs of all txs in the set.
82+
// Fail when on prev output is not found. We need to use the prevouts to determine fee fr
83+
// rbf!
84+
let prev_txouts = rbf_txs
85+
.values()
86+
.flat_map(|tx| &tx.input)
87+
.map(|txin| txin.previous_output)
88+
.map(|op| -> Option<(OutPoint, TxOut)> {
89+
let txout = self
90+
.txs
91+
.get(&op.txid)
92+
.and_then(|tx| tx.output.get(op.vout as usize))
93+
.cloned()?;
94+
Some((op, txout))
95+
})
96+
.collect::<Option<HashMap<_, _>>>()?;
97+
98+
// Remove rbf txs (and their descendants) from canoncial unspents.
99+
let to_remove_from_canoncial_unspents = rbf_txs.keys().chain(&to_remove_from_rbf_txs);
100+
for txid in to_remove_from_canoncial_unspents {
101+
if let Some(tx) = self.txs.remove(txid) {
102+
self.statuses.remove(txid);
103+
for txin in &tx.input {
104+
self.spends.remove(&txin.previous_output);
105+
}
106+
}
107+
}
108+
109+
RbfSet::new(rbf_txs.into_values(), prev_txouts)
110+
}
111+
112+
/// Whether outpoint is a leaf (unspent).
113+
pub fn is_unspent(&self, outpoint: OutPoint) -> bool {
114+
if self.spends.contains_key(&outpoint) {
115+
return false;
116+
}
117+
match self.txs.get(&outpoint.txid) {
118+
Some(tx) => {
119+
let vout: usize = outpoint.vout.try_into().expect("vout must fit into usize");
120+
vout < tx.output.len()
121+
}
122+
None => false,
123+
}
124+
}
125+
126+
/// Try get leaf (unspent) of given `outpoint`.
127+
pub fn try_get_unspent(&self, outpoint: OutPoint, plan: Plan) -> Option<Input> {
128+
if self.spends.contains_key(&outpoint) {
129+
return None;
130+
}
131+
let prev_tx = Arc::clone(self.txs.get(&outpoint.txid)?);
132+
Input::from_prev_tx(
133+
plan,
134+
prev_tx,
135+
outpoint.vout.try_into().expect("vout must fit into usize"),
136+
self.statuses.get(&outpoint.txid).cloned(),
137+
)
138+
.ok()
139+
}
140+
141+
/// Try get leaves of given `outpoints`.
142+
pub fn try_get_unspents<'a, O>(&'a self, outpoints: O) -> impl Iterator<Item = Input> + 'a
143+
where
144+
O: IntoIterator<Item = (OutPoint, Plan)>,
145+
O::IntoIter: 'a,
146+
{
147+
outpoints
148+
.into_iter()
149+
.filter_map(|(op, plan)| self.try_get_unspent(op, plan))
150+
}
151+
152+
/// Try get foreign leaf.
153+
/// TODO: Check psbt_input data with our own prev tx data.
154+
/// TODO: Create `try_get_foreign_leaves` method.
155+
pub fn try_get_foreign_unspent(
156+
&self,
157+
outpoint: OutPoint,
158+
sequence: Sequence,
159+
psbt_input: psbt::Input,
160+
satisfaction_weight: usize,
161+
) -> Option<Input> {
162+
if self.spends.contains_key(&outpoint) {
163+
return None;
164+
}
165+
let prev_tx = Arc::clone(self.txs.get(&outpoint.txid)?);
166+
let output_index: usize = outpoint.vout.try_into().expect("vout must fit into usize");
167+
let _txout = prev_tx.output.get(output_index)?;
168+
let status = self.statuses.get(&outpoint.txid).cloned();
169+
Input::from_psbt_input(outpoint, sequence, psbt_input, satisfaction_weight, status)
170+
}
171+
}

0 commit comments

Comments
 (0)