Skip to content

Commit 6b1816a

Browse files
committed
Merge #1: The new TxBuilder
c806d5e chore(ci): Make `pin-msrv.sh` exec (志宇) 22ebcfe refactor: Rename `InputStatus` to `TxStatus` (志宇) 0e4a4a6 fix(finalizer): return true if already finalized in `finalize_input` (valued mammal) ad78206 feat: Add `Selector::select_with_algorithm` (valued mammal) 866f881 feat!: Improve error handling (valued mammal) 1124439 ci: fix typo in rust.yml (valued mammal) 60fa9aa chore: Add examples directory (valued mammal) 386bdae ci: update for rust 1.63.0 (valued mammal) 9bdd0b1 feat!: Remove `bdk_chain` dependency (志宇) 4250de1 feat!: More progress (志宇) 3a5a67e feat!: Replace `PsbtUpdater` and `Builder` with `create_psbt` method (志宇) Pull request description: ## Rationale There were many problems with the design of the old `TxBuilder` currently residing in `bdk_wallet`. * **No separation of concerns** - All options are crammed into a single `TxParams` struct. * Some combinations of parameters are hard to reason about, or may even create invalid transactions. * It's difficult to maintain. Adding new features tends to exacerbate the issue by introducing even more variants. * **No planning module** - The planning module can help pick the best spending branch just by specifying available assets. * **Inaccurate weight calculations** - Coin selection does not currently account for varints of inputs/outputs. * "Policies" are applied to all transactions, irrelevant of which inputs were actually chosen. * Cannot accurately determine when relatively-locked outputs are spendable. * **The "no breaking changes" constraint** This made it difficult to introduce new features to the existing codebase without introducing more footguns. * **Coupled to wallet** - Limits flexibility. Rewriting the `TxBuilder` and decoupling it from `bdk_wallet` allows us to: * Make it usable independently of `bdk_wallet`. * Fix all the problems mentioned above cleanly. * Avoid waiting for `bdk_wallet v2.0` to ship these fixes/improvements. ## Proposed Repository Layout I propose turning this repo into a Cargo workspace, containing the following crates: * `bdk_tx_core` - This will contain the core `Input`, `InputGroup` types (and maybe more). Both `bdk_chain` and `bdk_wallet` would only need to output these types to interoperate with the rest of the tx-building crates. These types are expected to be relatively stable, with most changes being non-breaking. * `bdk_tx` - Handles the coin selection + psbt creation/signing/finalization. @nymius suggested splitting coin selection into its own crate, but I'm not fully convinced that's necessary (for now). * `bdk_coin_select` - It would be convenient to move this crate into the workspace for easier development. * `bdk_coin_control` - Contains `CoinControl`. This is the only crate in the workspace that depends on `bdk_chain` and `bdk_core`. It handles canonicalization and ensures the caller does not provide a bad set of input candidates (cc. @stevenroose). ## Where to Start Reviewing? Start with `tests/synopsis.rs` - For now, it's more of an example than a test. It includes: * A basic create-transaction example. * A more involved (but well-commented) cancel-transaction example. These examples demonstrate how the various types in the library interact across the different stages of transaction building. ## How can I help? The main architecture/flow is complete. However, there are still a whole bunch of TODOs scattered over the codebase and should be done before merging. These TODOs be summarized as follows (these should be done before merging): - [x] Fix up error types. Some methods return `Option`s when they should return custom error enums with variants. It is helpful to know why something failed. - [x] Documentation. Things should be better documented. evanlinjin#4 - [x] Examples. The `tests/synopsis.rs` should be moves to a `examples` folder. The `Wallet` type should be contained in `examples/common/mod.rs` so that it can be shared across examples. evanlinjin#5 - [x] Update README. This is enough to get a merge and get users to start using/testing. To help: * Review the codebase. * Add PRs addressing various TODO comments. The PRs should be made to my fork (`evanlinjin/bdk-tx`). ## How long would it take for this to be stable? Without any hold-backs, and multiple contributors, I think 2 months is a reasonable time. ACKs for top commit: evanlinjin: self-ACK c806d5e Tree-SHA512: a37fdd2683ed1967912ef2f2828d40a0939d956d8b4372591fcfa3e8e5d458bf478706345ae988d5966efcbf557f1396e4bb8f6f98c0dedc891b5cb790c97e77
2 parents 686bdb6 + c806d5e commit 6b1816a

File tree

18 files changed

+2999
-1390
lines changed

18 files changed

+2999
-1390
lines changed

.github/workflows/rust.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ jobs:
1414
strategy:
1515
matrix:
1616
rust:
17-
- toolchain: stable
18-
- toolchain: 1.63.0
17+
- version: stable
18+
- version: 1.63.0
1919
steps:
2020
- uses: actions/checkout@v4
2121
- name: Install Rust
2222
uses: dtolnay/rust-toolchain@v1
2323
with:
24-
toolchain: ${{ matrix.rust.toolchain }}
24+
toolchain: ${{ matrix.rust.version }}
25+
- name: Pin dependencies for MSRV
26+
if: matrix.rust.version == '1.63.0'
27+
run: ./ci/pin-msrv.sh
2528
- name: Test
2629
run: cargo test --no-fail-fast --all-features
2730

Cargo.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,23 @@ readme = "README.md"
1010

1111
[dependencies]
1212
miniscript = { version = "12", default-features = false }
13+
bdk_coin_select = "0.4.0"
1314

1415
[dev-dependencies]
1516
anyhow = "1"
16-
bdk_chain = { version = "0.21" }
1717
bdk_tx = { path = "." }
1818
bitcoin = { version = "0.32", features = ["rand-std"] }
19+
bdk_testenv = "0.11.1"
20+
bdk_bitcoind_rpc = "0.18.0"
21+
bdk_chain = { version = "0.21" }
1922

2023
[features]
2124
default = ["std"]
2225
std = ["miniscript/std"]
26+
27+
[[example]]
28+
name = "synopsis"
29+
30+
[[example]]
31+
name = "common"
32+
crate-type = ["lib"]

README.md

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# `bdk-tx`
1+
# `bdk_tx`
22

33
This is a transaction building library based on `rust-miniscript` that lets you build, update, and finalize PSBTs with minimal dependencies.
44

@@ -7,26 +7,9 @@ Because the project builds upon [miniscript] we support [descriptors] natively.
77

88
Refer to [BIP174], [BIP370], and [BIP371] to learn more about partially signed bitcoin transactions (PSBT).
99

10-
## Example
10+
**Note:**
11+
The library is unstable and API changes should be expected. Check the [examples] directory for detailed usage examples.
1112

12-
To get started see the `DataProvider` trait and the methods for adding inputs and outputs.
13-
14-
```rust
15-
use bdk_tx::Builder;
16-
use bdk_tx::DataProvider;
17-
18-
impl DataProvider for MyType { ... }
19-
20-
let mut builder = Builder::new();
21-
builder.add_input(plan_utxo);
22-
builder.add_output(script_pubkey, amount);
23-
let (mut psbt, finalizer) = builder.build_tx(data_provider)?;
24-
25-
// Your PSBT signing flow...
26-
27-
let result = finalizer.finalize(&mut psbt)?;
28-
assert!(result.is_finalized());
29-
```
3013

3114
## Contributing
3215
Found a bug, have an issue or a feature request? Feel free to open an issue on GitHub. This library is open source licensed under MIT.
@@ -36,3 +19,4 @@ Found a bug, have an issue or a feature request? Feel free to open an issue on G
3619
[BIP174]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
3720
[BIP370]: https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki
3821
[BIP371]: https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki
22+
[examples]: ./examples

ci/pin-msrv.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/bash
2+
3+
set -x
4+
set -euo pipefail
5+
6+
# Script to pin dependencies for MSRV
7+
8+
# cargo clean
9+
10+
# rm -f Cargo.lock
11+
12+
# rustup default 1.63.0
13+
14+
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
15+
cargo update -p time --precise "0.3.20"
16+
cargo update -p home --precise "0.5.5"
17+
cargo update -p flate2 --precise "1.0.35"
18+
cargo update -p once_cell --precise "1.20.3"
19+
cargo update -p bzip2-sys --precise "0.1.12"
20+
cargo update -p ring --precise "0.17.12"
21+
cargo update -p once_cell --precise "1.20.3"
22+
cargo update -p base64ct --precise "1.6.0"
23+
cargo update -p minreq --precise "2.13.2"

examples/common.rs

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
use std::sync::Arc;
2+
3+
use bdk_bitcoind_rpc::Emitter;
4+
use bdk_chain::{bdk_core, Anchor, Balance, ChainPosition, ConfirmationBlockTime};
5+
use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv};
6+
use bdk_tx::{CanonicalUnspents, Input, InputCandidates, RbfParams, TxStatus, TxWithStatus};
7+
use bitcoin::{absolute, Address, BlockHash, OutPoint, Transaction, Txid};
8+
use miniscript::{
9+
plan::{Assets, Plan},
10+
Descriptor, DescriptorPublicKey, ForEachKey,
11+
};
12+
13+
const EXTERNAL: &str = "external";
14+
const INTERNAL: &str = "internal";
15+
16+
pub struct Wallet {
17+
pub chain: bdk_chain::local_chain::LocalChain,
18+
pub graph: bdk_chain::IndexedTxGraph<
19+
bdk_core::ConfirmationBlockTime,
20+
bdk_chain::keychain_txout::KeychainTxOutIndex<&'static str>,
21+
>,
22+
}
23+
24+
impl Wallet {
25+
pub fn new(
26+
genesis_hash: BlockHash,
27+
external: Descriptor<DescriptorPublicKey>,
28+
internal: Descriptor<DescriptorPublicKey>,
29+
) -> anyhow::Result<Self> {
30+
let mut indexer = bdk_chain::keychain_txout::KeychainTxOutIndex::default();
31+
indexer.insert_descriptor(EXTERNAL, external)?;
32+
indexer.insert_descriptor(INTERNAL, internal)?;
33+
let graph = bdk_chain::IndexedTxGraph::new(indexer);
34+
let (chain, _) = bdk_chain::local_chain::LocalChain::from_genesis_hash(genesis_hash);
35+
Ok(Self { chain, graph })
36+
}
37+
38+
pub fn sync(&mut self, env: &TestEnv) -> anyhow::Result<()> {
39+
let client = env.rpc_client();
40+
let last_cp = self.chain.tip();
41+
let mut emitter = Emitter::new(client, last_cp, 0);
42+
while let Some(event) = emitter.next_block()? {
43+
let _ = self
44+
.graph
45+
.apply_block_relevant(&event.block, event.block_height());
46+
let _ = self.chain.apply_update(event.checkpoint);
47+
}
48+
let mempool = emitter.mempool()?;
49+
let _ = self.graph.batch_insert_relevant_unconfirmed(mempool);
50+
Ok(())
51+
}
52+
53+
pub fn next_address(&mut self) -> Option<Address> {
54+
let ((_, spk), _) = self.graph.index.next_unused_spk(EXTERNAL)?;
55+
Address::from_script(&spk, bitcoin::consensus::Params::REGTEST).ok()
56+
}
57+
58+
pub fn balance(&self) -> Balance {
59+
let outpoints = self.graph.index.outpoints().clone();
60+
self.graph.graph().balance(
61+
&self.chain,
62+
self.chain.tip().block_id(),
63+
outpoints,
64+
|_, _| true,
65+
)
66+
}
67+
68+
/// TODO: Add to chain sources.
69+
pub fn tip_info(
70+
&self,
71+
client: &impl RpcApi,
72+
) -> anyhow::Result<(absolute::Height, absolute::Time)> {
73+
let tip = self.chain.tip().block_id();
74+
let tip_info = client.get_block_header_info(&tip.hash)?;
75+
let tip_height = absolute::Height::from_consensus(tip.height)?;
76+
let tip_time =
77+
absolute::Time::from_consensus(tip_info.median_time.unwrap_or(tip_info.time) as _)?;
78+
Ok((tip_height, tip_time))
79+
}
80+
81+
// TODO: Maybe create an `AssetsBuilder` or `AssetsExt` that makes it easier to add
82+
// assets from descriptors, etc.
83+
pub fn assets(&self) -> Assets {
84+
let index = &self.graph.index;
85+
let tip = self.chain.tip().block_id();
86+
Assets::new()
87+
.after(absolute::LockTime::from_height(tip.height).expect("must be valid height"))
88+
.add({
89+
let mut pks = vec![];
90+
for (_, desc) in index.keychains() {
91+
desc.for_each_key(|k| {
92+
pks.extend(k.clone().into_single_keys());
93+
true
94+
});
95+
}
96+
pks
97+
})
98+
}
99+
100+
pub fn plan_of_output(&self, outpoint: OutPoint, assets: &Assets) -> Option<Plan> {
101+
let index = &self.graph.index;
102+
let ((k, i), _txout) = index.txout(outpoint)?;
103+
let desc = index.get_descriptor(k)?.at_derivation_index(i).ok()?;
104+
let plan = desc.plan(assets).ok()?;
105+
Some(plan)
106+
}
107+
108+
pub fn canonical_txs(&self) -> impl Iterator<Item = TxWithStatus<Arc<Transaction>>> + '_ {
109+
pub fn status_from_position(pos: ChainPosition<ConfirmationBlockTime>) -> Option<TxStatus> {
110+
match pos {
111+
bdk_chain::ChainPosition::Confirmed { anchor, .. } => Some(TxStatus {
112+
height: absolute::Height::from_consensus(
113+
anchor.confirmation_height_upper_bound(),
114+
)
115+
.expect("must convert to height"),
116+
time: absolute::Time::from_consensus(anchor.confirmation_time as _)
117+
.expect("must convert from time"),
118+
}),
119+
bdk_chain::ChainPosition::Unconfirmed { .. } => None,
120+
}
121+
}
122+
self.graph
123+
.graph()
124+
.list_canonical_txs(&self.chain, self.chain.tip().block_id())
125+
.map(|c_tx| (c_tx.tx_node.tx, status_from_position(c_tx.chain_position)))
126+
}
127+
128+
pub fn all_candidates(&self) -> bdk_tx::InputCandidates {
129+
let index = &self.graph.index;
130+
let assets = self.assets();
131+
let canon_utxos = CanonicalUnspents::new(self.canonical_txs());
132+
let can_select = canon_utxos.try_get_unspents(
133+
index
134+
.outpoints()
135+
.iter()
136+
.filter_map(|(_, op)| Some((*op, self.plan_of_output(*op, &assets)?))),
137+
);
138+
InputCandidates::new([], can_select)
139+
}
140+
141+
pub fn rbf_candidates(
142+
&self,
143+
replace: impl IntoIterator<Item = Txid>,
144+
tip_height: absolute::Height,
145+
) -> anyhow::Result<(bdk_tx::InputCandidates, RbfParams)> {
146+
let index = &self.graph.index;
147+
let assets = self.assets();
148+
let mut canon_utxos = CanonicalUnspents::new(self.canonical_txs());
149+
150+
// Exclude txs that reside-in `rbf_set`.
151+
let rbf_set = canon_utxos.extract_replacements(replace)?;
152+
let must_select = rbf_set
153+
.must_select_largest_input_of_each_original_tx(&canon_utxos)?
154+
.into_iter()
155+
.map(|op| canon_utxos.try_get_unspent(op, self.plan_of_output(op, &assets)?))
156+
.collect::<Option<Vec<Input>>>()
157+
.ok_or(anyhow::anyhow!(
158+
"failed to find input of tx we are intending to replace"
159+
))?;
160+
161+
let can_select = index.outpoints().iter().filter_map(|(_, op)| {
162+
canon_utxos.try_get_unspent(*op, self.plan_of_output(*op, &assets)?)
163+
});
164+
Ok((
165+
InputCandidates::new(must_select, can_select)
166+
.filter(rbf_set.candidate_filter(tip_height)),
167+
rbf_set.selector_rbf_params(),
168+
))
169+
}
170+
}

0 commit comments

Comments
 (0)